Files
schoolNews/schoolNewsWeb/src/views/admin/manage/logs/LoginLogsView.vue
2025-10-30 16:40:56 +08:00

626 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<AdminLayout title="登录日志" subtitle="登录日志管理">
<div class="login-logs">
<!-- 搜索栏 -->
<div class="search-bar">
<div class="search-group">
<label class="search-label">用户名</label>
<input
v-model="searchKeyword"
type="text"
class="search-input"
placeholder="输入用户名搜索"
@keyup.enter="handleSearch"
/>
</div>
<div class="search-group">
<label class="search-label">登录状态</label>
<select v-model="loginStatus" class="search-select">
<option value="">全部</option>
<option value="1">成功</option>
<option value="0">失败</option>
</select>
</div>
<div class="search-group">
<label class="search-label">开始日期</label>
<input
v-model="startDate"
type="date"
class="search-input"
/>
</div>
<div class="search-group">
<label class="search-label">结束日期</label>
<input
v-model="endDate"
type="date"
class="search-input"
/>
</div>
<div class="search-actions">
<button class="btn-search" @click="handleSearch">搜索</button>
<button class="btn-reset" @click="handleReset">重置</button>
</div>
</div>
<!-- 操作按钮 -->
<!-- <div class="toolbar">
<button class="btn-danger" @click="handleClear">清空日志</button>
</div> -->
<!-- 日志表格 -->
<div class="log-table-wrapper">
<table class="log-table">
<thead>
<tr>
<th width="120">用户名</th>
<th width="140">IP地址</th>
<th width="150">登录地点</th>
<th width="120">浏览器</th>
<th width="120">操作系统</th>
<th width="100">状态</th>
<th>信息</th>
<th width="180">登录时间</th>
</tr>
</thead>
<tbody v-if="loading">
<tr>
<td colspan="8" class="loading-cell">
<div class="loading-spinner"></div>
<span>加载中...</span>
</td>
</tr>
</tbody>
<tbody v-else-if="logs.length === 0">
<tr>
<td colspan="8" class="empty-cell">
<div class="empty-icon">📋</div>
<p>暂无登录日志</p>
</td>
</tr>
</tbody>
<tbody v-else>
<tr v-for="log in logs" :key="log.id" class="table-row">
<td>{{ log.username || '-' }}</td>
<td>{{ log.ipAddress || '-' }}</td>
<td>{{ log.location || '-' }}</td>
<td>{{ log.browser || '-' }}</td>
<td>{{ log.os || '-' }}</td>
<td>
<span class="status-tag" :class="log.status === 1 ? 'status-success' : 'status-failed'">
{{ log.status === 1 ? '成功' : '失败' }}
</span>
</td>
<td class="log-message">
<div class="message-text">{{ log.message || '-' }}</div>
</td>
<td>{{ log.loginTime || '-' }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div v-if="!loading && logs.length > 0" class="pagination">
<div class="pagination-info">
{{ total }} 条数据每页 {{ pageSize }}
</div>
<div class="pagination-controls">
<button
class="page-btn"
:disabled="currentPage === 1"
@click="handlePageChange(1)"
>
首页
</button>
<button
class="page-btn"
:disabled="currentPage === 1"
@click="handlePageChange(currentPage - 1)"
>
上一页
</button>
<div class="page-numbers">
<button
v-for="page in displayPages"
:key="page"
class="page-number"
:class="{ active: page === currentPage }"
@click="handlePageChange(page)"
:disabled="page === -1"
>
{{ page === -1 ? '...' : page }}
</button>
</div>
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="handlePageChange(currentPage + 1)"
>
下一页
</button>
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="handlePageChange(totalPages)"
>
末页
</button>
</div>
<div class="pagination-jump">
<span>跳转到</span>
<input
v-model.number="jumpPage"
type="number"
class="jump-input"
@keyup.enter="handleJumpPage"
/>
<span></span>
<button class="jump-btn" @click="handleJumpPage">跳转</button>
</div>
</div>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { AdminLayout } from '@/views/admin';
import { logApi } from '@/apis/system';
import type { LoginLog } from '@/types/log';
defineOptions({
name: 'LoginLogsView'
});
const loading = ref(false);
const searchKeyword = ref('');
const loginStatus = ref('');
const startDate = ref('');
const endDate = ref('');
const currentPage = ref(1);
const pageSize = ref(20);
const total = ref(0);
const logs = ref<LoginLog[]>([]);
const jumpPage = ref<number>();
// 计算总页数
const totalPages = computed(() => Math.ceil(total.value / pageSize.value) || 1);
// 计算显示的页码
const displayPages = computed(() => {
const pages: number[] = [];
const maxDisplay = 7;
if (totalPages.value <= maxDisplay) {
for (let i = 1; i <= totalPages.value; i++) {
pages.push(i);
}
} else {
if (currentPage.value <= 4) {
for (let i = 1; i <= 5; i++) {
pages.push(i);
}
pages.push(-1);
pages.push(totalPages.value);
} else if (currentPage.value >= totalPages.value - 3) {
pages.push(1);
pages.push(-1);
for (let i = totalPages.value - 4; i <= totalPages.value; i++) {
pages.push(i);
}
} else {
pages.push(1);
pages.push(-1);
for (let i = currentPage.value - 1; i <= currentPage.value + 1; i++) {
pages.push(i);
}
pages.push(-1);
pages.push(totalPages.value);
}
}
return pages;
});
onMounted(() => {
loadLogs();
});
/**
* 加载登录日志
*/
async function loadLogs() {
try {
loading.value = true;
// 构建查询条件
const query: LoginLog = {
username: searchKeyword.value || undefined,
status: loginStatus.value ? Number(loginStatus.value) : undefined,
startTime: startDate.value || undefined,
endTime: endDate.value || undefined,
};
const result = await logApi.getLoginLogPage(
{pageNumber: currentPage.value, pageSize: pageSize.value},
query
);
if (result.success) {
logs.value = result.pageDomain?.dataList || [];
total.value = result.pageDomain?.pageParam.totalElements || 0;
}
} catch (error) {
console.error('加载登录日志失败:', error);
alert('加载登录日志失败');
} finally {
loading.value = false;
}
}
/**
* 查询
*/
function handleSearch() {
currentPage.value = 1;
loadLogs();
}
/**
* 重置查询条件
*/
function handleReset() {
searchKeyword.value = '';
loginStatus.value = '';
startDate.value = '';
endDate.value = '';
currentPage.value = 1;
loadLogs();
}
/**
* 页码改变
*/
function handlePageChange(page: number) {
if (page < 1 || page > totalPages.value || page === -1) return;
currentPage.value = page;
loadLogs();
}
/**
* 跳转页面
*/
function handleJumpPage() {
if (!jumpPage.value || jumpPage.value < 1 || jumpPage.value > totalPages.value) {
alert('请输入有效的页码');
return;
}
currentPage.value = jumpPage.value;
loadLogs();
}
</script>
<style lang="scss" scoped>
.login-logs {
padding: 20px;
}
.search-bar {
background: #fff;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
display: flex;
gap: 16px;
flex-wrap: wrap;
align-items: flex-end;
}
.search-group {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 180px;
}
.search-label {
font-size: 14px;
font-weight: 500;
color: #606266;
}
.search-input,
.search-select {
padding: 8px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
color: #606266;
transition: border-color 0.3s;
&:focus {
outline: none;
border-color: #409eff;
}
}
.search-select {
cursor: pointer;
}
.search-actions {
display: flex;
gap: 12px;
margin-left: auto;
}
.btn-search,
.btn-reset {
padding: 8px 20px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
border: none;
}
.btn-search {
background: #409eff;
color: #fff;
&:hover {
background: #66b1ff;
}
}
.btn-reset {
background: #fff;
color: #606266;
border: 1px solid #dcdfe6;
&:hover {
color: #409eff;
border-color: #409eff;
}
}
.toolbar {
margin-bottom: 20px;
}
.btn-danger {
padding: 8px 20px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
border: none;
background: #f56c6c;
color: #fff;
&:hover {
background: #f78989;
}
}
.btn-primary {
padding: 8px 20px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
border: none;
background: #409eff;
color: #fff;
&:hover {
background: #66b1ff;
}
}
.log-table-wrapper {
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.log-table {
width: 100%;
border-collapse: collapse;
th {
background: #f5f7fa;
color: #606266;
font-weight: 600;
font-size: 14px;
padding: 12px 16px;
text-align: left;
border-bottom: 2px solid #e0e0e0;
}
td {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
color: #606266;
}
.table-row {
transition: background-color 0.3s;
&:hover {
background: #f5f7fa;
}
&:last-child td {
border-bottom: none;
}
}
}
.log-message {
max-width: 300px;
}
.message-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-tag {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
&.status-success {
background: #e1f3d8;
color: #67c23a;
}
&.status-failed {
background: #fde2e2;
color: #f56c6c;
}
}
.loading-cell,
.empty-cell {
text-align: center;
padding: 60px 20px !important;
color: #909399;
}
.loading-spinner {
display: inline-block;
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.empty-icon {
font-size: 48px;
margin-bottom: 12px;
}
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
flex-wrap: wrap;
gap: 16px;
}
.pagination-info {
font-size: 14px;
color: #606266;
}
.pagination-controls {
display: flex;
gap: 8px;
align-items: center;
}
.page-btn,
.page-number {
padding: 6px 12px;
border: 1px solid #dcdfe6;
background: #fff;
color: #606266;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
&:hover:not(:disabled) {
color: #409eff;
border-color: #409eff;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
&.active {
background: #409eff;
color: #fff;
border-color: #409eff;
}
}
.page-numbers {
display: flex;
gap: 4px;
}
.pagination-jump {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #606266;
.jump-input {
width: 60px;
padding: 6px 8px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
text-align: center;
&:focus {
outline: none;
border-color: #409eff;
}
}
.jump-btn {
padding: 6px 12px;
border: 1px solid #dcdfe6;
background: #fff;
color: #606266;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
&:hover {
color: #409eff;
border-color: #409eff;
}
}
}
</style>