feat(admin): Add activity logs page and fix AI chat markdown rendering
- Add paginated activity logs API with filters (date, module, action, keyword) - Add ActivityLogsPage with table, filters, and detail modal - Add markdown rendering support for AI chat messages - Remove prototype placeholder content from chat sidebar Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -290,6 +290,113 @@ export const activityService = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询运营日志(支持筛选)
|
||||||
|
*
|
||||||
|
* @param options 查询选项
|
||||||
|
*/
|
||||||
|
async getActivityLogs(options: {
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
module?: string;
|
||||||
|
action?: string;
|
||||||
|
keyword?: string;
|
||||||
|
}): Promise<{
|
||||||
|
data: Array<{
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
tenantId: string;
|
||||||
|
tenantName: string | null;
|
||||||
|
userId: string;
|
||||||
|
userName: string | null;
|
||||||
|
module: string;
|
||||||
|
feature: string;
|
||||||
|
action: string;
|
||||||
|
info: string | null;
|
||||||
|
}>;
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}> {
|
||||||
|
const { page, pageSize, startDate, endDate, module, action, keyword } = options;
|
||||||
|
|
||||||
|
// 构建 where 条件
|
||||||
|
const where: any = {};
|
||||||
|
|
||||||
|
// 日期范围筛选
|
||||||
|
if (startDate || endDate) {
|
||||||
|
where.created_at = {};
|
||||||
|
if (startDate) where.created_at.gte = startDate;
|
||||||
|
if (endDate) {
|
||||||
|
// endDate 设置到当天结束
|
||||||
|
const endOfDay = new Date(endDate);
|
||||||
|
endOfDay.setHours(23, 59, 59, 999);
|
||||||
|
where.created_at.lte = endOfDay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模块筛选
|
||||||
|
if (module) {
|
||||||
|
where.module = module;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动作筛选
|
||||||
|
if (action) {
|
||||||
|
where.action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关键词搜索(用户名或租户名)
|
||||||
|
if (keyword) {
|
||||||
|
where.OR = [
|
||||||
|
{ user_name: { contains: keyword, mode: 'insensitive' } },
|
||||||
|
{ tenant_name: { contains: keyword, mode: 'insensitive' } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并行查询数据和总数
|
||||||
|
const [logs, total] = await Promise.all([
|
||||||
|
prisma.simple_logs.findMany({
|
||||||
|
where,
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
created_at: true,
|
||||||
|
tenant_id: true,
|
||||||
|
tenant_name: true,
|
||||||
|
user_id: true,
|
||||||
|
user_name: true,
|
||||||
|
module: true,
|
||||||
|
feature: true,
|
||||||
|
action: true,
|
||||||
|
info: true,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
prisma.simple_logs.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: logs.map(log => ({
|
||||||
|
id: log.id,
|
||||||
|
createdAt: log.created_at,
|
||||||
|
tenantId: log.tenant_id,
|
||||||
|
tenantName: log.tenant_name,
|
||||||
|
userId: log.user_id,
|
||||||
|
userName: log.user_name,
|
||||||
|
module: log.module,
|
||||||
|
feature: log.feature,
|
||||||
|
action: log.action,
|
||||||
|
info: log.info,
|
||||||
|
})),
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理过期日志(180天前的数据)
|
* 清理过期日志(180天前的数据)
|
||||||
* 用于定时任务调用
|
* 用于定时任务调用
|
||||||
|
|||||||
@@ -98,6 +98,81 @@ export async function getUserOverview(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询运营日志
|
||||||
|
* GET /api/admin/stats/logs
|
||||||
|
*
|
||||||
|
* 查询参数:
|
||||||
|
* - page: 页码 (默认1)
|
||||||
|
* - pageSize: 每页条数 (默认20, 最大100)
|
||||||
|
* - startDate: 开始日期 (YYYY-MM-DD)
|
||||||
|
* - endDate: 结束日期 (YYYY-MM-DD)
|
||||||
|
* - module: 模块筛选
|
||||||
|
* - action: 动作筛选
|
||||||
|
* - keyword: 关键词搜索 (用户名/租户名)
|
||||||
|
*/
|
||||||
|
export async function getActivityLogs(
|
||||||
|
request: FastifyRequest<{
|
||||||
|
Querystring: {
|
||||||
|
page?: string;
|
||||||
|
pageSize?: string;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
module?: string;
|
||||||
|
action?: string;
|
||||||
|
keyword?: string;
|
||||||
|
}
|
||||||
|
}>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
page: pageStr,
|
||||||
|
pageSize: pageSizeStr,
|
||||||
|
startDate: startDateStr,
|
||||||
|
endDate: endDateStr,
|
||||||
|
module,
|
||||||
|
action,
|
||||||
|
keyword,
|
||||||
|
} = request.query;
|
||||||
|
|
||||||
|
// 解析分页参数
|
||||||
|
const page = Math.max(1, Number(pageStr) || 1);
|
||||||
|
const pageSize = Math.min(100, Math.max(1, Number(pageSizeStr) || 20));
|
||||||
|
|
||||||
|
// 解析日期参数
|
||||||
|
const startDate = startDateStr ? new Date(startDateStr) : undefined;
|
||||||
|
const endDate = endDateStr ? new Date(endDateStr) : undefined;
|
||||||
|
|
||||||
|
const result = await activityService.getActivityLogs({
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
module,
|
||||||
|
action,
|
||||||
|
keyword,
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply.send({
|
||||||
|
success: true,
|
||||||
|
data: result.data,
|
||||||
|
pagination: {
|
||||||
|
page: result.page,
|
||||||
|
pageSize: result.pageSize,
|
||||||
|
total: result.total,
|
||||||
|
totalPages: Math.ceil(result.total / result.pageSize),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('[StatsController] 获取运营日志失败', { error: error.message });
|
||||||
|
return reply.status(500).send({
|
||||||
|
success: false,
|
||||||
|
message: error.message || '获取运营日志失败',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理过期日志(管理接口)
|
* 清理过期日志(管理接口)
|
||||||
* POST /api/admin/stats/cleanup
|
* POST /api/admin/stats/cleanup
|
||||||
|
|||||||
@@ -36,6 +36,17 @@ export async function statsRoutes(fastify: FastifyInstance) {
|
|||||||
handler: statsController.getLiveFeed,
|
handler: statsController.getLiveFeed,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询运营日志
|
||||||
|
* GET /api/admin/stats/logs
|
||||||
|
*
|
||||||
|
* 权限: SUPER_ADMIN, PROMPT_ENGINEER
|
||||||
|
*/
|
||||||
|
fastify.get('/logs', {
|
||||||
|
preHandler: [authenticate, requirePermission('tenant:view')],
|
||||||
|
handler: statsController.getActivityLogs,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理过期日志
|
* 清理过期日志
|
||||||
* POST /api/admin/stats/cleanup
|
* POST /api/admin/stats/cleanup
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import UserDetailPage from './modules/admin/pages/UserDetailPage'
|
|||||||
// 系统知识库管理
|
// 系统知识库管理
|
||||||
import SystemKbListPage from './modules/admin/pages/SystemKbListPage'
|
import SystemKbListPage from './modules/admin/pages/SystemKbListPage'
|
||||||
import SystemKbDetailPage from './modules/admin/pages/SystemKbDetailPage'
|
import SystemKbDetailPage from './modules/admin/pages/SystemKbDetailPage'
|
||||||
|
// 运营日志
|
||||||
|
import ActivityLogsPage from './pages/admin/ActivityLogsPage'
|
||||||
// 个人中心页面
|
// 个人中心页面
|
||||||
import ProfilePage from './pages/user/ProfilePage'
|
import ProfilePage from './pages/user/ProfilePage'
|
||||||
|
|
||||||
@@ -115,6 +117,8 @@ function App() {
|
|||||||
{/* 系统知识库 */}
|
{/* 系统知识库 */}
|
||||||
<Route path="system-kb" element={<SystemKbListPage />} />
|
<Route path="system-kb" element={<SystemKbListPage />} />
|
||||||
<Route path="system-kb/:id" element={<SystemKbDetailPage />} />
|
<Route path="system-kb/:id" element={<SystemKbDetailPage />} />
|
||||||
|
{/* 运营日志 */}
|
||||||
|
<Route path="activity-logs" element={<ActivityLogsPage />} />
|
||||||
{/* 系统配置 */}
|
{/* 系统配置 */}
|
||||||
<Route path="system" element={<div className="text-center py-20">🚧 系统配置页面开发中...</div>} />
|
<Route path="system" element={<div className="text-center py-20">🚧 系统配置页面开发中...</div>} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
MenuUnfoldOutlined,
|
MenuUnfoldOutlined,
|
||||||
BellOutlined,
|
BellOutlined,
|
||||||
BookOutlined,
|
BookOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import type { MenuProps } from 'antd'
|
import type { MenuProps } from 'antd'
|
||||||
import { useAuth } from '../auth'
|
import { useAuth } from '../auth'
|
||||||
@@ -94,6 +95,11 @@ const AdminLayout = () => {
|
|||||||
icon: <TeamOutlined />,
|
icon: <TeamOutlined />,
|
||||||
label: '租户管理',
|
label: '租户管理',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: '/admin/activity-logs',
|
||||||
|
icon: <FileTextOutlined />,
|
||||||
|
label: '运营日志',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: '/admin/users',
|
key: '/admin/users',
|
||||||
icon: <UserOutlined />,
|
icon: <UserOutlined />,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { ThinkingBlock } from '@/shared/components/Chat';
|
|||||||
import { getAccessToken } from '@/framework/auth/api';
|
import { getAccessToken } from '@/framework/auth/api';
|
||||||
import { AGENTS, AGENT_PROMPTS, BRAND_COLORS } from '../constants';
|
import { AGENTS, AGENT_PROMPTS, BRAND_COLORS } from '../constants';
|
||||||
import type { AgentConfig, Conversation, Message } from '../types';
|
import type { AgentConfig, Conversation, Message } from '../types';
|
||||||
|
import { MarkdownContent } from '../protocol-agent/components/MarkdownContent';
|
||||||
import '../styles/chat-workspace.css';
|
import '../styles/chat-workspace.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -651,14 +652,6 @@ export const ChatWorkspace: React.FC<ChatWorkspaceProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 用户信息 */}
|
|
||||||
<div className="sidebar-user">
|
|
||||||
<div className="user-avatar">U</div>
|
|
||||||
<div className="user-info">
|
|
||||||
<div className="user-name">Dr. Wang</div>
|
|
||||||
<div className="user-plan">专业版会员</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* 主对话区 */}
|
{/* 主对话区 */}
|
||||||
@@ -750,7 +743,11 @@ export const ChatWorkspace: React.FC<ChatWorkspaceProps> = ({
|
|||||||
{/* 消息内容(有内容时才显示气泡) */}
|
{/* 消息内容(有内容时才显示气泡) */}
|
||||||
{(msg.content || (msg.role === 'assistant' && isStreaming && index === messages.length - 1)) && (
|
{(msg.content || (msg.role === 'assistant' && isStreaming && index === messages.length - 1)) && (
|
||||||
<div className={`message-bubble ${msg.role}`}>
|
<div className={`message-bubble ${msg.role}`}>
|
||||||
{msg.content}
|
{msg.role === 'assistant' ? (
|
||||||
|
<MarkdownContent content={msg.content || ''} />
|
||||||
|
) : (
|
||||||
|
msg.content
|
||||||
|
)}
|
||||||
{msg.role === 'assistant' && isStreaming && index === messages.length - 1 && (
|
{msg.role === 'assistant' && isStreaming && index === messages.length - 1 && (
|
||||||
<span className="typing-cursor">▊</span>
|
<span className="typing-cursor">▊</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -946,3 +946,93 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================ */
|
||||||
|
/* MarkdownContent 组件样式 */
|
||||||
|
/* ============================================ */
|
||||||
|
.message-bubble .markdown-content {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble .markdown-content p {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble .markdown-content p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble .markdown-content .md-divider {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #E5E7EB;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble .markdown-content h1,
|
||||||
|
.message-bubble .markdown-content h2,
|
||||||
|
.message-bubble .markdown-content h3 {
|
||||||
|
margin: 16px 0 8px 0;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble .markdown-content h1:first-child,
|
||||||
|
.message-bubble .markdown-content h2:first-child,
|
||||||
|
.message-bubble .markdown-content h3:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble .markdown-content h1 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble .markdown-content h2 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble .markdown-content h3 {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble .markdown-content ul,
|
||||||
|
.message-bubble .markdown-content ol {
|
||||||
|
margin: 8px 0 12px 0;
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble .markdown-content ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble .markdown-content ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble .markdown-content li {
|
||||||
|
margin: 6px 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble .markdown-content li::marker {
|
||||||
|
color: #4F6EF2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble .markdown-content strong {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble .markdown-content em {
|
||||||
|
font-style: italic;
|
||||||
|
color: #4B5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble .markdown-content code {
|
||||||
|
background: rgba(79, 110, 242, 0.1);
|
||||||
|
color: #4F6EF2;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
423
frontend-v2/src/pages/admin/ActivityLogsPage.tsx
Normal file
423
frontend-v2/src/pages/admin/ActivityLogsPage.tsx
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
/**
|
||||||
|
* 运营日志页面
|
||||||
|
*
|
||||||
|
* 提供完整的运营日志查看、筛选、搜索功能
|
||||||
|
*
|
||||||
|
* @date 2026-01-28
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useCallback } from 'react'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
DatePicker,
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
Tooltip,
|
||||||
|
Modal,
|
||||||
|
Typography,
|
||||||
|
} from 'antd'
|
||||||
|
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
MessageOutlined,
|
||||||
|
BookOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
LoginOutlined,
|
||||||
|
CloudUploadOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
FilterOutlined,
|
||||||
|
ClearOutlined,
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import type { Dayjs } from 'dayjs'
|
||||||
|
import { fetchActivityLogs, type ActivityLog } from './api/activityApi'
|
||||||
|
|
||||||
|
const { RangePicker } = DatePicker
|
||||||
|
const { Text, Paragraph } = Typography
|
||||||
|
|
||||||
|
// ==================== 常量映射 ====================
|
||||||
|
|
||||||
|
const MODULE_OPTIONS = [
|
||||||
|
{ label: '全部模块', value: '' },
|
||||||
|
{ label: '系统', value: 'SYSTEM' },
|
||||||
|
{ label: 'AI问答', value: 'AIA' },
|
||||||
|
{ label: '个人知识库', value: 'PKB' },
|
||||||
|
{ label: '智能文献', value: 'ASL' },
|
||||||
|
{ label: '数据清洗', value: 'DC' },
|
||||||
|
{ label: '智能循证', value: 'RVW' },
|
||||||
|
{ label: '智能IIT', value: 'IIT' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const ACTION_OPTIONS = [
|
||||||
|
{ label: '全部动作', value: '' },
|
||||||
|
{ label: '登录', value: 'login' },
|
||||||
|
{ label: '登出', value: 'logout' },
|
||||||
|
{ label: '对话', value: 'chat' },
|
||||||
|
{ label: '上传', value: 'upload' },
|
||||||
|
{ label: '导出', value: 'export' },
|
||||||
|
{ label: '创建', value: 'create' },
|
||||||
|
{ label: '删除', value: 'delete' },
|
||||||
|
{ label: '查看', value: 'view' },
|
||||||
|
]
|
||||||
|
|
||||||
|
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 />,
|
||||||
|
'logout': <LoginOutlined />,
|
||||||
|
'chat': <MessageOutlined />,
|
||||||
|
'upload': <CloudUploadOutlined />,
|
||||||
|
'delete': <DeleteOutlined />,
|
||||||
|
'export': <FileTextOutlined />,
|
||||||
|
'create': <FileTextOutlined />,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 组件 ====================
|
||||||
|
|
||||||
|
export default function ActivityLogsPage() {
|
||||||
|
// 筛选状态
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [pageSize, setPageSize] = useState(20)
|
||||||
|
const [dateRange, setDateRange] = useState<[Dayjs | null, Dayjs | null] | null>(null)
|
||||||
|
const [moduleFilter, setModuleFilter] = useState<string>('')
|
||||||
|
const [actionFilter, setActionFilter] = useState<string>('')
|
||||||
|
const [keyword, setKeyword] = useState<string>('')
|
||||||
|
const [searchKeyword, setSearchKeyword] = useState<string>('')
|
||||||
|
|
||||||
|
// 详情弹窗
|
||||||
|
const [detailVisible, setDetailVisible] = useState(false)
|
||||||
|
const [selectedLog, setSelectedLog] = useState<ActivityLog | null>(null)
|
||||||
|
|
||||||
|
// 构建查询参数
|
||||||
|
const queryParams = {
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
startDate: dateRange?.[0]?.format('YYYY-MM-DD'),
|
||||||
|
endDate: dateRange?.[1]?.format('YYYY-MM-DD'),
|
||||||
|
module: moduleFilter || undefined,
|
||||||
|
action: actionFilter || undefined,
|
||||||
|
keyword: searchKeyword || undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
const { data, isLoading, refetch } = useQuery({
|
||||||
|
queryKey: ['activityLogs', queryParams],
|
||||||
|
queryFn: () => fetchActivityLogs(queryParams),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理分页变化
|
||||||
|
const handleTableChange = useCallback((pagination: TablePaginationConfig) => {
|
||||||
|
setPage(pagination.current || 1)
|
||||||
|
setPageSize(pagination.pageSize || 20)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 处理搜索
|
||||||
|
const handleSearch = useCallback(() => {
|
||||||
|
setSearchKeyword(keyword)
|
||||||
|
setPage(1) // 重置到第一页
|
||||||
|
}, [keyword])
|
||||||
|
|
||||||
|
// 清空筛选
|
||||||
|
const handleClearFilters = useCallback(() => {
|
||||||
|
setDateRange(null)
|
||||||
|
setModuleFilter('')
|
||||||
|
setActionFilter('')
|
||||||
|
setKeyword('')
|
||||||
|
setSearchKeyword('')
|
||||||
|
setPage(1)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 显示详情
|
||||||
|
const handleShowDetail = useCallback((log: ActivityLog) => {
|
||||||
|
setSelectedLog(log)
|
||||||
|
setDetailVisible(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns: ColumnsType<ActivityLog> = [
|
||||||
|
{
|
||||||
|
title: '时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
key: 'createdAt',
|
||||||
|
width: 170,
|
||||||
|
render: (val: string) => (
|
||||||
|
<Text style={{ fontSize: 13 }}>
|
||||||
|
{dayjs(val).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '租户',
|
||||||
|
dataIndex: 'tenantName',
|
||||||
|
key: 'tenantName',
|
||||||
|
width: 120,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (val: string | null) => (
|
||||||
|
<Tooltip title={val}>
|
||||||
|
<Text style={{ fontSize: 13 }}>{val || '-'}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户',
|
||||||
|
dataIndex: 'userName',
|
||||||
|
key: 'userName',
|
||||||
|
width: 100,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (val: string | null) => (
|
||||||
|
<Tooltip title={val}>
|
||||||
|
<Text style={{ fontSize: 13 }}>{val || '-'}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '模块',
|
||||||
|
dataIndex: 'module',
|
||||||
|
key: 'module',
|
||||||
|
width: 100,
|
||||||
|
render: (val: string) => (
|
||||||
|
<Tag
|
||||||
|
icon={MODULE_ICONS[val]}
|
||||||
|
color={MODULE_COLORS[val] || 'default'}
|
||||||
|
>
|
||||||
|
{val}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '功能',
|
||||||
|
dataIndex: 'feature',
|
||||||
|
key: 'feature',
|
||||||
|
width: 120,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (val: string) => (
|
||||||
|
<Tooltip title={val}>
|
||||||
|
<Text style={{ fontSize: 13 }}>{val}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '动作',
|
||||||
|
dataIndex: 'action',
|
||||||
|
key: 'action',
|
||||||
|
width: 80,
|
||||||
|
render: (val: string) => (
|
||||||
|
<Space size={4}>
|
||||||
|
{ACTION_ICONS[val]}
|
||||||
|
<span style={{ fontSize: 13 }}>{val}</span>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '详情',
|
||||||
|
dataIndex: 'info',
|
||||||
|
key: 'info',
|
||||||
|
ellipsis: true,
|
||||||
|
render: (val: string | null) => (
|
||||||
|
<Tooltip title={val} placement="topLeft">
|
||||||
|
<Text
|
||||||
|
style={{ fontSize: 13, color: '#666' }}
|
||||||
|
ellipsis
|
||||||
|
>
|
||||||
|
{val || '-'}
|
||||||
|
</Text>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'actions',
|
||||||
|
width: 70,
|
||||||
|
fixed: 'right',
|
||||||
|
render: (_, record) => (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={() => handleShowDetail(record)}
|
||||||
|
>
|
||||||
|
查看
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
{/* 页面标题 */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800">运营日志</h2>
|
||||||
|
<p className="text-gray-500 text-sm mt-1">查看系统所有用户操作记录</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 筛选区域 */}
|
||||||
|
<Card className="mb-4" size="small">
|
||||||
|
<div className="flex flex-wrap gap-3 items-center">
|
||||||
|
<Space>
|
||||||
|
<FilterOutlined />
|
||||||
|
<span className="text-gray-600">筛选:</span>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<RangePicker
|
||||||
|
value={dateRange}
|
||||||
|
onChange={(dates) => {
|
||||||
|
setDateRange(dates as [Dayjs | null, Dayjs | null])
|
||||||
|
setPage(1)
|
||||||
|
}}
|
||||||
|
placeholder={['开始日期', '结束日期']}
|
||||||
|
style={{ width: 240 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={moduleFilter}
|
||||||
|
onChange={(val) => {
|
||||||
|
setModuleFilter(val)
|
||||||
|
setPage(1)
|
||||||
|
}}
|
||||||
|
options={MODULE_OPTIONS}
|
||||||
|
style={{ width: 130 }}
|
||||||
|
placeholder="选择模块"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={actionFilter}
|
||||||
|
onChange={(val) => {
|
||||||
|
setActionFilter(val)
|
||||||
|
setPage(1)
|
||||||
|
}}
|
||||||
|
options={ACTION_OPTIONS}
|
||||||
|
style={{ width: 120 }}
|
||||||
|
placeholder="选择动作"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input.Search
|
||||||
|
value={keyword}
|
||||||
|
onChange={(e) => setKeyword(e.target.value)}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
placeholder="搜索用户/租户名称"
|
||||||
|
style={{ width: 200 }}
|
||||||
|
enterButton={<SearchOutlined />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
icon={<ClearOutlined />}
|
||||||
|
onClick={handleClearFilters}
|
||||||
|
>
|
||||||
|
清空筛选
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={() => refetch()}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 数据表格 */}
|
||||||
|
<Card>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data?.data || []}
|
||||||
|
rowKey="id"
|
||||||
|
loading={isLoading}
|
||||||
|
scroll={{ x: 1100 }}
|
||||||
|
pagination={{
|
||||||
|
current: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
total: data?.pagination.total || 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total) => `共 ${total} 条记录`,
|
||||||
|
pageSizeOptions: ['10', '20', '50', '100'],
|
||||||
|
}}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
size="middle"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 详情弹窗 */}
|
||||||
|
<Modal
|
||||||
|
title="日志详情"
|
||||||
|
open={detailVisible}
|
||||||
|
onCancel={() => setDetailVisible(false)}
|
||||||
|
footer={[
|
||||||
|
<Button key="close" onClick={() => setDetailVisible(false)}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
{selectedLog && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex">
|
||||||
|
<span className="w-20 text-gray-500">时间:</span>
|
||||||
|
<span>{dayjs(selectedLog.createdAt).format('YYYY-MM-DD HH:mm:ss')}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="w-20 text-gray-500">租户:</span>
|
||||||
|
<span>{selectedLog.tenantName || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="w-20 text-gray-500">用户:</span>
|
||||||
|
<span>{selectedLog.userName || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="w-20 text-gray-500">模块:</span>
|
||||||
|
<Tag
|
||||||
|
icon={MODULE_ICONS[selectedLog.module]}
|
||||||
|
color={MODULE_COLORS[selectedLog.module] || 'default'}
|
||||||
|
>
|
||||||
|
{selectedLog.module}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="w-20 text-gray-500">功能:</span>
|
||||||
|
<span>{selectedLog.feature}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="w-20 text-gray-500">动作:</span>
|
||||||
|
<span>{selectedLog.action}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="w-20 text-gray-500">详情:</span>
|
||||||
|
<Paragraph
|
||||||
|
style={{ margin: 0, flex: 1 }}
|
||||||
|
ellipsis={{ rows: 5, expandable: true }}
|
||||||
|
>
|
||||||
|
{selectedLog.info || '-'}
|
||||||
|
</Paragraph>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
73
frontend-v2/src/pages/admin/api/activityApi.ts
Normal file
73
frontend-v2/src/pages/admin/api/activityApi.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* 运营日志 API
|
||||||
|
*
|
||||||
|
* @date 2026-01-28
|
||||||
|
*/
|
||||||
|
|
||||||
|
import apiClient from '../../../common/api/axios'
|
||||||
|
|
||||||
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
|
export interface ActivityLog {
|
||||||
|
id: string
|
||||||
|
createdAt: string
|
||||||
|
tenantId: string
|
||||||
|
tenantName: string | null
|
||||||
|
userId: string
|
||||||
|
userName: string | null
|
||||||
|
module: string
|
||||||
|
feature: string
|
||||||
|
action: string
|
||||||
|
info: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Pagination {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
total: number
|
||||||
|
totalPages: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActivityLogsResponse {
|
||||||
|
success: boolean
|
||||||
|
data: ActivityLog[]
|
||||||
|
pagination: Pagination
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActivityLogsParams {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
module?: string
|
||||||
|
action?: string
|
||||||
|
keyword?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== API 函数 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取运营日志列表(分页)
|
||||||
|
*/
|
||||||
|
export async function fetchActivityLogs(params: ActivityLogsParams = {}): Promise<{
|
||||||
|
data: ActivityLog[]
|
||||||
|
pagination: Pagination
|
||||||
|
}> {
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
|
||||||
|
if (params.page) queryParams.append('page', String(params.page))
|
||||||
|
if (params.pageSize) queryParams.append('pageSize', String(params.pageSize))
|
||||||
|
if (params.startDate) queryParams.append('startDate', params.startDate)
|
||||||
|
if (params.endDate) queryParams.append('endDate', params.endDate)
|
||||||
|
if (params.module) queryParams.append('module', params.module)
|
||||||
|
if (params.action) queryParams.append('action', params.action)
|
||||||
|
if (params.keyword) queryParams.append('keyword', params.keyword)
|
||||||
|
|
||||||
|
const url = `/api/admin/stats/logs${queryParams.toString() ? '?' + queryParams.toString() : ''}`
|
||||||
|
const res = await apiClient.get<ActivityLogsResponse>(url)
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: res.data.data,
|
||||||
|
pagination: res.data.pagination,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user