Files
TurboHR/frontend/src/pages/AuditLog.tsx
T
selfrelease d79e3baa34 feat: 系统优化Phase2 - 面包屑导航/侧边栏间距/制度公示阅读签收/模板变量中文化/通知类型补全
- 面包屑导航组件,集成至TopNav header
- 侧边栏菜单分组间距增大,分组间分隔线
- 制度公示员工阅读签收:PolicyReadRecord模型、portal路由、管理端阅读统计
- 修复Policies.tsx民主程序推进bug(字段名/API路径/参数)
- 用工文本模板变量名英文转中文显示
- 通知类型TYPE_LABELS补全(RISK_ALERT/SOCIAL_INS/OVERTIME_ALERT/PAYSLIP_READY)
- 通知示例数据补充
- h2标题统一为text-sm font-medium
- 新增run.md
2026-07-26 20:32:38 +08:00

197 lines
8.0 KiB
TypeScript
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.
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { ScrollText, Search, Filter } from 'lucide-react'
import api from '../lib/api'
import Card from '../components/ui/Card'
import Button from '../components/ui/Button'
import EmptyState from '../components/ui/EmptyState'
import Pagination from '../components/ui/Pagination'
const ACTION_LABELS: Record<string, string> = {
CREATE: '创建',
UPDATE: '更新',
DELETE: '删除',
CORRECT: '更正',
CREATE_EMPLOYEE: '创建员工',
UPDATE_EMPLOYEE: '更新员工',
DELETE_EMPLOYEE: '删除员工',
IMPORT_EMPLOYEES: '导入员工',
BATCH_RENEW: '批量续签',
ADD_CONTRACT: '添加合同',
CREATE_CONTRACT: '创建合同',
SIGN_CONTRACT: '签署合同',
TERMINATE: '解聘',
CREATE_TERMINATION: '创建解聘记录',
APPROVE_TERMINATION: '审批解聘',
REJECT_TERMINATION: '驳回解聘',
EXECUTE_TERMINATION: '执行解聘',
CANCEL_TERMINATION: '撤销解聘',
SUBMIT_TERMINATION: '提交解聘审批',
CREATE_DRAFT: '创建草稿',
REHIRE: '重新入职',
PAYROLL_GENERATE: '生成工资条',
PAYROLL_ARCHIVE: '归档发薪批次',
PAYROLL_PUBLISH: '发布工资条',
EXPORT_PAYROLL: '导出薪税',
AI_CHAT: 'AI 对话',
AI_REVIEW: 'AI 审查',
UPDATE_SETTINGS: '更新设置',
POLICY_ADVANCE: '制度流程推进',
POLICY_PUBLISH: '制度发布',
EVIDENCE_CREATE: '创建证据链',
ATTENDANCE_CONFIRM: '考勤确认',
}
const ENTITY_LABELS: Record<string, string> = {
EMPLOYEE: '员工',
CONTRACT: '合同',
TERMINATION: '解聘',
TERMINATION_RECORD: '解聘记录',
PAYROLL: '薪税',
PAYSLIP: '工资条',
SETTINGS: '设置',
POLICY: '制度',
POLICY_DOCUMENT: '制度文件',
EVIDENCE: '证据链',
AI: 'AI',
ATTENDANCE: '考勤',
DISCIPLINARY: '违纪',
EmployeeSocialInsRecord: '社保记录',
EmployeeHousingFundRecord: '公积金记录',
SocialMonthlyProcess: '社保月度流程',
RetirementPolicy: '退休政策',
NotificationSetting: '通知设置',
}
/**
* 系统操作日志页面
*/
export default function AuditLog() {
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [action, setAction] = useState('')
const [entity, setEntity] = useState('')
const [dateFrom, setDateFrom] = useState('')
const [dateTo, setDateTo] = useState('')
const { data, isLoading } = useQuery<any>({
queryKey: ['audit-logs', page, pageSize, action, entity, dateFrom, dateTo],
queryFn: async () => {
const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize) })
if (action) params.set('action', action)
if (entity) params.set('entity', entity)
if (dateFrom) params.set('dateFrom', dateFrom)
if (dateTo) params.set('dateTo', dateTo)
const res = await api.get(`/audit?${params}`) as any
return res.data
},
})
const { data: stats } = useQuery<any>({
queryKey: ['audit-stats'],
queryFn: async () => {
const res = await api.get('/audit/stats') as any
return res.data
},
})
return (
<div className="space-y-3">
<div className="flex items-center gap-2">
<ScrollText className="h-5 w-5 text-primary" />
<h1 className="text-base font-semibold"></h1>
</div>
<p className="text-sm text-gray-500">6</p>
{/* 统计概览 */}
{stats && stats.length > 0 && (
<Card>
<div className="text-xs font-medium text-gray-600 mb-2"></div>
<div className="flex flex-wrap gap-2">
{stats.slice(0, 10).map((s: any) => (
<div key={s.action} className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-gray-50 text-xs">
<span className="text-gray-600">{ACTION_LABELS[s.action] || s.action}</span>
<span className="font-bold text-primary">{s._count.action}</span>
</div>
))}
</div>
</Card>
)}
{/* 筛选栏 */}
<Card>
<div className="flex flex-wrap items-end gap-3">
<div>
<label className="text-xs text-gray-500"></label>
<select value={action} onChange={e => { setAction(e.target.value); setPage(1) }} className="block mt-1 px-2 py-1.5 text-sm border rounded-lg focus:outline-none focus:ring-1 focus:ring-primary">
<option value=""></option>
{Object.entries(ACTION_LABELS).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
</select>
</div>
<div>
<label className="text-xs text-gray-500"></label>
<select value={entity} onChange={e => { setEntity(e.target.value); setPage(1) }} className="block mt-1 px-2 py-1.5 text-sm border rounded-lg focus:outline-none focus:ring-1 focus:ring-primary">
<option value=""></option>
{Object.entries(ENTITY_LABELS).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
</select>
</div>
<div>
<label className="text-xs text-gray-500"></label>
<input type="date" value={dateFrom} onChange={e => { setDateFrom(e.target.value); setPage(1) }} className="block mt-1 px-2 py-1.5 text-sm border rounded-lg focus:outline-none focus:ring-1 focus:ring-primary" />
</div>
<div>
<label className="text-xs text-gray-500"></label>
<input type="date" value={dateTo} onChange={e => { setDateTo(e.target.value); setPage(1) }} className="block mt-1 px-2 py-1.5 text-sm border rounded-lg focus:outline-none focus:ring-1 focus:ring-primary" />
</div>
{(action || entity || dateFrom || dateTo) && (
<Button size="sm" variant="secondary" onClick={() => { setAction(''); setEntity(''); setDateFrom(''); setDateTo(''); setPage(1) }}></Button>
)}
</div>
</Card>
{/* 日志列表 */}
{isLoading ? (
<div className="text-center py-8 text-gray-500">...</div>
) : !data?.items || data.items.length === 0 ? (
<EmptyState title="暂无操作日志" description="系统操作将自动记录在此" />
) : (
<>
<Card>
<div className="space-y-1.5">
{data.items.map((log: any) => (
<div key={log.id} className="flex items-start gap-3 px-2 py-2 rounded-md hover:bg-gray-50 transition-colors">
<div className="flex items-center justify-center w-7 h-7 rounded-lg bg-primary/10 text-primary flex-shrink-0">
<ScrollText className="w-3.5 h-3.5" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{ACTION_LABELS[log.action] || log.action}</span>
<span className="px-1.5 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{ENTITY_LABELS[log.entity] || log.entity}</span>
</div>
{log.detail && (
<div className="text-xs text-gray-500 mt-0.5 truncate">
{typeof log.detail === 'object' ? JSON.stringify(log.detail).slice(0, 100) : String(log.detail).slice(0, 100)}
</div>
)}
</div>
<div className="text-xs text-gray-400 flex-shrink-0 text-right">
<div>{new Date(log.createdAt).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</div>
{log.ip && <div className="text-gray-300">{log.ip}</div>}
</div>
</div>
))}
</div>
</Card>
<Pagination
page={page}
pageSize={pageSize}
total={data.total}
onPageChange={setPage}
onPageSizeChange={(s) => { setPageSize(s); setPage(1) }}
/>
</>
)}
</div>
)
}