Compare commits

...
2 Commits
Author SHA1 Message Date
Stev_Wang 4f6ccda6e3 fix(web): 统一用户/角色/部门列表行样式与选中态
ci / 质量门禁 (push) Canceled after 0s
背景:
- 用户、角色列表行为全宽 + 1px 分隔线,选中行与相邻悬停行的高亮背景会盖住分隔线,
  两块色块粘连,看起来间距被挤在一起;
- 部门树为列表内留白 + 圆角行,其左侧 2px 竖条贴行左缘,而用户 / 角色的竖条贴卡片边缘,
  三页选中指示器不一致;部门子级缩进后竖条位置还会随层级漂移;
- 行宽收紧后角色副信息(编码 · 数据范围)会被硬裁掉半个字。

改动:
- 三页列表统一行几何:列表内留白 6px、行间 2px、行圆角 6px,去掉贯穿分隔线;
- 统一取消左侧 2px 竖条,选中态改为品牌浅底 + 品牌色 / 600 字重标题,与侧栏导航一致;
- 行高 64→60,保持 9 条一屏;部门树根级 / 子级行距同步;
- 新建角色页的角色模板列表套用同一套行样式;
- 角色副信息改为块级 text-overflow: ellipsis,右侧统计列 132→112px 归还宽度,
  1440 下角色编码与数据范围不再截断。

影响面:
- 仅管理中心用户 / 角色 / 部门三页与新建角色页的列表视觉;
- 结构与交互不变,接口、权限、路由、数据均不变;≤1280/1080 沿用原响应式收缩规则。

回滚方式:
- 回滚本提交即恢复全宽行 + 1px 分隔线 + 左侧竖条与原行宽。

验证证据:
- pnpm format:check / lint / typecheck 通过;pnpm test:web 120/120 通过;
- Chrome 1440×900 实机截图:选中 + 悬停相邻行留白清晰,三页选中态一致;
- 角色列表副信息溢出检测:overflow: [](无截断)。
2026-11-03 15:55:06 +08:00
Stev_Wang f849e443d7 refactor(web): 侧栏「用户与组织」更名为「用户管理」
背景:
- 管理中心侧栏菜单名为「用户与组织」,与「角色管理」「部门管理」的命名方式不一致;
- 页面标题、面包屑仍沿用旧名,三处展示不统一;
- A1 文档与代码注释中该功能域本来即称「用户管理」。

改动:
- nav-config:菜单 label 改为「用户管理」,description 改为「成员账号、部门归属与登录状态」;
- breadcrumbs:用户新建 / 详情子路由的面包屑 2 处同步;
- UsersPage:页面 h1 同步;
- nav-config.spec / breadcrumbs.spec / e2e a1-identity 的标签与 heading 断言同步;
- 11-前端路由与目录规范、AppLayout 注释同步;PRD P-01 功能名保持「用户与组织管理」不变。

影响面:
- 仅前端文案与对应断言;路由、权限点、接口、数据均不变。

回滚方式:
- 回滚本提交即恢复原「用户与组织」文案与断言。

验证证据:
- pnpm format:check / lint / typecheck 通过;
- pnpm test:web 120/120 通过;
- Chrome 1440×900 实机:侧栏、面包屑、页面标题均显示「用户管理」。
2026-11-03 15:54:13 +08:00
12 changed files with 55 additions and 42 deletions

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@ import { PageBreadcrumbs } from './PageBreadcrumbs';
import { SideNav } from './SideNav';
import { TopBar } from './TopBar';
/** 用户与组织、部门管理为「满高主从布局」,由页面自己控制内部滚动 */
/** 用户管理、部门管理为「满高主从布局」,由页面自己控制内部滚动 */
function isFlushRoute(pathname: string): boolean {
return (
pathname === ROUTES.system.users ||
+1 -1
View File
@@ -32,7 +32,7 @@ describe('面包屑子路由补全', () => {
expect(getUserSectionTrail('/system/users/new')?.map((entry) => entry.label)).toEqual([
'工作台',
'管理中心',
'用户与组织',
'用户管理',
'新建用户',
]);
expect(getUserSectionTrail('/system/users/01HZZZZZZZZZZZZZZZZZZZZZZZ')).not.toBeNull();
+3 -3
View File
@@ -25,14 +25,14 @@ export function getRoleSectionTrail(pathname: string): BreadcrumbEntry[] | null
/**
* 用户模块下的子路由(新建 / 详情)不在 nav-config 的菜单项里,
* 这里补齐面包屑:工作台 / 管理中心 / 用户与组织 / 当前页。
* 这里补齐面包屑:工作台 / 管理中心 / 用户管理 / 当前页。
*/
export function getUserSectionTrail(pathname: string): BreadcrumbEntry[] | null {
if (pathname === ROUTES.system.userNew) {
return [
{ key: 'home', label: '工作台', path: ROUTES.home },
{ key: 'system', label: '管理中心' },
{ key: 'users', label: '用户与组织', path: ROUTES.system.users },
{ key: 'users', label: '用户管理', path: ROUTES.system.users },
{ key: 'user-new', label: '新建用户' },
];
}
@@ -40,7 +40,7 @@ export function getUserSectionTrail(pathname: string): BreadcrumbEntry[] | null
return [
{ key: 'home', label: '工作台', path: ROUTES.home },
{ key: 'system', label: '管理中心' },
{ key: 'users', label: '用户与组织', path: ROUTES.system.users },
{ key: 'users', label: '用户管理', path: ROUTES.system.users },
{ key: 'user-detail', label: '成员详情' },
];
}
+3 -3
View File
@@ -26,8 +26,8 @@ describe('导航配置', () => {
expect(modules.map((module) => module.key)).toEqual(['dashboard']);
});
it('子路由(/system/users/new、/system/users/:id)也归属用户与组织', () => {
expect(findActiveNav(NAV_MODULES, '/system/users/new')?.item?.label).toBe('用户与组织');
it('子路由(/system/users/new、/system/users/:id)也归属用户管理', () => {
expect(findActiveNav(NAV_MODULES, '/system/users/new')?.item?.label).toBe('用户管理');
expect(findActiveNav(NAV_MODULES, '/system/users/01HZZZZZZZZZZZZZZZZZZZZZZZ')?.module.key).toBe(
'system',
);
@@ -63,7 +63,7 @@ describe('导航配置', () => {
it('面包屑从导航配置派生', () => {
expect(getBreadcrumbs(NAV_MODULES, '/')).toEqual([{ key: 'home', label: '工作台', path: '/' }]);
const breadcrumbs = getBreadcrumbs(NAV_MODULES, '/system/users');
expect(breadcrumbs.map((entry) => entry.label)).toEqual(['工作台', '管理中心', '用户与组织']);
expect(breadcrumbs.map((entry) => entry.label)).toEqual(['工作台', '管理中心', '用户管理']);
const deptBreadcrumbs = getBreadcrumbs(NAV_MODULES, '/system/departments');
expect(deptBreadcrumbs.map((entry) => entry.label)).toEqual(['工作台', '管理中心', '部门管理']);
});
+2 -2
View File
@@ -249,11 +249,11 @@ export const NAV_MODULES: readonly NavModule[] = [
items: [
{
key: 'users',
label: '用户与组织',
label: '用户管理',
path: '/system/users',
icon: IconUser,
permission: Permission.USER_VIEW,
description: '账号、组织与登录状态',
description: '成员账号、部门归属与登录状态',
},
{
key: 'departments',
@@ -83,7 +83,7 @@ function TemplateList({
onRetry={onRetry}
/>
) : (
<div role="listbox" aria-label="选择模板">
<div className="lx-role-template-list" role="listbox" aria-label="选择模板">
<button
type="button"
role="option"
@@ -31,7 +31,7 @@ function UsersHeader(): ReactNode {
return (
<header className="lx-users__header">
<div>
<h1>用户与组织</h1>
<h1>用户管理</h1>
<p>管理成员账号、角色与客户归属</p>
</div>
<Can permission={Permission.USER_MANAGE}>
+10 -1
View File
@@ -96,10 +96,17 @@
}
.lx-dept-tree__list {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px;
}
.lx-dept-tree__children {
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 2px;
padding-left: 14px;
}
@@ -110,15 +117,16 @@
gap: 6px;
padding: 4px 6px;
border-radius: var(--lx-radius-md);
transition: background var(--lx-motion-fast) var(--lx-ease-out);
}
.lx-dept-row:hover {
background: var(--lx-color-bg-hover);
}
/* 选中态:品牌浅底 + 品牌色标题(与侧栏导航同一套语言,不再画左侧竖条) */
.lx-dept-row.is-selected {
background: var(--lx-color-brand-soft);
box-shadow: inset 2px 0 0 var(--lx-color-brand);
}
.lx-dept-row.is-disabled .lx-dept-row__name b,
@@ -171,6 +179,7 @@
.lx-dept-row.is-selected .lx-dept-row__name b {
color: var(--lx-color-brand);
font-weight: 600;
}
.lx-dept-row__meta {
+22 -17
View File
@@ -220,8 +220,9 @@
.lx-role-list__items {
display: flex;
flex-direction: column;
gap: 2px;
margin: 0;
padding: 0;
padding: 6px 8px;
list-style: none;
}
@@ -229,20 +230,16 @@
position: relative;
display: flex;
width: 100%;
min-height: 64px;
min-height: 60px;
align-items: center;
gap: 10px;
padding: 8px 10px 8px 14px;
border-bottom: 1px solid var(--lx-color-border);
padding: 8px 10px 8px 12px;
border-radius: var(--lx-radius-md);
text-align: left;
cursor: pointer;
transition: background var(--lx-motion-fast) var(--lx-ease-out);
}
.lx-role-list__items li:last-child .lx-role-row {
border-bottom: none;
}
.lx-role-row:hover {
background: var(--lx-color-bg-hover);
}
@@ -252,9 +249,9 @@
outline-offset: -2px;
}
/* 选中态:品牌浅底 + 品牌色标题(与侧栏导航同一套语言,不再画左侧竖条) */
.lx-role-row.is-selected {
background: var(--lx-color-brand-soft);
box-shadow: inset 2px 0 0 var(--lx-color-brand);
}
.lx-role-row__icon {
@@ -302,17 +299,16 @@
.lx-role-row.is-selected .lx-role-row__name b {
color: var(--lx-color-brand);
font-weight: 600;
}
.lx-role-row.is-disabled .lx-role-row__name b {
color: var(--lx-color-text-secondary);
}
/* 块级 + ellipsis:角色编码与数据范围可能超宽,避免被侧栏裁掉半个字 */
.lx-role-row__meta {
display: flex;
overflow: hidden;
align-items: center;
gap: 4px;
font-size: var(--lx-font-size-xs);
line-height: 16px;
color: var(--lx-color-text-secondary);
@@ -322,13 +318,14 @@
}
.lx-role-row__dot {
margin: 0 4px;
color: var(--lx-color-border-strong);
}
.lx-role-row__side {
display: flex;
width: 132px;
flex: 0 0 132px;
width: 112px;
flex: 0 0 112px;
flex-direction: column;
align-items: flex-end;
gap: 4px;
@@ -756,13 +753,20 @@
padding: var(--lx-space-4);
}
.lx-role-template-list {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px 8px;
}
.lx-role-template {
display: flex;
width: 100%;
align-items: flex-start;
gap: 10px;
padding: 10px 14px;
border-bottom: 1px solid var(--lx-color-border);
padding: 10px 12px;
border-radius: var(--lx-radius-md);
text-align: left;
transition: background var(--lx-motion-fast) var(--lx-ease-out);
}
@@ -776,9 +780,9 @@
outline-offset: -2px;
}
/* 选中态:品牌浅底 + 品牌色标题(与侧栏导航同一套语言,不再画左侧竖条) */
.lx-role-template.is-active {
background: var(--lx-color-brand-soft);
box-shadow: inset 2px 0 0 var(--lx-color-brand);
}
.lx-role-template__icon {
@@ -817,6 +821,7 @@
.lx-role-template.is-active .lx-role-template__name {
color: var(--lx-color-brand);
font-weight: 600;
}
.lx-role-template__desc {
+8 -9
View File
@@ -1,4 +1,4 @@
/* A1 · 用户与组织(方案 B:主从分栏 + 分屏登录) */
/* A1 · 用户管理(方案 B:主从分栏 + 分屏登录) */
/* ---------- 页面骨架 ---------- */
@@ -159,31 +159,29 @@
.lx-user-list__items {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px 8px;
}
.lx-user-row {
display: flex;
width: 100%;
min-height: 64px;
min-height: 60px;
align-items: center;
gap: 10px;
padding: 8px var(--lx-space-4);
border-bottom: 1px solid var(--lx-color-border);
padding: 8px 12px;
border-radius: var(--lx-radius-md);
text-align: left;
transition: background var(--lx-motion-fast) var(--lx-ease-out);
}
.lx-user-row:last-child {
border-bottom: none;
}
.lx-user-row:hover {
background: var(--lx-color-bg-hover);
}
/* 选中态:品牌浅底 + 品牌色标题(与侧栏导航同一套语言,不再画左侧竖条) */
.lx-user-row.is-selected {
background: var(--lx-color-brand-soft);
box-shadow: inset 2px 0 0 var(--lx-color-brand);
}
.lx-user-row__avatar {
@@ -224,6 +222,7 @@
.lx-user-row.is-selected .lx-user-row__name {
color: var(--lx-color-brand);
font-weight: 600;
}
.lx-user-row__phone {
@@ -63,7 +63,7 @@ apps/web/src/pages/
├─ assets/ # 资产(对应 /assets)
├─ delivery/ # 交付(对应 /delivery)
├─ system/ # 管理中心(对应 /system)
│ └─ users/ # 用户与组织(对应 /system/users)
│ └─ users/ # 用户管理(对应 /system/users)
│ ├─ UsersPage.tsx
│ ├─ UserNewPage.tsx
│ ├─ UserListPanel.tsx
+2 -2
View File
@@ -408,7 +408,7 @@ async function submitLogin(page: Page): Promise<void> {
async function expectUsersPage(page: Page): Promise<void> {
await expect(page.getByTestId('app-shell')).toBeVisible();
await expect(page.getByRole('heading', { name: '用户与组织' })).toBeVisible();
await expect(page.getByRole('heading', { name: '用户管理' })).toBeVisible();
}
async function readSession(page: Page): Promise<SessionSnapshot> {
@@ -641,7 +641,7 @@ test('④ 停用后旧会话被拒(40301),再登录提示「账号已停
await page.goto('/system/users');
await expect(page).toHaveURL(/\/login/);
await expect(page.locator('.lx-login__notice')).toContainText('账号已被停用');
await expect(page.getByRole('heading', { name: '用户与组织' })).toHaveCount(0);
await expect(page.getByRole('heading', { name: '用户管理' })).toHaveCount(0);
await expect
.poll(
async () =>