From e95fbe5c19d78ba0037b7f2fafa4713bc4178dbd Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 18 Jul 2026 19:41:01 +0800 Subject: [PATCH] refine Semi UI account access readiness --- .../apps/web/src/v2/pages/UsersPage.test.tsx | 26 ++- .../apps/web/src/v2/pages/UsersPage.tsx | 72 +++++-- .../apps/web/src/v2/styles/workspace.css | 176 ++++++++++++++++++ 3 files changed, 252 insertions(+), 22 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.test.tsx index 95983c09..c5c17f51 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.test.tsx @@ -49,14 +49,15 @@ test('deduplicates vehicle candidates by VIN and renders granted vehicles plate expect(view.container.querySelector('.v2-page-heading')).not.toBeInTheDocument(); expect(view.container.querySelector('.v2-user-discovery-shell')).toContainElement(screen.getByLabelText('账号管理操作')); expect(screen.getByRole('heading', { name: '账号范围', level: 5 })).toBeInTheDocument(); - expect(screen.getByText('按客户名称、登录账号、客户标识与启停状态查找')).toBeInTheDocument(); + expect(screen.getByText('按客户名称、登录账号与权限可用状态查找')).toBeInTheDocument(); expect(screen.getAllByText('1 / 1 个账号')).toHaveLength(2); expect(view.container.querySelector('.v2-user-list.semi-card')).toBeInTheDocument(); expect(view.container.querySelector('.v2-user-filter-panel.v2-workspace-filter-panel.semi-card')).toBeInTheDocument(); expect(view.container.querySelector('.v2-customer-list.semi-list')).toBeInTheDocument(); - expect(screen.getByRole('row', { name: '客户账号目录列' })).toHaveTextContent('客户账号菜单权限车辆权限最近登录状态'); - expect(screen.getByText('启用 1')).toBeInTheDocument(); + expect(screen.getByRole('row', { name: '客户账号目录列' })).toHaveTextContent('客户账号菜单权限车辆权限最近登录访问状态'); + expect(screen.getByText('权限就绪 1')).toBeInTheDocument(); expect(screen.getByText('车辆授权 1')).toBeInTheDocument(); + expect(screen.getByText('权限就绪')).toBeInTheDocument(); expect(view.container.querySelector('.v2-user-directory-metrics')).not.toBeInTheDocument(); expect(customer).toHaveClass('semi-button', 'v2-user-list-item'); expect(customer.closest('.semi-list-item')).toHaveClass('v2-user-list-row'); @@ -65,6 +66,7 @@ test('deduplicates vehicle candidates by VIN and renders granted vehicles plate expect(document.querySelector('.v2-user-editor-sidesheet')).not.toBeInTheDocument(); fireEvent.click(customer); await waitFor(() => expect(customer).toHaveAttribute('aria-expanded', 'true')); + expect(screen.getByLabelText('账号权限概览')).toHaveTextContent('@customer-east菜单 1车辆 1最近登录 尚未登录权限就绪'); const editorSheet = document.querySelector('.v2-user-editor-sidesheet .semi-sidesheet-inner'); expect(editorSheet).toHaveAttribute('aria-label', '客户账号详情'); expect(editorSheet).toHaveStyle({ width: 'min(840px, 100vw)' }); @@ -287,7 +289,7 @@ test('filters the customer directory by status and exposes the active result sco expect(await screen.findByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /选择客户 西部客户/ })).toBeInTheDocument(); - fireEvent.click(screen.getByRole('combobox', { name: '账号状态' })); + fireEvent.click(screen.getByRole('combobox', { name: '访问状态' })); const disabledOption = await waitFor(() => { const option = [...document.querySelectorAll('.semi-select-option')] .find((item) => item.textContent?.includes('停用账号')); @@ -299,12 +301,26 @@ test('filters the customer directory by status and exposes the active result sco await waitFor(() => expect(screen.queryByRole('button', { name: /选择客户 华东客户/ })).not.toBeInTheDocument()); expect(screen.getByRole('button', { name: /选择客户 西部客户/ })).toBeInTheDocument(); expect(screen.getAllByText('1 / 2 个账号')).toHaveLength(2); - expect(screen.getByText('启用 0')).toBeInTheDocument(); + expect(screen.getByText('权限就绪 0')).toBeInTheDocument(); expect(screen.getByText('车辆授权 0')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '清空账号筛选' })); await waitFor(() => expect(screen.getByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument()); expect(screen.getByRole('button', { name: /选择客户 西部客户/ })).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('combobox', { name: '访问状态' })); + const attentionOption = await waitFor(() => { + const option = [...document.querySelectorAll('.semi-select-option')] + .find((item) => item.textContent?.includes('待完善权限')); + expect(option).toBeInTheDocument(); + return option!; + }); + fireEvent.click(attentionOption); + + await waitFor(() => expect(screen.queryByRole('button', { name: /选择客户 西部客户/ })).not.toBeInTheDocument()); + expect(screen.getByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument(); + expect(screen.getByText('待分配车辆')).toBeInTheDocument(); + expect(screen.getByText('待完善 1')).toBeInTheDocument(); }); test('uses a standard Semi empty state when no customer account exists', async () => { diff --git a/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.tsx index 27390e5c..9ef5659c 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.tsx @@ -33,6 +33,14 @@ type Draft = { }; type EditorSection = 'identity' | 'menus' | 'vehicles'; +type CustomerScopeFilter = 'all' | Draft['status'] | 'attention'; +type CustomerAccessState = { + key: 'ready' | 'missing-menus' | 'missing-vehicles' | 'disabled'; + label: string; + shortLabel: string; + color: 'green' | 'amber' | 'grey'; + attention: boolean; +}; 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 @@ -74,6 +82,20 @@ function formatGrantTime(value?: string) { return value.replace('T', ' ').slice(0, 16); } +function customerAccessState(status: Draft['status'], menuCount: number, vehicleCount: number): CustomerAccessState { + if (status === 'disabled') return { key: 'disabled', label: '账号已停用', shortLabel: '停用', color: 'grey', attention: false }; + if (menuCount === 0) return { key: 'missing-menus', label: '待开放菜单', shortLabel: '待菜单', color: 'amber', attention: true }; + if (vehicleCount === 0) return { key: 'missing-vehicles', label: '待分配车辆', shortLabel: '待车辆', color: 'amber', attention: true }; + return { key: 'ready', label: '权限就绪', shortLabel: '就绪', color: 'green', attention: false }; +} + +function customerScopeLabel(scope: CustomerScopeFilter) { + if (scope === 'enabled') return '启用账号'; + if (scope === 'disabled') return '停用账号'; + if (scope === 'attention') return '待完善权限'; + return '全部状态'; +} + export default function UsersPage() { const queryClient = useQueryClient(); const mobileLayout = useMobileLayout(); @@ -81,12 +103,14 @@ export default function UsersPage() { 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 [customerStatus, setCustomerStatus] = useState('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; + const accessState = customerAccessState(user.status, user.menuKeys.length, user.vehicles.length); + if (customerStatus === 'attention' && !accessState.attention) return false; + if (customerStatus !== 'all' && customerStatus !== 'attention' && 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)); @@ -107,7 +131,9 @@ export default function UsersPage() { const [vehicleLabels, setVehicleLabels] = useState>({}); const [feedback, setFeedback] = useState(''); const [editingGrantVIN, setEditingGrantVIN] = useState(''); - const enabledCustomers = useMemo(() => visibleCustomers.filter((user) => user.status === 'enabled').length, [visibleCustomers]); + const attentionCustomerCount = useMemo(() => customers.filter((user) => customerAccessState(user.status, user.menuKeys.length, user.vehicles.length).attention).length, [customers]); + const readyCustomers = useMemo(() => visibleCustomers.filter((user) => customerAccessState(user.status, user.menuKeys.length, user.vehicles.length).key === 'ready').length, [visibleCustomers]); + const attentionCustomers = useMemo(() => visibleCustomers.filter((user) => customerAccessState(user.status, user.menuKeys.length, user.vehicles.length).attention).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] : ''; @@ -115,6 +141,7 @@ export default function UsersPage() { const editorVisible = creating || Boolean(selected); const vehicleComposerVisible = !mobileLayout || creating || draft.vehicleGrants.length === 0 || vehicleComposerOpen; const hasUnsavedChanges = useMemo(() => draftSignature(draft) !== draftSignature(baselineDraft), [baselineDraft, draft]); + const draftAccessState = customerAccessState(draft.status, draft.menuKeys.length, draft.vehicleGrants.length); useSideSheetA11y(editorVisible, '.v2-user-editor-sidesheet', 'v2-user-editor-sheet', '客户账号详情', '关闭账号详情'); useSideSheetA11y(Boolean(editingGrant), '.v2-user-grant-sidesheet', 'v2-user-grant-window', '车辆授权有效期', '关闭车辆授权有效期'); @@ -311,15 +338,15 @@ export default function UsersPage() { ariaLabel="账号管理操作" title="客户访问治理" description="菜单与车辆按最小权限开放,变更最多 30 秒生效" - status={`${customers.length} 个客户账号`} + status={`${customers.length} 个客户账号${attentionCustomerCount ? ` · ${attentionCustomerCount} 待完善` : ' · 权限就绪'}`} meta={本地账号 · 可扩展外部身份源} actions={} /> } showClear value={customerKeyword} onChange={setCustomerKeyword} placeholder="搜索名称、账号或客户标识" />