feat(web): unify Semi UI workspace states

This commit is contained in:
lingniu
2026-07-18 00:59:11 +08:00
parent 159c80b0ae
commit 1cd92715a5
13 changed files with 639 additions and 46 deletions

View File

@@ -43,6 +43,12 @@ function resetWorkspaceScroll() {
content.scrollTo?.({ top: 0, left: 0, behavior: 'auto' });
}
function vehicleLastSeenLabel(value?: string) {
if (!value) return '暂无上报时间';
const normalized = value.replace('T', ' ');
return normalized.length >= 16 ? `${normalized.slice(5, 16)} 更新` : normalized;
}
function VehicleSearch() {
const navigate = useNavigate();
const { session } = usePlatformSession();
@@ -60,11 +66,11 @@ function VehicleSearch() {
const candidates = useQuery({
queryKey: ['vehicle-search-options', candidateParams.toString()],
queryFn: ({ signal }) => api.vehicles(candidateParams, signal),
enabled: candidatesOpen,
staleTime: 30_000,
gcTime: QUERY_MEMORY.optionGcTime
});
const options = useMemo(() => mergeVehicleCandidates(candidates.data?.items ?? []), [candidates.data?.items]);
const quickVehicles = useMemo(() => options.slice(0, mobileLayout ? 5 : 6), [mobileLayout, options]);
useEffect(() => () => window.clearTimeout(closeTimerRef.current), []);
const openCandidates = () => {
window.clearTimeout(closeTimerRef.current);
@@ -136,6 +142,41 @@ function VehicleSearch() {
</div>
{canAdminister(session) ? <Button className="v2-profile-sync-open" theme="borderless" onClick={() => setSyncOpen((value) => !value)}>{syncOpen ? '收起批量同步' : '批量同步主档'}</Button> : null}
</Card>
<Card className="v2-vehicle-recent-card" bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader
title={deferredKeyword ? '匹配车辆快捷入口' : '最近上报车辆'}
description={deferredKeyword ? '按当前搜索条件展示,可直接进入车辆数字档案' : '按照最新上报时间排列,无需搜索即可继续查看'}
meta={candidates.data ? `当前显示 ${formatZhNumber(quickVehicles.length)}` : '授权范围'}
/>
{candidates.isFetching && !candidates.data
? <div className="v2-vehicle-recent-state" role="status"><span className="v2-spinner" /><span></span></div>
: candidates.isError
? <div className="v2-vehicle-recent-state is-error" role="alert"><span>{candidates.error instanceof Error ? candidates.error.message : '授权车辆加载失败'}</span><Button size="small" theme="light" onClick={() => candidates.refetch()}></Button></div>
: quickVehicles.length
? <div className="v2-vehicle-recent-grid" role="list" aria-label={deferredKeyword ? '匹配车辆' : '最近上报车辆'}>
{quickVehicles.map((vehicle) => <Button
key={vehicle.vin}
className="v2-vehicle-recent-item"
theme="borderless"
type="tertiary"
role="listitem"
aria-label={`打开 ${vehicle.plate || '未绑定车牌'} 车辆档案`}
onClick={() => openVehicle(vehicle.vin)}
>
<span className="v2-vehicle-recent-identity">
<strong>{vehicle.plate || '未绑定车牌'}</strong>
<small>{vehicle.vin}</small>
</span>
<span className="v2-vehicle-recent-status">
<Tag color={vehicle.online ? 'green' : 'grey'} type="light" size="small">{vehicle.online ? '在线' : '离线'}</Tag>
<small>{vehicleLastSeenLabel(vehicle.lastSeen)}</small>
</span>
<span className="v2-vehicle-recent-protocols">{vehicle.protocols.slice(0, 2).map((protocol) => <Tag key={protocol} color="blue" type="light" size="small">{protocol}</Tag>)}</span>
<IconChevronRight className="v2-vehicle-recent-arrow" />
</Button>)}
</div>
: <Empty className="v2-vehicle-recent-empty" title={deferredKeyword ? '没有匹配车辆' : '暂无授权车辆'} description={deferredKeyword ? '调整车牌、VIN 或手机号后重试。' : '管理员分配车辆权限后会显示在这里。'} />}
</Card>
{syncOpen ? <Suspense fallback={<Card className="v2-profile-sync-panel v2-profile-sync-loading" bodyStyle={{ padding: 0 }}><span role="status"><span className="v2-spinner" /></span></Card>}><VehicleProfileSyncPanel onClose={() => setSyncOpen(false)} /></Suspense> : null}
</section>;
}