refactor(ui): refine semi account governance
This commit is contained in:
@@ -65,6 +65,7 @@ test('deduplicates vehicle candidates by VIN and renders granted vehicles plate
|
|||||||
expect(directoryMetrics).toHaveTextContent('车辆授权1当前范围合计');
|
expect(directoryMetrics).toHaveTextContent('车辆授权1当前范围合计');
|
||||||
expect(customer).toHaveClass('semi-button', 'v2-user-list-item');
|
expect(customer).toHaveClass('semi-button', 'v2-user-list-item');
|
||||||
expect(customer.closest('.semi-list-item')).toHaveClass('v2-user-list-row');
|
expect(customer.closest('.semi-list-item')).toHaveClass('v2-user-list-row');
|
||||||
|
expect(customer.querySelector('.v2-user-provider-tag')).toHaveTextContent('本地');
|
||||||
expect(customer).toHaveAttribute('aria-pressed', 'false');
|
expect(customer).toHaveAttribute('aria-pressed', 'false');
|
||||||
expect(customer).toHaveAttribute('aria-expanded', 'false');
|
expect(customer).toHaveAttribute('aria-expanded', 'false');
|
||||||
expect(document.querySelector('.v2-user-editor-sidesheet')).not.toBeInTheDocument();
|
expect(document.querySelector('.v2-user-editor-sidesheet')).not.toBeInTheDocument();
|
||||||
|
|||||||
@@ -113,6 +113,12 @@ function accessBadgeColor(color: CustomerAccessState['color']): WorkspaceSideShe
|
|||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function authProviderMeta(provider?: string) {
|
||||||
|
const normalized = provider?.trim();
|
||||||
|
if (!normalized || normalized === 'local') return { label: '本地', color: 'grey' as const, title: '本地账号' };
|
||||||
|
return { label: normalized, color: 'cyan' as const, title: `外部身份源:${normalized}` };
|
||||||
|
}
|
||||||
|
|
||||||
export default function UsersPage() {
|
export default function UsersPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const mobileLayout = useMobileLayout();
|
const mobileLayout = useMobileLayout();
|
||||||
@@ -545,6 +551,7 @@ export default function UsersPage() {
|
|||||||
emptyContent={<Empty className="v2-user-filter-empty" title="没有匹配账号" description="请调整关键词或访问状态筛选。" />}
|
emptyContent={<Empty className="v2-user-filter-empty" title="没有匹配账号" description="请调整关键词或访问状态筛选。" />}
|
||||||
renderItem={(user) => {
|
renderItem={(user) => {
|
||||||
const accessState = customerAccessState(user.status, user.menuKeys.length, user.vehicles.length);
|
const accessState = customerAccessState(user.status, user.menuKeys.length, user.vehicles.length);
|
||||||
|
const authProvider = authProviderMeta(user.authProvider);
|
||||||
return <List.Item key={user.id} className={`v2-user-list-row is-access-${accessState.key}${accessState.attention ? ' has-attention' : ''}${selectedID === user.id && !creating ? ' is-active' : ''}`}>
|
return <List.Item key={user.id} className={`v2-user-list-row is-access-${accessState.key}${accessState.attention ? ' has-attention' : ''}${selectedID === user.id && !creating ? ' is-active' : ''}`}>
|
||||||
<Button
|
<Button
|
||||||
className="v2-user-list-item"
|
className="v2-user-list-item"
|
||||||
@@ -556,7 +563,10 @@ export default function UsersPage() {
|
|||||||
onClick={() => selectCustomer(user)}
|
onClick={() => selectCustomer(user)}
|
||||||
>
|
>
|
||||||
<Avatar className="v2-user-avatar" color={user.status === 'enabled' ? 'light-blue' : 'grey'} shape="square" size="small">{user.displayName.slice(0, 1)}</Avatar>
|
<Avatar className="v2-user-avatar" color={user.status === 'enabled' ? 'light-blue' : 'grey'} shape="square" size="small">{user.displayName.slice(0, 1)}</Avatar>
|
||||||
<span className="v2-user-list-identity"><b>{user.displayName}</b><small>@{user.username}{user.customerRef ? ` · ${user.customerRef}` : ''}</small></span>
|
<span className="v2-user-list-identity">
|
||||||
|
<span className="v2-user-list-name"><b>{user.displayName}</b><span className="v2-user-provider-hint" title={authProvider.title}><Tag className="v2-user-provider-tag" color={authProvider.color} type="light" size="small">{authProvider.label}</Tag></span></span>
|
||||||
|
<small>@{user.username}{user.customerRef ? ` · ${user.customerRef}` : ''}</small>
|
||||||
|
</span>
|
||||||
<span className="v2-user-list-facts is-menu"><small>菜单权限</small><b>{user.menuKeys.length}<i> / {customerMenus.length}</i></b></span>
|
<span className="v2-user-list-facts is-menu"><small>菜单权限</small><b>{user.menuKeys.length}<i> / {customerMenus.length}</i></b></span>
|
||||||
<span className="v2-user-list-facts is-vehicle"><small>车辆权限</small><b>{user.vehicles.length} 辆</b></span>
|
<span className="v2-user-list-facts is-vehicle"><small>车辆权限</small><b>{user.vehicles.length} 辆</b></span>
|
||||||
<span className="v2-user-list-facts is-login"><small>最近登录</small><b>{formatTime(user.lastLoginAt)}</b></span>
|
<span className="v2-user-list-facts is-login"><small>最近登录</small><b>{formatTime(user.lastLoginAt)}</b></span>
|
||||||
|
|||||||
@@ -24281,6 +24281,171 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Semi UI account directory convergence.
|
||||||
|
* Customer identities now read as one evidence table on desktop, while
|
||||||
|
* permission counts and identity-provider provenance remain easy to scan.
|
||||||
|
*/
|
||||||
|
.v2-customer-list .semi-list-items {
|
||||||
|
gap: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #dfe7f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-customer-list .v2-user-list-row.semi-list-item {
|
||||||
|
min-height: 68px;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #e8edf3;
|
||||||
|
border-radius: 0;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-customer-list .v2-user-list-row.semi-list-item:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-customer-list .v2-user-list-row.semi-list-item:hover {
|
||||||
|
border-color: #e8edf3;
|
||||||
|
background: #f7faff;
|
||||||
|
box-shadow: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-customer-list .v2-user-list-row.is-active.semi-list-item {
|
||||||
|
border-color: #dce8f8;
|
||||||
|
background: linear-gradient(90deg, #edf5ff 0%, #f8fbff 62%, #fff 100%);
|
||||||
|
box-shadow: inset 3px 0 var(--v2-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-customer-list .v2-user-list-row.has-attention.semi-list-item {
|
||||||
|
border-color: #eee6d7;
|
||||||
|
background: linear-gradient(90deg, #fffaf1 0%, #fffdf9 30%, #fff 74%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-customer-list .v2-user-list-row.is-access-disabled.semi-list-item {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-customer-list .v2-user-list-item.semi-button {
|
||||||
|
min-height: 68px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-list-name {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-list-name > b {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-provider-hint {
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-provider-tag.semi-tag {
|
||||||
|
min-width: 36px;
|
||||||
|
height: 20px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding-inline: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-list-facts {
|
||||||
|
min-width: 0;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-list-facts.is-menu,
|
||||||
|
.v2-user-list-facts.is-vehicle {
|
||||||
|
min-height: 40px;
|
||||||
|
align-content: center;
|
||||||
|
border: 1px solid #e8edf3;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #f7f9fc;
|
||||||
|
padding: 5px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-list-facts.is-menu b,
|
||||||
|
.v2-user-list-facts.is-vehicle b {
|
||||||
|
color: #2d4966;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-list-trailing {
|
||||||
|
gap: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-list-trailing > svg {
|
||||||
|
color: #9ba9b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) and (min-width: 681px) {
|
||||||
|
.v2-user-list-facts.is-login {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.v2-customer-list .semi-list-items {
|
||||||
|
gap: 6px;
|
||||||
|
overflow: visible;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-customer-list .v2-user-list-row.semi-list-item,
|
||||||
|
.v2-customer-list .v2-user-list-row.semi-list-item:last-child {
|
||||||
|
min-height: 76px;
|
||||||
|
border: 1px solid #e2e9f2;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-customer-list .v2-user-list-row.is-active.semi-list-item {
|
||||||
|
border-color: #b8d1f3;
|
||||||
|
background: #f2f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-customer-list .v2-user-list-row.has-attention.semi-list-item {
|
||||||
|
border-color: #e7d8ba;
|
||||||
|
background: #fffaf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-provider-tag.semi-tag {
|
||||||
|
min-width: 32px;
|
||||||
|
height: 18px;
|
||||||
|
padding-inline: 5px;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-user-list-facts.is-menu,
|
||||||
|
.v2-user-list-facts.is-vehicle {
|
||||||
|
min-height: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Guided Semi UI empty workspaces.
|
* Guided Semi UI empty workspaces.
|
||||||
* Empty result canvases should explain the task path instead of leaving a
|
* Empty result canvases should explain the task path instead of leaving a
|
||||||
|
|||||||
@@ -1,61 +1,57 @@
|
|||||||
# 接入管理 Semi UI Design QA
|
# 账号管理 Semi UI Design QA
|
||||||
|
|
||||||
- source visual truth: Git `2c9399941` 的 `/access` 页面
|
- source visual truth: 当前生产版本 `/users`
|
||||||
- source screenshots:
|
- source screenshots:
|
||||||
- `/tmp/access-desktop-baseline.png`
|
- `/tmp/users-desktop-before.png`
|
||||||
- `/tmp/access-detail-baseline.png`
|
- `/tmp/users-mobile-before.png`
|
||||||
- `/tmp/access-mobile-baseline-full.jpg`
|
|
||||||
- implementation screenshots:
|
- implementation screenshots:
|
||||||
- `/tmp/access-desktop-updated.jpg`
|
- `/tmp/users-desktop-after.png`
|
||||||
- `/tmp/access-detail-updated.jpg`
|
- `/tmp/users-mobile-after.png`
|
||||||
- `/tmp/access-mobile-updated.jpg`
|
- `/tmp/users-mobile-detail-after.png`
|
||||||
|
- implementation paths:
|
||||||
|
- `apps/web/src/v2/pages/UsersPage.tsx`
|
||||||
|
- `apps/web/src/v2/pages/UsersPage.test.tsx`
|
||||||
|
- `apps/web/src/v2/styles/workspace.css`
|
||||||
- viewport and state:
|
- viewport and state:
|
||||||
- desktop `1440 × 900`,全部主车辆,未选中车辆
|
- desktop `1440 × 900`,生产基线截图原始高度为 `810px`,在对比图中补齐到 `900px`
|
||||||
- desktop detail `1440 × 900`,选中 `粤AG18312`
|
- mobile `390 × 844`
|
||||||
- mobile `390 × 844`,全部主车辆,列表首屏
|
- 使用生产管理员真实数据,共 5 个客户账号
|
||||||
- data source: local Vite UI plus the same `20302` mock API for both source and implementation
|
- 验证账号目录、账号选中状态、车辆授权详情和移动端侧滑面板
|
||||||
|
|
||||||
## Comparison Evidence
|
## Comparison Evidence
|
||||||
|
|
||||||
- desktop before/after: `/tmp/access-desktop-comparison.jpg`
|
- desktop before/after: `/tmp/users-desktop-comparison.png`
|
||||||
- detail before/after: `/tmp/access-detail-comparison.jpg`
|
- mobile before/after: `/tmp/users-mobile-comparison.png`
|
||||||
- mobile before/after: `/tmp/access-mobile-comparison.jpg`
|
- directory focus before/after: `/tmp/users-directory-focus-comparison.png`
|
||||||
- every comparison uses the same viewport, API data and interaction state; all three combined images were inspected with `view_image`.
|
- 三组对比图均已通过 `view_image` 并排检查。
|
||||||
|
|
||||||
## Fidelity Ledger
|
## Fidelity Ledger
|
||||||
|
|
||||||
| Surface | Source evidence | Implementation evidence | Result |
|
| Surface | Required fidelity | Implementation evidence | Result |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Information hierarchy | The vehicle list already owns the viewport and the two top metrics stay restrained. | The list-first structure, metric count, filters, pagination and governance entry remain unchanged. | Preserved. |
|
| Directory hierarchy | 账号列表必须占据主工作区,不能由相互割裂的大卡片主导。 | 桌面端改为连续的证据表,行间使用轻分隔线、克制悬浮反馈与选中左侧强调线。 | Improved. |
|
||||||
| Protocol identity | Desktop headers and protocol detail cards exposed internal names such as `GB32960`, `JT808` and `YUTONG_MQTT`. | Both surfaces now use the shared Semi protocol identity: `GB/T 32960`, `JT/T 808`, `宇通 MQTT`, with one consistent color grammar and human-readable descriptions. | Improved. |
|
| Account identity | 客户名称、登录账号和身份来源需形成稳定扫描顺序。 | 名称与账号仍居首,并新增 Semi `Tag` 标注“本地”或外部身份源。 | Improved. |
|
||||||
| Status scanability | Protocol state, time and provider were presented as loose text in wide table cells. | Each status is contained in a subtle semantic surface; online, offline and missing states remain distinguishable without increasing row height. | Improved. |
|
| Permission evidence | 菜单数、车辆数、状态和最近登录需保持对齐。 | 权限数字采用紧凑的浅色单元格表面;状态和时间列仍在原有网格位置。 | Preserved. |
|
||||||
| Selection feedback | Selection relied mainly on a light row fill. | The selected row keeps the light fill and adds a restrained blue leading accent; keyboard focus remains independently visible. | Improved. |
|
| Selected state | 当前编辑账号必须可快速定位。 | 选中行同时保留背景色和蓝色前导强调,详情侧滑面板与选中行一致。 | Improved. |
|
||||||
| Detail evidence | Detail cards had raw protocol headings while their state tag used Semi UI. | Protocol identity and state are now both Semi UI primitives, with the protocol description directly beneath the tag. | Improved. |
|
| Mobile density | 移动端不能为了桌面连续表牺牲触控与信息密度。 | `390 × 844` 下保留独立卡片间距、紧凑身份标签和可点击整行。 | Preserved. |
|
||||||
| Mobile density | Four vehicles were visible in the `390 × 844` viewport. | The same four-vehicle density, fixed pagination, touch targets and vertical-only scrolling are preserved. | Preserved. |
|
| Detail workflow | 打开账号、切换车辆权限、关闭面板必须完整可用。 | 桌面与移动端均验证侧滑面板;移动端车辆授权列表、状态、页签和底部动作无重叠。 | Passed. |
|
||||||
| Responsive behavior | No horizontal overflow at desktop or mobile. | Browser verification reports no document-level horizontal overflow at `390 × 844`; desktop fixed columns and protocol scrolling remain functional. | Passed. |
|
| Asset fidelity | 品牌 Logo 与图标体系不得被临时图形替换。 | 保留现有官方 Logo、Semi 图标及组件;没有新增手绘 SVG、CSS 图标或装饰性位图。 | Preserved. |
|
||||||
|
|
||||||
## Interaction And Accessibility Checks
|
## Interaction And Accessibility Checks
|
||||||
|
|
||||||
- desktop protocol headers expose complete accessible names including protocol and Chinese description;
|
- 点击账号行可打开详情,关闭后回到原目录状态;
|
||||||
- table row opens the vehicle detail sheet by pointer or keyboard;
|
- 车辆权限页签可点击,授权车辆卡片与状态信息完整;
|
||||||
- selected row exposes `aria-expanded=true`;
|
- 搜索与状态筛选入口保持原有行为;
|
||||||
- protocol evidence expands and collapses without an API refetch;
|
- 移动端侧滑面板没有遮挡页签、内容或底部动作;
|
||||||
- mobile list remains vertically scrollable and has no document-level horizontal overflow;
|
- 控制台无本次改动产生的运行时异常;仅存在 Semi UI 在 React StrictMode 下既有的 `findDOMNode` 弃用警告。
|
||||||
- existing filter, export, pagination and governance contracts remain covered by `AccessPage.test.tsx`.
|
|
||||||
|
|
||||||
## Comparison History
|
## Comparison History
|
||||||
|
|
||||||
### Pass 1 — passed
|
### Pass 1 — fixed
|
||||||
|
|
||||||
- No P0/P1/P2 visual or interaction issue was found.
|
- 原实现的桌面账号行被拆成多个带间距卡片,视觉体量偏大,证据列扫描节奏不连续。
|
||||||
- The desktop protocol columns are materially easier to scan while retaining the existing six-row viewport.
|
- 已收敛为连续目录表,并补充身份来源标签,让账号、授权和状态形成一条稳定阅读路径。
|
||||||
- The detail sheet now matches the shared protocol language used by alert and history evidence surfaces.
|
- 修复后对比图中没有发现 P0、P1 或 P2 视觉与交互问题。
|
||||||
- Mobile structure is intentionally unchanged because the existing density and task priority already meet the product requirement.
|
- 当前唯一低风险残余是生产数据均为本地身份源,外部身份源标签由渲染逻辑覆盖,尚无生产样本可做视觉实证。
|
||||||
|
|
||||||
## Intentional Constraints
|
|
||||||
|
|
||||||
- The compact coverage strip retains `32960 / 808 / 宇通` abbreviations because full tags would crowd the 292px context rail.
|
|
||||||
- Semantic state surfaces use low-contrast borders and tints; they do not become large cards or compete with the vehicle and discrepancy columns.
|
|
||||||
- No additional runtime query, animation or component state was introduced.
|
|
||||||
|
|
||||||
final result: passed
|
final result: passed
|
||||||
|
|||||||
Reference in New Issue
Block a user