feat: M2-04 admin — ImportMonitor page with detail drawer
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 9s
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 9s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
0b0612760c
commit
237913468c
@ -29,6 +29,7 @@ const AuditLogPage = lazy(() => import("./pages/AuditLog"))
|
|||||||
const Dashboard = lazy(() => import('./pages/Dashboard'))
|
const Dashboard = lazy(() => import('./pages/Dashboard'))
|
||||||
const UserManagement = lazy(() => import('./pages/UserManagement'))
|
const UserManagement = lazy(() => import('./pages/UserManagement'))
|
||||||
const MemberManagement = lazy(() => import('./pages/MemberManagement'))
|
const MemberManagement = lazy(() => import('./pages/MemberManagement'))
|
||||||
|
const ImportMonitorPage = lazy(() => import('./pages/ImportMonitor'))
|
||||||
const HermesSettings = lazy(() => import('./pages/HermesSettings'))
|
const HermesSettings = lazy(() => import('./pages/HermesSettings'))
|
||||||
const TaskAssistant = lazy(() => import('./pages/TaskAssistant'))
|
const TaskAssistant = lazy(() => import('./pages/TaskAssistant'))
|
||||||
const Placeholder = lazy(() => import('./pages/Placeholder'))
|
const Placeholder = lazy(() => import('./pages/Placeholder'))
|
||||||
@ -99,7 +100,7 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
<Route path="knowledge/bases" element={<Suspense fallback={<PageLoading />}><KnowledgeBasesPage /></Suspense>} />
|
<Route path="knowledge/bases" element={<Suspense fallback={<PageLoading />}><KnowledgeBasesPage /></Suspense>} />
|
||||||
<Route path="knowledge/sources" element={<Placeholder title="知识源列表" />} />
|
<Route path="knowledge/sources" element={<Placeholder title="知识源列表" />} />
|
||||||
<Route path="imports" element={<Placeholder title="文档导入" />} />
|
<Route path="imports" element={<Suspense fallback={<PageLoading />}><ImportMonitorPage /></Suspense>} />
|
||||||
<Route path="files" element={<Suspense fallback={<PageLoading />}><FilesAdminPage /></Suspense>} />
|
<Route path="files" element={<Suspense fallback={<PageLoading />}><FilesAdminPage /></Suspense>} />
|
||||||
<Route
|
<Route
|
||||||
path="settings"
|
path="settings"
|
||||||
|
|||||||
89
src/pages/ImportMonitor.tsx
Normal file
89
src/pages/ImportMonitor.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { Table, Tag, Typography, Button, App, Drawer, Space } from 'antd'
|
||||||
|
import { ReloadOutlined, RetweetOutlined, ImportOutlined, EyeOutlined } from '@ant-design/icons'
|
||||||
|
import { api } from '@/services/http-client'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
const { Title, Text } = Typography
|
||||||
|
|
||||||
|
export default function ImportMonitorPage() {
|
||||||
|
const { message } = App.useApp()
|
||||||
|
const qc = useQueryClient()
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||||
|
const [selectedJob, setSelectedJob] = useState<any>(null)
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ['admin-imports'],
|
||||||
|
queryFn: (): Promise<any> => api.get('/admin-api/imports?limit=50'),
|
||||||
|
staleTime: 10_000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: detail } = useQuery({
|
||||||
|
queryKey: ['admin-imports', selectedJob?.id],
|
||||||
|
queryFn: (): Promise<any> => api.get(`/admin-api/imports/${selectedJob?.id}`),
|
||||||
|
enabled: !!selectedJob?.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleRetry = async (id: string) => {
|
||||||
|
await api.post(`/admin-api/imports/${id}/retry`)
|
||||||
|
message.success('已重新入队')
|
||||||
|
qc.invalidateQueries({ queryKey: ['admin-imports'] })
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusColor: Record<string, string> = {
|
||||||
|
QUEUED: 'blue', PROCESSING: 'processing', COMPLETED: 'green', FAILED: 'red', CANCELLED: 'default',
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: '名称', dataIndex: 'sourceName', width: 180, ellipsis: true },
|
||||||
|
{ title: '类型', dataIndex: 'sourceType', width: 80 },
|
||||||
|
{ title: '状态', dataIndex: 'status', width: 90, render: (s: string) => <Tag color={statusColor[s] || 'default'}>{s}</Tag> },
|
||||||
|
{ title: '进度', dataIndex: 'progress', width: 70, render: (v: number) => `${v || 0}%` },
|
||||||
|
{ title: '重试', dataIndex: 'retryCount', width: 60, align: 'center' as const },
|
||||||
|
{ title: '步骤', dataIndex: 'step', width: 100, ellipsis: true },
|
||||||
|
{ title: '错误', dataIndex: 'errorMessage', width: 150, ellipsis: true, render: (v: string) => v ? <Text type="danger" style={{ fontSize: 12 }}>{v}</Text> : '-' },
|
||||||
|
{ title: '时间', dataIndex: 'createdAt', width: 130, render: (d: string) => dayjs(d).format('MM-DD HH:mm') },
|
||||||
|
{
|
||||||
|
title: '操作', width: 100,
|
||||||
|
render: (_: any, r: any) => (
|
||||||
|
<Space size="small">
|
||||||
|
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => { setSelectedJob(r); setDrawerOpen(true) }}>详情</Button>
|
||||||
|
{r.status === 'FAILED' && <Button type="link" size="small" icon={<RetweetOutlined />} onClick={() => handleRetry(r.id)}>重试</Button>}
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const stepCols = [
|
||||||
|
{ title: '步骤', dataIndex: 'step', width: 120 },
|
||||||
|
{ title: '状态', dataIndex: 'status', width: 80, render: (s: string) => <Tag color={s === 'completed' ? 'green' : s === 'failed' ? 'red' : 'blue'}>{s}</Tag> },
|
||||||
|
{ title: '详情', dataIndex: 'detail', ellipsis: true },
|
||||||
|
{ title: '开始', dataIndex: 'startedAt', width: 130, render: (d: string) => d ? dayjs(d).format('HH:mm:ss') : '-' },
|
||||||
|
{ title: '完成', dataIndex: 'completedAt', width: 130, render: (d: string) => d ? dayjs(d).format('HH:mm:ss') : '-' },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||||
|
<Title level={5} style={{ margin: 0 }}><ImportOutlined /> 文档导入</Title>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={() => qc.invalidateQueries({ queryKey: ['admin-imports'] })}>刷新</Button>
|
||||||
|
</div>
|
||||||
|
<Table dataSource={data?.items || []} columns={columns} rowKey="id" loading={isLoading} pagination={{ pageSize: 20 }} size="small" />
|
||||||
|
|
||||||
|
<Drawer title={selectedJob?.sourceName || '导入详情'} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={700}>
|
||||||
|
{detail && (
|
||||||
|
<>
|
||||||
|
<Title level={5} style={{ fontSize: 14 }}>Job: {detail.job?.id}</Title>
|
||||||
|
<Text>状态: <Tag color={statusColor[detail.job?.status]}>{detail.job?.status}</Tag></Text>
|
||||||
|
<Text style={{ marginLeft: 16 }}>进度: {detail.job?.progress || 0}%</Text>
|
||||||
|
<Text style={{ marginLeft: 16 }}>重试: {detail.job?.retryCount}/{detail.job?.maxRetries}</Text>
|
||||||
|
{detail.job?.errorMessage && <div style={{ marginTop: 8 }}><Text type="danger">{detail.job?.errorMessage}</Text></div>}
|
||||||
|
<Title level={5} style={{ fontSize: 14, marginTop: 16 }}>步骤日志 ({detail.steps?.length || 0})</Title>
|
||||||
|
<Table dataSource={detail.steps || []} columns={stepCols} rowKey="id" pagination={false} size="small" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user