- {msg.content}
+ {msg.role === 'assistant' ? (
+
+ ) : (
+ msg.content
+ )}
{msg.role === 'assistant' && isStreaming && index === messages.length - 1 && (
▊
)}
diff --git a/frontend-v2/src/modules/aia/styles/chat-workspace.css b/frontend-v2/src/modules/aia/styles/chat-workspace.css
index 1cfb78af..6e50bea2 100644
--- a/frontend-v2/src/modules/aia/styles/chat-workspace.css
+++ b/frontend-v2/src/modules/aia/styles/chat-workspace.css
@@ -946,3 +946,93 @@
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;
+}
+
diff --git a/frontend-v2/src/pages/admin/ActivityLogsPage.tsx b/frontend-v2/src/pages/admin/ActivityLogsPage.tsx
new file mode 100644
index 00000000..1c9995a5
--- /dev/null
+++ b/frontend-v2/src/pages/admin/ActivityLogsPage.tsx
@@ -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
= {
+ 'SYSTEM': ,
+ 'AIA': ,
+ 'PKB': ,
+ 'ASL': ,
+ 'DC': ,
+ 'RVW': ,
+ 'IIT': ,
+}
+
+const MODULE_COLORS: Record = {
+ 'SYSTEM': '#8c8c8c',
+ 'AIA': '#1890ff',
+ 'PKB': '#52c41a',
+ 'ASL': '#722ed1',
+ 'DC': '#fa8c16',
+ 'RVW': '#eb2f96',
+ 'IIT': '#13c2c2',
+}
+
+const ACTION_ICONS: Record = {
+ 'login': ,
+ 'logout': ,
+ 'chat': ,
+ 'upload': ,
+ 'delete': ,
+ 'export': ,
+ 'create': ,
+}
+
+// ==================== 组件 ====================
+
+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('')
+ const [actionFilter, setActionFilter] = useState('')
+ const [keyword, setKeyword] = useState('')
+ const [searchKeyword, setSearchKeyword] = useState('')
+
+ // 详情弹窗
+ const [detailVisible, setDetailVisible] = useState(false)
+ const [selectedLog, setSelectedLog] = useState(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 = [
+ {
+ title: '时间',
+ dataIndex: 'createdAt',
+ key: 'createdAt',
+ width: 170,
+ render: (val: string) => (
+
+ {dayjs(val).format('YYYY-MM-DD HH:mm:ss')}
+
+ ),
+ },
+ {
+ title: '租户',
+ dataIndex: 'tenantName',
+ key: 'tenantName',
+ width: 120,
+ ellipsis: true,
+ render: (val: string | null) => (
+
+ {val || '-'}
+
+ ),
+ },
+ {
+ title: '用户',
+ dataIndex: 'userName',
+ key: 'userName',
+ width: 100,
+ ellipsis: true,
+ render: (val: string | null) => (
+
+ {val || '-'}
+
+ ),
+ },
+ {
+ title: '模块',
+ dataIndex: 'module',
+ key: 'module',
+ width: 100,
+ render: (val: string) => (
+
+ {val}
+
+ ),
+ },
+ {
+ title: '功能',
+ dataIndex: 'feature',
+ key: 'feature',
+ width: 120,
+ ellipsis: true,
+ render: (val: string) => (
+
+ {val}
+
+ ),
+ },
+ {
+ title: '动作',
+ dataIndex: 'action',
+ key: 'action',
+ width: 80,
+ render: (val: string) => (
+
+ {ACTION_ICONS[val]}
+ {val}
+
+ ),
+ },
+ {
+ title: '详情',
+ dataIndex: 'info',
+ key: 'info',
+ ellipsis: true,
+ render: (val: string | null) => (
+
+
+ {val || '-'}
+
+
+ ),
+ },
+ {
+ title: '操作',
+ key: 'actions',
+ width: 70,
+ fixed: 'right',
+ render: (_, record) => (
+ }
+ onClick={() => handleShowDetail(record)}
+ >
+ 查看
+
+ ),
+ },
+ ]
+
+ return (
+
+ {/* 页面标题 */}
+
+
+ {/* 筛选区域 */}
+
+
+
+
+ 筛选:
+
+
+ {
+ setDateRange(dates as [Dayjs | null, Dayjs | null])
+ setPage(1)
+ }}
+ placeholder={['开始日期', '结束日期']}
+ style={{ width: 240 }}
+ />
+
+
+
+
+ {/* 数据表格 */}
+
+ `共 ${total} 条记录`,
+ pageSizeOptions: ['10', '20', '50', '100'],
+ }}
+ onChange={handleTableChange}
+ size="middle"
+ />
+
+
+ {/* 详情弹窗 */}
+ setDetailVisible(false)}
+ footer={[
+
+ ]}
+ width={600}
+ >
+ {selectedLog && (
+
+
+ 时间:
+ {dayjs(selectedLog.createdAt).format('YYYY-MM-DD HH:mm:ss')}
+
+
+ 租户:
+ {selectedLog.tenantName || '-'}
+
+
+ 用户:
+ {selectedLog.userName || '-'}
+
+
+ 模块:
+
+ {selectedLog.module}
+
+
+
+ 功能:
+ {selectedLog.feature}
+
+
+ 动作:
+ {selectedLog.action}
+
+
+
详情:
+
+ {selectedLog.info || '-'}
+
+
+
+ )}
+
+
+ )
+}
diff --git a/frontend-v2/src/pages/admin/api/activityApi.ts b/frontend-v2/src/pages/admin/api/activityApi.ts
new file mode 100644
index 00000000..40cc6a2e
--- /dev/null
+++ b/frontend-v2/src/pages/admin/api/activityApi.ts
@@ -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(url)
+
+ return {
+ data: res.data.data,
+ pagination: res.data.pagination,
+ }
+}