import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { FormEvent, useDeferredValue, useEffect, useMemo, useState } from 'react'; import { api } from '../../api/client'; import type { AdminUser, CustomerUserInput } from '../../api/types'; 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[]; vehicleVins: string[]; }; const emptyDraft: Draft = { username: '', displayName: '', password: '', status: 'enabled', customerRef: '', tenantRef: '', menuKeys: ['monitor', 'vehicles', 'tracks', 'statistics'], vehicleVins: [] }; function draftFromUser(user?: AdminUser): Draft { if (!user) return { ...emptyDraft, menuKeys: [...emptyDraft.menuKeys], vehicleVins: [] }; return { username: user.username, displayName: user.displayName, password: '', status: user.status, customerRef: user.customerRef, tenantRef: user.tenantRef, menuKeys: [...user.menuKeys], vehicleVins: [...user.vehicleVins] }; } function formatTime(value?: string) { if (!value) return '尚未登录'; return new Date(value).toLocaleString('zh-CN', { hour12: false }); } function uniqueVehicles(vehicles: T[]) { const byVIN = new Map(); for (const vehicle of vehicles) { const vin = vehicle.vin.trim().toUpperCase(); const current = byVIN.get(vin); if (!vin || (current?.plate && !vehicle.plate)) continue; byVIN.set(vin, vehicle); } return [...byVIN.values()]; } export default function UsersPage() { const queryClient = useQueryClient(); 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 [selectedID, setSelectedID] = useState(null); const [creating, setCreating] = useState(false); 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 [bulkVINs, setBulkVINs] = useState(''); const [vehicleLabels, setVehicleLabels] = useState>({}); const [feedback, setFeedback] = useState(''); 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.vehicleVins), [draft.vehicleVins]); const candidateVehicles = useMemo(() => uniqueVehicles(candidates.data?.items ?? []), [candidates.data?.items]); 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.vehicleVins }; 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); setDraft(draftFromUser()); setVehicleKeyword(''); setBulkVINs(''); setVehicleLabels({}); setFeedback(''); }; const selectCustomer = (user: AdminUser) => { setCreating(false); setSelectedID(user.id); setDraft(draftFromUser(user)); setVehicleKeyword(''); setBulkVINs(''); setFeedback(''); }; const toggleVIN = (vin: string) => setDraft((value) => ({ ...value, vehicleVins: value.vehicleVins.includes(vin) ? value.vehicleVins.filter((item) => item !== vin) : [...value.vehicleVins, vin].sort() })); const addBulkVINs = () => { const next = bulkVINs.split(/[\s,,;;]+/).map((value) => value.trim().toUpperCase()).filter(Boolean); setDraft((value) => ({ ...value, vehicleVins: [...new Set([...value.vehicleVins, ...next])].sort() })); setBulkVINs(''); }; const submit = (event: FormEvent) => { event.preventDefault(); setFeedback(''); save.mutate(); }; return

客户账号与数据权限

账号只会看到已分配的菜单和车辆;权限修改最多在 30 秒内对现有会话生效。

{!creating && !selected ?
选择一个客户账号

查看或调整账号状态、菜单和车辆范围。

:

{creating ? '创建客户账号' : selected?.displayName}

{creating ? '设置登录身份和最小必要权限' : `最近登录:${formatTime(selected?.lastLoginAt)}`}

登录身份

菜单权限 只能分配客户开放菜单

{customerMenus.map((menu) => )}

车辆权限 已选择 {draft.vehicleVins.length} 辆

{draft.vehicleVins.length ? : null}
{deferredVehicleKeyword ?
{candidates.isPending ?

正在搜索车辆…

: candidateVehicles.length === 0 ?

没有匹配车辆

: candidateVehicles.map((vehicle) => )}
: null} {draft.vehicleVins.length ?
{draft.vehicleVins.map((vin) => { const plate = vehicleLabels[vin]; return ; })}
:

尚未分配车辆,客户将无法看到任何车辆数据。

}
{feedback ?

{feedback}

: null}
}
; }