polish Semi UI admin and operations workspaces
This commit is contained in:
@@ -8,6 +8,7 @@ import { InlineError } from '../shared/AsyncState';
|
||||
import { PageHeader } from '../shared/PageHeader';
|
||||
import { SegmentedTabs } from '../shared/SegmentedTabs';
|
||||
import { VehicleCandidateList } from '../shared/VehicleCandidateList';
|
||||
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||
import { LIVE_QUERY_POLICY, QUERY_MEMORY } from '../queryPolicy';
|
||||
import { useMobileLayout } from '../hooks/useMobileLayout';
|
||||
@@ -208,6 +209,7 @@ function SourceDiagnosticWorkspace() {
|
||||
const deferredKeyword = useDeferredValue(keyword.trim());
|
||||
const [selected, setSelected] = useState<VehicleCoverageRow>();
|
||||
const [candidateOffset, setCandidateOffset] = useState(0);
|
||||
const [filtersCollapsed, setFiltersCollapsed] = useState(false);
|
||||
useEffect(() => setCandidateOffset(0), [deferredKeyword]);
|
||||
const candidates = useQuery({
|
||||
queryKey: ['ops-source-candidates', deferredKeyword, candidateOffset],
|
||||
@@ -227,6 +229,7 @@ function SourceDiagnosticWorkspace() {
|
||||
const choose = (vehicle: VehicleCoverageRow) => {
|
||||
setSelected(vehicle);
|
||||
setKeyword(vehicle.plate || vehicle.vin);
|
||||
if (mobileLayout) setFiltersCollapsed(true);
|
||||
};
|
||||
const submit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -235,6 +238,12 @@ function SourceDiagnosticWorkspace() {
|
||||
};
|
||||
const data = diagnostic.data;
|
||||
const editable = session.data?.role === 'admin';
|
||||
const selectedLabel = selected ? selected.plate || selected.vin : '';
|
||||
const filterStatus = selectedLabel
|
||||
? `已选 ${selectedLabel}`
|
||||
: deferredKeyword
|
||||
? candidates.isFetching ? '正在查找' : `${candidates.data?.total ?? 0} 辆候选`
|
||||
: '等待选择车辆';
|
||||
const onSourceSaved = (next: VehicleSourceDiagnostic) => {
|
||||
queryClient.setQueryData(['ops-source-diagnostic', next.evidence.vin], next);
|
||||
void Promise.all([
|
||||
@@ -243,31 +252,45 @@ function SourceDiagnosticWorkspace() {
|
||||
queryClient.invalidateQueries({ queryKey: ['ops-source-readiness-v2'] })
|
||||
]);
|
||||
};
|
||||
return <Card className="v2-source-diagnostic" bodyStyle={{ padding: 0 }}>
|
||||
<WorkspacePanelHeader
|
||||
title="单车多来源诊断"
|
||||
description="按需查询,不加载全量 RAW;普通客户无权访问。"
|
||||
actions={selected ? <Button theme="light" icon={<IconRefresh />} onClick={() => diagnostic.refetch()} disabled={diagnostic.isFetching}>刷新该车</Button> : null}
|
||||
/>
|
||||
<form className="v2-source-search" onSubmit={submit}>
|
||||
<Input aria-label="按车牌或 VIN 搜索诊断车辆" prefix={<IconSearch />} value={keyword} onChange={(value) => { setKeyword(value); setSelected(undefined); }} placeholder="输入车牌或 VIN,支持模糊搜索" />
|
||||
<Button htmlType="submit" theme="solid" disabled={!candidates.data?.items.length}>诊断首个结果</Button>
|
||||
{deferredKeyword && !selected ? <VehicleCandidateList
|
||||
className="v2-source-candidates"
|
||||
items={candidates.data?.items ?? []}
|
||||
loading={candidates.isFetching}
|
||||
loadingText="正在查询车辆…"
|
||||
emptyText="没有匹配的已绑定车辆"
|
||||
actionLabel="诊断"
|
||||
showProtocols
|
||||
onSelect={choose}
|
||||
footer={(candidates.data?.total ?? 0) > 20 ? <><span>第 {Math.floor(candidateOffset / 20) + 1} / {Math.ceil((candidates.data?.total ?? 0) / 20)} 页</span><div><Button theme="light" size="small" disabled={candidateOffset === 0} onClick={() => setCandidateOffset(Math.max(0, candidateOffset - 20))}>上一页</Button><Button theme="light" size="small" disabled={candidateOffset + 20 >= (candidates.data?.total ?? 0)} onClick={() => setCandidateOffset(candidateOffset + 20)}>下一页</Button></div></> : undefined}
|
||||
/> : null}
|
||||
</form>
|
||||
{diagnostic.isError ? <InlineError message={diagnostic.error.message} onRetry={() => diagnostic.refetch()} /> : null}
|
||||
{!selected ? <Empty className="v2-source-empty" title="先选择一辆车" description="将展示所有协议和同协议多终端、推荐原因、上报周期与可审计策略。" /> : null}
|
||||
{selected && diagnostic.isPending ? <div className="v2-source-loading" role="status"><Spin size="large" /><strong>正在读取来源证据</strong><span>只查询当前车辆,不会扫描整车队。</span></div> : null}
|
||||
{data ? <>
|
||||
return <>
|
||||
<WorkspaceFilterPanel
|
||||
className="v2-source-filter-panel"
|
||||
title="诊断车辆"
|
||||
description="按车牌或 VIN 选择一辆车,仅按需加载该车来源证据"
|
||||
mobileSummary={selectedLabel ? `已选 ${selectedLabel}` : deferredKeyword ? `正在查找 ${deferredKeyword}` : '尚未选择车辆'}
|
||||
expanded={!filtersCollapsed}
|
||||
status={filterStatus}
|
||||
statusColor={selectedLabel ? 'blue' : 'grey'}
|
||||
collapsedLabel="修改"
|
||||
onToggle={() => setFiltersCollapsed((value) => !value)}
|
||||
>
|
||||
<form className={`v2-source-search${filtersCollapsed ? ' is-mobile-collapsed' : ''}`} onSubmit={submit}>
|
||||
<Input aria-label="按车牌或 VIN 搜索诊断车辆" prefix={<IconSearch />} value={keyword} onChange={(value) => { setKeyword(value); setSelected(undefined); }} placeholder="输入车牌或 VIN,支持模糊搜索" />
|
||||
<Button htmlType="submit" theme="solid" disabled={!candidates.data?.items.length}>诊断首个结果</Button>
|
||||
{deferredKeyword && !selected ? <VehicleCandidateList
|
||||
className="v2-source-candidates"
|
||||
items={candidates.data?.items ?? []}
|
||||
loading={candidates.isFetching}
|
||||
loadingText="正在查询车辆…"
|
||||
emptyText="没有匹配的已绑定车辆"
|
||||
actionLabel="诊断"
|
||||
showProtocols
|
||||
onSelect={choose}
|
||||
footer={(candidates.data?.total ?? 0) > 20 ? <><span>第 {Math.floor(candidateOffset / 20) + 1} / {Math.ceil((candidates.data?.total ?? 0) / 20)} 页</span><div><Button theme="light" size="small" disabled={candidateOffset === 0} onClick={() => setCandidateOffset(Math.max(0, candidateOffset - 20))}>上一页</Button><Button theme="light" size="small" disabled={candidateOffset + 20 >= (candidates.data?.total ?? 0)} onClick={() => setCandidateOffset(candidateOffset + 20)}>下一页</Button></div></> : undefined}
|
||||
/> : null}
|
||||
</form>
|
||||
</WorkspaceFilterPanel>
|
||||
<Card className="v2-source-diagnostic" bodyStyle={{ padding: 0 }}>
|
||||
<WorkspacePanelHeader
|
||||
title="单车多来源诊断"
|
||||
description={selected ? `${selected.plate || '未绑定车牌'} · ${selected.vin}` : '协议、同协议多终端、推荐原因与可审计策略'}
|
||||
meta={<Tag color={data ? 'blue' : 'grey'} type="light" size="small">{data ? `${data.evidence.locationSources.length} 个来源` : selected ? '正在读取证据' : '等待选择车辆'}</Tag>}
|
||||
actions={selected ? <Button theme="light" icon={<IconRefresh />} onClick={() => diagnostic.refetch()} disabled={diagnostic.isFetching}>刷新该车</Button> : null}
|
||||
/>
|
||||
{diagnostic.isError ? <InlineError message={diagnostic.error.message} onRetry={() => diagnostic.refetch()} /> : null}
|
||||
{!selected ? <Empty className="v2-source-empty" title="先选择一辆车" description="将展示所有协议和同协议多终端、推荐原因、上报周期与可审计策略。" /> : null}
|
||||
{selected && diagnostic.isPending ? <div className="v2-source-loading" role="status"><Spin size="large" /><strong>正在读取来源证据</strong><span>只查询当前车辆,不会扫描整车队。</span></div> : null}
|
||||
{data ? <>
|
||||
<div className="v2-source-summary">
|
||||
<Card className="v2-source-summary-card"><small>车辆</small><strong>{data.evidence.plate || selected?.plate || '未绑定车牌'}</strong><span>{data.evidence.vin}</span></Card>
|
||||
<Card className="v2-source-summary-card"><small>当前推荐</small><strong>{data.evidence.recommendedLocationLabel || '暂无推荐'}</strong><Tag color="blue" type="light" size="small">{data.evidence.recommendedLocationProtocol || '—'}</Tag></Card>
|
||||
@@ -282,8 +305,9 @@ function SourceDiagnosticWorkspace() {
|
||||
<Card className="v2-source-audit" bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="最近策略审计" description="仅记录实际变更,原始 source_key 不对前端暴露" />
|
||||
{data.policy.audit.length ? <ol>{data.policy.audit.map((item) => <li key={`${item.changeType}-${item.version}-${item.sourceRef}`}><b>v{item.version}</b><span>{item.summary}</span><em>{item.changeType === 'provider' ? '提供方' : '策略'} · {item.actor} · {fmt(item.changedAt)}</em></li>)}</ol> : <p>该车辆尚无人工来源策略或提供方变更。</p>}
|
||||
</Card>
|
||||
</> : null}
|
||||
</Card>;
|
||||
</> : null}
|
||||
</Card>
|
||||
</>;
|
||||
}
|
||||
|
||||
export default function OperationsPage() {
|
||||
|
||||
Reference in New Issue
Block a user