feat: unify account directory metrics with semi ui

This commit is contained in:
lingniu
2026-07-19 12:10:36 +08:00
parent 7df015b0f0
commit 1e57fdfff1
4 changed files with 93 additions and 16 deletions

View File

@@ -51,15 +51,18 @@ test('deduplicates vehicle candidates by VIN and renders granted vehicles plate
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.getAllByText('1 / 1 个账号')).toHaveLength(2);
expect(screen.getByText('1 / 1 个账号')).toBeInTheDocument();
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.getByText('车辆授权 1')).toBeInTheDocument();
expect(screen.getByText('权限就绪')).toBeInTheDocument();
expect(view.container.querySelector('.v2-user-directory-metrics')).not.toBeInTheDocument();
const directoryMetrics = screen.getByRole('list', { name: '客户权限目录统计' });
expect(directoryMetrics.closest('.v2-user-directory-metric-rail')).toHaveClass('v2-workspace-metric-rail', 'is-queue');
expect(directoryMetrics.querySelectorAll(':scope > [role="listitem"]')).toHaveLength(4);
expect(directoryMetrics).toHaveTextContent('当前账号1 / 1全部客户账号');
expect(directoryMetrics).toHaveTextContent('待完善0缺少菜单或车辆');
expect(directoryMetrics).toHaveTextContent('权限就绪1启用且权限完整');
expect(directoryMetrics).toHaveTextContent('车辆授权1当前范围合计');
expect(customer).toHaveClass('semi-button', 'v2-user-list-item');
expect(customer.closest('.semi-list-item')).toHaveClass('v2-user-list-row');
expect(customer).toHaveAttribute('aria-pressed', 'false');
@@ -228,6 +231,7 @@ test('keeps the customer directory visible on mobile and opens details on demand
const view = render(<QueryClientProvider client={client}><UsersPage /></QueryClientProvider>);
expect(await screen.findByText('客户权限目录')).toBeInTheDocument();
expect(screen.getByRole('list', { name: '客户权限目录统计' }).querySelectorAll(':scope > [role="listitem"].is-primary')).toHaveLength(2);
const customer = await screen.findByRole('button', { name: /选择客户 华东客户/ });
expect(view.container.querySelector('.v2-user-mobile-picker')).not.toBeInTheDocument();
expect(view.container.querySelector('.v2-customer-list.semi-list')).toBeInTheDocument();
@@ -366,9 +370,10 @@ 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('1 / 2 个账号')).toBeInTheDocument();
expect(screen.getByRole('list', { name: '客户权限目录统计' })).toHaveTextContent('当前账号1 / 2筛选结果 / 全部客户');
expect(screen.getByRole('list', { name: '客户权限目录统计' })).toHaveTextContent('权限就绪0启用且权限完整');
expect(screen.getByRole('list', { name: '客户权限目录统计' })).toHaveTextContent('车辆授权0当前范围合计');
fireEvent.click(screen.getByRole('button', { name: '清空账号筛选' }));
await waitFor(() => expect(screen.getByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument());
@@ -395,7 +400,7 @@ 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.getByText('待分配车辆')).toBeInTheDocument();
expect(screen.getByText('待完善 1')).toBeInTheDocument();
expect(screen.getByRole('list', { name: '客户权限目录统计' })).toHaveTextContent('待完善1缺少菜单或车辆');
});
test('uses a standard Semi empty state when no customer account exists', async () => {

View File

@@ -12,6 +12,7 @@ import { VehicleCandidateList } from '../shared/VehicleCandidateList';
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
import { WorkspaceConfirmDialog } from '../shared/WorkspaceDialog';
import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail';
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
import { WorkspaceSideSheet, type WorkspaceSideSheetBadgeColor } from '../shared/WorkspaceSideSheet';
import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
@@ -153,6 +154,36 @@ export default function UsersPage() {
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 directoryMetrics: WorkspaceQueueMetricRailItem[] = [
{
label: '当前账号',
value: <>{visibleCustomers.length}<i className="v2-user-directory-metric-unit"> / {customers.length}</i></>,
note: customerKeyword.trim() || customerStatus !== 'all' ? '筛选结果 / 全部客户' : '全部客户账号',
tone: 'primary',
emphasis: 'primary'
},
{
label: '待完善',
value: attentionCustomers,
note: '缺少菜单或车辆',
tone: attentionCustomers ? 'warning' : 'success',
emphasis: 'primary'
},
{
label: '权限就绪',
value: readyCustomers,
note: '启用且权限完整',
tone: 'success',
emphasis: 'secondary'
},
{
label: '车辆授权',
value: grantedVehicles,
note: '当前范围合计',
tone: 'neutral',
emphasis: 'secondary'
}
];
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)));
@@ -469,12 +500,12 @@ export default function UsersPage() {
className="v2-user-directory-header"
title="客户权限目录"
description="当前范围内的账号、菜单与车辆授权摘要"
meta={<span className="v2-user-directory-summary">
<Tag color={visibleCustomers.length === customers.length ? 'grey' : 'blue'} type="light" size="small">{visibleCustomers.length} / {customers.length} </Tag>
<Typography.Text className="is-ready" type="tertiary"> {readyCustomers}</Typography.Text>
{attentionCustomers ? <Tag className="is-attention" color="amber" type="light" size="small"> {attentionCustomers}</Tag> : null}
<Typography.Text type="tertiary"> {grantedVehicles}</Typography.Text>
</span>}
/>
<WorkspaceMetricRail
variant="queue"
className="v2-user-directory-metric-rail"
ariaLabel="客户权限目录统计"
items={directoryMetrics}
/>
<div className="v2-user-directory-body">
{users.isPending ? <PanelLoading className="v2-user-list-loading" title="正在加载客户账号" description="账号目录和授权摘要就绪后会自动显示。" /> : customers.length === 0 ? <PanelEmpty className="v2-user-list-empty" title="还没有客户账号" description="创建客户账号后,可在这里配置菜单和车辆范围。" action={<Button theme="solid" onClick={startCreate}></Button>} /> : <>

View File

@@ -304,7 +304,10 @@ describe('V2 production entry', () => {
expect(corePageSources.UsersPage).toContain('className="v2-user-command-bar"');
expect(corePageSources.UsersPage).toContain('className="v2-user-discovery-shell"');
expect(corePageSources.UsersPage).toContain('className="v2-user-directory-columns"');
expect(corePageSources.UsersPage).not.toContain('v2-user-directory-metrics');
expect(corePageSources.UsersPage).toContain("from '../shared/WorkspaceMetricRail'");
expect(corePageSources.UsersPage).toContain('className="v2-user-directory-metric-rail"');
expect(corePageSources.UsersPage).toContain('ariaLabel="客户权限目录统计"');
expect(corePageSources.UsersPage).not.toContain('v2-user-directory-summary');
expect(corePageSources.UsersPage).toContain('aria-expanded={selectedID === user.id && !creating}');
expect(corePageSources.UsersPage).toContain('<Tabs className="v2-user-editor-tabs"');
expect(corePageSources.UsersPage).toContain('<Tabs.TabPane');

View File

@@ -23325,3 +23325,41 @@
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
/*
* Account governance uses the same compact evidence rail as the operational
* workspaces. It replaces small header metadata and keeps the directory itself
* as the dominant surface.
*/
.v2-user-directory-metric-rail.v2-workspace-metric-rail.is-queue.semi-card {
flex: 0 0 auto;
}
.v2-user-directory-metric-rail .v2-workspace-metric-list {
grid-template-columns: 1.15fr 1fr .95fr .95fr;
}
.v2-user-directory-metric-unit {
margin-left: 2px;
color: #6f90bb;
font-size: 10px;
font-style: normal;
font-weight: 600;
letter-spacing: 0;
}
.v2-user-directory-metric-rail .v2-workspace-metric-list > span.is-primary.is-warning {
background: linear-gradient(145deg, #fff7ea 0%, #fffdf8 100%);
box-shadow: inset 0 -3px #e59a2f;
}
.v2-user-directory-metric-rail .v2-workspace-metric-list > span.is-primary.is-success {
background: linear-gradient(145deg, #eefaf5 0%, #fbfefd 100%);
box-shadow: inset 0 -3px #20a678;
}
@media (max-width: 680px) {
.v2-user-directory-metric-rail .v2-workspace-metric-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}