diff --git a/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.test.tsx
index 2215110c..237d86a9 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.test.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.test.tsx
@@ -57,13 +57,14 @@ test('deduplicates vehicle candidates by VIN and renders granted vehicles plate
expect(customer.closest('.semi-list-item')).toHaveClass('v2-user-list-row');
expect(customer).toHaveAttribute('aria-pressed', 'false');
expect(customer).toHaveAttribute('aria-expanded', 'false');
- expect(view.container.querySelector('.v2-user-editor.semi-card')).not.toBeInTheDocument();
+ expect(document.querySelector('.v2-user-editor-sidesheet')).not.toBeInTheDocument();
fireEvent.click(customer);
await waitFor(() => expect(customer).toHaveAttribute('aria-expanded', 'true'));
- expect(view.container.querySelector('.v2-user-editor.semi-card')).toBeInTheDocument();
- expect(view.container.querySelector('.v2-user-editor-tabs.semi-tabs')).toBeInTheDocument();
+ expect(document.querySelector('.v2-user-editor-sidesheet .semi-sidesheet-inner')).toHaveAttribute('aria-label', '客户账号详情');
+ expect(document.querySelector('.v2-user-editor-sidesheet .v2-user-editor-tabs.semi-tabs')).toBeInTheDocument();
+ expect(document.querySelector('.v2-user-editor-form')).toHaveAttribute('id', 'v2-user-editor-form');
expect(screen.getAllByRole('tab')).toHaveLength(3);
- expect(view.container.querySelector('.v2-user-vehicle-section.semi-card')).toBeInTheDocument();
+ expect(document.querySelector('.v2-user-vehicle-section.semi-card')).toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: /菜单权限/ }));
expect(screen.getByText('客户开放菜单').closest('.semi-tag')).toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: /车辆权限/ }));
@@ -126,7 +127,7 @@ test('pages and filters large vehicle grants without nesting a vehicle-list scro
expect(screen.getAllByRole('button', { name: /^移除 / })).toHaveLength(10);
expect(screen.getByText('第 1–10 条,共 12 辆')).toBeInTheDocument();
- expect(view.container.querySelector('.v2-assigned-pagination')).toBeInTheDocument();
+ expect(document.querySelector('.v2-assigned-pagination')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
expect(await screen.findByRole('button', { name: '移除 粤A00012' })).toBeInTheDocument();
expect(screen.getAllByRole('button', { name: /^移除 / })).toHaveLength(2);
@@ -156,12 +157,12 @@ test('keeps the customer directory visible on mobile and opens details on demand
expect(filterToggle).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(filterToggle);
expect(screen.getByRole('button', { name: '收起账号范围:全部状态' })).toHaveAttribute('aria-expanded', 'true');
- expect(view.container.querySelector('.v2-user-editor')).not.toBeInTheDocument();
+ expect(document.querySelector('.v2-user-editor-sidesheet')).not.toBeInTheDocument();
fireEvent.click(customer);
expect(await screen.findByRole('button', { name: '关闭账号详情' })).toBeInTheDocument();
expect(customer).toHaveAttribute('aria-expanded', 'true');
fireEvent.click(screen.getByRole('button', { name: '关闭账号详情' }));
- expect(view.container.querySelector('.v2-user-editor')).not.toBeInTheDocument();
+ await waitFor(() => expect(document.querySelector('.v2-user-editor-form')).not.toBeInTheDocument());
expect(customer).toHaveAttribute('aria-expanded', 'false');
expect(screen.getAllByRole('button', { name: '新建客户账号' })).toHaveLength(1);
});
@@ -210,7 +211,7 @@ test('uses a standard Semi empty state when no customer account exists', async (
expect(await screen.findByText('还没有客户账号')).toBeInTheDocument();
expect(view.container.querySelector('.v2-user-list-empty.semi-empty')).toBeInTheDocument();
- expect(view.container.querySelector('.v2-user-editor')).not.toBeInTheDocument();
+ expect(document.querySelector('.v2-user-editor-sidesheet')).not.toBeInTheDocument();
expect(screen.getAllByRole('button', { name: '创建第一个客户账号' })).toHaveLength(1);
});
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.tsx
index 6cfc3510..07cc7b9d 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.tsx
@@ -1,5 +1,5 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { IconChevronRight, IconClose, IconDelete, IconPlus, IconRefresh, IconSearch } from '@douyinfe/semi-icons';
+import { IconChevronRight, IconDelete, IconPlus, IconRefresh, IconSearch } from '@douyinfe/semi-icons';
import { Avatar, Button, Card, Checkbox, Collapse, Empty, Input, List, Select, SideSheet, Spin, Switch, Tabs, Tag, Typography } from '@douyinfe/semi-ui';
import { FormEvent, useDeferredValue, useEffect, useMemo, useState } from 'react';
import { api } from '../../api/client';
@@ -102,6 +102,8 @@ export default function UsersPage() {
const editingGrant = useMemo(() => draft.vehicleGrants.find((grant) => grant.vin === editingGrantVIN), [draft.vehicleGrants, editingGrantVIN]);
const editingGrantPlate = editingGrant ? vehicleLabels[editingGrant.vin] : '';
const grantWindowInvalid = Boolean(editingGrant && (!editingGrant.validFrom || (editingGrant.validTo && editingGrant.validTo <= editingGrant.validFrom)));
+ const editorVisible = creating || Boolean(selected);
+ useSideSheetA11y(editorVisible, '.v2-user-editor-sidesheet', 'v2-user-editor-sheet', '客户账号详情', '关闭账号详情');
useSideSheetA11y(Boolean(editingGrant), '.v2-user-grant-sidesheet', 'v2-user-grant-window', '车辆授权有效期', '关闭车辆授权有效期');
useEffect(() => {
@@ -226,8 +228,8 @@ export default function UsersPage() {
setAssignedPage(1);
setBulkVINs('');
};
- const submit = (event: FormEvent) => {
- event.preventDefault();
+ const persistDraft = () => {
+ if (save.isPending) return;
setFeedback('');
if (!draft.displayName.trim() || (creating && (!draft.username.trim() || !draft.password))) {
setActiveSection('identity');
@@ -243,6 +245,10 @@ export default function UsersPage() {
}
save.mutate();
};
+ const submit = (event: FormEvent) => {
+ event.preventDefault();
+ persistDraft();
+ };
return
车辆授权{grantedVehicles}
{users.isPending ? : customers.length === 0 ? 创建第一个客户账号} /> : <>
-
}
- renderItem={(user) =>
-
- }
- />
+
+
}
+ renderItem={(user) =>
+
+ }
+ />
+
>}
- {creating || selected ?
- : null}
-
+ : null}
+
{
expect(corePageSources.UsersPage).toContain(' {
expect(corePageSources.UsersPage).toContain('aria-expanded={selectedID === user.id && !creating}');
expect(corePageSources.UsersPage).toContain(' .v2-customer-list.semi-list {
+ min-height: auto;
+ overflow: visible;
+}
+
+.v2-customer-list-scroll:focus-visible {
+ outline: 2px solid rgba(18, 104, 243, .35);
+ outline-offset: -2px;
+}
+
+.v2-user-editor-sidesheet .semi-sidesheet-inner {
+ overflow: hidden;
+ border-left: 1px solid #d8e2ee;
+ background: #f6f8fb;
+ box-shadow: -20px 0 58px rgba(27, 47, 73, .16);
+}
+
+.v2-user-editor-sidesheet .semi-sidesheet-header {
+ min-height: 76px;
+ border-bottom: 1px solid #e2e9f1;
+ background: #fff;
+ padding: 12px 18px;
+}
+
+.v2-user-editor-sheet-title {
+ display: flex;
+ width: 100%;
+ min-width: 0;
+ align-items: center;
+ justify-content: space-between;
+ gap: 18px;
+ padding-right: 8px;
+}
+
+.v2-user-editor-sheet-title > span {
+ display: grid;
+ min-width: 0;
+ gap: 4px;
+}
+
+.v2-user-editor-sheet-title strong {
+ overflow: hidden;
+ color: #20364e;
+ font-size: 18px;
+ font-weight: 750;
+ letter-spacing: -.025em;
+ line-height: 1.25;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.v2-user-editor-sheet-title small {
+ overflow: hidden;
+ color: #7b899a;
+ font-size: 10px;
+ font-weight: 500;
+ line-height: 1.4;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.v2-user-editor-sheet-title .v2-user-status {
+ display: flex;
+ min-width: 92px;
+ flex: 0 0 auto;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 7px;
+ color: #53667c;
+ font-size: 10px;
+ font-weight: 650;
+}
+
+.v2-user-editor-sidesheet .semi-sidesheet-body {
+ min-height: 0;
+ overflow: hidden;
+ background: #f6f8fb;
+ padding: 0;
+}
+
+.v2-user-editor-form {
+ display: flex;
+ height: 100%;
+ min-height: 0;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.v2-user-editor-sidesheet .v2-user-editor-tabs.semi-tabs {
+ width: 100%;
+ height: 100%;
+ min-height: 0;
+ flex: 1 1 0;
+ overflow: hidden;
+ margin: 0;
+ border: 0;
+ border-radius: 0;
+ background: #f6f8fb;
+}
+
+.v2-user-editor-sidesheet .v2-user-editor-tabs > .semi-tabs-bar {
+ min-height: 52px;
+ flex: 0 0 auto;
+ border-bottom: 1px solid #e2e9f1;
+ background: #fff;
+ padding-inline: 18px;
+}
+
+.v2-user-editor-sidesheet .v2-user-editor-tabs > .semi-tabs-bar .semi-tabs-tab {
+ min-height: 51px;
+ font-size: 11px;
+ font-weight: 650;
+}
+
+.v2-user-editor-sidesheet .v2-user-editor-tabs > .semi-tabs-content {
+ min-height: 0;
+ flex: 1 1 0;
+ overflow-x: hidden;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ background: #f6f8fb;
+ padding: 12px;
+ scrollbar-gutter: stable;
+}
+
+.v2-user-editor-sidesheet .v2-user-editor-tabs > .semi-tabs-content:focus-visible {
+ outline: 2px solid rgba(18, 104, 243, .35);
+ outline-offset: -2px;
+}
+
+.v2-user-editor-sidesheet .v2-user-editor-section.semi-card {
+ margin: 0;
+ border: 1px solid #dce5ef;
+ border-radius: 10px;
+ box-shadow: 0 6px 22px rgba(31, 53, 80, .05);
+}
+
+.v2-user-editor-sidesheet .semi-sidesheet-footer {
+ min-height: 64px;
+ border-top: 1px solid #e2e9f1;
+ background: #fff;
+ padding: 10px 14px;
+}
+
+.v2-user-editor-sheet-footer {
+ display: flex;
+ width: 100%;
+ min-width: 0;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+}
+
+.v2-user-editor-sheet-footer .v2-user-feedback.semi-typography {
+ min-width: 0;
+ overflow: hidden;
+ margin: 0;
+ font-size: 10px;
+ line-height: 1.45;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.v2-user-editor-sheet-footer > .semi-button {
+ min-width: 132px;
+ min-height: 40px;
+ flex: 0 0 auto;
+ border-radius: 9px;
+ font-size: 11px;
+ font-weight: 700;
+}
+
+@media (max-width: 680px) {
+ .v2-customer-list-scroll {
+ scrollbar-width: auto;
+ touch-action: pan-y;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .v2-customer-list-scroll::-webkit-scrollbar {
+ width: 11px;
+ }
+
+ .v2-customer-list-scroll::-webkit-scrollbar-thumb {
+ border: 3px solid transparent;
+ border-radius: 999px;
+ background: #9cacc0;
+ background-clip: padding-box;
+ }
+
+ .v2-user-editor-sidesheet .semi-sidesheet-inner {
+ width: 100% !important;
+ border-left: 0;
+ }
+
+ .v2-user-editor-sidesheet .semi-sidesheet-header {
+ min-height: 66px;
+ padding: 9px 10px 9px 12px;
+ }
+
+ .v2-user-editor-sheet-title {
+ gap: 8px;
+ padding-right: 2px;
+ }
+
+ .v2-user-editor-sheet-title strong {
+ font-size: 16px;
+ }
+
+ .v2-user-editor-sheet-title small {
+ max-width: 226px;
+ font-size: 9px;
+ }
+
+ .v2-user-editor-sheet-title .v2-user-status {
+ min-width: 34px;
+ }
+
+ .v2-user-editor-sheet-title .v2-user-status > span {
+ display: none;
+ }
+
+ .v2-user-editor-sidesheet .v2-user-editor-tabs > .semi-tabs-bar {
+ min-height: 48px;
+ overflow-x: auto;
+ padding-inline: 10px;
+ scrollbar-width: none;
+ }
+
+ .v2-user-editor-sidesheet .v2-user-editor-tabs > .semi-tabs-bar::-webkit-scrollbar {
+ display: none;
+ }
+
+ .v2-user-editor-sidesheet .v2-user-editor-tabs > .semi-tabs-bar .semi-tabs-tab {
+ min-height: 47px;
+ flex: 0 0 auto;
+ margin-right: 14px;
+ font-size: 10px;
+ }
+
+ .v2-user-editor-sidesheet .v2-user-editor-tabs > .semi-tabs-content {
+ padding: 7px;
+ scrollbar-width: auto;
+ touch-action: pan-y;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .v2-user-editor-sidesheet .v2-user-editor-tabs > .semi-tabs-content::-webkit-scrollbar {
+ width: 11px;
+ }
+
+ .v2-user-editor-sidesheet .v2-user-editor-tabs > .semi-tabs-content::-webkit-scrollbar-thumb {
+ border: 3px solid transparent;
+ border-radius: 999px;
+ background: #9cacc0;
+ background-clip: padding-box;
+ }
+
+ .v2-user-editor-sidesheet .semi-sidesheet-footer {
+ min-height: 62px;
+ padding: 8px;
+ }
+
+ .v2-user-editor-sheet-footer {
+ gap: 8px;
+ }
+
+ .v2-user-editor-sheet-footer .v2-user-feedback.semi-typography {
+ display: none;
+ }
+
+ .v2-user-editor-sheet-footer > .semi-button {
+ width: 100%;
+ min-width: 0;
+ min-height: 44px;
+ justify-content: center;
+ }
+}