feat(intelligent-eval): v4.2 UI — stat bar + single-line table + unified detail drawer
All checks were successful
CI / test (push) Successful in 3m58s
All checks were successful
CI / test (push) Successful in 3m58s
评估列表页(v4.2): - 顶部状态统计条(stat-chip 点击筛选,后端列表接口新增 stats 状态分布 + status 筛选参数) - 单行表格(名称/评测对象 ellipsis 省略)+ 会话进度迷你进度条 + 最后一列醒目查看按钮 + 整行可点击 - 详情/报告合一单层抽屉(1000px,Tabs:概览/决策过程/配置历史/报告), 审批/取消在抽屉头部;移除独立详情页 /intelligent-evals/detail 与 intelligentEvalNav store 任务队列页: - 与列表页统一 stat-chip 统计条 + 单行表格(评估状态小标、失败原因 tooltip) 清理:删除 IntelligentEvalDetail.tsx / intelligentEvalNav.ts;App.tsx 移除 detail 路由。任务队列保持独立菜单页。tsc 0 错误 vitest 19 passed 895 passed
This commit is contained in:
parent
a69fa8a797
commit
00ad929d68
@ -10,7 +10,7 @@
|
|||||||
非法转换抛 IntelligentEvalTransitionError,路由层映射为 409。
|
非法转换抛 IntelligentEvalTransitionError,路由层映射为 409。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
|
|
||||||
@ -218,10 +218,17 @@ def list_evals(session: Session) -> list[IntelligentEval]:
|
|||||||
return IntelligentEvalRepository(session).list_all()
|
return IntelligentEvalRepository(session).list_all()
|
||||||
|
|
||||||
|
|
||||||
def list_evals_page(session: Session, offset: int, limit: int) -> tuple[list[IntelligentEval], int]:
|
def list_evals_page(
|
||||||
"""Return one page of evals (newest first) plus the total count."""
|
session: Session, offset: int, limit: int, status: Optional[str] = None
|
||||||
|
) -> tuple[list[IntelligentEval], int, dict[str, int]]:
|
||||||
|
"""Return one page of evals (newest first), total count, and per-status counts."""
|
||||||
repo = IntelligentEvalRepository(session)
|
repo = IntelligentEvalRepository(session)
|
||||||
return repo.list_page(offset, limit), repo.count()
|
return repo.list_page(offset, limit, status), repo.count(), repo.count_by_status()
|
||||||
|
|
||||||
|
|
||||||
|
def eval_status_counts(session: Session) -> dict[str, int]:
|
||||||
|
"""Count evaluations per status (for the list page stat bar)."""
|
||||||
|
return IntelligentEvalRepository(session).count_by_status()
|
||||||
|
|
||||||
|
|
||||||
def _get_session_or_raise(repo: IntelligentEvalSessionRepository, session_id: str) -> IntelligentEvalSession:
|
def _get_session_or_raise(repo: IntelligentEvalSessionRepository, session_id: str) -> IntelligentEvalSession:
|
||||||
|
|||||||
@ -101,20 +101,32 @@ class IntelligentEvalRepository:
|
|||||||
statement = select(IntelligentEvalDB).order_by(IntelligentEvalDB.created_at.desc())
|
statement = select(IntelligentEvalDB).order_by(IntelligentEvalDB.created_at.desc())
|
||||||
return [self._from_db(r) for r in self.session.exec(statement).all()]
|
return [self._from_db(r) for r in self.session.exec(statement).all()]
|
||||||
|
|
||||||
def list_page(self, offset: int, limit: int) -> list[IntelligentEval]:
|
def list_page(
|
||||||
"""Return one page of evals (created_at desc) with the same ordering as ``list_all``."""
|
self, offset: int, limit: int, status: Optional[str] = None
|
||||||
statement = (
|
) -> list[IntelligentEval]:
|
||||||
select(IntelligentEvalDB)
|
"""Return one page of evals (created_at desc), optionally filtered by status."""
|
||||||
.order_by(IntelligentEvalDB.created_at.desc())
|
statement = select(IntelligentEvalDB).order_by(IntelligentEvalDB.created_at.desc())
|
||||||
.offset(offset)
|
if status:
|
||||||
.limit(limit)
|
statement = statement.where(IntelligentEvalDB.status == status)
|
||||||
)
|
statement = statement.offset(offset).limit(limit)
|
||||||
return [self._from_db(r) for r in self.session.exec(statement).all()]
|
return [self._from_db(r) for r in self.session.exec(statement).all()]
|
||||||
|
|
||||||
def count(self) -> int:
|
def count(self) -> int:
|
||||||
"""Total number of evaluations (for pagination metadata)."""
|
"""Total number of evaluations (for pagination metadata)."""
|
||||||
return self.session.exec(select(func.count()).select_from(IntelligentEvalDB)).one()
|
return self.session.exec(select(func.count()).select_from(IntelligentEvalDB)).one()
|
||||||
|
|
||||||
|
def count_by_status(self) -> dict[str, int]:
|
||||||
|
"""Count evaluations per status (for the list page stat bar)."""
|
||||||
|
rows = self.session.exec(
|
||||||
|
select(IntelligentEvalDB.status, func.count(IntelligentEvalDB.id)).group_by(
|
||||||
|
IntelligentEvalDB.status
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
stats = {status.value: 0 for status in IntelligentEvalStatus}
|
||||||
|
for status, cnt in rows:
|
||||||
|
stats[status] = cnt
|
||||||
|
return stats
|
||||||
|
|
||||||
def get(self, eval_id: str) -> Optional[IntelligentEval]:
|
def get(self, eval_id: str) -> Optional[IntelligentEval]:
|
||||||
db = self.session.get(IntelligentEvalDB, eval_id)
|
db = self.session.get(IntelligentEvalDB, eval_id)
|
||||||
return self._from_db(db) if db else None
|
return self._from_db(db) if db else None
|
||||||
|
|||||||
@ -97,12 +97,14 @@ async def create_eval(request: CreateEvalRequest, session: Session = Depends(get
|
|||||||
async def list_evals(
|
async def list_evals(
|
||||||
page: int | None = None,
|
page: int | None = None,
|
||||||
page_size: int = 20,
|
page_size: int = 20,
|
||||||
|
status: str | None = None,
|
||||||
session: Session = Depends(get_db),
|
session: Session = Depends(get_db),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""List intelligent evaluations, optionally paginated.
|
"""List intelligent evaluations, optionally paginated and filtered by status.
|
||||||
|
|
||||||
不传 ``page`` 时返回全部(向后兼容);传 ``page``(从 1 起)时按
|
不传 ``page`` 时返回全部(向后兼容);传 ``page``(从 1 起)时按
|
||||||
``created_at`` 倒序分页,返回 ``total`` 供前端服务端分页。
|
``created_at`` 倒序分页,返回 ``total`` + ``stats``(各状态计数)供前端
|
||||||
|
服务端分页与状态统计条。
|
||||||
"""
|
"""
|
||||||
reader = IntelligentEvalReadModel(session)
|
reader = IntelligentEvalReadModel(session)
|
||||||
if page is None:
|
if page is None:
|
||||||
@ -110,10 +112,11 @@ async def list_evals(
|
|||||||
return {"intelligent_evals": [item.model_dump(mode="json") for item in reader.list_items(evals)]}
|
return {"intelligent_evals": [item.model_dump(mode="json") for item in reader.list_items(evals)]}
|
||||||
page_size = max(1, min(page_size, 100))
|
page_size = max(1, min(page_size, 100))
|
||||||
offset = (max(1, page) - 1) * page_size
|
offset = (max(1, page) - 1) * page_size
|
||||||
evals, total = lifecycle.list_evals_page(session, offset, page_size)
|
evals, total, stats = lifecycle.list_evals_page(session, offset, page_size, status)
|
||||||
return {
|
return {
|
||||||
"intelligent_evals": [item.model_dump(mode="json") for item in reader.list_items(evals)],
|
"intelligent_evals": [item.model_dump(mode="json") for item in reader.list_items(evals)],
|
||||||
"total": total,
|
"total": total,
|
||||||
|
"stats": stats,
|
||||||
"page": page,
|
"page": page,
|
||||||
"page_size": page_size,
|
"page_size": page_size,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,6 @@ const FilesPage = lazy(() => import('./pages/Files'))
|
|||||||
const ModelConfigsPage = lazy(() => import('./pages/ModelConfigs'))
|
const ModelConfigsPage = lazy(() => import('./pages/ModelConfigs'))
|
||||||
const IntelligentEvalsPage = lazy(() => import('./pages/IntelligentEvals'))
|
const IntelligentEvalsPage = lazy(() => import('./pages/IntelligentEvals'))
|
||||||
const IntelligentEvalTasksPage = lazy(() => import('./pages/IntelligentEvalTasks'))
|
const IntelligentEvalTasksPage = lazy(() => import('./pages/IntelligentEvalTasks'))
|
||||||
const IntelligentEvalDetailPage = lazy(() => import('./pages/IntelligentEvalDetail'))
|
|
||||||
|
|
||||||
function PageLoader({ children }: { children: ReactNode }) {
|
function PageLoader({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
@ -71,7 +70,6 @@ const routeConfigs: RouteConfig[] = [
|
|||||||
{ path: '/reports', name: '评测报告', icon: <BarChartOutlined />, component: () => <PageLoader><ReportsPage /></PageLoader> },
|
{ path: '/reports', name: '评测报告', icon: <BarChartOutlined />, component: () => <PageLoader><ReportsPage /></PageLoader> },
|
||||||
{ path: '/intelligent-evals', name: '智能评估', icon: <BulbOutlined />, component: () => <PageLoader><IntelligentEvalsPage /></PageLoader> },
|
{ path: '/intelligent-evals', name: '智能评估', icon: <BulbOutlined />, component: () => <PageLoader><IntelligentEvalsPage /></PageLoader> },
|
||||||
{ path: '/intelligent-evals/tasks', name: '任务队列', icon: <UnorderedListOutlined />, component: () => <PageLoader><IntelligentEvalTasksPage /></PageLoader> },
|
{ path: '/intelligent-evals/tasks', name: '任务队列', icon: <UnorderedListOutlined />, component: () => <PageLoader><IntelligentEvalTasksPage /></PageLoader> },
|
||||||
{ path: '/intelligent-evals/detail', name: '评估详情', icon: <FileTextOutlined />, component: () => <PageLoader><IntelligentEvalDetailPage /></PageLoader> },
|
|
||||||
{ path: '/models', name: '模型配置', icon: <CloudServerOutlined />, component: () => <PageLoader><ModelConfigsPage /></PageLoader> },
|
{ path: '/models', name: '模型配置', icon: <CloudServerOutlined />, component: () => <PageLoader><ModelConfigsPage /></PageLoader> },
|
||||||
{ path: '/files', name: '原始文件', icon: <FolderOpenOutlined />, component: () => <PageLoader><FilesPage /></PageLoader> },
|
{ path: '/files', name: '原始文件', icon: <FolderOpenOutlined />, component: () => <PageLoader><FilesPage /></PageLoader> },
|
||||||
]
|
]
|
||||||
|
|||||||
@ -785,8 +785,14 @@ export interface TaskQueueList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const intelligentEvalsApi = {
|
export const intelligentEvalsApi = {
|
||||||
list: (params?: { page?: number; page_size?: number }) =>
|
list: (params?: { page?: number; page_size?: number; status?: string }) =>
|
||||||
api.get<{ intelligent_evals: IntelligentEval[]; total?: number; page?: number; page_size?: number }>(
|
api.get<{
|
||||||
|
intelligent_evals: IntelligentEval[]
|
||||||
|
total?: number
|
||||||
|
stats?: Record<string, number>
|
||||||
|
page?: number
|
||||||
|
page_size?: number
|
||||||
|
}>(
|
||||||
'/intelligent-evals',
|
'/intelligent-evals',
|
||||||
{ params },
|
{ params },
|
||||||
),
|
),
|
||||||
|
|||||||
@ -64,7 +64,7 @@ interface EvalOverviewProps {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 评估详情"概览"子视图(纯展示):基本信息、用户输入、粗计划、会话进度。
|
* 评估详情"概览"子视图(纯展示):基本信息、用户输入、粗计划、会话进度。
|
||||||
* 审批/取消等动作由详情页(IntelligentEvalDetail)统一管理。
|
* 审批/取消等动作由评估列表页的详情抽屉头部统一管理。
|
||||||
*/
|
*/
|
||||||
export default function EvalOverview({ ev, targetName }: EvalOverviewProps) {
|
export default function EvalOverview({ ev, targetName }: EvalOverviewProps) {
|
||||||
const meta = EVAL_STATUS[ev.status] ?? { label: ev.status, color: 'default' }
|
const meta = EVAL_STATUS[ev.status] ?? { label: ev.status, color: 'default' }
|
||||||
|
|||||||
@ -1,44 +1,49 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Alert, Button, Empty, Select, Space, Statistic, Table, Tag, Tooltip, message,
|
Button, Empty, Space, Table, Tag, Tooltip, message,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import { ReloadOutlined } from '@ant-design/icons'
|
import { ReloadOutlined } from '@ant-design/icons'
|
||||||
import { intelligentEvalsApi, type TaskQueueItem, type TaskQueueStatus } from '../../api'
|
import { intelligentEvalsApi, type TaskQueueItem, type TaskQueueStatus } from '../../api'
|
||||||
import { usePolling } from '../../hooks/usePolling'
|
import { usePolling } from '../../hooks/usePolling'
|
||||||
import { colors, statusColors } from '../../tokens'
|
import { colors } from '../../tokens'
|
||||||
import { formatDateTime } from '../../utils/date'
|
import { formatDateTime } from '../../utils/date'
|
||||||
|
|
||||||
const STATUS_META: Record<TaskQueueStatus, { label: string; color: string }> = {
|
const STATUS_META: Record<TaskQueueStatus, { label: string; color: string }> = {
|
||||||
pending: { label: '待处理', color: 'orange' },
|
pending: { label: '待认领', color: 'orange' },
|
||||||
assigned: { label: '执行中', color: 'blue' },
|
assigned: { label: '执行中', color: 'blue' },
|
||||||
completed: { label: '已完成', color: 'green' },
|
completed: { label: '已完成', color: 'green' },
|
||||||
failed: { label: '失败', color: 'red' },
|
failed: { label: '失败', color: 'red' },
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_OPTIONS = (Object.keys(STATUS_META) as TaskQueueStatus[]).map((s) => ({
|
const EVAL_STATUS_META: Record<string, { label: string; color: string }> = {
|
||||||
value: s,
|
executing: { label: '执行中', color: 'blue' },
|
||||||
label: STATUS_META[s].label,
|
completed: { label: '已完成', color: 'green' },
|
||||||
}))
|
cancelled: { label: '已取消', color: 'default' },
|
||||||
|
failed: { label: '失败', color: 'red' },
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterKey = 'all' | 'unresolved' | TaskQueueStatus
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 任务队列监控(方案③可视化)。
|
* 任务队列监控(方案③可视化,与评估列表页统一 stat-chip + 单行表格)。
|
||||||
*
|
*
|
||||||
* 方案③的"定时触发"(scan loop 每 60s 扫描入队 + 触发 OpenClaw worker)
|
* 平台每 60s 扫描 executing 评估入队 + 触发 OpenClaw worker 执行。本组件
|
||||||
* 此前只有 Worker 消费端 API,无可查看的列表。这里展示任务队列明细与
|
* 展示任务明细与状态分布,5s 轮询;统计条点击筛选。
|
||||||
* 状态分布,5s 轮询,让平台侧的定时触发对用户可见。
|
|
||||||
*/
|
*/
|
||||||
export default function TaskQueueMonitor() {
|
export default function TaskQueueMonitor() {
|
||||||
const [tasks, setTasks] = useState<TaskQueueItem[] | null>(null)
|
const [tasks, setTasks] = useState<TaskQueueItem[] | null>(null)
|
||||||
const [stats, setStats] = useState<{ pending: number; assigned: number; completed: number; failed: number; unresolved: number } | null>(null)
|
const [stats, setStats] = useState<{
|
||||||
const [statusFilter, setStatusFilter] = useState<TaskQueueStatus | 'all'>('all')
|
pending: number; assigned: number; completed: number; failed: number; unresolved: number
|
||||||
|
} | null>(null)
|
||||||
|
const [filter, setFilter] = useState<FilterKey>('all')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const res = await intelligentEvalsApi.listTasks(
|
const res = await intelligentEvalsApi.listTasks(
|
||||||
statusFilter === 'all' ? undefined : { status: statusFilter },
|
filter === 'all' || filter === 'unresolved' ? undefined : { status: filter },
|
||||||
)
|
)
|
||||||
setTasks(res.data.tasks)
|
setTasks(res.data.tasks)
|
||||||
setStats(res.data.stats)
|
setStats(res.data.stats)
|
||||||
@ -49,111 +54,97 @@ export default function TaskQueueMonitor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial fetch (usePolling owns the 5s interval + visibility pause).
|
// Initial fetch + reload on filter change (usePolling owns the 5s interval).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadData()
|
void loadData()
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [statusFilter])
|
}, [filter])
|
||||||
|
|
||||||
usePolling(() => { void loadData() }, 5000, true)
|
usePolling(() => { void loadData() }, 5000, true)
|
||||||
|
|
||||||
|
const chipMeta: { key: FilterKey; label: string; color?: string; get: () => number }[] = [
|
||||||
|
{ key: 'all', label: '全部任务', get: () => (stats ? Object.values(stats).reduce((a, b) => a + b, 0) : (tasks?.length ?? 0)) },
|
||||||
|
{ key: 'unresolved', label: '待处理', color: colors.warning, get: () => stats?.unresolved ?? 0 },
|
||||||
|
{ key: 'pending', label: '待认领', get: () => stats?.pending ?? 0 },
|
||||||
|
{ key: 'assigned', label: '执行中', color: colors.primary, get: () => stats?.assigned ?? 0 },
|
||||||
|
{ key: 'completed', label: '已完成', color: '#52c41a', get: () => stats?.completed ?? 0 },
|
||||||
|
{ key: 'failed', label: '失败', color: '#ff4d4f', get: () => stats?.failed ?? 0 },
|
||||||
|
]
|
||||||
|
|
||||||
const columns: ColumnsType<TaskQueueItem> = [
|
const columns: ColumnsType<TaskQueueItem> = [
|
||||||
{
|
{
|
||||||
title: '入队时间', dataIndex: 'created_at', key: 'created_at', width: 170,
|
title: '入队时间', dataIndex: 'created_at', key: 'created_at', width: 165,
|
||||||
render: (v: string | null) => (v ? formatDateTime(v) : '—'),
|
render: (v: string | null) => (v ? formatDateTime(v) : '—'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '评估', dataIndex: 'eval_name', key: 'eval_name',
|
title: '评估', key: 'eval', width: 230, ellipsis: true,
|
||||||
render: (name: string | null, t) => (
|
render: (_, t) => (
|
||||||
<Space size={6} direction="vertical" style={{ gap: 2 }}>
|
<Space size={6}>
|
||||||
<span style={{ fontWeight: 500 }}>{name ?? t.eval_id.slice(0, 8)}</span>
|
<span style={{ fontWeight: 500 }}>{t.eval_name ?? t.eval_id.slice(0, 8)}</span>
|
||||||
{t.eval_status && (
|
{t.eval_status && EVAL_STATUS_META[t.eval_status] && (
|
||||||
<span style={{ fontSize: 12, color: colors.textSecondary }}>{t.eval_status}</span>
|
<Tag color={EVAL_STATUS_META[t.eval_status].color} style={{ margin: 0, fontSize: 11 }}>
|
||||||
|
{EVAL_STATUS_META[t.eval_status].label}
|
||||||
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态', dataIndex: 'status', key: 'status', width: 100,
|
title: '队列状态', dataIndex: 'status', key: 'status', width: 96,
|
||||||
render: (s: TaskQueueStatus) => {
|
render: (s: TaskQueueStatus) => {
|
||||||
const meta = STATUS_META[s]
|
const meta = STATUS_META[s]
|
||||||
return <Tag color={meta.color}>{meta.label}</Tag>
|
return <Tag color={meta.color}>{meta.label}</Tag>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '优先级', dataIndex: 'priority', key: 'priority', width: 80,
|
title: '优先级', dataIndex: 'priority', key: 'priority', width: 76,
|
||||||
render: (p: number) => <Tag>{p}</Tag>,
|
render: (p: number) => <Tag>{p}</Tag>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '原因', dataIndex: 'reason', key: 'reason',
|
title: '原因', dataIndex: 'reason', key: 'reason', ellipsis: true,
|
||||||
render: (r: string) => (
|
render: (r: string) => <Tooltip title={r}><span>{r}</span></Tooltip>,
|
||||||
<Tooltip title={r}>
|
|
||||||
<span style={{ display: 'inline-block', maxWidth: 220, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
||||||
{r}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '认领方', dataIndex: 'assigned_cron_id', key: 'assigned_cron_id', width: 140,
|
title: '完成时间', dataIndex: 'completed_at', key: 'completed_at', width: 165,
|
||||||
render: (v: string | null, t) => {
|
|
||||||
if (!v) return <span style={{ color: colors.textSecondary }}>—</span>
|
|
||||||
return (
|
|
||||||
<Tooltip title={t.assigned_at ? `认领于 ${formatDateTime(t.assigned_at)}` : undefined}>
|
|
||||||
<span style={{ fontFamily: 'monospace', fontSize: 12 }}>{v}</span>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '完成时间', dataIndex: 'completed_at', key: 'completed_at', width: 170,
|
|
||||||
render: (v: string | null, t) => {
|
render: (v: string | null, t) => {
|
||||||
if (t.status === 'failed' && t.error) {
|
if (t.status === 'failed' && t.error) {
|
||||||
return <Tooltip title={t.error}><span style={{ color: statusColors.failed }}>失败</span></Tooltip>
|
return <Tooltip title={t.error}><span style={{ color: '#ff4d4f' }}>失败</span></Tooltip>
|
||||||
}
|
}
|
||||||
return v ? formatDateTime(v) : '—'
|
return v ? formatDateTime(v) : <span style={{ color: colors.textMuted }}>—</span>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
|
<div className="stat-bar">
|
||||||
<Space size={16} wrap>
|
{chipMeta.map((c) => (
|
||||||
<Statistic title="待处理" value={stats?.unresolved ?? 0} valueStyle={{ color: colors.warning }} />
|
<div
|
||||||
<Statistic title="待认领" value={stats?.pending ?? 0} />
|
key={c.key}
|
||||||
<Statistic title="执行中" value={stats?.assigned ?? 0} valueStyle={{ color: colors.primary }} />
|
className={`stat-chip ${filter === c.key ? 'active' : ''}`}
|
||||||
<Statistic title="已完成" value={stats?.completed ?? 0} valueStyle={{ color: statusColors.completed }} />
|
onClick={() => setFilter(c.key)}
|
||||||
<Statistic title="失败" value={stats?.failed ?? 0} valueStyle={{ color: statusColors.failed }} />
|
>
|
||||||
</Space>
|
<div className="n" style={{ color: c.color ?? colors.text }}>{c.get()}</div>
|
||||||
<Space>
|
<div className="l">{c.label}</div>
|
||||||
<Select
|
</div>
|
||||||
value={statusFilter}
|
))}
|
||||||
onChange={(v) => setStatusFilter(v)}
|
|
||||||
style={{ width: 110 }}
|
|
||||||
options={[{ value: 'all', label: '全部状态' }, ...STATUS_OPTIONS]}
|
|
||||||
/>
|
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} />
|
|
||||||
</Space>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Alert
|
<div style={{ background: colors.bgContainer, borderRadius: 8, padding: 16, flex: 1, minHeight: 0, overflow: 'hidden' }}>
|
||||||
type="info"
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 12 }}>
|
||||||
showIcon
|
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} />
|
||||||
message="平台每 60 秒扫描 executing 评估并入队,有任务时通过 docker exec 触发 OpenClaw worker 执行(取代外部 Channel 的方案③)。"
|
</div>
|
||||||
style={{ fontSize: 12 }}
|
<div style={{ height: 'calc(100% - 44px)', overflowY: 'auto' }}>
|
||||||
/>
|
<Table
|
||||||
|
rowKey="id"
|
||||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
size="middle"
|
||||||
<Table
|
loading={loading}
|
||||||
rowKey="id"
|
dataSource={tasks ?? []}
|
||||||
size="small"
|
columns={columns}
|
||||||
loading={loading}
|
pagination={(tasks?.length ?? 0) > 10 ? { pageSize: 10, showTotal: (t) => `共 ${t} 条` } : false}
|
||||||
dataSource={tasks ?? []}
|
locale={{ emptyText: <Empty description="暂无任务" /> }}
|
||||||
columns={columns}
|
/>
|
||||||
pagination={(tasks?.length ?? 0) > 50 ? { pageSize: 50, showTotal: (t) => `共 ${t} 条` } : false}
|
</div>
|
||||||
locale={{ emptyText: <Empty description="暂无任务" /> }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -64,6 +64,39 @@ body {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 状态统计条(评估列表 / 任务队列页共用):一排可点击的状态计数卡 */
|
||||||
|
.stat-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.stat-chip {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 110px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.stat-chip:hover {
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
.stat-chip.active {
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: 0 0 0 1px #1677ff inset;
|
||||||
|
}
|
||||||
|
.stat-chip .n {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
.stat-chip .l {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* intelligent-detail-tabs:评估详情页的 Tabs 需承载内容面板(概览/决策过程/
|
* intelligent-detail-tabs:评估详情页的 Tabs 需承载内容面板(概览/决策过程/
|
||||||
* 配置历史/报告)。建立高度链:Tabs 撑满父容器,content-holder 占剩余空间,
|
* 配置历史/报告)。建立高度链:Tabs 撑满父容器,content-holder 占剩余空间,
|
||||||
|
|||||||
@ -1,193 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react'
|
|
||||||
import {
|
|
||||||
Button, Empty, Input, Modal, Popconfirm, Space, Spin, Tabs, Tag, message,
|
|
||||||
} from 'antd'
|
|
||||||
import type { TabsProps } from 'antd'
|
|
||||||
import { StopOutlined } from '@ant-design/icons'
|
|
||||||
import { intelligentEvalsApi, targetsApi, type IntelligentEval } from '../api'
|
|
||||||
import PageWrapper from '../components/PageWrapper'
|
|
||||||
import EvalOverview from '../components/intelligent_eval/EvalOverview'
|
|
||||||
import DecisionProcess from '../components/intelligent_eval/DecisionProcess'
|
|
||||||
import ConfigSnapshots from '../components/intelligent_eval/ConfigSnapshots'
|
|
||||||
import EvalReport from '../components/intelligent_eval/EvalReport'
|
|
||||||
import { EVAL_STATUS } from '../components/intelligent_eval/status'
|
|
||||||
import { useResource } from '../hooks/useResource'
|
|
||||||
import { usePolling } from '../hooks/usePolling'
|
|
||||||
import { useIntelligentEvalNav, type EvalDetailTab } from '../stores/intelligentEvalNav'
|
|
||||||
import { colors } from '../tokens'
|
|
||||||
|
|
||||||
const ACTIVE_STATUSES = new Set(['planning', 'pending_approval', 'executing'])
|
|
||||||
|
|
||||||
function isActive(status: string | undefined): boolean {
|
|
||||||
return status != null && ACTIVE_STATUSES.has(status)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 评估详情独立页(keep-alive 多页模式下的子 tab 页)。
|
|
||||||
*
|
|
||||||
* 由评估列表页点击"详情/报告"进入:选中评估 id 通过 intelligentEvalNav store
|
|
||||||
* 传递,本页读取并加载,用页内 Tabs 承载概览/决策过程/配置历史/报告子视图,
|
|
||||||
* 取代原列表页内的 Drawer 嵌套。
|
|
||||||
*/
|
|
||||||
export default function IntelligentEvalDetailPage() {
|
|
||||||
const evalId = useIntelligentEvalNav((s) => s.selectedEvalId)
|
|
||||||
const initialTab = useIntelligentEvalNav((s) => s.detailTab)
|
|
||||||
const [detail, setDetail] = useState<IntelligentEval | null>(null)
|
|
||||||
const [busy, setBusy] = useState(false)
|
|
||||||
const [rejectOpen, setRejectOpen] = useState(false)
|
|
||||||
const [feedback, setFeedback] = useState('')
|
|
||||||
const [activeTab, setActiveTab] = useState<EvalDetailTab>(initialTab)
|
|
||||||
const { data: targets } = useResource(
|
|
||||||
() => targetsApi.list().then((r) => r.data),
|
|
||||||
{ tabPath: '/intelligent-evals/detail' },
|
|
||||||
)
|
|
||||||
|
|
||||||
const load = async () => {
|
|
||||||
if (!evalId) return
|
|
||||||
try {
|
|
||||||
const res = await intelligentEvalsApi.get(evalId)
|
|
||||||
setDetail(res.data)
|
|
||||||
} catch {
|
|
||||||
message.error('加载评估详情失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (evalId) {
|
|
||||||
setActiveTab(initialTab)
|
|
||||||
void load()
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [evalId, initialTab])
|
|
||||||
|
|
||||||
usePolling(() => { void load() }, 5000, evalId != null && isActive(detail?.status))
|
|
||||||
|
|
||||||
const runAction = async (fn: () => Promise<unknown>, okMsg: string) => {
|
|
||||||
setBusy(true)
|
|
||||||
try {
|
|
||||||
await fn()
|
|
||||||
message.success(okMsg)
|
|
||||||
await load()
|
|
||||||
} finally {
|
|
||||||
setBusy(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const approve = () => runAction(() => intelligentEvalsApi.approve(evalId!), '已批准,进入执行')
|
|
||||||
const cancel = () => runAction(() => intelligentEvalsApi.cancel(evalId!), '已取消')
|
|
||||||
const submitReject = () => runAction(async () => {
|
|
||||||
await intelligentEvalsApi.reject(evalId!, feedback)
|
|
||||||
setRejectOpen(false)
|
|
||||||
setFeedback('')
|
|
||||||
}, '已打回,等待重新规划')
|
|
||||||
|
|
||||||
if (!evalId) {
|
|
||||||
return (
|
|
||||||
<PageWrapper title="评估详情" inline fullHeight>
|
|
||||||
<div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
||||||
<Empty description="从「智能评估 → 评估列表」点击某条评估进入详情" />
|
|
||||||
</div>
|
|
||||||
</PageWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!detail) {
|
|
||||||
return (
|
|
||||||
<PageWrapper title="评估详情" inline fullHeight>
|
|
||||||
<div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
||||||
<Spin />
|
|
||||||
</div>
|
|
||||||
</PageWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const meta = EVAL_STATUS[detail.status] ?? { label: detail.status, color: 'default' }
|
|
||||||
const targetName = (id: string) =>
|
|
||||||
targets?.find((t) => t.id === id)?.name ?? id.slice(0, 8)
|
|
||||||
const canApprove = detail.status === 'pending_approval'
|
|
||||||
const canCancel = detail.status === 'pending_approval' || detail.status === 'executing'
|
|
||||||
|
|
||||||
const items: TabsProps['items'] = [
|
|
||||||
{
|
|
||||||
key: 'overview',
|
|
||||||
label: '概览',
|
|
||||||
children: (
|
|
||||||
<div style={{ height: '100%', overflowY: 'auto', padding: '0 16px 16px' }}>
|
|
||||||
<EvalOverview ev={detail} targetName={targetName(detail.target_id)} />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'decision',
|
|
||||||
label: '决策过程',
|
|
||||||
children: <DecisionProcess evalId={evalId} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'history',
|
|
||||||
label: '配置历史',
|
|
||||||
children: <ConfigSnapshots evalId={evalId} />,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
if (detail.status === 'completed') {
|
|
||||||
items.push({
|
|
||||||
key: 'report',
|
|
||||||
label: '评估报告',
|
|
||||||
children: <EvalReport ev={detail} />,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageWrapper title="评估详情" inline fullHeight>
|
|
||||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
|
|
||||||
padding: '8px 16px', background: colors.bgContainer, borderBottom: `1px solid ${colors.border}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ fontSize: 15, fontWeight: 600, color: colors.text }}>{detail.name}</span>
|
|
||||||
<Tag color={meta.color}>{meta.label}</Tag>
|
|
||||||
<div style={{ flex: 1 }} />
|
|
||||||
<Space>
|
|
||||||
{canApprove && (
|
|
||||||
<>
|
|
||||||
<Button danger loading={busy} onClick={() => setRejectOpen(true)}>打回</Button>
|
|
||||||
<Button type="primary" loading={busy} onClick={approve}>批准并执行</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{canCancel && (
|
|
||||||
<Popconfirm title="取消该智能评估?" onConfirm={cancel}>
|
|
||||||
<Button danger icon={<StopOutlined />} loading={busy}>取消</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ flex: 1, minHeight: 0, padding: '0 16px', background: colors.bgLayout }}>
|
|
||||||
<Tabs
|
|
||||||
className="intelligent-detail-tabs"
|
|
||||||
activeKey={activeTab}
|
|
||||||
onChange={(k) => setActiveTab(k as EvalDetailTab)}
|
|
||||||
items={items}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
title="打回计划"
|
|
||||||
open={rejectOpen}
|
|
||||||
onCancel={() => setRejectOpen(false)}
|
|
||||||
onOk={submitReject}
|
|
||||||
okText="打回"
|
|
||||||
okButtonProps={{ danger: true, disabled: !feedback.trim(), loading: busy }}
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
|
||||||
rows={4}
|
|
||||||
value={feedback}
|
|
||||||
onChange={(e) => setFeedback(e.target.value)}
|
|
||||||
placeholder="请填写打回原因与改进方向,OpenClaw 会据此重新规划"
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
</PageWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,21 +1,25 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
Button, Empty, Form, Input, InputNumber, Select, Space, Table, Tag, Tooltip, message,
|
Button, Drawer, Empty, Form, Input, InputNumber, Popconfirm, Progress, Select, Space, Spin, Table, Tabs, Tag, message,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
|
import type { TabsProps } from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import {
|
import {
|
||||||
PlusOutlined, ReloadOutlined, EyeOutlined, FileTextOutlined, UnorderedListOutlined,
|
PlusOutlined, ReloadOutlined, StopOutlined, UnorderedListOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import FormDrawer from '../components/FormDrawer'
|
import FormDrawer from '../components/FormDrawer'
|
||||||
import PageWrapper from '../components/PageWrapper'
|
import PageWrapper from '../components/PageWrapper'
|
||||||
|
import EvalOverview from '../components/intelligent_eval/EvalOverview'
|
||||||
|
import DecisionProcess from '../components/intelligent_eval/DecisionProcess'
|
||||||
|
import ConfigSnapshots from '../components/intelligent_eval/ConfigSnapshots'
|
||||||
|
import EvalReport from '../components/intelligent_eval/EvalReport'
|
||||||
import { EVAL_STATUS } from '../components/intelligent_eval/status'
|
import { EVAL_STATUS } from '../components/intelligent_eval/status'
|
||||||
import { useResource } from '../hooks/useResource'
|
import { useResource } from '../hooks/useResource'
|
||||||
import {
|
import {
|
||||||
intelligentEvalsApi, targetsApi,
|
intelligentEvalsApi, targetsApi,
|
||||||
type CreateIntelligentEvalPayload, type IntelligentEval, type Target,
|
type CreateIntelligentEvalPayload, type IntelligentEval, type Target,
|
||||||
} from '../api'
|
} from '../api'
|
||||||
import { useIntelligentEvalNav, type EvalDetailTab } from '../stores/intelligentEvalNav'
|
|
||||||
import { useTabStore, type TabItem } from '../stores/tabStore'
|
import { useTabStore, type TabItem } from '../stores/tabStore'
|
||||||
import { colors } from '../tokens'
|
import { colors } from '../tokens'
|
||||||
import { formatDateTime } from '../utils/date'
|
import { formatDateTime } from '../utils/date'
|
||||||
@ -31,47 +35,74 @@ interface CreateFormValues {
|
|||||||
time_window_hours: number
|
time_window_hours: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const DETAIL_TAB: TabItem = {
|
type DetailTab = 'overview' | 'decision' | 'history' | 'report'
|
||||||
key: '/intelligent-evals/detail', title: '评估详情', icon: <EyeOutlined />, closable: true,
|
|
||||||
}
|
|
||||||
const TASKS_TAB: TabItem = {
|
const TASKS_TAB: TabItem = {
|
||||||
key: '/intelligent-evals/tasks', title: '任务队列', icon: <UnorderedListOutlined />, closable: true,
|
key: '/intelligent-evals/tasks', title: '任务队列', icon: <UnorderedListOutlined />, closable: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 状态统计条:全部 + 5 个主要状态(cancelled 不单列)
|
||||||
|
const STAT_CHIPS: { key: string; label: string; color?: string }[] = [
|
||||||
|
{ key: 'all', label: '全部评估' },
|
||||||
|
{ key: 'planning', label: '规划中' },
|
||||||
|
{ key: 'pending_approval', label: '待审批' },
|
||||||
|
{ key: 'executing', label: '执行中' },
|
||||||
|
{ key: 'completed', label: '已完成', color: '#52c41a' },
|
||||||
|
{ key: 'failed', label: '失败', color: '#ff4d4f' },
|
||||||
|
]
|
||||||
|
|
||||||
export default function IntelligentEvalsPage() {
|
export default function IntelligentEvalsPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const openTab = useTabStore((s) => s.openTab)
|
const openTab = useTabStore((s) => s.openTab)
|
||||||
const openDetailNav = useIntelligentEvalNav((s) => s.openDetail)
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
|
const [detailTab, setDetailTab] = useState<DetailTab>('overview')
|
||||||
const [createOpen, setCreateOpen] = useState(false)
|
const [createOpen, setCreateOpen] = useState(false)
|
||||||
|
const [statusFilter, setStatusFilter] = useState('all')
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(20)
|
const [pageSize, setPageSize] = useState(20)
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
const [form] = Form.useForm<CreateFormValues>()
|
const [form] = Form.useForm<CreateFormValues>()
|
||||||
|
|
||||||
const { list, reloadList } = useIntelligentEvalRead(null, undefined, page, pageSize)
|
const { list, detail, reloadList, reloadDetail } = useIntelligentEvalRead(
|
||||||
|
selectedId,
|
||||||
|
undefined,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
statusFilter === 'all' ? undefined : statusFilter,
|
||||||
|
)
|
||||||
const evals = list.value
|
const evals = list.value
|
||||||
const loading = list.phase === 'loading'
|
const loading = list.phase === 'loading'
|
||||||
|
const stats = list.stats
|
||||||
const { data: targets } = useResource(
|
const { data: targets } = useResource(
|
||||||
() => targetsApi.list().then((r) => r.data),
|
() => targetsApi.list().then((r) => r.data),
|
||||||
{ tabPath: '/intelligent-evals' },
|
{ tabPath: '/intelligent-evals' },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const selected = selectedId != null && detail.value?.id === selectedId ? detail.value : null
|
||||||
|
|
||||||
const targetName = (id: string) =>
|
const targetName = (id: string) =>
|
||||||
targets?.find((t) => t.id === id)?.name ?? id.slice(0, 8)
|
targets?.find((t) => t.id === id)?.name ?? id.slice(0, 8)
|
||||||
|
|
||||||
const openDetail = (id: string, tab: EvalDetailTab = 'overview') => {
|
const openDetail = (id: string, tab: DetailTab = 'overview') => {
|
||||||
openDetailNav(id, tab)
|
setSelectedId(id)
|
||||||
openTab(DETAIL_TAB)
|
setDetailTab(tab)
|
||||||
navigate('/intelligent-evals/detail')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const openReport = (id: string) => openDetail(id, 'report')
|
const closeDrawer = () => {
|
||||||
|
setSelectedId(null)
|
||||||
|
void reloadList()
|
||||||
|
}
|
||||||
|
|
||||||
const openTasks = () => {
|
const openTasks = () => {
|
||||||
openTab(TASKS_TAB)
|
openTab(TASKS_TAB)
|
||||||
navigate('/intelligent-evals/tasks')
|
navigate('/intelligent-evals/tasks')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setFilter = (key: string) => {
|
||||||
|
setStatusFilter(key)
|
||||||
|
setPage(1)
|
||||||
|
}
|
||||||
|
|
||||||
const submitCreate = async () => {
|
const submitCreate = async () => {
|
||||||
const values = await form.validateFields()
|
const values = await form.validateFields()
|
||||||
let seeds: Record<string, unknown> = {}
|
let seeds: Record<string, unknown> = {}
|
||||||
@ -112,45 +143,61 @@ export default function IntelligentEvalsPage() {
|
|||||||
|
|
||||||
const columns: ColumnsType<IntelligentEval> = [
|
const columns: ColumnsType<IntelligentEval> = [
|
||||||
{
|
{
|
||||||
title: '名称', dataIndex: 'name', key: 'name',
|
title: '名称', dataIndex: 'name', key: 'name', width: 300, ellipsis: true,
|
||||||
render: (name: string) => <span style={{ fontWeight: 500 }}>{name}</span>,
|
render: (name: string) => <span style={{ fontWeight: 500 }}>{name}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '评测对象', dataIndex: 'target_id', key: 'target', width: 180,
|
title: '评测对象', dataIndex: 'target_id', key: 'target', width: 180, ellipsis: true,
|
||||||
render: (id: string) => targetName(id),
|
render: (id: string) => <span style={{ fontSize: 13, color: colors.textSecondary }}>{targetName(id)}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态', dataIndex: 'status', key: 'status', width: 110,
|
title: '状态', dataIndex: 'status', key: 'status', width: 96,
|
||||||
render: (status: IntelligentEval['status']) => {
|
render: (status: IntelligentEval['status']) => {
|
||||||
const meta = EVAL_STATUS[status] ?? { label: status, color: 'default' }
|
const meta = EVAL_STATUS[status] ?? { label: status, color: 'default' }
|
||||||
return <Tag color={meta.color}>{meta.label}</Tag>
|
return <Tag color={meta.color}>{meta.label}</Tag>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '会话进度', key: 'progress', width: 120,
|
title: '会话进度', key: 'progress', width: 170,
|
||||||
render: (_, ev) => `${ev.completed_sessions}/${ev.session_count}`,
|
render: (_, ev) => {
|
||||||
|
const pct = ev.session_count ? Math.round((ev.completed_sessions / ev.session_count) * 100) : 0
|
||||||
|
return (
|
||||||
|
<Space size={6}>
|
||||||
|
<Progress percent={pct} size="small" style={{ width: 90 }} />
|
||||||
|
<span style={{ fontSize: 12, color: colors.textSecondary }}>{ev.completed_sessions}/{ev.session_count}</span>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 170,
|
title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 160,
|
||||||
render: (v: string | null) => (v ? formatDateTime(v) : '—'),
|
render: (v: string | null) => (v ? formatDateTime(v) : '—'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作', key: 'action', width: 110, fixed: 'right' as const,
|
title: '操作', key: 'action', width: 88, fixed: 'right' as const,
|
||||||
render: (_, ev) => (
|
render: (_, ev) => (
|
||||||
<Space size={4}>
|
<Button size="small" onClick={() => openDetail(ev.id)}>查看</Button>
|
||||||
<Tooltip title="详情">
|
|
||||||
<Button size="small" type="text" icon={<EyeOutlined />} onClick={() => openDetail(ev.id)} />
|
|
||||||
</Tooltip>
|
|
||||||
{ev.status === 'completed' && (
|
|
||||||
<Tooltip title="查看报告">
|
|
||||||
<Button size="small" type="text" icon={<FileTextOutlined />} onClick={() => openReport(ev.id)} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const drawerItems: TabsProps['items'] = selected
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: 'overview',
|
||||||
|
label: '概览',
|
||||||
|
children: (
|
||||||
|
<div style={{ height: '100%', overflowY: 'auto', padding: '0 16px 16px' }}>
|
||||||
|
<EvalOverview ev={selected} targetName={targetName(selected.target_id)} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ key: 'decision', label: '决策过程', children: <DecisionProcess evalId={selected.id} /> },
|
||||||
|
{ key: 'history', label: '配置历史', children: <ConfigSnapshots evalId={selected.id} /> },
|
||||||
|
...(selected.status === 'completed' ? [{ key: 'report' as const, label: '评估报告', children: <EvalReport ev={selected} /> }] : []),
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper
|
<PageWrapper
|
||||||
title="智能评估"
|
title="智能评估"
|
||||||
@ -170,27 +217,86 @@ export default function IntelligentEvalsPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div style={{ height: '100%', overflowY: 'auto', padding: '0 16px 16px' }}>
|
<div style={{ height: '100%', overflowY: 'auto', padding: '0 16px 16px' }}>
|
||||||
<Table
|
<div className="stat-bar">
|
||||||
rowKey="id"
|
{STAT_CHIPS.map((c) => {
|
||||||
loading={loading}
|
const n = c.key === 'all'
|
||||||
dataSource={evals ?? []}
|
? (stats ? Object.values(stats).reduce((a, b) => a + b, 0) : list.total)
|
||||||
columns={columns}
|
: (stats?.[c.key] ?? 0)
|
||||||
pagination={{
|
return (
|
||||||
current: page,
|
<div key={c.key} className={`stat-chip ${statusFilter === c.key ? 'active' : ''}`} onClick={() => setFilter(c.key)}>
|
||||||
pageSize,
|
<div className="n" style={{ color: c.color ?? colors.text }}>{n}</div>
|
||||||
total: list.total,
|
<div className="l">{c.label}</div>
|
||||||
showSizeChanger: true,
|
</div>
|
||||||
pageSizeOptions: [10, 20, 50],
|
)
|
||||||
showTotal: (t) => `共 ${t} 个`,
|
})}
|
||||||
onChange: (p, ps) => {
|
</div>
|
||||||
setPage(p)
|
|
||||||
setPageSize(ps)
|
<div style={{ background: colors.bgContainer, borderRadius: 8, padding: 16 }}>
|
||||||
},
|
<Table
|
||||||
}}
|
rowKey="id"
|
||||||
locale={{ emptyText: <Empty description="还没有智能评估" /> }}
|
loading={loading}
|
||||||
/>
|
dataSource={evals ?? []}
|
||||||
|
columns={columns}
|
||||||
|
onRow={(record) => ({ onClick: () => openDetail(record.id), style: { cursor: 'pointer' } })}
|
||||||
|
pagination={{
|
||||||
|
current: page,
|
||||||
|
pageSize,
|
||||||
|
total: list.total,
|
||||||
|
showSizeChanger: true,
|
||||||
|
pageSizeOptions: [10, 20, 50],
|
||||||
|
showTotal: (t) => `共 ${t} 个`,
|
||||||
|
onChange: (p, ps) => {
|
||||||
|
setPage(p)
|
||||||
|
setPageSize(ps)
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
locale={{ emptyText: <Empty description="该状态下暂无评估" /> }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
title={selected?.name ?? '评估详情'}
|
||||||
|
open={selectedId != null}
|
||||||
|
onClose={closeDrawer}
|
||||||
|
width={1000}
|
||||||
|
destroyOnClose
|
||||||
|
styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column' } }}
|
||||||
|
extra={selected && (
|
||||||
|
<Space>
|
||||||
|
<Tag color={EVAL_STATUS[selected.status]?.color ?? 'default'}>
|
||||||
|
{EVAL_STATUS[selected.status]?.label ?? selected.status}
|
||||||
|
</Tag>
|
||||||
|
{selected.status === 'pending_approval' && (
|
||||||
|
<Button type="primary" onClick={() => void intelligentEvalsApi.approve(selected.id).then(() => { message.success('已批准,进入执行'); void reloadDetail() })}>
|
||||||
|
批准并执行
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{(selected.status === 'pending_approval' || selected.status === 'executing') && (
|
||||||
|
<Popconfirm
|
||||||
|
title="取消该智能评估?"
|
||||||
|
onConfirm={() => void intelligentEvalsApi.cancel(selected.id).then(() => { message.success('已取消'); void reloadDetail() })}
|
||||||
|
>
|
||||||
|
<Button danger icon={<StopOutlined />}>取消</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selected == null ? (
|
||||||
|
<div style={{ padding: 48, textAlign: 'center' }}><Spin /></div>
|
||||||
|
) : (
|
||||||
|
<div style={{ flex: 1, minHeight: 0, padding: '0 16px', background: colors.bgLayout }}>
|
||||||
|
<Tabs
|
||||||
|
className="intelligent-detail-tabs"
|
||||||
|
activeKey={detailTab}
|
||||||
|
onChange={(k) => setDetailTab(k as DetailTab)}
|
||||||
|
items={drawerItems}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
<FormDrawer
|
<FormDrawer
|
||||||
title="新建智能评估"
|
title="新建智能评估"
|
||||||
open={createOpen}
|
open={createOpen}
|
||||||
|
|||||||
@ -16,21 +16,22 @@ describe('intelligent evaluation read state', () => {
|
|||||||
type: 'list_succeeded',
|
type: 'list_succeeded',
|
||||||
value: [evaluation],
|
value: [evaluation],
|
||||||
total: 42,
|
total: 42,
|
||||||
|
stats: { planning: 1 },
|
||||||
})
|
})
|
||||||
expect(ready.list).toEqual({ phase: 'ready', value: [evaluation], error: null, total: 42 })
|
expect(ready.list).toEqual({ phase: 'ready', value: [evaluation], error: null, total: 42, stats: { planning: 1 } })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps the last list snapshot during silent refresh and failure', () => {
|
it('keeps the last list snapshot during silent refresh and failure', () => {
|
||||||
const ready: IntelligentEvalReadState = {
|
const ready: IntelligentEvalReadState = {
|
||||||
...initialIntelligentEvalReadState,
|
...initialIntelligentEvalReadState,
|
||||||
list: { phase: 'ready', value: [evaluation], error: null, total: 1 },
|
list: { phase: 'ready', value: [evaluation], error: null, total: 1, stats: null },
|
||||||
}
|
}
|
||||||
const refreshing = intelligentEvalReadReducer(ready, { type: 'list_requested', silent: true })
|
const refreshing = intelligentEvalReadReducer(ready, { type: 'list_requested', silent: true })
|
||||||
expect(refreshing.list.phase).toBe('refreshing')
|
expect(refreshing.list.phase).toBe('refreshing')
|
||||||
expect(refreshing.list.total).toBe(1)
|
expect(refreshing.list.total).toBe(1)
|
||||||
|
|
||||||
const afterFailure = intelligentEvalReadReducer(refreshing, { type: 'list_failed', error: '网络错误' })
|
const afterFailure = intelligentEvalReadReducer(refreshing, { type: 'list_failed', error: '网络错误' })
|
||||||
expect(afterFailure.list).toEqual({ phase: 'ready', value: [evaluation], error: null, total: 1 })
|
expect(afterFailure.list).toEqual({ phase: 'ready', value: [evaluation], error: null, total: 1, stats: null })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('surfaces an initial detail failure without an existing snapshot', () => {
|
it('surfaces an initial detail failure without an existing snapshot', () => {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export interface ReadSlot<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IntelligentEvalReadState {
|
export interface IntelligentEvalReadState {
|
||||||
list: ReadSlot<IntelligentEval[]> & { total: number }
|
list: ReadSlot<IntelligentEval[]> & { total: number; stats: Record<string, number> | null }
|
||||||
detail: ReadSlot<IntelligentEval | null> & {
|
detail: ReadSlot<IntelligentEval | null> & {
|
||||||
selectedId: string | null
|
selectedId: string | null
|
||||||
requestId: number
|
requestId: number
|
||||||
@ -18,7 +18,7 @@ export interface IntelligentEvalReadState {
|
|||||||
|
|
||||||
export type IntelligentEvalReadAction =
|
export type IntelligentEvalReadAction =
|
||||||
| { type: 'list_requested'; silent?: boolean }
|
| { type: 'list_requested'; silent?: boolean }
|
||||||
| { type: 'list_succeeded'; value: IntelligentEval[]; total: number }
|
| { type: 'list_succeeded'; value: IntelligentEval[]; total: number; stats: Record<string, number> | null }
|
||||||
| { type: 'list_failed'; error: string }
|
| { type: 'list_failed'; error: string }
|
||||||
| { type: 'detail_cleared'; requestId: number }
|
| { type: 'detail_cleared'; requestId: number }
|
||||||
| { type: 'detail_requested'; id: string; requestId: number; silent?: boolean }
|
| { type: 'detail_requested'; id: string; requestId: number; silent?: boolean }
|
||||||
@ -26,24 +26,33 @@ export type IntelligentEvalReadAction =
|
|||||||
| { type: 'detail_failed'; id: string; requestId: number; error: string }
|
| { type: 'detail_failed'; id: string; requestId: number; error: string }
|
||||||
|
|
||||||
export interface IntelligentEvalReadAdapter {
|
export interface IntelligentEvalReadAdapter {
|
||||||
list: (page?: number, pageSize?: number) => Promise<{ items: IntelligentEval[]; total: number }>
|
list: (page?: number, pageSize?: number, status?: string) => Promise<{
|
||||||
|
items: IntelligentEval[]
|
||||||
|
total: number
|
||||||
|
stats: Record<string, number> | null
|
||||||
|
}>
|
||||||
get: (id: string) => Promise<IntelligentEval>
|
get: (id: string) => Promise<IntelligentEval>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const intelligentEvalReadAdapter: IntelligentEvalReadAdapter = {
|
export const intelligentEvalReadAdapter: IntelligentEvalReadAdapter = {
|
||||||
list: (page, pageSize) =>
|
list: (page, pageSize, status) =>
|
||||||
intelligentEvalsApi
|
intelligentEvalsApi
|
||||||
.list(page != null ? { page, page_size: pageSize ?? 20 } : undefined)
|
.list(
|
||||||
|
page != null
|
||||||
|
? { page, page_size: pageSize ?? 20, ...(status ? { status } : {}) }
|
||||||
|
: undefined,
|
||||||
|
)
|
||||||
.then((response) => ({
|
.then((response) => ({
|
||||||
items: response.data.intelligent_evals,
|
items: response.data.intelligent_evals,
|
||||||
// 兼容不分页响应:无 total 时以本页长度为总数
|
// 兼容不分页响应:无 total 时以本页长度为总数
|
||||||
total: response.data.total ?? response.data.intelligent_evals.length,
|
total: response.data.total ?? response.data.intelligent_evals.length,
|
||||||
|
stats: response.data.stats ?? null,
|
||||||
})),
|
})),
|
||||||
get: (id) => intelligentEvalsApi.get(id).then((response) => response.data),
|
get: (id) => intelligentEvalsApi.get(id).then((response) => response.data),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialIntelligentEvalReadState: IntelligentEvalReadState = {
|
export const initialIntelligentEvalReadState: IntelligentEvalReadState = {
|
||||||
list: { phase: 'idle', value: [], error: null, total: 0 },
|
list: { phase: 'idle', value: [], error: null, total: 0, stats: null },
|
||||||
detail: { phase: 'idle', value: null, error: null, selectedId: null, requestId: 0 },
|
detail: { phase: 'idle', value: null, error: null, selectedId: null, requestId: 0 },
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +68,10 @@ export function intelligentEvalReadReducer(
|
|||||||
case 'list_requested':
|
case 'list_requested':
|
||||||
return { ...state, list: requestPhase(state.list, action.silent) }
|
return { ...state, list: requestPhase(state.list, action.silent) }
|
||||||
case 'list_succeeded':
|
case 'list_succeeded':
|
||||||
return { ...state, list: { phase: 'ready', value: action.value, error: null, total: action.total } }
|
return {
|
||||||
|
...state,
|
||||||
|
list: { phase: 'ready', value: action.value, error: null, total: action.total, stats: action.stats },
|
||||||
|
}
|
||||||
case 'list_failed':
|
case 'list_failed':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@ -28,7 +28,7 @@ describe('useIntelligentEvalRead', () => {
|
|||||||
it('refreshes active evaluations every five seconds and stops at terminal state', async () => {
|
it('refreshes active evaluations every five seconds and stops at terminal state', async () => {
|
||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
const adapter: IntelligentEvalReadAdapter = {
|
const adapter: IntelligentEvalReadAdapter = {
|
||||||
list: vi.fn().mockResolvedValue({ items: [evaluation], total: 1 }),
|
list: vi.fn().mockResolvedValue({ items: [evaluation], total: 1, stats: null }),
|
||||||
get: vi.fn().mockResolvedValue(evaluation),
|
get: vi.fn().mockResolvedValue(evaluation),
|
||||||
}
|
}
|
||||||
const { result } = renderHook(() => useIntelligentEvalRead(null, adapter))
|
const { result } = renderHook(() => useIntelligentEvalRead(null, adapter))
|
||||||
@ -41,7 +41,7 @@ describe('useIntelligentEvalRead', () => {
|
|||||||
expect(adapter.list).toHaveBeenCalledTimes(2)
|
expect(adapter.list).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
const terminal = { ...evaluation, status: 'completed' } as IntelligentEval
|
const terminal = { ...evaluation, status: 'completed' } as IntelligentEval
|
||||||
vi.mocked(adapter.list).mockResolvedValue({ items: [terminal], total: 1 })
|
vi.mocked(adapter.list).mockResolvedValue({ items: [terminal], total: 1, stats: null })
|
||||||
await act(async () => { await vi.advanceTimersByTimeAsync(5000) })
|
await act(async () => { await vi.advanceTimersByTimeAsync(5000) })
|
||||||
await settle()
|
await settle()
|
||||||
await act(async () => { await vi.advanceTimersByTimeAsync(10000) })
|
await act(async () => { await vi.advanceTimersByTimeAsync(10000) })
|
||||||
@ -68,7 +68,7 @@ describe('useIntelligentEvalRead', () => {
|
|||||||
|
|
||||||
it('passes page/pageSize to the adapter and reloads on change', async () => {
|
it('passes page/pageSize to the adapter and reloads on change', async () => {
|
||||||
const adapter: IntelligentEvalReadAdapter = {
|
const adapter: IntelligentEvalReadAdapter = {
|
||||||
list: vi.fn().mockResolvedValue({ items: [], total: 0 }),
|
list: vi.fn().mockResolvedValue({ items: [], total: 0, stats: null }),
|
||||||
get: vi.fn().mockResolvedValue(evaluation),
|
get: vi.fn().mockResolvedValue(evaluation),
|
||||||
}
|
}
|
||||||
const { rerender } = renderHook(
|
const { rerender } = renderHook(
|
||||||
@ -76,11 +76,11 @@ describe('useIntelligentEvalRead', () => {
|
|||||||
{ initialProps: { page: 1 } },
|
{ initialProps: { page: 1 } },
|
||||||
)
|
)
|
||||||
await settle()
|
await settle()
|
||||||
expect(adapter.list).toHaveBeenCalledWith(1, 20)
|
expect(adapter.list).toHaveBeenCalledWith(1, 20, undefined)
|
||||||
|
|
||||||
rerender({ page: 2 })
|
rerender({ page: 2 })
|
||||||
await settle()
|
await settle()
|
||||||
expect(adapter.list).toHaveBeenCalledWith(2, 20)
|
expect(adapter.list).toHaveBeenCalledWith(2, 20, undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('clears the old detail and ignores a response for the previous selection', async () => {
|
it('clears the old detail and ignores a response for the previous selection', async () => {
|
||||||
@ -88,7 +88,7 @@ describe('useIntelligentEvalRead', () => {
|
|||||||
const second = deferred<IntelligentEval>()
|
const second = deferred<IntelligentEval>()
|
||||||
const secondEvaluation = { ...evaluation, id: 'eval-2', name: '评估二' } as IntelligentEval
|
const secondEvaluation = { ...evaluation, id: 'eval-2', name: '评估二' } as IntelligentEval
|
||||||
const adapter: IntelligentEvalReadAdapter = {
|
const adapter: IntelligentEvalReadAdapter = {
|
||||||
list: vi.fn().mockResolvedValue({ items: [], total: 0 }),
|
list: vi.fn().mockResolvedValue({ items: [], total: 0, stats: null }),
|
||||||
get: vi.fn((id: string) => (id === 'eval-1' ? first.promise : second.promise)),
|
get: vi.fn((id: string) => (id === 'eval-1' ? first.promise : second.promise)),
|
||||||
}
|
}
|
||||||
const { result, rerender } = renderHook(
|
const { result, rerender } = renderHook(
|
||||||
@ -109,7 +109,7 @@ describe('useIntelligentEvalRead', () => {
|
|||||||
|
|
||||||
it('surfaces failure for a newly selected detail instead of retaining the old snapshot', async () => {
|
it('surfaces failure for a newly selected detail instead of retaining the old snapshot', async () => {
|
||||||
const adapter: IntelligentEvalReadAdapter = {
|
const adapter: IntelligentEvalReadAdapter = {
|
||||||
list: vi.fn().mockResolvedValue({ items: [], total: 0 }),
|
list: vi.fn().mockResolvedValue({ items: [], total: 0, stats: null }),
|
||||||
get: vi.fn()
|
get: vi.fn()
|
||||||
.mockResolvedValueOnce(evaluation)
|
.mockResolvedValueOnce(evaluation)
|
||||||
.mockRejectedValueOnce(new Error('不存在')),
|
.mockRejectedValueOnce(new Error('不存在')),
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export function useIntelligentEvalRead(
|
|||||||
adapter: IntelligentEvalReadAdapter = intelligentEvalReadAdapter,
|
adapter: IntelligentEvalReadAdapter = intelligentEvalReadAdapter,
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
pageSize: number = 20,
|
pageSize: number = 20,
|
||||||
|
status?: string,
|
||||||
): IntelligentEvalReadState & { reloadList: () => Promise<void>; reloadDetail: () => Promise<void> } {
|
): IntelligentEvalReadState & { reloadList: () => Promise<void>; reloadDetail: () => Promise<void> } {
|
||||||
const [state, dispatch] = useReducer(intelligentEvalReadReducer, initialIntelligentEvalReadState)
|
const [state, dispatch] = useReducer(intelligentEvalReadReducer, initialIntelligentEvalReadState)
|
||||||
const detailRequestId = useRef(0)
|
const detailRequestId = useRef(0)
|
||||||
@ -30,12 +31,12 @@ export function useIntelligentEvalRead(
|
|||||||
const loadList = useCallback(async (silent = false) => {
|
const loadList = useCallback(async (silent = false) => {
|
||||||
dispatch({ type: 'list_requested', silent })
|
dispatch({ type: 'list_requested', silent })
|
||||||
try {
|
try {
|
||||||
const { items, total } = await adapter.list(page, pageSize)
|
const { items, total, stats } = await adapter.list(page, pageSize, status)
|
||||||
dispatch({ type: 'list_succeeded', value: items, total })
|
dispatch({ type: 'list_succeeded', value: items, total, stats })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch({ type: 'list_failed', error: errorMessage(error) })
|
dispatch({ type: 'list_failed', error: errorMessage(error) })
|
||||||
}
|
}
|
||||||
}, [adapter, page, pageSize])
|
}, [adapter, page, pageSize, status])
|
||||||
|
|
||||||
const loadDetail = useCallback(async (id: string, silent = false) => {
|
const loadDetail = useCallback(async (id: string, silent = false) => {
|
||||||
const requestId = ++detailRequestId.current
|
const requestId = ++detailRequestId.current
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
import { create } from 'zustand'
|
|
||||||
|
|
||||||
export type EvalDetailTab = 'overview' | 'decision' | 'history' | 'report'
|
|
||||||
|
|
||||||
interface IntelligentEvalNav {
|
|
||||||
/** 当前详情页展示的评估 id(由评估列表页进入时设置)。 */
|
|
||||||
selectedEvalId: string | null
|
|
||||||
/** 详情页初始激活的子视图 tab。 */
|
|
||||||
detailTab: EvalDetailTab
|
|
||||||
openDetail: (id: string, tab?: EvalDetailTab) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 评估列表 → 详情页 之间的选中状态桥(keep-alive 多页模式)。
|
|
||||||
* 详情页是独立 tab(/intelligent-evals/detail),与列表页同时挂载,
|
|
||||||
* 选中哪个评估由这里传递;列表页导航前调用 openDetail 再 openTab + navigate。
|
|
||||||
*/
|
|
||||||
export const useIntelligentEvalNav = create<IntelligentEvalNav>((set) => ({
|
|
||||||
selectedEvalId: null,
|
|
||||||
detailTab: 'overview',
|
|
||||||
openDetail: (id, tab = 'overview') => set({ selectedEvalId: id, detailTab: tab }),
|
|
||||||
}))
|
|
||||||
Loading…
Reference in New Issue
Block a user