From 80f688048868baf0f1a05424447d0d8c454fbfbc Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 18 Jul 2026 05:02:31 +0800 Subject: [PATCH] refine Semi UI account workspace --- .../apps/web/src/v2/pages/UsersPage.test.tsx | 17 +- .../apps/web/src/v2/pages/UsersPage.tsx | 87 +++--- .../apps/web/src/v2/productionEntry.test.ts | 5 +- .../apps/web/src/v2/styles/workspace.css | 289 ++++++++++++++++++ 4 files changed, 354 insertions(+), 44 deletions(-) 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 ? -
-

{creating ? '创建客户账号' : selected?.displayName}

{creating ? '设置登录身份和最小必要权限' : `@${selected?.username} · ${draft.menuKeys.length} 个菜单 · ${draft.vehicleGrants.length} 辆车 · 最近登录 ${formatTime(selected?.lastLoginAt)}`}

+ + + {creating ? '创建客户账号' : selected?.displayName}{creating ? '设置登录身份和最小必要权限' : `@${selected?.username} · ${draft.menuKeys.length} 个菜单 · ${draft.vehicleGrants.length} 辆车 · 最近登录 ${formatTime(selected?.lastLoginAt)}`} + + } + onCancel={closeEditor} + footer={
+ {feedback || '权限变更最多 30 秒生效,保存前请核对车辆有效期'} + +
} + > + {editorVisible ? setActiveSection(String(key) as EditorSection)}> @@ -413,11 +435,8 @@ export default function UsersPage() { - {feedback ?

{feedback}

: null} -
- -
: 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; + } +}