refine Semi UI account access readiness
This commit is contained in:
@@ -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-page-heading')).not.toBeInTheDocument();
|
||||||
expect(view.container.querySelector('.v2-user-discovery-shell')).toContainElement(screen.getByLabelText('账号管理操作'));
|
expect(view.container.querySelector('.v2-user-discovery-shell')).toContainElement(screen.getByLabelText('账号管理操作'));
|
||||||
expect(screen.getByRole('heading', { name: '账号范围', level: 5 })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: '账号范围', level: 5 })).toBeInTheDocument();
|
||||||
expect(screen.getByText('按客户名称、登录账号、客户标识与启停状态查找')).toBeInTheDocument();
|
expect(screen.getByText('按客户名称、登录账号与权限可用状态查找')).toBeInTheDocument();
|
||||||
expect(screen.getAllByText('1 / 1 个账号')).toHaveLength(2);
|
expect(screen.getAllByText('1 / 1 个账号')).toHaveLength(2);
|
||||||
expect(view.container.querySelector('.v2-user-list.semi-card')).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-user-filter-panel.v2-workspace-filter-panel.semi-card')).toBeInTheDocument();
|
||||||
expect(view.container.querySelector('.v2-customer-list.semi-list')).toBeInTheDocument();
|
expect(view.container.querySelector('.v2-customer-list.semi-list')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('row', { name: '客户账号目录列' })).toHaveTextContent('客户账号菜单权限车辆权限最近登录状态');
|
expect(screen.getByRole('row', { name: '客户账号目录列' })).toHaveTextContent('客户账号菜单权限车辆权限最近登录访问状态');
|
||||||
expect(screen.getByText('启用 1')).toBeInTheDocument();
|
expect(screen.getByText('权限就绪 1')).toBeInTheDocument();
|
||||||
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(view.container.querySelector('.v2-user-directory-metrics')).not.toBeInTheDocument();
|
||||||
expect(customer).toHaveClass('semi-button', 'v2-user-list-item');
|
expect(customer).toHaveClass('semi-button', 'v2-user-list-item');
|
||||||
expect(customer.closest('.semi-list-item')).toHaveClass('v2-user-list-row');
|
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();
|
expect(document.querySelector('.v2-user-editor-sidesheet')).not.toBeInTheDocument();
|
||||||
fireEvent.click(customer);
|
fireEvent.click(customer);
|
||||||
await waitFor(() => expect(customer).toHaveAttribute('aria-expanded', 'true'));
|
await waitFor(() => expect(customer).toHaveAttribute('aria-expanded', 'true'));
|
||||||
|
expect(screen.getByLabelText('账号权限概览')).toHaveTextContent('@customer-east菜单 1车辆 1最近登录 尚未登录权限就绪');
|
||||||
const editorSheet = document.querySelector<HTMLElement>('.v2-user-editor-sidesheet .semi-sidesheet-inner');
|
const editorSheet = document.querySelector<HTMLElement>('.v2-user-editor-sidesheet .semi-sidesheet-inner');
|
||||||
expect(editorSheet).toHaveAttribute('aria-label', '客户账号详情');
|
expect(editorSheet).toHaveAttribute('aria-label', '客户账号详情');
|
||||||
expect(editorSheet).toHaveStyle({ width: 'min(840px, 100vw)' });
|
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(await screen.findByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('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 disabledOption = await waitFor(() => {
|
||||||
const option = [...document.querySelectorAll<HTMLElement>('.semi-select-option')]
|
const option = [...document.querySelectorAll<HTMLElement>('.semi-select-option')]
|
||||||
.find((item) => item.textContent?.includes('停用账号'));
|
.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());
|
await waitFor(() => expect(screen.queryByRole('button', { name: /选择客户 华东客户/ })).not.toBeInTheDocument());
|
||||||
expect(screen.getByRole('button', { name: /选择客户 西部客户/ })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /选择客户 西部客户/ })).toBeInTheDocument();
|
||||||
expect(screen.getAllByText('1 / 2 个账号')).toHaveLength(2);
|
expect(screen.getAllByText('1 / 2 个账号')).toHaveLength(2);
|
||||||
expect(screen.getByText('启用 0')).toBeInTheDocument();
|
expect(screen.getByText('权限就绪 0')).toBeInTheDocument();
|
||||||
expect(screen.getByText('车辆授权 0')).toBeInTheDocument();
|
expect(screen.getByText('车辆授权 0')).toBeInTheDocument();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: '清空账号筛选' }));
|
fireEvent.click(screen.getByRole('button', { name: '清空账号筛选' }));
|
||||||
await waitFor(() => expect(screen.getByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument());
|
||||||
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<HTMLElement>('.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 () => {
|
test('uses a standard Semi empty state when no customer account exists', async () => {
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ type Draft = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type EditorSection = 'identity' | 'menus' | 'vehicles';
|
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', {
|
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
|
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);
|
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() {
|
export default function UsersPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const mobileLayout = useMobileLayout();
|
const mobileLayout = useMobileLayout();
|
||||||
@@ -81,12 +103,14 @@ export default function UsersPage() {
|
|||||||
const customers = useMemo(() => (users.data ?? []).filter((user) => user.userType === 'customer'), [users.data]);
|
const customers = useMemo(() => (users.data ?? []).filter((user) => user.userType === 'customer'), [users.data]);
|
||||||
const [customerKeyword, setCustomerKeyword] = useState('');
|
const [customerKeyword, setCustomerKeyword] = useState('');
|
||||||
const deferredCustomerKeyword = useDeferredValue(customerKeyword.trim());
|
const deferredCustomerKeyword = useDeferredValue(customerKeyword.trim());
|
||||||
const [customerStatus, setCustomerStatus] = useState<'all' | Draft['status']>('all');
|
const [customerStatus, setCustomerStatus] = useState<CustomerScopeFilter>('all');
|
||||||
const [filtersCollapsed, setFiltersCollapsed] = useState(true);
|
const [filtersCollapsed, setFiltersCollapsed] = useState(true);
|
||||||
const visibleCustomers = useMemo(() => {
|
const visibleCustomers = useMemo(() => {
|
||||||
const keyword = deferredCustomerKeyword.toLocaleLowerCase('zh-CN');
|
const keyword = deferredCustomerKeyword.toLocaleLowerCase('zh-CN');
|
||||||
return customers.filter((user) => {
|
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;
|
if (!keyword) return true;
|
||||||
return [user.displayName, user.username, user.customerRef, user.tenantRef]
|
return [user.displayName, user.username, user.customerRef, user.tenantRef]
|
||||||
.some((value) => value?.toLocaleLowerCase('zh-CN').includes(keyword));
|
.some((value) => value?.toLocaleLowerCase('zh-CN').includes(keyword));
|
||||||
@@ -107,7 +131,9 @@ export default function UsersPage() {
|
|||||||
const [vehicleLabels, setVehicleLabels] = useState<Record<string, string>>({});
|
const [vehicleLabels, setVehicleLabels] = useState<Record<string, string>>({});
|
||||||
const [feedback, setFeedback] = useState('');
|
const [feedback, setFeedback] = useState('');
|
||||||
const [editingGrantVIN, setEditingGrantVIN] = 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 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 editingGrant = useMemo(() => draft.vehicleGrants.find((grant) => grant.vin === editingGrantVIN), [draft.vehicleGrants, editingGrantVIN]);
|
||||||
const editingGrantPlate = editingGrant ? vehicleLabels[editingGrant.vin] : '';
|
const editingGrantPlate = editingGrant ? vehicleLabels[editingGrant.vin] : '';
|
||||||
@@ -115,6 +141,7 @@ export default function UsersPage() {
|
|||||||
const editorVisible = creating || Boolean(selected);
|
const editorVisible = creating || Boolean(selected);
|
||||||
const vehicleComposerVisible = !mobileLayout || creating || draft.vehicleGrants.length === 0 || vehicleComposerOpen;
|
const vehicleComposerVisible = !mobileLayout || creating || draft.vehicleGrants.length === 0 || vehicleComposerOpen;
|
||||||
const hasUnsavedChanges = useMemo(() => draftSignature(draft) !== draftSignature(baselineDraft), [baselineDraft, draft]);
|
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(editorVisible, '.v2-user-editor-sidesheet', 'v2-user-editor-sheet', '客户账号详情', '关闭账号详情');
|
||||||
useSideSheetA11y(Boolean(editingGrant), '.v2-user-grant-sidesheet', 'v2-user-grant-window', '车辆授权有效期', '关闭车辆授权有效期');
|
useSideSheetA11y(Boolean(editingGrant), '.v2-user-grant-sidesheet', 'v2-user-grant-window', '车辆授权有效期', '关闭车辆授权有效期');
|
||||||
|
|
||||||
@@ -311,15 +338,15 @@ export default function UsersPage() {
|
|||||||
ariaLabel="账号管理操作"
|
ariaLabel="账号管理操作"
|
||||||
title="客户访问治理"
|
title="客户访问治理"
|
||||||
description="菜单与车辆按最小权限开放,变更最多 30 秒生效"
|
description="菜单与车辆按最小权限开放,变更最多 30 秒生效"
|
||||||
status={`${customers.length} 个客户账号`}
|
status={`${customers.length} 个客户账号${attentionCustomerCount ? ` · ${attentionCustomerCount} 待完善` : ' · 权限就绪'}`}
|
||||||
meta={<Typography.Text type="tertiary">本地账号 · 可扩展外部身份源</Typography.Text>}
|
meta={<Typography.Text type="tertiary">本地账号 · 可扩展外部身份源</Typography.Text>}
|
||||||
actions={<Button className="v2-workspace-mobile-tool-button is-primary" theme="solid" icon={<IconPlus />} aria-label="新建客户账号" onClick={startCreate}>新建客户账号</Button>}
|
actions={<Button className="v2-workspace-mobile-tool-button is-primary" theme="solid" icon={<IconPlus />} aria-label="新建客户账号" onClick={startCreate}>新建客户账号</Button>}
|
||||||
/>
|
/>
|
||||||
<WorkspaceFilterPanel
|
<WorkspaceFilterPanel
|
||||||
className="v2-user-filter-panel"
|
className="v2-user-filter-panel"
|
||||||
title="账号范围"
|
title="账号范围"
|
||||||
description="按客户名称、登录账号、客户标识与启停状态查找"
|
description="按客户名称、登录账号与权限可用状态查找"
|
||||||
mobileSummary={`${customerStatus === 'all' ? '全部状态' : customerStatus === 'enabled' ? '启用账号' : '停用账号'}${customerKeyword.trim() ? ` · ${customerKeyword.trim()}` : ''}`}
|
mobileSummary={`${customerScopeLabel(customerStatus)}${customerKeyword.trim() ? ` · ${customerKeyword.trim()}` : ''}`}
|
||||||
expanded={!filtersCollapsed}
|
expanded={!filtersCollapsed}
|
||||||
status={users.isPending ? '正在读取账号' : `${visibleCustomers.length} / ${customers.length} 个账号`}
|
status={users.isPending ? '正在读取账号' : `${visibleCustomers.length} / ${customers.length} 个账号`}
|
||||||
statusColor={customerKeyword.trim() || customerStatus !== 'all' ? 'blue' : 'grey'}
|
statusColor={customerKeyword.trim() || customerStatus !== 'all' ? 'blue' : 'grey'}
|
||||||
@@ -332,7 +359,7 @@ export default function UsersPage() {
|
|||||||
<Input className="v2-user-list-search" aria-label="搜索客户账号" prefix={<IconSearch />} showClear value={customerKeyword} onChange={setCustomerKeyword} placeholder="搜索名称、账号或客户标识" />
|
<Input className="v2-user-list-search" aria-label="搜索客户账号" prefix={<IconSearch />} showClear value={customerKeyword} onChange={setCustomerKeyword} placeholder="搜索名称、账号或客户标识" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span id="v2-user-status-filter-label">账号状态</span>
|
<span id="v2-user-status-filter-label">访问状态</span>
|
||||||
<Select
|
<Select
|
||||||
aria-labelledby="v2-user-status-filter-label"
|
aria-labelledby="v2-user-status-filter-label"
|
||||||
value={customerStatus}
|
value={customerStatus}
|
||||||
@@ -340,7 +367,8 @@ export default function UsersPage() {
|
|||||||
optionList={[
|
optionList={[
|
||||||
{ value: 'all', label: '全部状态' },
|
{ value: 'all', label: '全部状态' },
|
||||||
{ value: 'enabled', label: '启用账号' },
|
{ value: 'enabled', label: '启用账号' },
|
||||||
{ value: 'disabled', label: '停用账号' }
|
{ value: 'disabled', label: '停用账号' },
|
||||||
|
{ value: 'attention', label: '待完善权限' }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@@ -369,7 +397,8 @@ export default function UsersPage() {
|
|||||||
description="当前范围内的账号、菜单与车辆授权摘要"
|
description="当前范围内的账号、菜单与车辆授权摘要"
|
||||||
meta={<span className="v2-user-directory-summary">
|
meta={<span className="v2-user-directory-summary">
|
||||||
<Tag color={visibleCustomers.length === customers.length ? 'grey' : 'blue'} type="light" size="small">{visibleCustomers.length} / {customers.length} 个账号</Tag>
|
<Tag color={visibleCustomers.length === customers.length ? 'grey' : 'blue'} type="light" size="small">{visibleCustomers.length} / {customers.length} 个账号</Tag>
|
||||||
<Typography.Text type="tertiary">启用 {enabledCustomers}</Typography.Text>
|
<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>
|
<Typography.Text type="tertiary">车辆授权 {grantedVehicles}</Typography.Text>
|
||||||
</span>}
|
</span>}
|
||||||
/>
|
/>
|
||||||
@@ -380,31 +409,34 @@ export default function UsersPage() {
|
|||||||
<span>菜单权限</span>
|
<span>菜单权限</span>
|
||||||
<span>车辆权限</span>
|
<span>车辆权限</span>
|
||||||
<span>最近登录</span>
|
<span>最近登录</span>
|
||||||
<span>状态</span>
|
<span>访问状态</span>
|
||||||
</div> : null}
|
</div> : null}
|
||||||
<div className="v2-customer-list-scroll" role="region" aria-label="客户账号目录,可上下滚动" tabIndex={0}>
|
<div className="v2-customer-list-scroll" role="region" aria-label="客户账号目录,可上下滚动" tabIndex={0}>
|
||||||
<List
|
<List
|
||||||
className="v2-customer-list"
|
className="v2-customer-list"
|
||||||
dataSource={visibleCustomers}
|
dataSource={visibleCustomers}
|
||||||
emptyContent={<Empty className="v2-user-filter-empty" title="没有匹配账号" description="请更换名称或账号关键词。" />}
|
emptyContent={<Empty className="v2-user-filter-empty" title="没有匹配账号" description="请调整关键词或访问状态筛选。" />}
|
||||||
renderItem={(user) => <List.Item key={user.id} className={`v2-user-list-row${selectedID === user.id && !creating ? ' is-active' : ''}`}>
|
renderItem={(user) => {
|
||||||
|
const accessState = customerAccessState(user.status, user.menuKeys.length, user.vehicles.length);
|
||||||
|
return <List.Item key={user.id} className={`v2-user-list-row is-access-${accessState.key}${accessState.attention ? ' has-attention' : ''}${selectedID === user.id && !creating ? ' is-active' : ''}`}>
|
||||||
<Button
|
<Button
|
||||||
className="v2-user-list-item"
|
className="v2-user-list-item"
|
||||||
theme="borderless"
|
theme="borderless"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
aria-label={`选择客户 ${user.displayName},账号 ${user.username},${user.vehicles.length} 辆授权车`}
|
aria-label={`选择客户 ${user.displayName},账号 ${user.username},${user.vehicles.length} 辆授权车,${accessState.label}`}
|
||||||
aria-pressed={selectedID === user.id && !creating}
|
aria-pressed={selectedID === user.id && !creating}
|
||||||
aria-expanded={selectedID === user.id && !creating}
|
aria-expanded={selectedID === user.id && !creating}
|
||||||
onClick={() => selectCustomer(user)}
|
onClick={() => selectCustomer(user)}
|
||||||
>
|
>
|
||||||
<Avatar className="v2-user-avatar" color={user.status === 'enabled' ? 'light-blue' : 'grey'} shape="square" size="small">{user.displayName.slice(0, 1)}</Avatar>
|
<Avatar className="v2-user-avatar" color={user.status === 'enabled' ? 'light-blue' : 'grey'} shape="square" size="small">{user.displayName.slice(0, 1)}</Avatar>
|
||||||
<span className="v2-user-list-identity"><b>{user.displayName}</b><small>@{user.username}{user.customerRef ? ` · ${user.customerRef}` : ''}</small></span>
|
<span className="v2-user-list-identity"><b>{user.displayName}</b><small>@{user.username}{user.customerRef ? ` · ${user.customerRef}` : ''}</small></span>
|
||||||
<span className="v2-user-list-facts is-menu"><small>菜单权限</small><b>{user.menuKeys.length} 项</b></span>
|
<span className="v2-user-list-facts is-menu"><small>菜单权限</small><b>{user.menuKeys.length}<i> / {customerMenus.length}</i></b></span>
|
||||||
<span className="v2-user-list-facts is-vehicle"><small>车辆权限</small><b>{user.vehicles.length} 辆</b></span>
|
<span className="v2-user-list-facts is-vehicle"><small>车辆权限</small><b>{user.vehicles.length} 辆</b></span>
|
||||||
<span className="v2-user-list-facts is-login"><small>最近登录</small><b>{formatTime(user.lastLoginAt)}</b></span>
|
<span className="v2-user-list-facts is-login"><small>最近登录</small><b>{formatTime(user.lastLoginAt)}</b></span>
|
||||||
<span className="v2-user-list-trailing"><Tag className={`v2-user-status-tag is-${user.status}`} color={user.status === 'enabled' ? 'green' : 'grey'} type="light" size="small">{user.status === 'enabled' ? '启用' : '停用'}</Tag><IconChevronRight /></span>
|
<span className="v2-user-list-trailing"><Tag className={`v2-user-status-tag is-${accessState.key}`} color={accessState.color} type="light" size="small"><span className="v2-user-status-tag-desktop">{accessState.label}</span><span className="v2-user-status-tag-mobile">{accessState.shortLabel}</span></Tag><IconChevronRight /></span>
|
||||||
</Button>
|
</Button>
|
||||||
</List.Item>}
|
</List.Item>;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>}
|
</>}
|
||||||
@@ -417,7 +449,13 @@ export default function UsersPage() {
|
|||||||
width={mobileLayout ? '100%' : 'min(840px, 100vw)'}
|
width={mobileLayout ? '100%' : 'min(840px, 100vw)'}
|
||||||
aria-label="客户账号详情"
|
aria-label="客户账号详情"
|
||||||
title={<div className="v2-user-editor-sheet-title">
|
title={<div className="v2-user-editor-sheet-title">
|
||||||
<span><strong>{creating ? '创建客户账号' : selected?.displayName}</strong><small>{creating ? '设置登录身份和最小必要权限' : <><span>@{selected?.username} · {draft.menuKeys.length} 个菜单 · {draft.vehicleGrants.length} 辆车</span><span className="v2-user-editor-last-login"> · 最近登录 {formatTime(selected?.lastLoginAt)}</span></>}</small></span>
|
<span><strong>{creating ? '创建客户账号' : selected?.displayName}</strong>{creating ? <small>设置登录身份和最小必要权限</small> : <span className="v2-user-editor-context" aria-label="账号权限概览">
|
||||||
|
<span className="is-account">@{selected?.username}</span>
|
||||||
|
<span>菜单 <b>{draft.menuKeys.length}</b></span>
|
||||||
|
<span>车辆 <b>{draft.vehicleGrants.length}</b></span>
|
||||||
|
<span className="v2-user-editor-last-login">最近登录 <b>{formatTime(selected?.lastLoginAt)}</b></span>
|
||||||
|
<Tag className="v2-user-editor-access-tag" color={draftAccessState.color} type="light" size="small">{draftAccessState.label}</Tag>
|
||||||
|
</span>}</span>
|
||||||
<label className="v2-user-status"><Switch aria-label={draft.status === 'enabled' ? '账号启用' : '账号停用'} checked={draft.status === 'enabled'} onChange={(checked) => setDraft((value) => ({ ...value, status: checked ? 'enabled' : 'disabled' }))} /><span>{draft.status === 'enabled' ? '账号启用' : '账号停用'}</span></label>
|
<label className="v2-user-status"><Switch aria-label={draft.status === 'enabled' ? '账号启用' : '账号停用'} checked={draft.status === 'enabled'} onChange={(checked) => setDraft((value) => ({ ...value, status: checked ? 'enabled' : 'disabled' }))} /><span>{draft.status === 'enabled' ? '账号启用' : '账号停用'}</span></label>
|
||||||
</div>}
|
</div>}
|
||||||
onCancel={requestCloseEditor}
|
onCancel={requestCloseEditor}
|
||||||
|
|||||||
@@ -15512,3 +15512,179 @@
|
|||||||
font-size: 7px;
|
font-size: 7px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Semi UI account access readiness.
|
||||||
|
* The directory now answers the operator's first question — can this customer
|
||||||
|
* actually use the platform — before exposing the underlying counts.
|
||||||
|
*/
|
||||||
|
.v2-user-directory-summary > .semi-tag.is-attention {
|
||||||
|
min-width: 70px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-directory-summary > .semi-typography.is-ready {
|
||||||
|
color: #397b60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-customer-list .v2-user-list-row.has-attention.semi-list-item {
|
||||||
|
border-color: #ead9b8;
|
||||||
|
background: linear-gradient(110deg, #fffdf8 0%, #fbfcfe 72%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-customer-list .v2-user-list-row.is-access-disabled.semi-list-item {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-customer-list .v2-user-list-row.has-attention.semi-list-item:hover {
|
||||||
|
border-color: #ddc38f;
|
||||||
|
box-shadow: 0 7px 18px rgba(125, 87, 25, .07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-list-facts b > i {
|
||||||
|
color: #8a98aa;
|
||||||
|
font-size: 10px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 550;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-status-tag.semi-tag {
|
||||||
|
min-width: 76px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-status-tag.is-ready.semi-tag {
|
||||||
|
color: #287456;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-status-tag.is-missing-menus.semi-tag,
|
||||||
|
.v2-user-status-tag.is-missing-vehicles.semi-tag {
|
||||||
|
color: #8a611e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-status-tag-mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-editor-sheet-title > span {
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-editor-context {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
color: #728197;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 550;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-editor-context > span {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-editor-context > span + span {
|
||||||
|
margin-left: 9px;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-editor-context > span + span::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #a8b4c3;
|
||||||
|
content: '';
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-editor-context > span > b {
|
||||||
|
color: #344c65;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-editor-context > .is-account {
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 180px;
|
||||||
|
color: #5e7188;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-editor-access-tag.semi-tag {
|
||||||
|
min-height: 22px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding-inline: 8px;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) and (min-width: 681px) {
|
||||||
|
.v2-user-status-tag.semi-tag {
|
||||||
|
min-width: 68px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-editor-last-login {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.v2-user-directory-summary > .semi-tag.is-attention {
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-status-tag.semi-tag {
|
||||||
|
min-width: 48px;
|
||||||
|
padding-inline: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-status-tag-desktop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-status-tag-mobile {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-list-facts b > i {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-editor-sidesheet .semi-sidesheet-header {
|
||||||
|
min-height: 76px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-editor-context {
|
||||||
|
max-width: 262px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-editor-context > span + span {
|
||||||
|
margin-left: 6px;
|
||||||
|
padding-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-editor-context > .is-account {
|
||||||
|
max-width: 94px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-editor-last-login {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-editor-access-tag.semi-tag {
|
||||||
|
min-height: 20px;
|
||||||
|
margin-left: 7px;
|
||||||
|
padding-inline: 6px;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user