refine Semi UI identity permissions

This commit is contained in:
lingniu
2026-07-18 17:20:25 +08:00
parent dbd1d9d6ca
commit 29b9ce9594
3 changed files with 303 additions and 6 deletions

View File

@@ -72,8 +72,16 @@ test('deduplicates vehicle candidates by VIN and renders granted vehicles plate
expect(document.querySelector('.v2-user-editor-form')).toHaveAttribute('id', 'v2-user-editor-form');
expect(screen.getAllByRole('tab')).toHaveLength(3);
expect(document.querySelector('.v2-user-vehicle-section.semi-card')).toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: '登录身份' }));
expect(screen.getByLabelText('登录身份摘要')).toHaveTextContent('登录账号customer-east不可修改');
expect(screen.getByLabelText('登录身份摘要')).toHaveTextContent('客户显示名称华东客户已设置');
expect(screen.getByRole('group', { name: '基础身份' })).toHaveTextContent('用于客户登录和平台内展示');
expect(screen.getByRole('group', { name: '安全与系统映射' })).toHaveTextContent('密码留空则保持不变');
fireEvent.click(screen.getByRole('tab', { name: /菜单权限/ }));
expect(screen.getByText('客户开放菜单').closest('.semi-tag')).toBeInTheDocument();
expect(screen.getByLabelText('已开放 1 个菜单,共 4 个')).toHaveTextContent('客户工作台按最小权限开放');
expect(screen.getAllByText('已开放')).toHaveLength(1);
expect(screen.getAllByText('未开放')).toHaveLength(3);
fireEvent.click(screen.getByRole('tab', { name: /车辆权限/ }));
expect(document.querySelector('.v2-user-vehicle-section .v2-user-section-title .semi-tag')).toHaveTextContent('1 辆');
const granted = await screen.findByRole('button', { name: '移除 粤A11111' });

View File

@@ -433,17 +433,57 @@ export default function UsersPage() {
<Tabs className="v2-user-editor-tabs" activeKey={activeSection} onChange={(key) => setActiveSection(String(key) as EditorSection)}>
<Tabs.TabPane tab="登录身份" itemKey="identity">
<Card className="v2-user-editor-section v2-user-identity-section" title="登录身份" headerLine>
<div className="v2-user-fields">
<label><span></span><Input required disabled={!creating} value={draft.username} onChange={(value) => setDraft((current) => ({ ...current, username: value }))} placeholder="例如 customer-huadong" /></label>
<label><span></span><Input required value={draft.displayName} onChange={(value) => setDraft((current) => ({ ...current, displayName: value }))} placeholder="显示在平台右上角" /></label>
<label><span>{creating ? '初始密码' : '重置密码(可选)'}</span><Input required={creating} mode="password" autoComplete="new-password" value={draft.password} onChange={(value) => setDraft((current) => ({ ...current, password: value }))} placeholder="至少 10 位,包含三类字符" /></label>
<label><span></span><Input value={draft.customerRef} onChange={(value) => setDraft((current) => ({ ...current, customerRef: value }))} placeholder="为 OneOS / RuoYi 映射预留" /></label>
<div className="v2-user-identity-overview" aria-label="登录身份摘要">
<div>
<span><small></small><strong>{draft.username || '待设置'}</strong></span>
<Tag color={creating ? 'blue' : 'grey'} type="light" size="small">{creating ? '新账号' : '不可修改'}</Tag>
</div>
<div>
<span><small></small><strong>{draft.displayName || '待设置'}</strong></span>
<Tag color={draft.displayName ? 'green' : 'amber'} type="light" size="small">{draft.displayName ? '已设置' : '待完善'}</Tag>
</div>
<div>
<span><small></small><strong>{selected?.authProvider && selected.authProvider !== 'local' ? selected.authProvider : '本地账号'}</strong></span>
<Tag color="blue" type="light" size="small"></Tag>
</div>
</div>
<div className="v2-user-field-group" role="group" aria-labelledby="v2-user-primary-identity">
<header>
<span><strong id="v2-user-primary-identity"></strong><small></small></span>
<Tag color="blue" type="light" size="small"></Tag>
</header>
<div className="v2-user-fields is-primary">
<label><span></span><Input required disabled={!creating} value={draft.username} onChange={(value) => setDraft((current) => ({ ...current, username: value }))} placeholder="例如 customer-huadong" /></label>
<label><span></span><Input required value={draft.displayName} onChange={(value) => setDraft((current) => ({ ...current, displayName: value }))} placeholder="显示在平台右上角" /></label>
</div>
</div>
<div className="v2-user-field-group" role="group" aria-labelledby="v2-user-security-mapping">
<header>
<span><strong id="v2-user-security-mapping"></strong><small>{creating ? '设置初始密码,并按需关联外部客户标识' : '密码留空则保持不变,客户标识用于后续系统对接'}</small></span>
<Tag color="grey" type="light" size="small">{creating ? '密码必填' : '按需修改'}</Tag>
</header>
<div className="v2-user-fields is-secondary">
<label><span>{creating ? '初始密码' : '重置密码(可选)'}</span><Input required={creating} mode="password" autoComplete="new-password" value={draft.password} onChange={(value) => setDraft((current) => ({ ...current, password: value }))} placeholder="至少 10 位,包含三类字符" /></label>
<label><span></span><Input value={draft.customerRef} onChange={(value) => setDraft((current) => ({ ...current, customerRef: value }))} placeholder="为 OneOS / RuoYi 映射预留" /></label>
</div>
</div>
</Card>
</Tabs.TabPane>
<Tabs.TabPane tab={<span className="v2-user-tab-label"><Tag color="blue" type="light" size="small">{draft.menuKeys.length}</Tag></span>} itemKey="menus">
<Card className="v2-user-editor-section v2-user-menu-section" title={<span className="v2-user-section-title"> <Tag color="blue" type="light" size="small"></Tag></span>} headerLine>
<div className="v2-menu-permissions">{customerMenus.map((menu) => <label key={menu.key} className={draft.menuKeys.includes(menu.key) ? 'is-selected' : ''}><Checkbox checked={draft.menuKeys.includes(menu.key)} onChange={() => setDraft((value) => ({ ...value, menuKeys: value.menuKeys.includes(menu.key) ? value.menuKeys.filter((item) => item !== menu.key) : [...value.menuKeys, menu.key] }))} /><span><b>{menu.label}</b><small>{menu.description}</small></span></label>)}</div>
<div className="v2-user-menu-overview" aria-label={`已开放 ${draft.menuKeys.length} 个菜单,共 ${customerMenus.length}`}>
<span><small></small><strong>{draft.menuKeys.length}<i> / {customerMenus.length}</i></strong></span>
<p><b>{draft.menuKeys.length === customerMenus.length ? '客户工作台已完整开放' : '客户工作台按最小权限开放'}</b><small> 30 </small></p>
<Tag color={draft.menuKeys.length ? 'green' : 'amber'} type="light" size="small">{draft.menuKeys.length ? '权限已配置' : '尚未开放'}</Tag>
</div>
<div className="v2-menu-permissions">{customerMenus.map((menu) => {
const selectedMenu = draft.menuKeys.includes(menu.key);
return <label key={menu.key} className={selectedMenu ? 'is-selected' : ''}>
<Checkbox checked={selectedMenu} onChange={() => setDraft((value) => ({ ...value, menuKeys: value.menuKeys.includes(menu.key) ? value.menuKeys.filter((item) => item !== menu.key) : [...value.menuKeys, menu.key] }))} />
<span className="v2-menu-permission-copy"><b>{menu.label}</b><small>{menu.description}</small></span>
<Tag className="v2-menu-permission-state" color={selectedMenu ? 'blue' : 'grey'} type="light" size="small">{selectedMenu ? '已开放' : '未开放'}</Tag>
</label>;
})}</div>
</Card>
</Tabs.TabPane>
<Tabs.TabPane tab={<span className="v2-user-tab-label"><Tag color={draft.vehicleGrants.length ? 'blue' : 'grey'} type="light" size="small">{draft.vehicleGrants.length}</Tag></span>} itemKey="vehicles">

View File

@@ -14184,3 +14184,252 @@
margin-top: 6px;
}
}
/*
* Semi UI account identity and menu permissions: one shared hierarchy from
* account context to editable groups and the resulting customer workspace.
*/
.v2-user-editor-sidesheet .v2-user-identity-section > .semi-card-body,
.v2-user-editor-sidesheet .v2-user-menu-section > .semi-card-body {
display: grid;
gap: 12px;
}
.v2-user-identity-overview {
display: grid;
overflow: hidden;
grid-template-columns: repeat(3, minmax(0, 1fr));
border: 1px solid #dbe5f0;
border-radius: 10px;
background: linear-gradient(115deg, #f5f9ff 0%, #fbfdff 58%, #f7fbfa 100%);
}
.v2-user-identity-overview > div {
display: flex;
min-width: 0;
min-height: 70px;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 12px 14px;
}
.v2-user-identity-overview > div + div {
border-left: 1px solid #e1e9f2;
}
.v2-user-identity-overview > div > span,
.v2-user-field-group > header > span {
display: grid;
min-width: 0;
gap: 3px;
}
.v2-user-identity-overview small,
.v2-user-field-group > header small,
.v2-user-menu-overview small {
color: #7c8ca0;
font-size: 10px;
line-height: 1.45;
}
.v2-user-identity-overview strong {
overflow: hidden;
color: #243b54;
font-size: 14px;
line-height: 1.45;
text-overflow: ellipsis;
white-space: nowrap;
}
.v2-user-identity-overview .semi-tag,
.v2-user-field-group > header .semi-tag,
.v2-user-menu-overview > .semi-tag,
.v2-menu-permission-state.semi-tag {
min-height: 22px;
flex: 0 0 auto;
justify-content: center;
border-radius: 999px;
font-size: 9px;
font-weight: 700;
}
.v2-user-field-group {
overflow: hidden;
border: 1px solid #e1e8f0;
border-radius: 10px;
background: #fff;
}
.v2-user-field-group > header {
display: flex;
min-height: 52px;
align-items: center;
justify-content: space-between;
gap: 12px;
border-bottom: 1px solid #e8edf3;
background: #fbfcfe;
padding: 9px 12px;
}
.v2-user-field-group > header strong {
color: #31465e;
font-size: 12px;
}
.v2-user-field-group > .v2-user-fields {
gap: 12px;
padding: 12px;
}
.v2-user-menu-overview {
display: grid;
min-height: 76px;
grid-template-columns: 116px minmax(0, 1fr) auto;
align-items: center;
gap: 16px;
border: 1px solid #d8e5f4;
border-radius: 10px;
background: linear-gradient(115deg, #edf5ff 0%, #f8fbff 62%, #f6fbf8 100%);
padding: 11px 14px;
}
.v2-user-menu-overview > span,
.v2-user-menu-overview > p {
display: grid;
min-width: 0;
gap: 3px;
margin: 0;
}
.v2-user-menu-overview > span {
border-right: 1px solid #d8e4f1;
}
.v2-user-menu-overview > span strong {
color: #1268f3;
font-size: 24px;
line-height: 1;
}
.v2-user-menu-overview > span i {
color: #74869b;
font-size: 11px;
font-style: normal;
font-weight: 600;
}
.v2-user-menu-overview > p b {
color: #2d435b;
font-size: 12px;
}
.v2-user-editor-sidesheet .v2-user-menu-section .v2-menu-permissions label {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 11px;
}
.v2-menu-permission-copy {
align-self: center;
}
.v2-menu-permission-state.semi-tag {
min-width: 50px;
}
@media (max-width: 680px) {
.v2-user-editor-sidesheet .v2-user-identity-section > .semi-card-header,
.v2-user-editor-sidesheet .v2-user-menu-section > .semi-card-header {
min-height: 42px;
}
.v2-user-editor-sidesheet .v2-user-identity-section > .semi-card-body,
.v2-user-editor-sidesheet .v2-user-menu-section > .semi-card-body {
gap: 8px;
padding: 8px;
}
.v2-user-identity-overview {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.v2-user-identity-overview > div {
min-height: 58px;
padding: 8px 9px;
}
.v2-user-identity-overview > div:nth-child(3) {
grid-column: 1 / -1;
border-top: 1px solid #e1e9f2;
border-left: 0;
}
.v2-user-identity-overview strong {
font-size: 12px;
}
.v2-user-identity-overview .semi-tag {
min-width: 44px;
padding-inline: 6px;
}
.v2-user-field-group > header {
min-height: 46px;
padding: 7px 9px;
}
.v2-user-field-group > header small {
overflow: hidden;
max-width: 245px;
text-overflow: ellipsis;
white-space: nowrap;
}
.v2-user-field-group > .v2-user-fields {
grid-template-columns: minmax(0, 1fr);
gap: 8px;
padding: 9px;
}
.v2-user-menu-overview {
min-height: 70px;
grid-template-columns: 74px minmax(0, 1fr);
gap: 9px;
padding: 9px 10px;
}
.v2-user-menu-overview > span strong {
font-size: 21px;
}
.v2-user-menu-overview > p b {
font-size: 11px;
}
.v2-user-menu-overview > p small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.v2-user-menu-overview > .semi-tag {
display: none;
}
.v2-user-editor-sidesheet .v2-user-menu-section .v2-menu-permissions {
gap: 7px;
}
.v2-user-editor-sidesheet .v2-user-menu-section .v2-menu-permissions label {
min-height: 68px;
gap: 8px;
padding: 10px;
}
.v2-menu-permission-state.semi-tag {
min-width: 46px;
padding-inline: 6px;
}
}