import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import axios from 'axios' import { ActivityIcon, DownloadIcon, RefreshIcon, SearchIcon, ClockIcon, ShieldIcon } from '../components/Icons' import { setPageTitle } from '../branding' /* --------------------------------------------------------------------------- 系统日志(审计轨迹)—— 侧栏「系统管理」下的独立页面。 参考 GoodJob 的系统日志页:四格统计 + 等级/分类筛选 + 搜索 + 导出 + 列表 + 展开详情。 数据源 GET /api/logs(后端按 action 前缀归分类、按 ok 归等级)。 入口与路由都认 audit.view —— 按 Wayne 的口径,仅系统管理员可见。 --------------------------------------------------------------------------- */ const LEVELS = [ { key: '', label: '全部' }, { key: 'info', label: '信息' }, { key: 'success', label: '成功' }, { key: 'warning', label: '警告' }, { key: 'error', label: '错误' } ] const PAGE_SIZE = 50 function fmtTime(value) { if (!value) return '' try { return new Date(value).toLocaleString('zh-CN', { hour12: false }) } catch { return String(value) } } function SystemLogs() { const [level, setLevel] = useState('') const [category, setCategory] = useState('') const [keyword, setKeyword] = useState('') const [search, setSearch] = useState('') const [offset, setOffset] = useState(0) const [data, setData] = useState(null) const [openId, setOpenId] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [exporting, setExporting] = useState(false) const alive = useRef(true) useEffect(() => { setPageTitle('系统日志') }, []) /* 搜索框防抖,避免每敲一个字打一次接口 */ useEffect(() => { const timer = setTimeout(() => { setSearch(keyword.trim()); setOffset(0) }, 300) return () => clearTimeout(timer) }, [keyword]) const load = useCallback(async () => { setLoading(true) setError('') try { const { data: payload } = await axios.get('/api/logs', { params: { level: level || undefined, category: category || undefined, q: search || undefined, limit: PAGE_SIZE, offset } }) if (!alive.current) return setData(payload) } catch (e) { if (alive.current) setError(e?.response?.data?.error || e.message) } finally { if (alive.current) setLoading(false) } }, [level, category, search, offset]) useEffect(() => { alive.current = true; return () => { alive.current = false } }, []) useEffect(() => { load() }, [load]) const items = data?.items || [] const stats = data?.stats || { total: 0, today: 0, warning: 0, error: 0 } const total = data?.total || 0 const rangeText = useMemo(() => { if (!total) return '暂无记录' const from = offset + 1 const to = Math.min(offset + PAGE_SIZE, total) return `第 ${from}–${to} 条 / 共 ${total} 条` }, [offset, total]) const exportCsv = async () => { setExporting(true) setError('') try { const response = await axios.get('/api/logs/export', { params: { level: level || undefined, category: category || undefined, q: search || undefined }, responseType: 'blob' }) const url = URL.createObjectURL(response.data) const link = document.createElement('a') link.href = url link.download = `system-logs-${new Date().toISOString().slice(0, 10)}.csv` document.body.appendChild(link) link.click() link.remove() URL.revokeObjectURL(url) } catch (e) { setError(e?.response?.data?.error || '导出失败') } finally { setExporting(false) } } return (

系统日志

账号登录与核心业务操作的审计轨迹,用于追溯与排障。仅系统管理员可见。 {total > 0 && ` 共 ${total} 条记录。`}

{stats.total} 记录
{stats.today} 今日
{stats.warning} 警告
{stats.error} 错误
{LEVELS.map(item => ( ))}
{(data?.categories || []).map(item => ( ))}
{error &&

{error}

} {!loading && !items.length && (
暂无日志记录 登录系统或改动用户、权限、客户、商机与模型接入时,操作会自动记录到这里。
)} {!!items.length && ( )} {(total > PAGE_SIZE || offset > 0) && (
{rangeText}
)}
) } export default SystemLogs