refine Semi UI account permission workspace
This commit is contained in:
@@ -169,6 +169,42 @@ test('keeps the customer directory visible on mobile and opens details on demand
|
||||
expect(screen.getAllByRole('button', { name: '新建客户账号' })).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('keeps granted vehicles primary and reveals mobile assignment tools on demand', async () => {
|
||||
layout.mobile = true;
|
||||
const vehicles = Array.from({ length: 5 }, (_, index) => ({
|
||||
vin: `VIN${String(index + 1).padStart(3, '0')}`,
|
||||
plate: `粤A${String(index + 1).padStart(5, '0')}`,
|
||||
validFrom: '2026-06-05T00:00:00+08:00',
|
||||
sourceSystem: 'manual',
|
||||
grantedBy: '平台管理员'
|
||||
}));
|
||||
mocks.adminUsers.mockResolvedValue([{
|
||||
id: 7, username: 'customer-east', displayName: '华东客户', userType: 'customer', status: 'enabled',
|
||||
customerRef: '', tenantRef: '', authProvider: 'local', menuKeys: ['monitor', 'vehicles', 'tracks', 'statistics'],
|
||||
vehicleVins: vehicles.map((vehicle) => vehicle.vin), vehicles, grantHistory: [],
|
||||
createdAt: '2026-07-16T00:00:00Z', updatedAt: '2026-07-16T00:00:00Z'
|
||||
}]);
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><UsersPage /></QueryClientProvider>);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: /选择客户 华东客户/ }));
|
||||
|
||||
const toggle = await screen.findByRole('button', { name: '添加授权车辆' });
|
||||
expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(document.querySelector('.v2-user-vehicle-section')).toHaveClass('is-composer-collapsed');
|
||||
expect(screen.queryByRole('textbox', { name: '按车牌或 VIN 搜索' })).not.toBeInTheDocument();
|
||||
expect(screen.getAllByRole('button', { name: /^移除 / })).toHaveLength(4);
|
||||
expect(screen.getByText('第 1–4 条,共 5 辆')).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-user-editor-last-login')).toHaveTextContent('最近登录');
|
||||
|
||||
fireEvent.click(toggle);
|
||||
|
||||
expect(screen.getByRole('button', { name: '收起添加车辆' })).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(document.querySelector('.v2-user-vehicle-section')).not.toHaveClass('is-composer-collapsed');
|
||||
expect(screen.getByRole('textbox', { name: '按车牌或 VIN 搜索' })).toBeInTheDocument();
|
||||
expect(document.querySelector('#v2-user-vehicle-composer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('filters the customer directory by status and exposes the active result scope', async () => {
|
||||
mocks.adminUsers.mockResolvedValue([
|
||||
{
|
||||
|
||||
@@ -94,6 +94,7 @@ export default function UsersPage() {
|
||||
const [assignedKeyword, setAssignedKeyword] = useState('');
|
||||
const [assignedPage, setAssignedPage] = useState(1);
|
||||
const [bulkVINs, setBulkVINs] = useState('');
|
||||
const [vehicleComposerOpen, setVehicleComposerOpen] = useState(false);
|
||||
const [vehicleLabels, setVehicleLabels] = useState<Record<string, string>>({});
|
||||
const [feedback, setFeedback] = useState('');
|
||||
const [editingGrantVIN, setEditingGrantVIN] = useState('');
|
||||
@@ -103,6 +104,7 @@ export default function UsersPage() {
|
||||
const editingGrantPlate = editingGrant ? vehicleLabels[editingGrant.vin] : '';
|
||||
const grantWindowInvalid = Boolean(editingGrant && (!editingGrant.validFrom || (editingGrant.validTo && editingGrant.validTo <= editingGrant.validFrom)));
|
||||
const editorVisible = creating || Boolean(selected);
|
||||
const vehicleComposerVisible = !mobileLayout || creating || draft.vehicleGrants.length === 0 || vehicleComposerOpen;
|
||||
useSideSheetA11y(editorVisible, '.v2-user-editor-sidesheet', 'v2-user-editor-sheet', '客户账号详情', '关闭账号详情');
|
||||
useSideSheetA11y(Boolean(editingGrant), '.v2-user-grant-sidesheet', 'v2-user-grant-window', '车辆授权有效期', '关闭车辆授权有效期');
|
||||
|
||||
@@ -173,6 +175,7 @@ export default function UsersPage() {
|
||||
setAssignedKeyword('');
|
||||
setAssignedPage(1);
|
||||
setBulkVINs('');
|
||||
setVehicleComposerOpen(true);
|
||||
setVehicleLabels({});
|
||||
setFeedback('');
|
||||
setEditingGrantVIN('');
|
||||
@@ -185,6 +188,7 @@ export default function UsersPage() {
|
||||
setAssignedKeyword('');
|
||||
setAssignedPage(1);
|
||||
setBulkVINs('');
|
||||
setVehicleComposerOpen(false);
|
||||
setVehicleLabels({});
|
||||
setFeedback('');
|
||||
setEditingGrantVIN('');
|
||||
@@ -198,6 +202,7 @@ export default function UsersPage() {
|
||||
setAssignedKeyword('');
|
||||
setAssignedPage(1);
|
||||
setBulkVINs('');
|
||||
setVehicleComposerOpen(user.vehicles.length === 0);
|
||||
setFeedback('');
|
||||
setEditingGrantVIN('');
|
||||
};
|
||||
@@ -355,7 +360,7 @@ export default function UsersPage() {
|
||||
width={mobileLayout ? '100%' : 'min(840px, 100vw)'}
|
||||
aria-label="客户账号详情"
|
||||
title={<div className="v2-user-editor-sheet-title">
|
||||
<span><strong>{creating ? '创建客户账号' : selected?.displayName}</strong><small>{creating ? '设置登录身份和最小必要权限' : `@${selected?.username} · ${draft.menuKeys.length} 个菜单 · ${draft.vehicleGrants.length} 辆车 · 最近登录 ${formatTime(selected?.lastLoginAt)}`}</small></span>
|
||||
<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>
|
||||
<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>}
|
||||
onCancel={closeEditor}
|
||||
@@ -382,19 +387,42 @@ export default function UsersPage() {
|
||||
</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">
|
||||
<Card className="v2-user-editor-section v2-user-vehicle-section" title={<span className="v2-user-section-title">车辆权限与有效期 <Tag color={draft.vehicleGrants.length ? 'blue' : 'grey'} type="light" size="small">{draft.vehicleGrants.length} 辆</Tag></span>} headerLine headerExtraContent={draft.vehicleGrants.length ? <Button theme="borderless" type="tertiary" size="small" onClick={() => setDraft((value) => ({ ...value, vehicleGrants: [] }))}>清空</Button> : null}>
|
||||
<div className="v2-vehicle-permission-tools"><label><span>按车牌或 VIN 搜索</span><Input aria-label="按车牌或 VIN 搜索" value={vehicleKeyword} onChange={setVehicleKeyword} placeholder="输入后显示候选车辆" /></label><label><span>批量粘贴 VIN</span><span className="v2-bulk-vin"><Input value={bulkVINs} onChange={setBulkVINs} placeholder="空格、逗号或换行分隔" /><Button theme="light" disabled={!bulkVINs.trim()} onClick={addBulkVINs}>加入</Button></span></label></div>
|
||||
{deferredVehicleKeyword ? <VehicleCandidateList
|
||||
className="v2-vehicle-candidates"
|
||||
layout="grid"
|
||||
items={candidateVehicles}
|
||||
loading={candidates.isPending}
|
||||
error={candidates.isError ? (candidates.error instanceof Error ? candidates.error.message : '车辆候选加载失败') : undefined}
|
||||
onRetry={() => candidates.refetch()}
|
||||
selectedVins={assigned}
|
||||
selectedLabel="已分配"
|
||||
onSelect={(vehicle) => toggleVIN(vehicle.vin)}
|
||||
/> : null}
|
||||
<Card
|
||||
className={`v2-user-editor-section v2-user-vehicle-section${mobileLayout && !vehicleComposerVisible ? ' is-composer-collapsed' : ''}`}
|
||||
title={<span className="v2-user-section-title">车辆权限与有效期 <Tag color={draft.vehicleGrants.length ? 'blue' : 'grey'} type="light" size="small">{draft.vehicleGrants.length} 辆</Tag></span>}
|
||||
headerLine
|
||||
headerExtraContent={draft.vehicleGrants.length ? <div className="v2-user-section-actions">
|
||||
{mobileLayout && draft.vehicleGrants.length ? <Button
|
||||
className="v2-user-vehicle-tools-toggle"
|
||||
theme="light"
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={!vehicleComposerOpen ? <IconPlus /> : undefined}
|
||||
aria-controls="v2-user-vehicle-composer"
|
||||
aria-expanded={vehicleComposerOpen}
|
||||
aria-label={vehicleComposerOpen ? '收起添加车辆' : '添加授权车辆'}
|
||||
onClick={() => setVehicleComposerOpen((value) => !value)}
|
||||
>{vehicleComposerOpen ? '收起' : '添加'}</Button> : null}
|
||||
{draft.vehicleGrants.length ? <Button theme="borderless" type="tertiary" size="small" onClick={() => {
|
||||
setDraft((value) => ({ ...value, vehicleGrants: [] }));
|
||||
setVehicleComposerOpen(true);
|
||||
}}>清空</Button> : null}
|
||||
</div> : null}
|
||||
>
|
||||
{vehicleComposerVisible ? <div id="v2-user-vehicle-composer" className="v2-user-vehicle-composer">
|
||||
<div className="v2-vehicle-permission-tools"><label><span>按车牌或 VIN 搜索</span><Input aria-label="按车牌或 VIN 搜索" value={vehicleKeyword} onChange={setVehicleKeyword} placeholder="输入后显示候选车辆" /></label><label><span>批量粘贴 VIN</span><span className="v2-bulk-vin"><Input value={bulkVINs} onChange={setBulkVINs} placeholder="空格、逗号或换行分隔" /><Button theme="light" disabled={!bulkVINs.trim()} onClick={addBulkVINs}>加入</Button></span></label></div>
|
||||
{deferredVehicleKeyword ? <VehicleCandidateList
|
||||
className="v2-vehicle-candidates"
|
||||
layout="grid"
|
||||
items={candidateVehicles}
|
||||
loading={candidates.isPending}
|
||||
error={candidates.isError ? (candidates.error instanceof Error ? candidates.error.message : '车辆候选加载失败') : undefined}
|
||||
onRetry={() => candidates.refetch()}
|
||||
selectedVins={assigned}
|
||||
selectedLabel="已分配"
|
||||
onSelect={(vehicle) => toggleVIN(vehicle.vin)}
|
||||
/> : null}
|
||||
</div> : null}
|
||||
{draft.vehicleGrants.length ? <>
|
||||
<div className="v2-assigned-toolbar">
|
||||
<span><strong>已授权车辆</strong><small>{assignedKeyword ? `${filteredAssignedGrants.length} 条匹配结果` : `${draft.vehicleGrants.length} 辆车拥有当前访问权限`}</small></span>
|
||||
|
||||
@@ -237,7 +237,8 @@ describe('V2 production entry', () => {
|
||||
expect(corePageSources.UsersPage).toContain("'关闭账号详情'");
|
||||
expect(corePageSources.UsersPage).toContain('<Card className="v2-user-editor-section v2-user-identity-section"');
|
||||
expect(corePageSources.UsersPage).toContain('<Card className="v2-user-editor-section v2-user-menu-section"');
|
||||
expect(corePageSources.UsersPage).toContain('<Card className="v2-user-editor-section v2-user-vehicle-section"');
|
||||
expect(corePageSources.UsersPage).toContain('v2-user-editor-section v2-user-vehicle-section');
|
||||
expect(corePageSources.UsersPage).toContain('id="v2-user-vehicle-composer"');
|
||||
expect(corePageSources.UsersPage).toContain('<PanelEmpty className="v2-user-list-empty"');
|
||||
expect(corePageSources.UsersPage).not.toContain('className="v2-user-editor-empty"');
|
||||
expect(corePageSources.UsersPage).toContain('<Empty className="v2-user-vehicle-empty"');
|
||||
|
||||
@@ -9163,6 +9163,8 @@
|
||||
}
|
||||
|
||||
.v2-user-editor-sheet-title small {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #7b899a;
|
||||
font-size: 10px;
|
||||
@@ -9172,6 +9174,14 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.v2-user-editor-sheet-title small > span {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.v2-user-editor-last-login {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.v2-user-editor-sheet-title .v2-user-status {
|
||||
display: flex;
|
||||
min-width: 92px;
|
||||
@@ -9248,6 +9258,17 @@
|
||||
box-shadow: 0 6px 22px rgba(31, 53, 80, .05);
|
||||
}
|
||||
|
||||
.v2-user-section-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.v2-user-vehicle-composer {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.v2-user-editor-sidesheet .semi-sidesheet-footer {
|
||||
min-height: 64px;
|
||||
border-top: 1px solid #e2e9f1;
|
||||
@@ -9325,6 +9346,10 @@
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.v2-user-editor-last-login {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.v2-user-editor-sheet-title .v2-user-status {
|
||||
min-width: 34px;
|
||||
}
|
||||
@@ -9358,6 +9383,54 @@
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.v2-user-section-actions > .semi-button {
|
||||
min-width: 0;
|
||||
min-height: 32px;
|
||||
border-radius: 7px;
|
||||
padding-inline: 8px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.v2-user-vehicle-tools-toggle.semi-button {
|
||||
min-width: 58px;
|
||||
}
|
||||
|
||||
.v2-user-vehicle-composer {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.v2-user-editor-sidesheet .v2-vehicle-permission-tools {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.v2-user-vehicle-section.is-composer-collapsed .v2-assigned-toolbar {
|
||||
margin-top: 0;
|
||||
border-top: 0;
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.v2-user-editor-sidesheet .v2-assigned-toolbar {
|
||||
grid-template-columns: 104px minmax(0, 1fr);
|
||||
align-items: end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.v2-user-editor-sidesheet .v2-assigned-toolbar > .semi-input-wrapper {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.v2-user-editor-sidesheet .v2-assigned-vins > .v2-assigned-vehicle-card.semi-card {
|
||||
min-height: 90px;
|
||||
padding: 9px 10px;
|
||||
contain-intrinsic-size: auto 90px;
|
||||
}
|
||||
|
||||
.v2-user-editor-sidesheet .v2-assigned-vehicle-card > .semi-card-body > .v2-assigned-vehicle-interval {
|
||||
gap: 6px;
|
||||
margin-top: 7px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.v2-user-editor-sidesheet .v2-user-editor-tabs > .semi-tabs-content::-webkit-scrollbar {
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user