refine Semi UI access governance

This commit is contained in:
lingniu
2026-07-18 19:12:07 +08:00
parent 84684f2dc1
commit 5d4aa92cc1
3 changed files with 327 additions and 8 deletions

View File

@@ -21,6 +21,11 @@ import { useMobileLayout } from '../hooks/useMobileLayout';
import { useSideSheetA11y } from '../hooks/useSideSheetA11y';
const PROTOCOLS = ['GB32960', 'JT808', 'YUTONG_MQTT'] as const;
const protocolThresholdMeta: Record<(typeof PROTOCOLS)[number], { label: string; description: string }> = {
GB32960: { label: 'GB32960', description: '国标远程服务' },
JT808: { label: 'JT808', description: '道路运输终端' },
YUTONG_MQTT: { label: 'YUTONG MQTT', description: '宇通车辆平台' }
};
const compactAccessTimeFormatter = new Intl.DateTimeFormat('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'Asia/Shanghai' });
const EMPTY_FILTERS = { keyword: '', protocol: '', oem: '', connectionState: '', onlineState: '', model: '', provider: '', firstSeenFrom: '', firstSeenTo: '', latestSeenFrom: '', latestSeenTo: '', delayState: '' };
type Filters = typeof EMPTY_FILTERS;
@@ -261,22 +266,52 @@ function VehicleInspector({ row, onClose, sheet = false, mobile = false }: { row
function IdentityQueue({ items, total }: { items: AccessUnresolvedIdentity[]; total: number }) {
if (!total) return null;
return <Collapse id="access-identity-queue" className="v2-access-identity-queue-v3" keepDOM>
return <Collapse id="access-identity-queue" className="v2-access-identity-queue-v3" defaultActiveKey="unresolved-identities" keepDOM={false} lazyRender>
<Collapse.Panel itemKey="unresolved-identities" header={<span className="v2-access-collapse-title"><span><strong></strong><small> VIN </small></span><Tag color="orange" type="light" size="small">{total.toLocaleString('zh-CN')} </Tag></span>}>
<div className="v2-access-identity-grid">{items.slice(0, 6).map((item) => <Card key={item.id} className="v2-access-identity-card" bodyStyle={{ padding: 0 }}>
<header><b>{item.identifierMasked}</b><Tag color="blue" type="light" size="small">{item.protocol}</Tag></header>
<span>{item.plate || '车牌待核对'} · {formatAccessTime(item.latestSeenAt)}</span>
<small>{item.recommendedAction}</small>
<header><span><small></small><b>{item.identifierMasked}</b></span><Tag color="blue" type="light" size="small">{item.protocol}</Tag></header>
<div className="v2-access-identity-facts">
<span><small></small><strong>{item.plate || '待核对'}</strong></span>
<span><small></small><strong><AccessTime value={item.latestSeenAt} compact /></strong></span>
</div>
<footer><small></small><strong>{item.recommendedAction}</strong></footer>
</Card>)}</div>
{total > 6 ? <p className="v2-access-identity-more"> 6 {(total - 6).toLocaleString('zh-CN')} </p> : null}
</Collapse.Panel>
</Collapse>;
}
function ThresholdSettings({ config, draft, editable, saving, error, onChange, onSave }: { config?: AccessThresholdConfig; draft?: AccessThresholdUpdate; editable: boolean; saving: boolean; error?: string; onChange: (next: AccessThresholdUpdate) => void; onSave: () => void }) {
if (!draft) return null;
return <Collapse className="v2-access-settings" keepDOM>
const thresholdField = (label: string, description: string, value: number, update: (next: number) => void) => <label>
<span><b>{label}</b><small>{description}</small></span>
<Input suffix="秒" type="number" value={String(value)} onChange={(next) => update(Number(next))} />
<em>{formatSeconds(value)}</em>
</label>;
return <Collapse className="v2-access-settings" keepDOM={false} lazyRender>
<Collapse.Panel itemKey="access-thresholds" header={<span className="v2-access-collapse-title"><span><strong>线</strong><small>线线</small></span><Tag color="blue" type="light" size="small">v{config?.version ?? '—'}</Tag></span>}>
<fieldset disabled={!editable}><label><span></span><Input suffix="秒" type="number" value={String(draft.defaultThresholdSec)} onChange={(value) => onChange({ ...draft, defaultThresholdSec: Number(value) })} /></label><label><span>线</span><Input suffix="秒" type="number" value={String(draft.longOfflineSec)} onChange={(value) => onChange({ ...draft, longOfflineSec: Number(value) })} /></label>{PROTOCOLS.map((protocol) => <label key={protocol}><span>{protocol}</span><Input suffix="秒" type="number" value={String(draft.protocols.find((item) => item.protocol === protocol)?.thresholdSec ?? draft.defaultThresholdSec)} onChange={(value) => onChange({ ...draft, protocols: updateProtocolThreshold(draft.protocols, protocol, Number(value)) })} /></label>)}{error ? <p>{error}</p> : null}{editable ? <Button theme="solid" icon={<IconSave />} onClick={onSave} disabled={saving}>{saving ? '保存中' : '保存阈值'}</Button> : <small></small>}</fieldset>
<fieldset disabled={!editable}>
<section className="v2-access-threshold-group">
<header><strong></strong><small></small></header>
<div className="v2-access-threshold-fields">
{thresholdField('全局在线阈值', '未单独配置协议时使用', draft.defaultThresholdSec, (value) => onChange({ ...draft, defaultThresholdSec: value }))}
{thresholdField('数据延迟异常', '事件时间与接收时间差值', draft.delayThresholdSec, (value) => onChange({ ...draft, delayThresholdSec: value }))}
{thresholdField('长离线阈值', '进入长期离线治理范围', draft.longOfflineSec, (value) => onChange({ ...draft, longOfflineSec: value }))}
</div>
</section>
<section className="v2-access-threshold-group">
<header><strong>线</strong><small></small></header>
<div className="v2-access-threshold-fields">
{PROTOCOLS.map((protocol) => {
const value = draft.protocols.find((item) => item.protocol === protocol)?.thresholdSec ?? draft.defaultThresholdSec;
const meta = protocolThresholdMeta[protocol];
return <span key={protocol}>{thresholdField(meta.label, meta.description, value, (next) => onChange({ ...draft, protocols: updateProtocolThreshold(draft.protocols, protocol, next) }))}</span>;
})}
</div>
</section>
{error ? <p className="v2-access-threshold-error" role="alert">{error}</p> : null}
{editable ? <footer className="v2-access-threshold-footer"><span><strong></strong><small>线</small></span><Button theme="solid" icon={<IconSave />} onClick={onSave} disabled={saving}>{saving ? '保存中' : '保存阈值'}</Button></footer> : <small></small>}
</fieldset>
</Collapse.Panel>
</Collapse>;
}
@@ -440,7 +475,7 @@ export default function AccessPage() {
>
{governanceOpen ? <div className="v2-access-governance">
<Card className="v2-access-governance-summary" bodyStyle={{ padding: 0 }}>
<div><small></small><strong>{(unresolvedQuery.data?.total ?? 0).toLocaleString('zh-CN')}</strong></div>
<div className={(unresolvedQuery.data?.total ?? 0) > 0 ? 'is-attention' : ''}><small></small><strong>{(unresolvedQuery.data?.total ?? 0).toLocaleString('zh-CN')}</strong><span>{(unresolvedQuery.data?.total ?? 0) > 0 ? '优先处理' : '暂无待办'}</span></div>
<div><small></small><strong>v{thresholdQuery.data?.version ?? '—'}</strong></div>
<div><small></small><strong>{(summary?.totalVehicles ?? 0).toLocaleString('zh-CN')}</strong></div>
</Card>