refine Semi UI access governance
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user