refine Semi UI account workspace

This commit is contained in:
lingniu
2026-07-18 10:30:28 +08:00
parent 41b5c41998
commit 3cd19b276d
4 changed files with 458 additions and 65 deletions

View File

@@ -47,12 +47,17 @@ test('deduplicates vehicle candidates by VIN and renders granted vehicles plate
expect(screen.getByLabelText('账号管理操作')).toHaveTextContent('客户访问治理');
expect(screen.getByLabelText('账号管理操作')).toHaveTextContent('变更最多 30 秒生效');
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.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.getByText('车辆授权 1')).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');
expect(customer).toHaveAttribute('aria-pressed', 'false');
@@ -235,7 +240,8 @@ 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('启用账号').nextSibling).toHaveTextContent('0');
expect(screen.getByText('启用 0')).toBeInTheDocument();
expect(screen.getByText('车辆授权 0')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '清空账号筛选' }));
await waitFor(() => expect(screen.getByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument());

View File

@@ -256,75 +256,83 @@ export default function UsersPage() {
};
return <div className="v2-user-admin">
<WorkspaceCommandBar
className="v2-user-command-bar"
ariaLabel="账号管理操作"
title="客户访问治理"
description="菜单与车辆按最小权限开放,变更最多 30 秒生效"
status={`${customers.length} 个客户账号`}
meta={<Typography.Text type="tertiary"> · </Typography.Text>}
actions={<Button theme="solid" icon={<IconPlus />} aria-label="新建客户账号" onClick={startCreate}></Button>}
/>
<WorkspaceFilterPanel
className="v2-user-filter-panel"
title="账号范围"
description="按客户名称、登录账号、客户标识与启停状态查找"
mobileSummary={`${customerStatus === 'all' ? '全部状态' : customerStatus === 'enabled' ? '启用账号' : '停用账号'}${customerKeyword.trim() ? ` · ${customerKeyword.trim()}` : ''}`}
expanded={!filtersCollapsed}
status={users.isPending ? '正在读取账号' : `${visibleCustomers.length} / ${customers.length} 个账号`}
statusColor={customerKeyword.trim() || customerStatus !== 'all' ? 'blue' : 'grey'}
collapsedLabel="修改"
onToggle={() => setFiltersCollapsed((value) => !value)}
>
<div className={`v2-user-filter-form${filtersCollapsed ? ' is-mobile-collapsed' : ''}`}>
<label className="v2-user-filter-keyword">
<span></span>
<Input className="v2-user-list-search" aria-label="搜索客户账号" prefix={<IconSearch />} showClear value={customerKeyword} onChange={setCustomerKeyword} placeholder="搜索名称、账号或客户标识" />
</label>
<label>
<span id="v2-user-status-filter-label"></span>
<Select
aria-labelledby="v2-user-status-filter-label"
value={customerStatus}
onChange={(value) => setCustomerStatus(String(value) as typeof customerStatus)}
optionList={[
{ value: 'all', label: '全部状态' },
{ value: 'enabled', label: '启用账号' },
{ value: 'disabled', label: '用账号' }
]}
/>
</label>
<Button
className="v2-user-filter-reset"
theme="light"
type="tertiary"
icon={<IconRefresh />}
aria-label="清空账号筛选"
disabled={!customerKeyword.trim() && customerStatus === 'all'}
onClick={() => {
setCustomerKeyword('');
setCustomerStatus('all');
}}
>
</Button>
</div>
</WorkspaceFilterPanel>
<div className="v2-user-discovery-shell">
<WorkspaceCommandBar
className="v2-user-command-bar"
ariaLabel="账号管理操作"
title="客户访问治理"
description="菜单与车辆按最小权限开放,变更最多 30 秒生效"
status={`${customers.length} 个客户账号`}
meta={<Typography.Text type="tertiary"> · </Typography.Text>}
actions={<Button theme="solid" icon={<IconPlus />} aria-label="新建客户账号" onClick={startCreate}></Button>}
/>
<WorkspaceFilterPanel
className="v2-user-filter-panel"
title="账号范围"
description="按客户名称、登录账号、客户标识与启停状态查找"
mobileSummary={`${customerStatus === 'all' ? '全部状态' : customerStatus === 'enabled' ? '启用账号' : '停用账号'}${customerKeyword.trim() ? ` · ${customerKeyword.trim()}` : ''}`}
expanded={!filtersCollapsed}
status={users.isPending ? '正在读取账号' : `${visibleCustomers.length} / ${customers.length} 个账号`}
statusColor={customerKeyword.trim() || customerStatus !== 'all' ? 'blue' : 'grey'}
collapsedLabel="修改"
onToggle={() => setFiltersCollapsed((value) => !value)}
>
<div className={`v2-user-filter-form${filtersCollapsed ? ' is-mobile-collapsed' : ''}`}>
<label className="v2-user-filter-keyword">
<span></span>
<Input className="v2-user-list-search" aria-label="搜索客户账号" prefix={<IconSearch />} showClear value={customerKeyword} onChange={setCustomerKeyword} placeholder="搜索名称、账号或客户标识" />
</label>
<label>
<span id="v2-user-status-filter-label"></span>
<Select
aria-labelledby="v2-user-status-filter-label"
value={customerStatus}
onChange={(value) => setCustomerStatus(String(value) as typeof customerStatus)}
optionList={[
{ value: 'all', label: '全部状态' },
{ value: 'enabled', label: '用账号' },
{ value: 'disabled', label: '停用账号' }
]}
/>
</label>
<Button
className="v2-user-filter-reset"
theme="light"
type="tertiary"
icon={<IconRefresh />}
aria-label="清空账号筛选"
disabled={!customerKeyword.trim() && customerStatus === 'all'}
onClick={() => {
setCustomerKeyword('');
setCustomerStatus('all');
}}
>
</Button>
</div>
</WorkspaceFilterPanel>
</div>
<div className={`v2-user-admin-grid${creating || selected ? ' is-editor-open' : ''}`}>
<Card className="v2-user-list" bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader
className="v2-user-directory-header"
title="客户权限目录"
description="当前范围内的账号、菜单与车辆授权摘要"
meta={<Tag color={visibleCustomers.length === customers.length ? 'grey' : 'blue'} type="light" size="small">{visibleCustomers.length} / {customers.length} </Tag>}
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 type="tertiary"> {enabledCustomers}</Typography.Text>
<Typography.Text type="tertiary"> {grantedVehicles}</Typography.Text>
</span>}
/>
<div className="v2-user-directory-body">
<div className="v2-user-directory-metrics" aria-label="客户账号概览">
<span className="is-primary"><small></small><strong>{enabledCustomers}</strong></span>
<span><small></small><strong>{customers.length - enabledCustomers}</strong></span>
<span><small></small><strong>{grantedVehicles}</strong></span>
</div>
{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>} /> : <>
{visibleCustomers.length ? <div className="v2-user-directory-columns" role="row" aria-label="客户账号目录列">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div> : null}
<div className="v2-customer-list-scroll" role="region" aria-label="客户账号目录,可上下滚动" tabIndex={0}>
<List
className="v2-customer-list"
@@ -341,9 +349,9 @@ export default function UsersPage() {
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>
<span className="v2-user-list-identity"><b>{user.displayName}</b><small>@{user.username}</small></span>
<span className="v2-user-list-facts"><small></small><b>{user.menuKeys.length} </b></span>
<span className="v2-user-list-facts"><small></small><b>{user.vehicles.length} </b></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-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-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>
</Button>

View File

@@ -237,7 +237,9 @@ describe('V2 production entry', () => {
expect(corePageSources.UsersPage).toContain('<List');
expect(corePageSources.UsersPage).toContain('<List.Item');
expect(corePageSources.UsersPage).toContain('className="v2-user-command-bar"');
expect(corePageSources.UsersPage).toContain('v2-user-directory-metrics');
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('aria-expanded={selectedID === user.id && !creating}');
expect(corePageSources.UsersPage).toContain('<Tabs className="v2-user-editor-tabs"');
expect(corePageSources.UsersPage).toContain('<Tabs.TabPane');

View File

@@ -9562,6 +9562,383 @@
}
}
/* Semi UI account workspace: one discovery surface and one scan-friendly directory. */
.v2-user-admin {
height: 100%;
min-height: 0;
align-content: start;
gap: 10px;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
padding: 14px 16px 24px;
scrollbar-gutter: stable;
}
.v2-user-discovery-shell {
display: grid;
width: 100%;
max-width: 1480px;
flex: 0 0 auto;
gap: 0;
overflow: visible;
margin-inline: auto;
border: 1px solid #dce4ee;
border-radius: 14px;
background: #fff;
box-shadow: 0 9px 28px rgba(31, 53, 80, .055);
}
.v2-user-discovery-shell > .v2-workspace-command-bar {
width: 100%;
min-height: 50px;
border: 0;
border-bottom: 1px solid #e6ecf3;
border-radius: 14px 14px 0 0;
box-shadow: none;
}
.v2-user-discovery-shell > .v2-user-filter-panel.v2-workspace-filter-panel.semi-card {
width: 100%;
overflow: visible;
border: 0;
border-radius: 0 0 14px 14px;
box-shadow: none;
}
.v2-user-discovery-shell .v2-user-filter-form {
min-height: 54px;
border-radius: 0 0 14px 14px;
background: #fbfcfe;
}
.v2-user-admin-grid {
min-height: 0;
flex: 0 0 auto;
align-items: start;
}
.v2-user-list.semi-card {
height: auto;
min-height: 0;
flex: 0 0 auto;
border-radius: 12px;
box-shadow: 0 8px 26px rgba(31, 50, 76, .05);
}
.v2-user-list > .semi-card-body {
height: auto;
min-height: 0;
}
.v2-user-directory-header.v2-workspace-panel-header {
min-height: 58px;
padding: 9px 14px;
}
.v2-user-directory-summary {
display: flex;
min-width: 0;
align-items: center;
gap: 12px;
}
.v2-user-directory-summary > .semi-tag {
min-height: 24px;
flex: 0 0 auto;
border-radius: 999px;
padding-inline: 9px;
font-size: 10px;
font-weight: 650;
}
.v2-user-directory-summary > .semi-typography {
position: relative;
margin: 0;
padding-left: 12px;
color: #718197;
font-size: 10px;
font-weight: 600;
white-space: nowrap;
}
.v2-user-directory-summary > .semi-typography::before {
position: absolute;
top: 50%;
left: 0;
width: 1px;
height: 14px;
background: #e0e7f0;
content: '';
transform: translateY(-50%);
}
.v2-user-directory-body {
display: block;
min-height: 0;
flex: 0 0 auto;
padding: 0 10px 10px;
}
.v2-user-directory-columns {
display: grid;
min-height: 34px;
grid-template-columns: 36px minmax(170px, 1.45fr) minmax(70px, .45fr) minmax(70px, .45fr) minmax(150px, .9fr) auto;
align-items: center;
gap: 10px;
border-bottom: 1px solid #e6ecf3;
padding: 0 10px;
color: #8a97a8;
font-size: 9px;
font-weight: 650;
letter-spacing: .02em;
}
.v2-user-directory-columns > span:first-child {
grid-column: 1 / 3;
}
.v2-user-directory-columns > span:last-child {
min-width: 72px;
text-align: right;
}
.v2-customer-list-scroll {
flex: 0 0 auto;
overflow: visible;
padding-top: 7px;
}
.v2-customer-list .semi-list-items {
gap: 6px;
}
.v2-customer-list .v2-user-list-row.semi-list-item {
min-height: 64px;
border: 1px solid #e5ebf2;
border-radius: 9px;
background: #fbfcfe;
}
.v2-customer-list .v2-user-list-row.semi-list-item:hover {
border-color: #cfdceb;
background: #fff;
box-shadow: 0 7px 18px rgba(35, 57, 84, .055);
transform: translateY(-1px);
}
.v2-customer-list .v2-user-list-row.is-active.semi-list-item {
border-color: #a9c9f7;
background: linear-gradient(110deg, #edf5ff 0%, #f9fbff 100%);
box-shadow: inset 3px 0 var(--v2-blue), 0 7px 18px rgba(18, 104, 243, .07);
}
.v2-customer-list .v2-user-list-item.semi-button {
min-height: 64px;
padding: 8px 10px;
}
.v2-customer-list .v2-user-list-item .semi-button-content {
grid-template-columns: 36px minmax(170px, 1.45fr) minmax(70px, .45fr) minmax(70px, .45fr) minmax(150px, .9fr) auto;
gap: 10px;
}
.v2-user-avatar.semi-avatar {
width: 36px;
height: 36px;
border: 1px solid rgba(18, 104, 243, .08);
border-radius: 9px;
font-size: 13px;
font-weight: 750;
}
.v2-user-list-identity {
gap: 3px;
}
.v2-user-list-identity b {
color: #253a52;
font-size: 13px;
font-weight: 720;
}
.v2-user-list-identity small {
overflow: hidden;
color: #8390a1;
font-size: 9px;
text-overflow: ellipsis;
white-space: nowrap;
}
.v2-user-list-facts {
display: block;
}
.v2-user-list-facts small {
display: none;
}
.v2-user-list-facts b {
color: #4c6077;
font-size: 11px;
font-weight: 650;
}
.v2-user-list-facts.is-login b {
color: #6d7d91;
font-size: 10px;
font-variant-numeric: tabular-nums;
}
.v2-user-list-trailing {
min-width: 72px;
justify-content: flex-end;
gap: 7px;
}
.v2-user-list-trailing > svg {
color: #9aa7b7;
font-size: 14px;
}
.v2-user-status-tag.semi-tag {
min-width: 42px;
justify-content: center;
border-radius: 999px;
font-size: 9px;
font-weight: 700;
}
.v2-user-list-loading,
.v2-user-list-empty.semi-empty {
min-height: 280px;
}
.v2-user-filter-empty.semi-empty {
min-height: 230px;
}
@media (max-width: 980px) and (min-width: 681px) {
.v2-user-directory-columns,
.v2-customer-list .v2-user-list-item .semi-button-content {
grid-template-columns: 36px minmax(150px, 1fr) 70px 70px auto;
}
.v2-user-directory-columns > span:nth-child(4) {
display: none;
}
}
@media (max-width: 680px) {
.v2-user-admin {
height: 100%;
gap: 7px;
overflow-x: hidden;
overflow-y: auto;
padding: 7px 7px 12px;
scrollbar-gutter: auto;
-webkit-overflow-scrolling: touch;
}
.v2-user-discovery-shell {
display: contents;
}
.v2-user-discovery-shell > .v2-workspace-command-bar {
min-height: 48px;
border: 1px solid #dce6f2;
border-radius: 10px;
background: linear-gradient(105deg, #fff 0%, #f8fbff 100%);
box-shadow: 0 6px 20px rgba(31, 50, 76, .035);
}
.v2-user-discovery-shell > .v2-user-filter-panel.v2-workspace-filter-panel.semi-card {
border: 1px solid #dce6f2;
border-radius: 10px;
box-shadow: var(--v2-shadow);
}
.v2-user-discovery-shell .v2-user-filter-form {
border-radius: 0 0 10px 10px;
background: #fff;
}
.v2-user-admin-grid {
flex: 0 0 auto;
overflow: visible;
}
.v2-user-list.semi-card {
height: auto;
overflow: hidden;
border-radius: 10px;
}
.v2-user-directory-header.v2-workspace-panel-header {
min-height: 50px;
padding: 7px 10px;
}
.v2-user-directory-summary {
gap: 0;
}
.v2-user-directory-summary > .semi-typography {
display: none;
}
.v2-user-directory-body {
padding: 0 7px 7px;
}
.v2-user-directory-columns {
display: none;
}
.v2-customer-list-scroll {
overflow: visible;
padding-top: 7px;
}
.v2-customer-list .v2-user-list-row.semi-list-item {
min-height: 74px;
}
.v2-customer-list .v2-user-list-item.semi-button {
min-height: 74px;
padding: 8px;
}
.v2-user-list-facts {
display: grid;
gap: 2px;
}
.v2-user-list-facts small {
display: block;
}
.v2-user-list-facts b {
font-size: 10px;
}
.v2-user-list-trailing {
min-width: 0;
}
.v2-user-list-loading,
.v2-user-list-empty.semi-empty,
.v2-user-filter-empty.semi-empty {
min-height: 190px;
}
}
@media (prefers-reduced-motion: reduce) {
.v2-customer-list .v2-user-list-row.semi-list-item {
transition: none;
}
}
/* Semi UI workspace migration: global monitor command bar and fleet overview. */
.v2-filterbar.semi-card {
border-color: #dce5ef;