- feat(web): ClassList.tsx 对接 update/deactivate/reset-code API - 编辑班级: PUT /diary/classes/:id - 停用班级: PATCH /diary/classes/:id/deactivate (Popconfirm 确认) - 重置班级码: POST /diary/classes/:id/reset-code (Popconfirm 确认) - 数据源改用 listAll() 获取所有班级 - fix(web): JournalList.tsx 班级筛选改用 classApi.listAll() - fix(app): EditorPage 加载已有日记数据 (journalId 非空时) - 从 Isar 恢复笔画/元素/标签/心情/标题 - _EditorView 改为 StatefulWidget + initState 加载 - chore(web): HMS 遗留代码清理 - 删除 api/copilot.ts, healthFixtures.ts, healthHandlers.ts - AuditLogViewer 资源类型替换为日记模块类型 - auth.test.ts / renderWithProviders 权限码 health.* → diary.* - docs: 确认 M6 NotificationService 为误报 (已在 3 处调用)
146 lines
6.2 KiB
TypeScript
146 lines
6.2 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import { Table, Select, Input, Tag } from 'antd';
|
|
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
|
import { listAuditLogs, type AuditLogItem, type AuditLogQuery } from '../../api/auditLogs';
|
|
import { listUsers } from '../../api/users';
|
|
import { useThemeMode } from '../../hooks/useThemeMode';
|
|
import { usePaginatedData } from '../../hooks/usePaginatedData';
|
|
|
|
const RESOURCE_TYPE_OPTIONS = [
|
|
{ value: 'user', label: '用户' }, { value: 'role', label: '角色' },
|
|
{ value: 'position', label: '岗位' }, { value: 'organization', label: '组织' },
|
|
{ value: 'department', label: '部门' }, { value: 'process_instance', label: '流程实例' },
|
|
{ value: 'process_definition', label: '流程定义' }, { value: 'task', label: '流程任务' },
|
|
{ value: 'dictionary', label: '字典' }, { value: 'menu', label: '菜单' },
|
|
{ value: 'setting', label: '设置' }, { value: 'numbering_rule', label: '编号规则' },
|
|
{ value: 'journal_entry', label: '日记' }, { value: 'school_class', label: '班级' },
|
|
{ value: 'class_member', label: '班级成员' }, { value: 'topic_assignment', label: '主题布置' },
|
|
{ value: 'comment', label: '评论' }, { value: 'sticker_pack', label: '贴纸包' },
|
|
];
|
|
|
|
const ACTION_STYLES: Record<string, { bg: string; color: string; text: string }> = {
|
|
create: { bg: '#ECFDF5', color: '#059669', text: '创建' },
|
|
update: { bg: '#eff6ff', color: '#2563eb', text: '更新' },
|
|
delete: { bg: '#FEF2F2', color: '#dc2626', text: '删除' },
|
|
};
|
|
|
|
function formatDateTime(value: string): string {
|
|
return new Date(value).toLocaleString('zh-CN', {
|
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
});
|
|
}
|
|
|
|
export default function AuditLogViewer() {
|
|
const isDark = useThemeMode();
|
|
const userNameCache = useRef<Record<string, string>>({});
|
|
const cacheLoaded = useRef(false);
|
|
|
|
const { data: logs, total, page, loading, filters, setFilters, refresh } = usePaginatedData<AuditLogItem, AuditLogQuery>(
|
|
async (p, pageSize, query) => {
|
|
const result = await listAuditLogs({ ...query, page: p, page_size: pageSize });
|
|
return { data: result.data, total: result.total };
|
|
},
|
|
{ pageSize: 20, defaultFilters: { page: 1, page_size: 20 } as unknown as AuditLogQuery, autoFetch: false },
|
|
);
|
|
|
|
// Load user name cache
|
|
useEffect(() => {
|
|
if (cacheLoaded.current) return;
|
|
let cancelled = false;
|
|
const loadAllUsers = async () => {
|
|
try {
|
|
let currentPage = 1;
|
|
const pageSize = 100;
|
|
let hasMore = true;
|
|
while (hasMore && !cancelled) {
|
|
const result = await listUsers(currentPage, pageSize);
|
|
for (const user of result.data) {
|
|
userNameCache.current[user.id] = user.display_name || user.username;
|
|
}
|
|
hasMore = result.data.length >= pageSize;
|
|
currentPage += 1;
|
|
}
|
|
if (!cancelled) cacheLoaded.current = true;
|
|
} catch { /* silent */ }
|
|
};
|
|
loadAllUsers();
|
|
return () => { cancelled = true; };
|
|
}, []);
|
|
|
|
useEffect(() => { refresh(1); }, []);
|
|
|
|
const handleFilterChange = (field: keyof AuditLogQuery, value: string | undefined) => {
|
|
setFilters((prev) => ({ ...prev, [field]: value || undefined, page: 1 }));
|
|
};
|
|
|
|
const columns: ColumnsType<AuditLogItem> = [
|
|
{
|
|
title: '操作', dataIndex: 'action', key: 'action', width: 100,
|
|
render: (action: string) => {
|
|
const info = ACTION_STYLES[action] || { bg: '#f8fafc', color: '#475569', text: action };
|
|
return <Tag style={{ background: info.bg, border: 'none', color: info.color, fontWeight: 500 }}>{info.text}</Tag>;
|
|
},
|
|
},
|
|
{
|
|
title: '资源类型', dataIndex: 'resource_type', key: 'resource_type', width: 120,
|
|
render: (v: string) => <Tag style={{ background: isDark ? '#0f172a' : '#f8fafc', border: 'none', color: isDark ? '#CBD5E1' : '#475569' }}>{v}</Tag>,
|
|
},
|
|
{
|
|
title: '资源 ID', dataIndex: 'resource_id', key: 'resource_id', width: 200, ellipsis: true,
|
|
render: (v: string) => <span style={{ fontFamily: 'monospace', fontSize: 12, color: isDark ? '#94a3b8' : '#475569' }}>{v}</span>,
|
|
},
|
|
{
|
|
title: '操作用户', dataIndex: 'user_id', key: 'user_id', width: 200, ellipsis: true,
|
|
render: (v: string) => {
|
|
const name = userNameCache.current[v];
|
|
return <span title={v} style={{ fontSize: 13, color: isDark ? '#CBD5E1' : '#334155' }}>{name || v}</span>;
|
|
},
|
|
},
|
|
{
|
|
title: '时间', dataIndex: 'created_at', key: 'created_at', width: 180,
|
|
render: (value: string) => <span style={{ color: isDark ? '#475569' : '#94a3b8', fontSize: 13 }}>{formatDateTime(value)}</span>,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
{/* 筛选工具栏 */}
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16, padding: 12,
|
|
background: isDark ? '#111827' : '#FFFFFF', borderRadius: 10,
|
|
border: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`,
|
|
}}>
|
|
<Select
|
|
allowClear placeholder="资源类型" style={{ width: 160 }}
|
|
options={RESOURCE_TYPE_OPTIONS}
|
|
value={filters.resource_type}
|
|
onChange={(value) => handleFilterChange('resource_type', value)}
|
|
/>
|
|
<Input
|
|
allowClear placeholder="操作用户 ID" style={{ width: 240 }}
|
|
value={filters.user_id ?? ''}
|
|
onChange={(e) => handleFilterChange('user_id', e.target.value)}
|
|
/>
|
|
<span style={{ fontSize: 13, color: isDark ? '#475569' : '#94a3b8', marginLeft: 'auto' }}>共 {total} 条日志</span>
|
|
</div>
|
|
|
|
{/* 表格 */}
|
|
<div style={{
|
|
background: isDark ? '#111827' : '#FFFFFF', borderRadius: 12,
|
|
border: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`, overflow: 'hidden',
|
|
}}>
|
|
<Table
|
|
rowKey="id" columns={columns} dataSource={logs} loading={loading}
|
|
onChange={(pagination: TablePaginationConfig) => refresh(pagination.current)}
|
|
pagination={{
|
|
current: page, pageSize: 20, total,
|
|
showSizeChanger: true, showTotal: (t) => `共 ${t} 条`,
|
|
}}
|
|
scroll={{ x: 900 }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|