refine Semi UI identity permissions
This commit is contained in:
@@ -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' });
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user