import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { IconChevronRight, IconDelete, IconPlus, IconRefresh, IconSearch } from '@douyinfe/semi-icons'; import { Avatar, Button, Card, Checkbox, Collapse, Empty, Input, List, Select, SideSheet, Spin, Switch, Tabs, Tag, Typography } from '@douyinfe/semi-ui'; import { FormEvent, useDeferredValue, useEffect, useMemo, useState } from 'react'; import { api } from '../../api/client'; import type { AdminUser, CustomerUserInput, CustomerVehicleGrantInput } from '../../api/types'; import { useMobileLayout } from '../hooks/useMobileLayout'; import { useSideSheetA11y } from '../hooks/useSideSheetA11y'; import { TablePagination } from '../shared/TablePagination'; import { VehicleCandidateList } from '../shared/VehicleCandidateList'; import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { mergeVehicleCandidates } from '../shared/vehicleCandidates'; import { PanelEmpty, PanelLoading } from '../shared/AsyncState'; const customerMenus = [ { key: 'monitor', label: '全局监控', description: '查看授权车辆的实时位置与状态' }, { key: 'vehicles', label: '车辆查询', description: '查看车辆档案与最新遥测' }, { key: 'tracks', label: '轨迹回放', description: '查询授权车辆的历史轨迹' }, { key: 'statistics', label: '里程查询', description: '查询授权车辆的每日与区间里程' } ]; type Draft = { username: string; displayName: string; password: string; status: 'enabled' | 'disabled'; customerRef: string; tenantRef: string; menuKeys: string[]; vehicleGrants: CustomerVehicleGrantInput[]; }; type EditorSection = 'identity' | 'menus' | 'vehicles'; const shanghaiDateTimeFormatter = new Intl.DateTimeFormat('sv-SE', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }); function dateTimeInput(value?: string) { if (!value) return ''; const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) return ''; return shanghaiDateTimeFormatter.format(parsed).replace(' ', 'T'); } const emptyDraft: Draft = { username: '', displayName: '', password: '', status: 'enabled', customerRef: '', tenantRef: '', menuKeys: ['monitor', 'vehicles', 'tracks', 'statistics'], vehicleGrants: [] }; function draftFromUser(user?: AdminUser): Draft { if (!user) return { ...emptyDraft, menuKeys: [...emptyDraft.menuKeys], vehicleGrants: [] }; return { username: user.username, displayName: user.displayName, password: '', status: user.status, customerRef: user.customerRef, tenantRef: user.tenantRef, menuKeys: [...user.menuKeys], vehicleGrants: user.vehicles.map((vehicle) => ({ vin: vehicle.vin, validFrom: dateTimeInput(vehicle.validFrom), validTo: dateTimeInput(vehicle.validTo) })) }; } function formatTime(value?: string) { if (!value) return '尚未登录'; return new Date(value).toLocaleString('zh-CN', { hour12: false }); } function formatGrantTime(value?: string) { if (!value) return '未设置'; return value.replace('T', ' ').slice(0, 16); } export default function UsersPage() { const queryClient = useQueryClient(); const mobileLayout = useMobileLayout(); const users = useQuery({ queryKey: ['admin-users'], queryFn: ({ signal }) => api.adminUsers(signal), staleTime: 10_000 }); const customers = useMemo(() => (users.data ?? []).filter((user) => user.userType === 'customer'), [users.data]); const [customerKeyword, setCustomerKeyword] = useState(''); const deferredCustomerKeyword = useDeferredValue(customerKeyword.trim()); const [customerStatus, setCustomerStatus] = useState<'all' | Draft['status']>('all'); const [filtersCollapsed, setFiltersCollapsed] = useState(true); const visibleCustomers = useMemo(() => { const keyword = deferredCustomerKeyword.toLocaleLowerCase('zh-CN'); return customers.filter((user) => { if (customerStatus !== 'all' && user.status !== customerStatus) return false; if (!keyword) return true; return [user.displayName, user.username, user.customerRef, user.tenantRef] .some((value) => value?.toLocaleLowerCase('zh-CN').includes(keyword)); }); }, [customerStatus, customers, deferredCustomerKeyword]); const [selectedID, setSelectedID] = useState(null); const [creating, setCreating] = useState(false); const [activeSection, setActiveSection] = useState('identity'); const selected = useMemo(() => customers.find((user) => user.id === selectedID), [customers, selectedID]); const [draft, setDraft] = useState(() => draftFromUser()); const [vehicleKeyword, setVehicleKeyword] = useState(''); const deferredVehicleKeyword = useDeferredValue(vehicleKeyword.trim()); const [assignedKeyword, setAssignedKeyword] = useState(''); const [assignedPage, setAssignedPage] = useState(1); const [bulkVINs, setBulkVINs] = useState(''); const [vehicleLabels, setVehicleLabels] = useState>({}); const [feedback, setFeedback] = useState(''); const [editingGrantVIN, setEditingGrantVIN] = useState(''); const enabledCustomers = useMemo(() => visibleCustomers.filter((user) => user.status === 'enabled').length, [visibleCustomers]); const grantedVehicles = useMemo(() => visibleCustomers.reduce((total, user) => total + user.vehicles.length, 0), [visibleCustomers]); const editingGrant = useMemo(() => draft.vehicleGrants.find((grant) => grant.vin === editingGrantVIN), [draft.vehicleGrants, editingGrantVIN]); const editingGrantPlate = editingGrant ? vehicleLabels[editingGrant.vin] : ''; const grantWindowInvalid = Boolean(editingGrant && (!editingGrant.validFrom || (editingGrant.validTo && editingGrant.validTo <= editingGrant.validFrom))); const editorVisible = creating || Boolean(selected); useSideSheetA11y(editorVisible, '.v2-user-editor-sidesheet', 'v2-user-editor-sheet', '客户账号详情', '关闭账号详情'); useSideSheetA11y(Boolean(editingGrant), '.v2-user-grant-sidesheet', 'v2-user-grant-window', '车辆授权有效期', '关闭车辆授权有效期'); useEffect(() => { if (!creating && selected) { setDraft(draftFromUser(selected)); setVehicleLabels(Object.fromEntries(selected.vehicles.map((vehicle) => [vehicle.vin, vehicle.plate]))); } }, [creating, selected]); const candidates = useQuery({ queryKey: ['permission-vehicle-candidates', deferredVehicleKeyword], queryFn: ({ signal }) => api.vehicleCoverage(new URLSearchParams({ keyword: deferredVehicleKeyword, limit: '20', offset: '0' }), signal), enabled: deferredVehicleKeyword.length >= 1, staleTime: 30_000 }); const assigned = useMemo(() => new Set(draft.vehicleGrants.map((grant) => grant.vin)), [draft.vehicleGrants]); const candidateVehicles = useMemo(() => mergeVehicleCandidates(candidates.data?.items ?? []), [candidates.data?.items]); const assignedPageSize = mobileLayout ? 4 : 10; const filteredAssignedGrants = useMemo(() => { const keyword = assignedKeyword.trim().toLocaleLowerCase('zh-CN'); if (!keyword) return draft.vehicleGrants; return draft.vehicleGrants.filter((grant) => [grant.vin, vehicleLabels[grant.vin]] .some((value) => value?.toLocaleLowerCase('zh-CN').includes(keyword))); }, [assignedKeyword, draft.vehicleGrants, vehicleLabels]); const assignedTotalPages = Math.max(1, Math.ceil(filteredAssignedGrants.length / assignedPageSize)); const safeAssignedPage = Math.min(assignedPage, assignedTotalPages); const visibleAssignedGrants = useMemo(() => { const offset = (safeAssignedPage - 1) * assignedPageSize; return filteredAssignedGrants.slice(offset, offset + assignedPageSize); }, [assignedPageSize, filteredAssignedGrants, safeAssignedPage]); useEffect(() => { if (candidateVehicles.length === 0) return; setVehicleLabels((current) => { const next = { ...current }; for (const vehicle of candidateVehicles) next[vehicle.vin] = vehicle.plate; return next; }); }, [candidateVehicles]); const save = useMutation({ mutationFn: async () => { const input: CustomerUserInput = { displayName: draft.displayName.trim(), password: draft.password || undefined, status: draft.status, customerRef: draft.customerRef.trim(), tenantRef: draft.tenantRef.trim(), menuKeys: draft.menuKeys, vehicleVins: draft.vehicleGrants.map((grant) => grant.vin), vehicleGrants: draft.vehicleGrants }; if (creating) return api.createCustomerUser({ ...input, username: draft.username.trim(), password: draft.password }); if (!selected) throw new Error('请先选择客户账号'); return api.updateCustomerUser(selected.id, input); }, onSuccess: async (result) => { setFeedback(creating ? '客户账号已创建' : '账号与权限已更新'); setCreating(false); setSelectedID(result.id); setDraft((value) => ({ ...value, password: '' })); await queryClient.invalidateQueries({ queryKey: ['admin-users'] }); }, onError: (error) => setFeedback(error instanceof Error ? error.message : '保存失败') }); const startCreate = () => { setCreating(true); setSelectedID(null); setActiveSection('identity'); setDraft(draftFromUser()); setVehicleKeyword(''); setAssignedKeyword(''); setAssignedPage(1); setBulkVINs(''); setVehicleLabels({}); setFeedback(''); setEditingGrantVIN(''); }; const closeEditor = () => { setCreating(false); setSelectedID(null); setDraft(draftFromUser()); setVehicleKeyword(''); setAssignedKeyword(''); setAssignedPage(1); setBulkVINs(''); setVehicleLabels({}); setFeedback(''); setEditingGrantVIN(''); }; const selectCustomer = (user: AdminUser) => { setCreating(false); setSelectedID(user.id); setActiveSection('vehicles'); setDraft(draftFromUser(user)); setVehicleKeyword(''); setAssignedKeyword(''); setAssignedPage(1); setBulkVINs(''); setFeedback(''); setEditingGrantVIN(''); }; const toggleVIN = (vin: string) => { if (!assigned.has(vin)) { setAssignedKeyword(''); setAssignedPage(1); } setDraft((value) => ({ ...value, vehicleGrants: value.vehicleGrants.some((grant) => grant.vin === vin) ? value.vehicleGrants.filter((grant) => grant.vin !== vin) : [...value.vehicleGrants, { vin, validFrom: dateTimeInput(new Date().toISOString()), validTo: '' }].sort((left, right) => left.vin.localeCompare(right.vin)) })); if (editingGrantVIN === vin) setEditingGrantVIN(''); }; const updateGrant = (vin: string, patch: Partial) => setDraft((value) => ({ ...value, vehicleGrants: value.vehicleGrants.map((grant) => grant.vin === vin ? { ...grant, ...patch } : grant) })); const addBulkVINs = () => { const next = bulkVINs.split(/[\s,,;;]+/).map((value) => value.trim().toUpperCase()).filter(Boolean); setDraft((value) => { const existing = new Set(value.vehicleGrants.map((grant) => grant.vin)); const added = next.filter((vin) => !existing.has(vin)).map((vin) => ({ vin, validFrom: dateTimeInput(new Date().toISOString()), validTo: '' })); return { ...value, vehicleGrants: [...value.vehicleGrants, ...added].sort((left, right) => left.vin.localeCompare(right.vin)) }; }); setAssignedKeyword(''); setAssignedPage(1); setBulkVINs(''); }; const persistDraft = () => { if (save.isPending) return; setFeedback(''); if (!draft.displayName.trim() || (creating && (!draft.username.trim() || !draft.password))) { setActiveSection('identity'); setFeedback('请先完善登录身份中的必填信息'); return; } const invalidGrant = draft.vehicleGrants.find((grant) => !grant.validFrom || (grant.validTo && grant.validTo <= grant.validFrom)); if (invalidGrant) { setActiveSection('vehicles'); setEditingGrantVIN(invalidGrant.vin); setFeedback('请检查车辆授权有效期:停用时间必须晚于启用时间'); return; } save.mutate(); }; const submit = (event: FormEvent) => { event.preventDefault(); persistDraft(); }; return
本地账号 · 可扩展外部身份源} actions={} /> setFiltersCollapsed((value) => !value)} >
菜单权限{draft.menuKeys.length}} itemKey="menus"> 菜单权限 客户开放菜单} headerLine>
{customerMenus.map((menu) => )}
车辆权限{draft.vehicleGrants.length}} itemKey="vehicles"> 车辆权限与有效期 {draft.vehicleGrants.length} 辆} headerLine headerExtraContent={draft.vehicleGrants.length ? : null}>
{deferredVehicleKeyword ? candidates.refetch()} selectedVins={assigned} selectedLabel="已分配" onSelect={(vehicle) => toggleVIN(vehicle.vin)} /> : null} {draft.vehicleGrants.length ? <>
已授权车辆{assignedKeyword ? `${filteredAssignedGrants.length} 条匹配结果` : `${draft.vehicleGrants.length} 辆车拥有当前访问权限`} } showClear value={assignedKeyword} onChange={(value) => { setAssignedKeyword(value); setAssignedPage(1); }} placeholder="筛选已授权车牌或 VIN" />
{visibleAssignedGrants.length ?
{visibleAssignedGrants.map((grant) => { const plate = vehicleLabels[grant.vin]; return
{plate || '未登记车牌'}{grant.vin}
启用{formatGrantTime(grant.validFrom)}停用{grant.validTo ? formatGrantTime(grant.validTo) : '持续有效'}
; })}
: }
第 {(safeAssignedPage - 1) * assignedPageSize + (filteredAssignedGrants.length ? 1 : 0)}–{Math.min(safeAssignedPage * assignedPageSize, filteredAssignedGrants.length)} 条,共 {filteredAssignedGrants.length} 辆} onPageChange={setAssignedPage} />
: } {!creating && selected?.grantHistory?.length ? 历次开放记录 {selected.grantHistory.length} 条}>
{selected.grantHistory.map((item) =>
{item.plate || '未登记车牌'}{item.vin}{item.validTo ? '已结束' : '有效中'}

{formatTime(item.validFrom)} {item.validTo ? formatTime(item.validTo) : '持续有效'}

开通:{item.grantedBy || '—'}{item.revokedBy ? ` · 停用:${item.revokedBy}` : ''} · {item.sourceSystem}
)}
: null}
: null} 调整车辆有效期{editingGrant ? `${editingGrantPlate || '未登记车牌'} · ${editingGrant.vin}` : '车辆授权时间窗口'}
} onCancel={() => setEditingGrantVIN('')} footer={
完成后仍需保存账号权限才会生效
} > {editingGrant ?
车牌{editingGrantPlate || '未登记车牌'} VIN{editingGrant.vin} {grantWindowInvalid ?

请设置启用时间,并确保停用时间晚于启用时间。

:

不设置停用时间时,车辆授权将持续有效。

}
: null} ; }