fix(intelligent-eval): P0 UI/UX refresh defects (audit)

UI/UX 全面盘点(.scratch/ui-ux-audit.md)P0 功能缺陷修复:
- DecisionProcess/ConfigSnapshots:useState(()=>load()) 一次性加载改为
  useEffect 依赖 evalId(切评估重拉)+ 头部补 ReloadOutlined 刷新按钮
- useRunSession:自写 setInterval(3000) 轮询改 usePolling(pauseWhenHidden
  默认开启:后台标签页暂停、回前台立即刷新);selectedIdRef 守卫保留防串;
  删除 pollRef/clearPolling
- useFiles:接入 useOnTabActive('/files'),keep-alive 跳回自动刷新
tsc 0 错误 vitest 19 passed
This commit is contained in:
sinohqb 2026-08-17 22:59:23 +08:00
parent 00ad929d68
commit 3626ab288b
5 changed files with 30 additions and 32 deletions

View File

@ -1,9 +1,9 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import {
Button, Card, Descriptions, Empty, Space, Table, Tag, message,
} from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { ArrowLeftOutlined, DiffOutlined } from '@ant-design/icons'
import { ArrowLeftOutlined, DiffOutlined, ReloadOutlined } from '@ant-design/icons'
import { intelligentEvalsApi, type ConfigSnapshot, type ConfigSnapshotComparison } from '../../api'
import { colors } from '../../tokens'
import { formatDateTime } from '../../utils/date'
@ -40,9 +40,10 @@ export default function ConfigSnapshots({ evalId, onBack }: ConfigSnapshotsProps
}
}
useState(() => {
void loadSnapshots()
})
useEffect(() => {
if (evalId) void loadSnapshots()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [evalId])
const handleViewDetail = async (snapshot: ConfigSnapshot) => {
try {
@ -244,6 +245,7 @@ export default function ConfigSnapshots({ evalId, onBack }: ConfigSnapshotsProps
{onBack && <Button icon={<ArrowLeftOutlined />} onClick={onBack}></Button>}
<span style={{ fontSize: 16, fontWeight: 600 }}></span>
<div style={{ flex: 1 }} />
<Button icon={<ReloadOutlined />} onClick={() => void loadSnapshots()}></Button>
<Button
icon={<DiffOutlined />}
disabled={selectedForCompare.length !== 2}

View File

@ -1,9 +1,9 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import {
Button, Card, Empty, Select, Table, Tag, Timeline, message,
} from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { ArrowLeftOutlined, DownloadOutlined } from '@ant-design/icons'
import { ArrowLeftOutlined, DownloadOutlined, ReloadOutlined } from '@ant-design/icons'
import { intelligentEvalsApi, type DecisionLog } from '../../api'
import { colors } from '../../tokens'
import { formatDateTime } from '../../utils/date'
@ -38,9 +38,10 @@ export default function DecisionProcess({ evalId, onBack }: DecisionProcessProps
}
}
useState(() => {
void loadLogs()
})
useEffect(() => {
if (evalId) void loadLogs()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [evalId])
const handleExport = () => {
if (!logs) return
@ -120,6 +121,7 @@ export default function DecisionProcess({ evalId, onBack }: DecisionProcessProps
{ label: '开始分析', value: 'start_analysis' },
]}
/>
<Button icon={<ReloadOutlined />} onClick={() => void loadLogs()}></Button>
<Button icon={<DownloadOutlined />} onClick={handleExport}></Button>
</div>

View File

@ -6,13 +6,14 @@ import {
type FileUploadConfig,
} from '../api'
import { categoryContains, findCategory } from '../utils/fileTree'
import { useOnTabActive } from './useOnTabActive'
const DEFAULT_CONFIG: FileUploadConfig = {
allowed_extensions: [],
max_upload_size_mb: 50,
}
export function useFiles() {
export function useFiles(tabPath?: string) {
const [categories, setCategories] = useState<FileCategory[]>([])
const [files, setFiles] = useState<FileRecord[]>([])
const [config, setConfig] = useState<FileUploadConfig>(DEFAULT_CONFIG)
@ -46,6 +47,11 @@ export function useFiles() {
void Promise.allSettled([loadCategories(), loadFiles(), loadConfig()])
}, [loadCategories, loadConfig, loadFiles])
// keep-alive 下 tab 重新激活时刷新(跳走再回来数据不过期)
useOnTabActive(tabPath ?? '', () => {
if (tabPath) void refresh()
})
const selectCategory = useCallback((categoryId: string | null) => {
setSelectedCategoryId(categoryId)
void loadFiles(categoryId)

View File

@ -5,6 +5,7 @@ import {
sessionReducer,
type WsEvent,
} from './sessionReducer'
import { usePolling } from './usePolling'
// WebSocket reconnect config
const WS_MAX_RETRIES = 5
@ -81,18 +82,10 @@ export function useRunSession(): RunSession {
const [state, dispatch] = useReducer(sessionReducer, initialSessionState)
const wsRef = useRef<WebSocket | null>(null)
const pollRef = useRef<number | null>(null)
const selectedIdRef = useRef<string | null>(null)
const reconnectTimerRef = useRef<number | null>(null)
const reconnectCountRef = useRef<number>(0)
const clearPolling = () => {
if (pollRef.current) {
window.clearInterval(pollRef.current)
pollRef.current = null
}
}
const clearReconnect = () => {
if (reconnectTimerRef.current) {
window.clearTimeout(reconnectTimerRef.current)
@ -174,7 +167,6 @@ export function useRunSession(): RunSession {
const select = useCallback((r: Run | null, opts?: { live?: boolean }) => {
closeWs()
clearPolling()
dispatch({ type: 'RESET' })
setRun(r)
selectedIdRef.current = r?.id ?? null
@ -250,21 +242,17 @@ export function useRunSession(): RunSession {
} catch { /* noop */ }
}, [run])
// Poll during live runs to keep the Run object fresh (status/summary/completed_at)
useEffect(() => {
if (!state.isLive || !run) {
clearPolling()
return
}
pollRef.current = window.setInterval(() => {
// Poll during live runs to keep the Run object fresh (status/summary/completed_at).
// usePolling 统一轮询pauseWhenHidden 默认开启,后台标签页暂停、回到前台立即刷新。
usePolling(() => {
if (run && selectedIdRef.current === run.id) {
runsApi.get(run.id).then((res) => {
if (selectedIdRef.current === run.id) setRun(res.data)
}).catch(() => { /* noop */ })
}, 3000)
return () => clearPolling()
}, [state.isLive, run?.id])
}
}, 3000, state.isLive && run != null)
useEffect(() => () => { closeWs(); clearPolling(); clearReconnect() }, [])
useEffect(() => () => { closeWs(); clearReconnect() }, [])
return {
run,

View File

@ -37,7 +37,7 @@ export default function FilesPage() {
uploadFiles,
deleteFile,
downloadFile,
} = useFiles()
} = useFiles('/files')
const [uploadOpen, setUploadOpen] = useState(false)
const [categoryDialog, setCategoryDialog] = useState<CategoryDialogState>(CLOSED_CATEGORY_DIALOG)
const [categorySaving, setCategorySaving] = useState(false)