feat(admin): 添加系统日志功能

## 新功能

1. **系统日志数据库**
   - 新增 system_logs 表
   - 支持日志级别:debug/info/warn/error
   - 支持日志分类:auth/user/file/share/system/security
   - 记录用户ID、用户名、IP地址、User-Agent

2. **日志记录**
   - 用户注册成功/失败
   - 用户登录成功/失败(密码错误)
   - 系统操作(日志清理等)

3. **管理员API**
   - GET /api/admin/logs - 查询日志(支持分页和筛选)
   - GET /api/admin/logs/stats - 获取日志统计
   - POST /api/admin/logs/cleanup - 清理旧日志

4. **前端界面**
   - 日志列表展示(时间、级别、分类、内容、用户、IP)
   - 筛选功能(级别、分类、关键词搜索)
   - 分页导航
   - 清理旧日志功能

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-27 19:54:46 +08:00
parent 15ea15518c
commit c3bc58a88b
4 changed files with 598 additions and 3 deletions

View File

@@ -1874,6 +1874,118 @@
</div>
</div>
<!-- 系统日志 -->
<div class="card" style="margin-bottom: 30px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0;">
<i class="fas fa-clipboard-list"></i> 系统日志
</h3>
<div style="display: flex; gap: 10px;">
<button class="btn btn-secondary" @click="cleanupLogs" title="清理90天前的日志">
<i class="fas fa-trash"></i> 清理旧日志
</button>
<button class="btn btn-primary" @click="loadSystemLogs(1)" :disabled="systemLogs.loading">
<i class="fas" :class="systemLogs.loading ? 'fa-spinner fa-spin' : 'fa-sync'"></i>
刷新
</button>
</div>
</div>
<!-- 筛选器 -->
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 15px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
<div style="display: flex; align-items: center; gap: 8px;">
<label style="font-size: 13px; color: #666;">级别:</label>
<select v-model="systemLogs.filters.level" @change="filterLogs" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px;">
<option value="">全部</option>
<option value="info">信息</option>
<option value="warn">警告</option>
<option value="error">错误</option>
</select>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<label style="font-size: 13px; color: #666;">分类:</label>
<select v-model="systemLogs.filters.category" @change="filterLogs" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px;">
<option value="">全部</option>
<option value="auth">认证</option>
<option value="user">用户</option>
<option value="file">文件</option>
<option value="share">分享</option>
<option value="system">系统</option>
<option value="security">安全</option>
</select>
</div>
<div style="display: flex; align-items: center; gap: 8px; flex: 1; min-width: 200px;">
<label style="font-size: 13px; color: #666;">搜索:</label>
<input type="text" v-model="systemLogs.filters.keyword" @keyup.enter="filterLogs"
placeholder="搜索日志内容..." style="flex: 1; padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<button class="btn btn-secondary" @click="clearLogFilters" style="padding: 6px 12px;">
<i class="fas fa-times"></i> 清除筛选
</button>
</div>
<!-- 日志统计 -->
<div v-if="systemLogs.total > 0" style="margin-bottom: 15px; font-size: 13px; color: #666;">
共 {{ systemLogs.total }} 条日志,第 {{ systemLogs.page }}/{{ systemLogs.totalPages }} 页
</div>
<!-- 日志列表 -->
<div v-if="systemLogs.logs.length > 0" style="max-height: 500px; overflow-y: auto;">
<div v-for="log in systemLogs.logs" :key="log.id"
style="display: flex; gap: 12px; padding: 12px; border-bottom: 1px solid #eee; align-items: flex-start;">
<!-- 时间 -->
<div style="width: 140px; flex-shrink: 0; font-size: 12px; color: #888;">
{{ formatLogTime(log.created_at) }}
</div>
<!-- 级别标签 -->
<div style="width: 50px; flex-shrink: 0;">
<span :style="getLogLevelColor(log.level)" style="padding: 2px 8px; border-radius: 4px; font-size: 11px;">
{{ getLogLevelText(log.level) }}
</span>
</div>
<!-- 分类图标 -->
<div style="width: 70px; flex-shrink: 0; display: flex; align-items: center; gap: 4px; font-size: 12px; color: #666;">
<i class="fas" :class="getLogCategoryIcon(log.category)"></i>
{{ getLogCategoryText(log.category) }}
</div>
<!-- 内容 -->
<div style="flex: 1; min-width: 0;">
<div style="font-weight: 500; margin-bottom: 4px;">{{ log.action }}</div>
<div style="font-size: 13px; color: #555;">{{ log.message }}</div>
<div v-if="log.username || log.ip_address" style="font-size: 11px; color: #888; margin-top: 4px;">
<span v-if="log.username"><i class="fas fa-user"></i> {{ log.username }}</span>
<span v-if="log.ip_address" style="margin-left: 10px;"><i class="fas fa-globe"></i> {{ log.ip_address }}</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else-if="!systemLogs.loading" style="text-align: center; padding: 40px; color: #888;">
<i class="fas fa-clipboard" style="font-size: 48px; margin-bottom: 15px;"></i>
<p>暂无日志记录</p>
</div>
<!-- 加载中 -->
<div v-if="systemLogs.loading" style="text-align: center; padding: 40px; color: #888;">
<i class="fas fa-spinner fa-spin" style="font-size: 24px;"></i>
<p>加载中...</p>
</div>
<!-- 分页 -->
<div v-if="systemLogs.totalPages > 1" style="display: flex; justify-content: center; gap: 8px; margin-top: 15px;">
<button class="btn btn-secondary" @click="loadSystemLogs(systemLogs.page - 1)" :disabled="systemLogs.page <= 1" style="padding: 6px 12px;">
<i class="fas fa-chevron-left"></i> 上一页
</button>
<span style="display: flex; align-items: center; padding: 0 15px; color: #666;">
{{ systemLogs.page }} / {{ systemLogs.totalPages }}
</span>
<button class="btn btn-secondary" @click="loadSystemLogs(systemLogs.page + 1)" :disabled="systemLogs.page >= systemLogs.totalPages" style="padding: 6px 12px;">
下一页 <i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
<h3 style="margin-bottom: 20px;">用户管理</h3>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; table-layout: fixed; min-width: 900px;">

View File

@@ -157,6 +157,21 @@ createApp({
checks: []
},
// 系统日志
systemLogs: {
loading: false,
logs: [],
total: 0,
page: 1,
pageSize: 30,
totalPages: 0,
filters: {
level: '',
category: '',
keyword: ''
}
},
// Toast通知
toasts: [],
toastIdCounter: 0,
@@ -2050,11 +2065,12 @@ handleDragLeave(e) {
this.loadShares();
break;
case 'admin':
// 切换到管理后台时,重新加载用户列表健康检测
// 切换到管理后台时,重新加载用户列表健康检测和系统日志
if (this.user && this.user.is_admin) {
this.loadUsers();
this.loadServerStorageStats();
this.loadHealthCheck();
this.loadSystemLogs(1);
}
break;
case 'settings':
@@ -2331,6 +2347,125 @@ handleDragLeave(e) {
return texts[status] || '未知';
},
// ===== 系统日志 =====
async loadSystemLogs(page = 1) {
this.systemLogs.loading = true;
try {
const params = new URLSearchParams({
page: page,
pageSize: this.systemLogs.pageSize
});
if (this.systemLogs.filters.level) {
params.append('level', this.systemLogs.filters.level);
}
if (this.systemLogs.filters.category) {
params.append('category', this.systemLogs.filters.category);
}
if (this.systemLogs.filters.keyword) {
params.append('keyword', this.systemLogs.filters.keyword);
}
const response = await axios.get(`${this.apiBase}/api/admin/logs?${params}`, {
headers: { Authorization: `Bearer ${this.token}` }
});
if (response.data.success) {
this.systemLogs.logs = response.data.logs;
this.systemLogs.total = response.data.total;
this.systemLogs.page = response.data.page;
this.systemLogs.totalPages = response.data.totalPages;
}
} catch (error) {
console.error('加载系统日志失败:', error);
this.showToast('error', '错误', '加载系统日志失败');
} finally {
this.systemLogs.loading = false;
}
},
filterLogs() {
this.loadSystemLogs(1);
},
clearLogFilters() {
this.systemLogs.filters = { level: '', category: '', keyword: '' };
this.loadSystemLogs(1);
},
getLogLevelColor(level) {
const colors = {
debug: 'background: #6c757d; color: white;',
info: 'background: #17a2b8; color: white;',
warn: 'background: #ffc107; color: black;',
error: 'background: #dc3545; color: white;'
};
return colors[level] || 'background: #6c757d; color: white;';
},
getLogLevelText(level) {
const texts = { debug: '调试', info: '信息', warn: '警告', error: '错误' };
return texts[level] || level;
},
getLogCategoryText(category) {
const texts = {
auth: '认证',
user: '用户',
file: '文件',
share: '分享',
system: '系统',
security: '安全'
};
return texts[category] || category;
},
getLogCategoryIcon(category) {
const icons = {
auth: 'fa-key',
user: 'fa-user',
file: 'fa-file',
share: 'fa-share-alt',
system: 'fa-cog',
security: 'fa-shield-alt'
};
return icons[category] || 'fa-info';
},
formatLogTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
},
async cleanupLogs() {
if (!confirm('确定要清理90天前的日志吗此操作不可恢复。')) return;
try {
const response = await axios.post(
`${this.apiBase}/api/admin/logs/cleanup`,
{ keepDays: 90 },
{ headers: { Authorization: `Bearer ${this.token}` } }
);
if (response.data.success) {
this.showToast('success', '成功', response.data.message);
this.loadSystemLogs(1);
}
} catch (error) {
console.error('清理日志失败:', error);
this.showToast('error', '错误', '清理日志失败');
}
},
// ===== 上传工具管理 =====
// 检测上传工具是否存在