Summary: - Add SimpleLog table for activity tracking (admin_schema) - Implement ActivityService with fire-and-forget pattern - Add stats API endpoints (overview/live-feed/user-overview/cleanup) - Complete activity logging for 7 modules (SYSTEM/AIA/PKB/ASL/DC/RVW/IIT) - Update Admin Dashboard with DAU/DAT metrics and live feed - Fix user module permission display logic - Fix login redirect to /ai-qa instead of homepage - Replace top navigation LOGO with brand image - Fix PKB workspace layout CSS conflict (rename to .pa-chat-container) New files: - backend/src/common/services/activity.service.ts - backend/src/modules/admin/controllers/statsController.ts - backend/src/modules/admin/routes/statsRoutes.ts - frontend-v2/src/modules/admin/api/statsApi.ts - docs/03-.../04-operational-monitoring-mvp-plan.md - docs/03-.../04-operational-monitoring-mvp-implementation.md Tested: All features verified locally
280 lines
8.3 KiB
TypeScript
280 lines
8.3 KiB
TypeScript
/**
|
|
* 运营统计看板
|
|
*
|
|
* 展示 DAU/DAT、模块使用统计、实时流水账
|
|
*/
|
|
|
|
import { useState } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { Card, Statistic, Row, Col, Table, Tag, Spin, Empty, Tooltip } from 'antd';
|
|
import {
|
|
UserOutlined,
|
|
BankOutlined,
|
|
ExportOutlined,
|
|
MessageOutlined,
|
|
BookOutlined,
|
|
SearchOutlined,
|
|
FileTextOutlined,
|
|
SyncOutlined,
|
|
LoginOutlined,
|
|
CloudUploadOutlined,
|
|
DeleteOutlined,
|
|
WarningOutlined,
|
|
ReloadOutlined,
|
|
} from '@ant-design/icons';
|
|
import { getOverview, getLiveFeed } from '../api/statsApi';
|
|
import type { OverviewData, ActivityLog } from '../api/statsApi';
|
|
|
|
// ==================== 模块图标映射 ====================
|
|
|
|
const MODULE_ICONS: Record<string, React.ReactNode> = {
|
|
'SYSTEM': <LoginOutlined />,
|
|
'AIA': <MessageOutlined />,
|
|
'PKB': <BookOutlined />,
|
|
'ASL': <SearchOutlined />,
|
|
'DC': <FileTextOutlined />,
|
|
'RVW': <FileTextOutlined />,
|
|
'IIT': <SyncOutlined />,
|
|
};
|
|
|
|
const MODULE_COLORS: Record<string, string> = {
|
|
'SYSTEM': '#8c8c8c',
|
|
'AIA': '#1890ff',
|
|
'PKB': '#52c41a',
|
|
'ASL': '#722ed1',
|
|
'DC': '#fa8c16',
|
|
'RVW': '#eb2f96',
|
|
'IIT': '#13c2c2',
|
|
};
|
|
|
|
const ACTION_ICONS: Record<string, React.ReactNode> = {
|
|
'LOGIN': <LoginOutlined style={{ color: '#52c41a' }} />,
|
|
'USE': <MessageOutlined style={{ color: '#1890ff' }} />,
|
|
'EXPORT': <CloudUploadOutlined style={{ color: '#722ed1' }} />,
|
|
'CREATE': <ExportOutlined style={{ color: '#fa8c16' }} />,
|
|
'DELETE': <DeleteOutlined style={{ color: '#ff4d4f' }} />,
|
|
'ERROR': <WarningOutlined style={{ color: '#ff4d4f' }} />,
|
|
};
|
|
|
|
// ==================== 组件 ====================
|
|
|
|
export default function StatsDashboardPage() {
|
|
const [liveFeedLimit] = useState(50);
|
|
|
|
// 获取大盘数据
|
|
const { data: overview, isLoading: overviewLoading, refetch: refetchOverview } = useQuery({
|
|
queryKey: ['admin-stats-overview'],
|
|
queryFn: getOverview,
|
|
refetchInterval: 30000, // 30秒自动刷新
|
|
});
|
|
|
|
// 获取实时流水账
|
|
const { data: liveFeed, isLoading: liveFeedLoading, refetch: refetchLiveFeed } = useQuery({
|
|
queryKey: ['admin-stats-live-feed', liveFeedLimit],
|
|
queryFn: () => getLiveFeed(liveFeedLimit),
|
|
refetchInterval: 10000, // 10秒自动刷新
|
|
});
|
|
|
|
// 刷新所有数据
|
|
const handleRefresh = () => {
|
|
refetchOverview();
|
|
refetchLiveFeed();
|
|
};
|
|
|
|
// 流水账表格列
|
|
const columns = [
|
|
{
|
|
title: '时间',
|
|
dataIndex: 'createdAt',
|
|
key: 'createdAt',
|
|
width: 90,
|
|
render: (time: string) => {
|
|
const date = new Date(time);
|
|
return (
|
|
<Tooltip title={date.toLocaleString()}>
|
|
<span className="text-gray-500 text-xs">
|
|
{date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
|
</span>
|
|
</Tooltip>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: '动作',
|
|
dataIndex: 'action',
|
|
key: 'action',
|
|
width: 80,
|
|
render: (action: string) => (
|
|
<span className="flex items-center gap-1">
|
|
{ACTION_ICONS[action] || null}
|
|
<span className="text-xs">{action}</span>
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
title: '模块',
|
|
dataIndex: 'module',
|
|
key: 'module',
|
|
width: 80,
|
|
render: (module: string) => (
|
|
<Tag
|
|
icon={MODULE_ICONS[module]}
|
|
color={MODULE_COLORS[module]}
|
|
className="text-xs"
|
|
>
|
|
{module}
|
|
</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '功能',
|
|
dataIndex: 'feature',
|
|
key: 'feature',
|
|
width: 150,
|
|
ellipsis: true,
|
|
},
|
|
{
|
|
title: '用户',
|
|
dataIndex: 'userName',
|
|
key: 'userName',
|
|
width: 100,
|
|
render: (name: string | null, record: ActivityLog) => (
|
|
<Tooltip title={record.tenantName || '未知租户'}>
|
|
<span className="text-gray-600">{name || '-'}</span>
|
|
</Tooltip>
|
|
),
|
|
},
|
|
{
|
|
title: '详情',
|
|
dataIndex: 'info',
|
|
key: 'info',
|
|
ellipsis: true,
|
|
render: (info: string | null) => (
|
|
<span className="text-gray-400 text-xs">{info || '-'}</span>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="p-6 bg-gray-50 min-h-screen">
|
|
{/* 页面标题 */}
|
|
<div className="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-800">运营监控看板</h1>
|
|
<p className="text-gray-500 text-sm mt-1">实时监控系统使用情况</p>
|
|
</div>
|
|
<button
|
|
onClick={handleRefresh}
|
|
className="flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
|
>
|
|
<ReloadOutlined className={overviewLoading || liveFeedLoading ? 'animate-spin' : ''} />
|
|
刷新数据
|
|
</button>
|
|
</div>
|
|
|
|
{/* 核心指标卡片 */}
|
|
<Row gutter={[16, 16]} className="mb-6">
|
|
<Col xs={24} sm={8}>
|
|
<Card className="shadow-sm hover:shadow-md transition-shadow">
|
|
<Statistic
|
|
title={<span className="text-gray-600">今日活跃医生 (DAU)</span>}
|
|
value={overview?.dau ?? 0}
|
|
prefix={<UserOutlined className="text-blue-500" />}
|
|
loading={overviewLoading}
|
|
valueStyle={{ color: '#1890ff', fontWeight: 'bold' }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} sm={8}>
|
|
<Card className="shadow-sm hover:shadow-md transition-shadow">
|
|
<Statistic
|
|
title={<span className="text-gray-600">今日活跃租户 (DAT)</span>}
|
|
value={overview?.dat ?? 0}
|
|
prefix={<BankOutlined className="text-green-500" />}
|
|
loading={overviewLoading}
|
|
valueStyle={{ color: '#52c41a', fontWeight: 'bold' }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} sm={8}>
|
|
<Card className="shadow-sm hover:shadow-md transition-shadow">
|
|
<Statistic
|
|
title={<span className="text-gray-600">今日导出次数</span>}
|
|
value={overview?.exportCount ?? 0}
|
|
prefix={<ExportOutlined className="text-purple-500" />}
|
|
loading={overviewLoading}
|
|
valueStyle={{ color: '#722ed1', fontWeight: 'bold' }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
|
|
{/* 模块使用统计 */}
|
|
{overview?.moduleStats && Object.keys(overview.moduleStats).length > 0 && (
|
|
<Card
|
|
title="模块使用统计"
|
|
className="mb-6 shadow-sm"
|
|
extra={<span className="text-gray-400 text-xs">今日</span>}
|
|
>
|
|
<Row gutter={[16, 16]}>
|
|
{Object.entries(overview.moduleStats).map(([module, count]) => (
|
|
<Col key={module} xs={12} sm={8} md={6} lg={4}>
|
|
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
|
<div className="text-2xl mb-2">
|
|
{MODULE_ICONS[module] || <MessageOutlined />}
|
|
</div>
|
|
<div className="text-2xl font-bold" style={{ color: MODULE_COLORS[module] }}>
|
|
{count}
|
|
</div>
|
|
<div className="text-gray-500 text-sm">{module}</div>
|
|
</div>
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
</Card>
|
|
)}
|
|
|
|
{/* 实时流水账 */}
|
|
<Card
|
|
title="实时操作流水"
|
|
className="shadow-sm"
|
|
extra={
|
|
<span className="text-gray-400 text-xs">
|
|
最近 {liveFeedLimit} 条 · 10秒自动刷新
|
|
</span>
|
|
}
|
|
>
|
|
{liveFeedLoading ? (
|
|
<div className="flex justify-center py-12">
|
|
<Spin tip="加载中..." />
|
|
</div>
|
|
) : liveFeed && liveFeed.length > 0 ? (
|
|
<Table
|
|
dataSource={liveFeed}
|
|
columns={columns}
|
|
rowKey="id"
|
|
size="small"
|
|
pagination={false}
|
|
scroll={{ y: 400 }}
|
|
className="activity-table"
|
|
/>
|
|
) : (
|
|
<Empty description="暂无操作记录" />
|
|
)}
|
|
</Card>
|
|
|
|
{/* 自定义样式 */}
|
|
<style>{`
|
|
.activity-table .ant-table-tbody > tr:hover > td {
|
|
background-color: #f0f5ff !important;
|
|
}
|
|
.activity-table .ant-table-thead > tr > th {
|
|
background-color: #fafafa;
|
|
font-weight: 600;
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
}
|
|
|