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

View File

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

View File

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

View File

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

View File

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