初始化项目版本

This commit is contained in:
Axhub Make
2026-07-29 16:04:39 +08:00
commit 4305a1082b
2629 changed files with 760590 additions and 0 deletions

View File

@@ -0,0 +1,292 @@
# OneOS V2 Complex Ledger Column Customization Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add complete, discoverable OneOS V2 design-system rules for PC/Web complex-ledger column resizing, column visibility, column ordering, persistence, recovery, and accessibility.
**Architecture:** Keep the complete normative rules in the existing Table component section, then add narrow scope and accessibility references in the Layout and Accessibility chapters. Add only a concise mandatory summary to the top-level design-system document so the overview stays readable while all implementation details remain in one authoritative section.
**Tech Stack:** Markdown design-system documentation, OneOS V2 design tokens, existing React vehicle-assets table as the standard example.
---
## File Map
- Modify `src/resources/design-system/chapters/03-components.md`: authoritative component, interaction, persistence, recovery, and acceptance rules.
- Modify `src/resources/design-system/chapters/02-layout.md`: PC/Web scope, H5 exclusion, and horizontal-overflow boundary.
- Modify `src/resources/design-system/chapters/06-accessibility.md`: keyboard, focus, live-region, and drag-alternative requirements.
- Modify `src/resources/design-system/DESIGN.md`: mandatory overview summary and links to the authoritative chapter rules.
- Reference only `src/prototypes/vehicle-management/components/ListView.tsx`: existing standard example; do not change its business behavior in this plan.
The workspace is not a Git repository, so this plan intentionally contains no commit commands. Each task ends with a file-level verification checkpoint instead.
### Task 1: Add the authoritative Table column-personalization rules
**Files:**
- Modify: `src/resources/design-system/chapters/03-components.md`, immediately after the current `### 3.1 TableWeb` base bullets and before `### 3.2 List / Description / KPI / Card`
- Reference: `docs/superpowers/specs/2026-07-26-complex-ledger-column-customization-design.md`
- [ ] **Step 1: Capture the expected section contract before editing**
Run:
```bash
rg -n "复杂台账列个性化|列宽调整|列设置|userId.*ledgerId.*schemaVersion" \
src/resources/design-system/chapters/03-components.md
```
Expected: no matches, proving the normative section is currently absent.
- [ ] **Step 2: Add the complete normative section**
Insert this Markdown:
```markdown
#### 3.1.1 复杂台账列个性化PC / Web 强制)
适用于字段较多、需要横向浏览或用户角色关注字段不同的复杂台账。车辆资产页
`src/prototypes/vehicle-management/components/ListView.tsx` 为标准交互示例。
**能力与入口**
- 表头列分隔线提供列宽调节把手;拖动时实时预览,释放后保存。
- 台账工具栏提供“列设置”图标按钮,弹层集中处理列显隐、列顺序和恢复默认。
- 弹层内修改先暂存;“确认”后一次应用并保存,“取消”不改变当前列表。
**列宽**
- 调节把手命中区约 `12px`默认显示细分隔线Hover、Focus 和拖动时使用主题主色增强。
- 普通业务列限制为 `90480px`;首个核心识别列建议限制为 `120360px`
- 达到边界后停止变化,不允许无限缩小或撑宽。
- 双击把手只恢复当前列默认宽度。
- 表格继续在自身滚动容器内横向滚动,不得造成整页横向滚动。
**固定列与业务列**
- 首个核心识别列:必须显示、不可换序、允许在限制范围内调宽。
- 末尾操作列:必须显示、不可换序、不可调宽,宽度依据 `OperationActions` 规范固定。
- 其他业务列:允许显隐、换序和调宽;排序不得越过两端固定列。
- “全选/全不选”只影响可选业务列。固定列必须展示“固定”标识及限制说明。
**保存与恢复**
- 配置按“用户 + 台账页面”自动保存;建议保存键包含
`userId + ledgerId + schemaVersion`
- 配置至少包含 `columnWidths``visibleColumns``columnOrder`
- 列宽在拖动结束后保存;列显隐与顺序在弹层确认后保存。
- 再次进入同一台账时恢复配置;不同用户和不同台账之间隔离。
- “恢复默认”在弹层内预览默认列宽、显隐和顺序,用户确认后应用并保存,不使用危险确认弹窗。
- 新增字段按设计默认显隐状态和默认位置合并;已删除字段自动忽略,历史配置不得整体失效。
- 配置损坏、版本不兼容或加载失败时回退默认配置,不阻塞台账数据加载。
- 保存失败时保留当前会话配置并轻提示:
“当前设置暂未同步,下次进入可能恢复默认”。
**主题与反馈**
- 把手、焦点、选中和拖拽目标状态必须消费 OneOS V2 主题 Token不得硬编码品牌色。
- 固定和禁用状态必须有文字或图标加文字说明,不能只依赖颜色。
- 保存过程不得阻塞台账浏览和操作。
```
- [ ] **Step 3: Verify the authoritative section is complete and unique**
Run:
```bash
rg -n "复杂台账列个性化|90480px|120360px|columnWidths|当前设置暂未同步" \
src/resources/design-system/chapters/03-components.md
```
Expected: each required phrase appears once inside section `3.1.1`.
### Task 2: Define platform and overflow boundaries
**Files:**
- Modify: `src/resources/design-system/chapters/02-layout.md`, in `### 4.1 Web 列表页`
- Reference: `src/resources/design-system/DESIGN.md`, existing H5 table-to-card rule
- [ ] **Step 1: Verify the platform boundary is missing**
Run:
```bash
rg -n "H5.*忽略.*列配置|列个性化.*PC|整页横向滚动.*列宽" \
src/resources/design-system/chapters/02-layout.md
```
Expected: no complete rule matching all three concerns.
- [ ] **Step 2: Add the PC/Web and H5 layout rule**
Append this block within `### 4.1 Web 列表页`:
```markdown
#### 4.1.1 复杂台账列个性化的端侧边界
- 列宽、列显隐和列顺序仅适用于 PC / Web 表格。
- H5≤767px按本规范转换为单列卡片不显示列设置入口也不提供表头拖拽。
- H5 忽略但不得删除同一用户已经保存的 PC 列配置。
- 用户扩大业务列后,只允许表格自身滚动容器横向滚动;页面壳层不得产生横向滚动。
- 完整交互和保存规则见 [03-components §3.1.1](./03-components.md#311-复杂台账列个性化pc--web-强制)。
```
- [ ] **Step 3: Verify the layout chapter contains the boundary and link**
Run:
```bash
rg -n "4\\.1\\.1 复杂台账列个性化的端侧边界|H5 忽略但不得删除|03-components §3\\.1\\.1" \
src/resources/design-system/chapters/02-layout.md
```
Expected: three matches in the new subsection.
### Task 3: Add keyboard and screen-reader equivalence
**Files:**
- Modify: `src/resources/design-system/chapters/06-accessibility.md`, under `## 2. 焦点与键盘Web`
- [ ] **Step 1: Capture the current accessibility gap**
Run:
```bash
rg -n "Shift.*24px|Home.*默认宽度|上移/下移|aria-live.*列" \
src/resources/design-system/chapters/06-accessibility.md
```
Expected: no matches; the current chapter only states the general requirement that drag sorting needs an alternative.
- [ ] **Step 2: Add the complex-ledger accessibility rules**
Append these bullets under `## 2. 焦点与键盘Web`:
```markdown
### 2.1 复杂台账列控制
- “列设置”按钮必须提供 `aria-label``aria-expanded` 和弹层关联;打开后焦点进入弹层,关闭后回到触发按钮,`Esc` 可关闭。
- 列宽调节把手必须可聚焦:方向键每次调整 `8px``Shift + 方向键` 每次调整 `24px``Home` 恢复当前列默认宽度。
- 读屏必须播报当前列名和调整后的像素宽度。
- 列排序除拖拽外必须提供“上移/下移”按钮;达到边界或属于固定列时禁用并说明原因。
- 显隐复选框必须与列名关联;固定列控件禁用并以文字说明“固定列不可隐藏”。
- 调宽、排序、恢复默认、保存成功和保存失败使用 `aria-live="polite"` 播报,不能只依赖颜色。
- 视觉分隔线可以为 `23px`,但鼠标命中区保持约 `12px`;所有可聚焦控件使用主题 Focus Token。
```
- [ ] **Step 3: Verify all keyboard operations and announcements are present**
Run:
```bash
rg -n "aria-expanded|方向键每次调整 `8px`|Shift \\+ 方向键|Home.*默认宽度|上移/下移|aria-live=\"polite\"" \
src/resources/design-system/chapters/06-accessibility.md
```
Expected: all six patterns match inside `2.1 复杂台账列控制`.
### Task 4: Add the mandatory overview summary
**Files:**
- Modify: `src/resources/design-system/DESIGN.md`, under `## 1. 设计原则`
- Modify: `src/resources/design-system/DESIGN.md`, frontmatter version and document-version row
- [ ] **Step 1: Verify the overview principle is absent**
Run:
```bash
rg -n "复杂台账列个性化|列宽、列显隐、列顺序" src/resources/design-system/DESIGN.md
```
Expected: no matches.
- [ ] **Step 2: Add the seventh design principle**
Append this item after the current sixth principle:
```markdown
7. **复杂台账列个性化PC / Web 强制)**:字段较多的复杂台账必须支持列宽调整、可选业务列显隐、可选业务列顺序调整及恢复默认;配置按“用户 + 台账页面”保存。首个核心识别列与末尾操作列遵守固定列限制H5 卡片列表不提供列操作。完整规则见 [03-components §3.1.1](./chapters/03-components.md#311-复杂台账列个性化pc--web-强制)、[02-layout §4.1.1](./chapters/02-layout.md#411-复杂台账列个性化的端侧边界) 与 [06-accessibility §2.1](./chapters/06-accessibility.md#21-复杂台账列控制)。
```
- [ ] **Step 3: Update both version declarations**
Replace the frontmatter version and version-row content with:
```markdown
version: "2.6"
| 文档版本 | **v2.6 复杂台账列个性化规范**2026-07-26 |
```
- [ ] **Step 4: Verify the overview stays concise and links all rule owners**
Run:
```bash
rg -n '^version: "2\\.6"|v2\\.6 复杂台账列个性化规范|03-components §3\\.1\\.1|02-layout §4\\.1\\.1|06-accessibility §2\\.1' \
src/resources/design-system/DESIGN.md
```
Expected: both version declarations match and the seventh principle links all three chapters.
### Task 5: Cross-document verification
**Files:**
- Verify: `src/resources/design-system/DESIGN.md`
- Verify: `src/resources/design-system/chapters/02-layout.md`
- Verify: `src/resources/design-system/chapters/03-components.md`
- Verify: `src/resources/design-system/chapters/06-accessibility.md`
- [ ] **Step 1: Check the confirmed requirements are represented**
Run:
```bash
rg -n \
"列宽|列显隐|列顺序|恢复默认|用户 \\+ 台账页面|固定列|H5|方向键|上移/下移|保存失败" \
src/resources/design-system/DESIGN.md \
src/resources/design-system/chapters/02-layout.md \
src/resources/design-system/chapters/03-components.md \
src/resources/design-system/chapters/06-accessibility.md
```
Expected: every requirement category appears in its designated owner document, with the complete behavior in `03-components.md`.
- [ ] **Step 2: Check for unfinished or ambiguous markers**
Run:
```bash
rg -n "TB""D|TO""DO|待""定|待""确认|FIX""ME|酌情|视情况" \
src/resources/design-system/DESIGN.md \
src/resources/design-system/chapters/02-layout.md \
src/resources/design-system/chapters/03-components.md \
src/resources/design-system/chapters/06-accessibility.md
```
Expected: no new matches in the edited sections.
- [ ] **Step 3: Confirm the standard example still resolves**
Run:
```bash
test -f src/prototypes/vehicle-management/components/ListView.tsx
rg -n "DEFAULT_COLUMN_WIDTHS|列表设置(自定义显隐与列顺序)|va-th-resizer" \
src/prototypes/vehicle-management/components/ListView.tsx
```
Expected: the file exists and all three reference patterns match.
- [ ] **Step 4: Review the final diff manually**
Because this workspace has no Git metadata, run:
```bash
sed -n '1,220p' src/resources/design-system/chapters/03-components.md
sed -n '55,125p' src/resources/design-system/chapters/02-layout.md
sed -n '15,75p' src/resources/design-system/chapters/06-accessibility.md
sed -n '1,55p' src/resources/design-system/DESIGN.md
```
Expected: headings are correctly nested, cross-links use existing relative paths, detailed rules are not duplicated in the overview, and no unrelated content changed.

View File

@@ -0,0 +1,499 @@
# OneOS V2 Global Operation Actions Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make OneOS V2 list action columns expose “查看/详情” first and one workflow action second, while moving management, history, and dangerous actions into the shared “更多” menu; migrate the vehicle asset list to that shared behavior.
**Architecture:** Keep `OperationActions` as the only renderer and menu owner. Split detail-entry recognition from history recognition in the flat-item adapter, then consume the public `view`, `edit`, and `more` inputs from the vehicle list. Update every normative document that currently describes the old “details in more” behavior.
**Tech Stack:** React 18, TypeScript 5.9, Vitest 4, Lucide React, Vite, CSS.
---
## File map
- Create `src/common/OperationActions.test.tsx`: focused regression tests for action ordering, detail/history classification, hidden actions, and empty menus.
- Modify `src/common/OperationActions.tsx`: render detail first, allow one workflow primary second, and keep history items in `more`.
- Modify `src/common/operation-actions-spec.md`: public component contract and migration guidance.
- Modify `src/resources/design-system/DESIGN.md`: global normative summary.
- Modify `src/resources/design-system/chapters/03-components.md`: component chapter summary and widths.
- Modify `src/prototypes/vm-shared/DESIGN.md`: full shared pattern, examples, and forbidden patterns.
- Modify `src/prototypes/vehicle-management/components/ListView.tsx`: remove the private menu and use `OperationActions`.
- Modify `src/prototypes/vehicle-management/style.css`: remove vehicle-only action-menu skin and size the sticky operation column for the shared component.
### Task 1: Lock the shared action contract with tests
**Files:**
- Create: `src/common/OperationActions.test.tsx`
- [ ] **Step 1: Write regression tests for classification and markup order**
Create the following test file:
```tsx
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, expect, it, vi } from 'vitest';
import {
OperationActions,
splitOperationActions,
type OperationActionItem,
} from './OperationActions';
const action = (key: string, label: string): OperationActionItem => ({
key,
label,
onClick: vi.fn(),
});
describe('splitOperationActions', () => {
it('separates the detail entry from history actions', () => {
const items = [
action('history', '操作记录'),
action('edit', '编辑'),
action('view', '查看详情'),
action('owner', '设置运维负责人'),
];
const result = splitOperationActions(items);
expect(result.view?.label).toBe('查看详情');
expect(result.edit?.label).toBe('编辑');
expect(result.more.map((item) => item.label)).toEqual([
'操作记录',
'设置运维负责人',
]);
});
it('does not promote 查看记录 as a detail entry', () => {
const result = splitOperationActions([
action('viewRecords', '查看记录'),
action('process', '处置'),
]);
expect(result.view).toBeUndefined();
expect(result.process?.label).toBe('处置');
expect(result.more.map((item) => item.label)).toEqual(['查看记录']);
});
});
describe('OperationActions', () => {
it('renders detail before the workflow action and more', () => {
const html = renderToStaticMarkup(
<OperationActions
view={{ label: '查看', onClick: vi.fn() }}
edit={{ label: '编辑', onClick: vi.fn() }}
more={[action('owner', '设置运维负责人')]}
/>,
);
expect(html.indexOf('>查看<')).toBeLessThan(html.indexOf('>编辑<'));
expect(html.indexOf('>编辑<')).toBeLessThan(html.indexOf('aria-label="更多操作"'));
});
it('does not render more when all menu actions are hidden', () => {
const html = renderToStaticMarkup(
<OperationActions
view={{ label: '详情', onClick: vi.fn() }}
more={[{ ...action('owner', '设置运维负责人'), hidden: true }]}
/>,
);
expect(html).toContain('>详情<');
expect(html).not.toContain('aria-label="更多操作"');
});
});
```
- [ ] **Step 2: Run the focused tests and verify the old behavior fails**
Run:
```bash
npm run test:run -- src/common/OperationActions.test.tsx
```
Expected: at least the ordering and classification assertions fail because the current implementation puts `view` into “更多” when `edit` or `process` exists and treats history labels as `view`.
### Task 2: Implement the global shared-component behavior
**Files:**
- Modify: `src/common/OperationActions.tsx`
- Test: `src/common/OperationActions.test.tsx`
- [ ] **Step 1: Update the public contract and primary ordering**
Replace the `view` prop comment, primary-selection block, and `outsidePrimaries` construction with:
```tsx
/**
* 查看 / 详情:对象详情入口,固定在外侧首位。
* 历史型“查看记录 / 操作记录”应通过 more 传入。
*/
view?: OperationPrimaryAction;
```
```tsx
const visibleEdit = edit && !edit.hidden ? edit : undefined;
const visibleProcess = process && !process.hidden ? process : undefined;
const visibleView = view && !view.hidden ? view : undefined;
const outsidePrimaries = useMemo(() => {
const list: Array<{ kind: PrimaryKind; action: OperationPrimaryAction; defaultLabel: string }> =
[];
if (visibleView) {
list.push({
kind: 'view',
action: visibleView,
defaultLabel: visibleView.label || '详情',
});
}
if (visibleEdit) {
list.push({ kind: 'edit', action: visibleEdit, defaultLabel: '编辑' });
} else if (visibleProcess) {
list.push({
kind: 'process',
action: visibleProcess,
defaultLabel: visibleProcess.label || '处理',
});
}
return list.slice(0, 2);
}, [visibleEdit, visibleProcess, visibleView]);
```
Delete `hasWorkPrimary`, `viewInMore`, `viewOutside`, and the block that injects `visibleView` into `visibleMore`.
- [ ] **Step 2: Keep deduplication without hiding history actions**
Replace `visibleMore` with:
```tsx
const visibleMore = useMemo(() => {
const items: OperationActionItem[] = [];
more.forEach((item) => {
if (item.hidden) return;
if (visibleEdit && (item.key === 'edit' || item.label === '编辑' || item.label === '编辑合同')) {
return;
}
if (
visibleProcess &&
(item.key === 'process' ||
item.label === '处理' ||
item.label === '处置' ||
item.label === (visibleProcess.label || ''))
) {
return;
}
if (
visibleView &&
(item.key === 'view' ||
item.label === '查看' ||
item.label === '详情' ||
item.label === '查看详情')
) {
return;
}
items.push(item);
});
return items;
}, [more, visibleView, visibleEdit, visibleProcess]);
```
- [ ] **Step 3: Separate detail labels from history labels in the adapter**
Replace the view label set and view classification with:
```tsx
const DETAIL_LABELS = new Set(['详情', '查看', '查看详情']);
const HISTORY_LABELS = new Set(['查看记录', '操作记录']);
```
```tsx
const viewItem = visible.find(
(item) => item.key === 'view' || DETAIL_LABELS.has(item.label),
);
const more = visible.filter(
(item) =>
item !== editItem &&
item !== processItem &&
item !== viewItem,
);
```
Return `viewItem.label` unchanged. Preserve history items—including `key === 'viewRecords'`, `key === 'history'`, and `HISTORY_LABELS` matches—in `more`.
- [ ] **Step 4: Update the component-level summary comment**
Use:
```tsx
/**
* 列表操作列(强制):详情入口固定首位,编辑/处理外显一个,
* 历史、低频管理和危险操作进入更多。
*/
```
- [ ] **Step 5: Run the focused tests**
Run:
```bash
npm run test:run -- src/common/OperationActions.test.tsx
```
Expected: 4 tests pass.
- [ ] **Step 6: Run TypeScript checking**
Run:
```bash
npm run typecheck
```
Expected: exit code 0 with no TypeScript diagnostics.
### Task 3: Update every normative OneOS V2 document
**Files:**
- Modify: `src/resources/design-system/DESIGN.md`
- Modify: `src/resources/design-system/chapters/03-components.md`
- Modify: `src/prototypes/vm-shared/DESIGN.md`
- Modify: `src/common/operation-actions-spec.md`
- [ ] **Step 1: Replace the global §3.16 layout summary**
In `src/resources/design-system/DESIGN.md`, define:
```text
[查看/详情] [编辑/处理/处置] [⋮ 更多]
↑ 固定首位 ↑ 最多一个 ↑ 历史 / 管理 / 危险操作
```
State that detail entry is external, “查看记录 / 操作记录” remains in more, only one workflow action is external when detail exists, and two workflow actions are allowed only when there is no detail entry.
- [ ] **Step 2: Update the component chapter summary**
In `src/resources/design-system/chapters/03-components.md`, replace the old sentence with:
```markdown
- 操作列(强制)`OperationActions`:对象入口 `[查看/详情]` 固定首位,常用 `[编辑/处理]` 外显一个;历史、低频管理与危险操作进入 `[⋮ 更多]`;列宽 148184推荐 `fixed: 'right'`。详见 `vm-shared/DESIGN.md`
```
- [ ] **Step 3: Rewrite the shared full pattern**
In `src/prototypes/vm-shared/DESIGN.md`:
- change the diagram to the approved three-part order;
- define `查看/详情` as the object entry;
- define `查看记录/操作记录` as history and keep them in more;
- change the suggested width to `148184px`;
- update the example so `view={{ label: '查看详情', ... }}` opens the object and `more` contains a history action;
- replace the old forbidden-pattern text with prohibitions against hiding the detail entry, exposing history as the detail entry, or showing more than two primary actions.
- [ ] **Step 4: Rewrite the public component spec**
In `src/common/operation-actions-spec.md`, document the same ordering and replace the example with:
```tsx
<OperationActions
view={{ label: '查看', onClick: () => openDetail(record) }}
edit={canEdit ? { onClick: () => openEdit(record) } : undefined}
more={[
{ key: 'history', label: '操作记录', onClick: () => openHistory(record) },
]}
/>
```
- [ ] **Step 5: Scan for stale normative wording**
Run:
```bash
rg -n "查看详情.*更多|查看记录.*外显|低频.*查看详情|\\[编辑\\].*\\[处理\\]" \
src/resources/design-system \
src/prototypes/vm-shared/DESIGN.md \
src/common/operation-actions-spec.md
```
Expected: no statement contradicts the new rules. Historical background text is acceptable only when explicitly marked as the old behavior.
### Task 4: Migrate the vehicle asset operation column
**Files:**
- Modify: `src/prototypes/vehicle-management/components/ListView.tsx`
- Modify: `src/prototypes/vehicle-management/style.css`
- [ ] **Step 1: Import the shared component and remove private-menu imports**
Add the correct relative import:
```tsx
import { OperationActions } from '../../../common/OperationActions';
```
Remove `Eye`, `MoreHorizontal`, `ShieldAlert`, and `UserCog` from Lucide imports only if no remaining usage exists after migration. Keep imports still used elsewhere in `ListView.tsx`.
- [ ] **Step 2: Delete the private `ActionsMoreMenu` component**
Remove the complete `ActionsMoreMenu` function, including its local `open` state, click-outside listener, escape listener, and private menu markup.
- [ ] **Step 3: Replace both vehicle action-cell call sites**
At each current `<ActionsMoreMenu ... />` site, render:
```tsx
<OperationActions
view={{ label: '查看', onClick: () => onOpenDetail(row) }}
edit={{ label: '编辑', onClick: () => onEdit(row) }}
more={[
{
key: 'owner',
label: '设置运维负责人',
onClick: () => onOps(row),
},
]}
/>
```
If an existing local permission flag controls edit or operations management, pass `undefined` or `hidden: true` before rendering rather than creating an empty menu. Do not introduce new permission semantics.
- [ ] **Step 4: Remove the private menu skin**
Delete vehicle-only selectors whose sole owner was `ActionsMoreMenu`, including:
```css
.va-actions-more-wrap
.va-btn-more
.va-actions-menu
.va-actions-menu__item
.va-actions-menu__icon
.va-actions-menu__text
.va-actions-menu__title
.va-actions-menu__sub
```
Also remove `:has(.va-actions-menu)` stacking workarounds made obsolete by the portal-based shared menu. Keep unrelated table and sticky-column rules.
- [ ] **Step 5: Size and align the shared operation column**
Set the vehicle column styles to:
```css
.va-th-actions,
.va-td-actions {
min-width: 184px !important;
width: 184px !important;
max-width: 184px !important;
box-sizing: border-box;
}
.va-td-actions {
overflow: visible !important;
vertical-align: middle !important;
}
.va-td-actions .vm-operation-actions {
width: 100%;
justify-content: flex-start;
}
```
Use the same width in the `ListView.tsx` column definition or width map if it is also hard-coded there.
- [ ] **Step 6: Run static validation**
Run:
```bash
npm run typecheck
npm run test:run -- src/common/OperationActions.test.tsx
npm run build
```
Expected: all commands exit 0.
### Task 5: Browser verification
**Files:**
- Verify: `src/prototypes/vehicle-management/components/ListView.tsx`
- Verify: `src/common/OperationActions.tsx`
- Verify: `src/prototypes/vehicle-management/style.css`
- [ ] **Step 1: Start the local prototype**
Run:
```bash
npm run dev -- --host 127.0.0.1
```
Expected: Vite prints a local URL and remains running.
- [ ] **Step 2: Verify desktop behavior**
Open the vehicle asset list and confirm:
- every row shows `查看`, then `编辑`, then the ellipsis;
- `查看` enters the same detail flow as before;
- `编辑` opens the same edit flow as before;
- the ellipsis menu contains only `设置运维负责人`;
- the menu is not clipped by the sticky operation column or scrolling container;
- buttons remain on one line and the operation column is visually stable while horizontally scrolling.
- [ ] **Step 3: Verify keyboard and constrained width**
At desktop and a narrow viewport:
- use Tab to focus `查看`, `编辑`, and the ellipsis in order;
- press Enter or Space to open the menu;
- press Escape and confirm it closes;
- confirm no empty ellipsis appears when a permission-controlled menu item is absent;
- confirm the shared 44px hit areas remain usable and do not overlap.
- [ ] **Step 4: Record verification evidence**
Capture at least one desktop screenshot with the menu open and one narrow-viewport screenshot. Note the tested page path and any permission state used in the final handoff.
### Task 6: Final consistency and completion checks
**Files:**
- Verify: `docs/superpowers/specs/2026-07-26-global-operation-actions-design.md`
- Verify: all files modified in Tasks 14
- [ ] **Step 1: Run the full verification suite**
Run:
```bash
npm run test:run
npm run typecheck
npm run build
```
Expected: all commands exit 0.
- [ ] **Step 2: Re-run the normative consistency scan**
Run:
```bash
rg -n "查看详情.*更多|低频.*查看详情|查看记录.*外显" \
src/resources/design-system \
src/prototypes/vm-shared/DESIGN.md \
src/common/operation-actions-spec.md \
src/common/OperationActions.tsx
```
Expected: no contradictory active rule remains.
- [ ] **Step 3: Review the final change scope**
Because `/Users/sylvawong/oneos-v2` is not currently a Git repository, list changed files by explicit path and verify that `.superpowers/brainstorm/` artifacts are not included in the deliverable. Do not claim commits were created.

View File

@@ -0,0 +1,921 @@
# Vehicle Detail Tabs V2 Redesign Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make lifecycle the default vehicle-detail tab and rebuild model parameters, licenses, insurance, and all subsequent record tabs with the OneOS 1.2 field/function contract expressed through OneOS V2 components and layout.
**Architecture:** `DetailView` remains the vehicle-context and tab-layout coordinator; it no longer renders business tables inline. Focused tab components own their data, filters, pagination, and operations, while shared record primitives provide the V2 filter shell, table, and footer. License data is normalized by a pure utility so the license component can render real current data, empty states, and optional image previews without inventing records.
**Tech Stack:** React 18, TypeScript 5.9, Vitest 4, jsdom 26, OneOS V2 form kit, common `TablePagination`, common `OperationActions`, Lucide React, CSS.
---
## File map
### Create
- `tests/vehicle-management/detailTestUtils.tsx` — shared jsdom render/interaction helpers.
- `tests/vehicle-management/DetailView.tabs.test.tsx` — default tab and sidebar/full-width layout.
- `tests/vehicle-management/DetailModelParamsTab.test.tsx` — description-grid semantics and maintenance table.
- `tests/vehicle-management/DetailLicenseTab.test.tsx` — eight license categories, status index, anchor behavior, image preview, and empty states.
- `tests/vehicle-management/DetailRecordTabs.test.tsx` — reference field contracts, query/reset, pagination, and operations.
- `src/prototypes/vehicle-management/utils/vehicleLicenses.ts` — normalized eight-category license view model.
- `src/prototypes/vehicle-management/components/DetailLicenseTab.tsx` — license index/cards/preview.
- `src/prototypes/vehicle-management/components/DetailRecordPrimitives.tsx` — V2 filter shell, record table, link cell, and compact footer.
- `src/prototypes/vehicle-management/components/DetailInsuranceRecordsTab.tsx` — insurance overview and purchase history.
- `src/prototypes/vehicle-management/components/DetailRecordTabs.tsx` — lease, accident, violation, movement, transfer, and annual review tabs.
### Modify
- `src/prototypes/vehicle-management/components/DetailView.tsx` — default tab, layout mode, and child-tab integration.
- `src/prototypes/vehicle-management/components/DetailModelParamsTab.tsx` — V2 description-list markup.
- `src/prototypes/vehicle-management/components/DetailFaultRecordsTab.tsx` — self-contained V2 form imports and global action semantics.
- `src/prototypes/vehicle-management/style.css` — detail layout, description grids, license cards, record filters/tables, and responsive rules.
- `src/prototypes/vehicle-management/index.tsx` — import the V2 form stylesheet through the form-kit entrypoint if the child imports do not already bundle it.
### Keep unchanged
- `VehicleLifecyclePanel.tsx`
- basic-information content inside `DetailView`
- all JSON data files
- vehicle header/summary/location fields
## Task 1: Lock default-tab and adaptive-layout behavior
**Files:**
- Create: `tests/vehicle-management/detailTestUtils.tsx`
- Create: `tests/vehicle-management/DetailView.tabs.test.tsx`
- Modify: `src/prototypes/vehicle-management/components/DetailView.tsx`
- [ ] **Step 1: Add shared test rendering utilities**
Create:
```tsx
// @vitest-environment jsdom
import React, { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
export async function renderReact(element: React.ReactElement) {
const host = document.createElement('div');
document.body.appendChild(host);
const root: Root = createRoot(host);
await act(async () => root.render(element));
return {
host,
async cleanup() {
await act(async () => root.unmount());
host.remove();
document.body.replaceChildren();
},
};
}
export async function click(element: HTMLElement) {
await act(async () => element.click());
}
export function buttonByName(root: ParentNode, name: string) {
return Array.from(root.querySelectorAll('button')).find(
(button) => button.getAttribute('aria-label') === name || button.textContent?.trim() === name,
) as HTMLButtonElement | undefined;
}
```
- [ ] **Step 2: Write failing layout tests**
Use the first vehicle from `data/vehicles.json` and no-op callbacks. Assert:
```tsx
expect(host.querySelector('[role="tab"][aria-selected="true"]')?.textContent).toBe('生命周期');
expect(host.querySelector('.va-form-page-grid')?.classList.contains('is-wide-tab')).toBe(false);
expect(host.querySelector('aside[aria-label="车辆侧栏信息"]')).not.toBeNull();
```
After clicking `型号参数`:
```tsx
expect(host.querySelector('.va-form-page-grid')?.classList.contains('is-wide-tab')).toBe(true);
expect(host.querySelector('aside[aria-label="车辆侧栏信息"]')).toBeNull();
```
After clicking `基本信息`, assert the aside returns and `is-wide-tab` is removed.
- [ ] **Step 3: Run the test and verify RED**
Run:
```bash
npm run test:run -- tests/vehicle-management/DetailView.tabs.test.tsx
```
Expected: default-tab assertion fails because current state initializes to `basic`; wide-tab assertions also fail.
- [ ] **Step 4: Implement the layout coordinator**
Add:
```tsx
export const DEFAULT_DETAIL_TAB: DetailTabId = 'lifecycle';
export const DETAIL_TABS_WITH_ASIDE = new Set<DetailTabId>(['lifecycle', 'basic']);
export function detailTabUsesAside(tab: DetailTabId): boolean {
return DETAIL_TABS_WITH_ASIDE.has(tab);
}
```
Initialize:
```tsx
const [tab, setTab] = useState<DetailTabId>(DEFAULT_DETAIL_TAB);
const showDetailAside = detailTabUsesAside(tab);
```
Apply:
```tsx
<div className={`va-form-page-grid${showDetailAside ? '' : ' is-wide-tab'}`}>
```
Render the aside only when `showDetailAside`. Keep summary and location cards unchanged.
- [ ] **Step 5: Verify GREEN**
Run:
```bash
npm run test:run -- tests/vehicle-management/DetailView.tabs.test.tsx
```
Expected: all layout tests pass.
## Task 2: Convert model parameters to a V2 description grid
**Files:**
- Create: `tests/vehicle-management/DetailModelParamsTab.test.tsx`
- Modify: `src/prototypes/vehicle-management/components/DetailModelParamsTab.tsx`
- Modify: `src/prototypes/vehicle-management/style.css`
- [ ] **Step 1: Write the failing model presentation test**
Render `DetailModelParamsTab` with the first vehicle and assert:
```tsx
expect(host.querySelector('[aria-label="型号参数(只读)"]')).not.toBeNull();
expect(host.querySelectorAll('.va-description-item').length).toBeGreaterThanOrEqual(10);
expect(host.querySelectorAll('.va-model-param-item__value').length).toBe(0);
expect(host.querySelectorAll('input,select,textarea').length).toBe(0);
expect(host.querySelector('table')?.textContent).toContain('保养公里周期');
```
Also assert section headings:
```tsx
['基本情况', '轮胎情况', '电气情况', '供氢系统情况', '其他系统情况', '保养规则']
```
- [ ] **Step 2: Verify RED**
Run:
```bash
npm run test:run -- tests/vehicle-management/DetailModelParamsTab.test.tsx
```
Expected: description-item and heading assertions fail against the current input-like visual classes.
- [ ] **Step 3: Replace field markup**
Use semantic definition markup:
```tsx
function ParamField({ label, value, wide = false }: ParamFieldProps) {
const empty = isEmptyValue(value);
return (
<div className={`va-description-item${wide ? ' is-wide' : ''}`}>
<dt className="va-description-item__label">{label}</dt>
<dd className={`va-description-item__value${empty ? ' is-empty' : ''}`}>
{empty ? '—' : value}
</dd>
</div>
);
}
```
Each section renders:
```tsx
<section className="va-detail-section va-model-param-section">
<h3 className="va-detail-section__title">{title}</h3>
<dl className={`va-description-grid is-cols-${columns}`}>{children}</dl>
</section>
```
Rename the maintenance heading to `保养规则`; preserve every current row and field.
- [ ] **Step 4: Replace the input-like CSS**
Remove background/border/control-height styling from `.va-model-param-item__value`. Add:
```css
.va-description-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
column-gap: 32px;
row-gap: 20px;
margin: 0;
}
.va-description-item {
min-width: 0;
margin: 0;
}
.va-description-item.is-wide {
grid-column: span 2;
}
.va-description-item__label {
margin: 0 0 6px;
color: var(--ln-muted);
font-size: 12px;
line-height: 1.4;
}
.va-description-item__value {
margin: 0;
color: var(--ln-ink);
font-size: 14px;
font-weight: 600;
line-height: 1.5;
overflow-wrap: anywhere;
}
```
At `max-width: 767px`, use one column and reset `grid-column`.
- [ ] **Step 5: Verify the model tab**
Run:
```bash
npm run test:run -- tests/vehicle-management/DetailModelParamsTab.test.tsx
```
Expected: all tests pass.
## Task 3: Normalize the eight license categories
**Files:**
- Create: `src/prototypes/vehicle-management/utils/vehicleLicenses.ts`
- Create: `tests/vehicle-management/DetailLicenseTab.test.tsx`
- [ ] **Step 1: Define the license view-model contract**
Create:
```ts
export type VehicleLicenseStatus = 'normal' | 'expiring' | 'expired' | 'missing';
export interface VehicleLicenseImage {
id: string;
src: string;
alt: string;
}
export interface VehicleLicenseField {
label: string;
value: string;
expiryDays?: number | null;
}
export interface VehicleLicenseGroup {
id:
| 'driver'
| 'transport'
| 'registration'
| 'special-registration'
| 'special-mark'
| 'hydrogen-card'
| 'safety-valve'
| 'pressure-gauge';
label: string;
status: VehicleLicenseStatus;
statusLabel: string;
images: VehicleLicenseImage[];
fields: VehicleLicenseField[];
emptyTitle?: string;
emptyDescription?: string;
}
```
- [ ] **Step 2: Write failing utility assertions in the license test**
Assert `resolveVehicleLicenses(record)` returns the labels in this exact order:
```ts
[
'行驶证',
'道路运输证',
'登记证',
'特种设备使用登记证',
'特种设备使用标识',
'加氢卡',
'安全阀',
'压力表',
]
```
Assert driver fields contain `注册日期`, `发证日期`, `强制报废日期`, `检验有效期至`; transport contains `经营许可证号`, `核发时间`, `证件有效期`, `审验有效期`.
- [ ] **Step 3: Verify RED**
Run:
```bash
npm run test:run -- tests/vehicle-management/DetailLicenseTab.test.tsx
```
Expected: module import fails because `vehicleLicenses.ts` does not exist.
- [ ] **Step 4: Implement deterministic normalization**
Use current `VehicleRecord` fields only:
- driver: `regDate`, unavailable issue date as `—`, `scrapDate`, `inspectExpire`;
- transport/registration/special categories: unavailable values as `—`;
- hydrogen card: derive bound/missing from `telematicsLinked`;
- no current image URL means `images: []`, never a generated placeholder.
Use `daysUntilExpire` for known expiry dates. Apply:
```ts
export function licenseStatusFromDays(
days: number | null,
warnDays: number,
hasDocument: boolean,
): VehicleLicenseStatus {
if (!hasDocument) return 'missing';
if (days === null) return 'normal';
if (days < 0) return 'expired';
if (days <= warnDays) return 'expiring';
return 'normal';
}
```
Thresholds: driver 90 days; transport, special mark, safety valve, pressure gauge 60 days.
- [ ] **Step 5: Verify the utility assertions**
Run the same focused command. Expected: normalization assertions pass; component assertions remain pending until Task 4.
## Task 4: Build the V2 license index, cards, anchors, and preview
**Files:**
- Create: `src/prototypes/vehicle-management/components/DetailLicenseTab.tsx`
- Modify: `tests/vehicle-management/DetailLicenseTab.test.tsx`
- Modify: `src/prototypes/vehicle-management/style.css`
- [ ] **Step 1: Add failing component tests**
Render with `record`. Assert:
- navigation `证照分类` has eight buttons;
- legend exposes `正常`, `临期`, `已到期`, `未上传`;
- eight `article[data-license-id]` sections exist;
- the driver article contains its four fields;
- a missing group uses V2 empty-state text, not an `<img>`.
Render with an injected `groups` fixture containing one real test image:
```ts
images: [{ id: 'driver-front', src: 'data:image/png;base64,iVBORw0KGgo=', alt: '行驶证正页' }]
```
Click `预览行驶证正页`; assert dialog `证照图片预览` opens. Click its close button and assert removal.
Stub:
```ts
const scrollIntoView = vi.fn();
Element.prototype.scrollIntoView = scrollIntoView;
```
Click the `行驶证` index and assert `scrollIntoView({ behavior: 'smooth', block: 'start' })`.
- [ ] **Step 2: Verify RED**
Run the focused license test. Expected: component import or role assertions fail.
- [ ] **Step 3: Implement the component**
Public API:
```tsx
interface DetailLicenseTabProps {
record: VehicleRecord;
groups?: VehicleLicenseGroup[];
}
```
Use `groups ?? resolveVehicleLicenses(record)`. Render:
- `section.va-license-index`;
- `nav[aria-label="证照分类"]`;
- status dots/tags;
- one `article` per group;
- image buttons only for actual image entries;
- description grid for fields;
- `V2Empty` for missing image/content;
- a body-level modal/dialog for image preview with close button and `alt`.
- [ ] **Step 4: Add license styling**
Add V2 token-based styles for:
- index card and legend;
- wrapping pill buttons;
- two-column license-card grid at desktop;
- full-width cards for dense categories;
- image thumbnails with 4:3 ratio;
- description grid;
- sticky-scroll offset via `scroll-margin-top`;
- one-column cards at `max-width: 767px`.
- [ ] **Step 5: Verify the license tab**
Run:
```bash
npm run test:run -- tests/vehicle-management/DetailLicenseTab.test.tsx
```
Expected: all utility and component tests pass.
## Task 5: Create shared V2 record-tab primitives
**Files:**
- Create: `src/prototypes/vehicle-management/components/DetailRecordPrimitives.tsx`
- Create: `tests/vehicle-management/DetailRecordTabs.test.tsx`
- Modify: `src/prototypes/vehicle-management/style.css`
- [ ] **Step 1: Write failing primitive tests**
Test a minimal fixture through `DetailRecordTable`:
```tsx
const columns = [
{ key: 'code', label: '编号', render: (row) => row.code },
{ key: 'status', label: '状态', render: (row) => row.status || '—' },
];
```
Assert headers, `—` empty values, empty table row, and common pagination total.
Test `DetailRecordFilterBar`:
- form/region accessible name;
- `重置` and `查询`;
- `is-expanded` removed after clicking query/reset when initially expanded.
- [ ] **Step 2: Verify RED**
Run:
```bash
npm run test:run -- tests/vehicle-management/DetailRecordTabs.test.tsx
```
Expected: primitives module import fails.
- [ ] **Step 3: Implement the primitives**
Create:
```tsx
export interface DetailRecordColumn<Row> {
key: string;
label: string;
width?: number;
className?: string;
render: (row: Row) => React.ReactNode;
}
export function DetailRecordFilterBar(props: {
title: string;
children: React.ReactNode;
onQuery: () => void;
onReset: () => void;
}) {
const [expanded, setExpanded] = React.useState(true);
const query = () => {
props.onQuery();
setExpanded(false);
};
const reset = () => {
props.onReset();
setExpanded(false);
};
return (
<section
className={`va-record-filter${expanded ? ' is-expanded' : ''}`}
aria-label={`${props.title}筛选`}
>
<div className="va-record-filter__grid">{props.children}</div>
<div className="va-record-filter__actions">
<button type="button" className="vm-btn vm-btn-ghost" onClick={reset}></button>
<button type="button" className="vm-btn vm-btn-primary" onClick={query}></button>
</div>
</section>
);
}
export function DetailRecordTable<Row extends { id: string }>(props: {
columns: DetailRecordColumn<Row>[];
rows: Row[];
emptyText?: string;
}) {
const emptyText = props.emptyText || '暂无数据';
return (
<div className="vm-table-card va-record-table-card">
<div className="vm-table-wrap vm-table-wrap--detail-tab">
<table className="vm-table">
<thead>
<tr>
{props.columns.map((column) => (
<th key={column.key} style={column.width ? { minWidth: column.width } : undefined}>
{column.label}
</th>
))}
</tr>
</thead>
<tbody>
{props.rows.length === 0 ? (
<tr>
<td colSpan={props.columns.length} className="vm-empty-cell">{emptyText}</td>
</tr>
) : props.rows.map((row) => (
<tr key={row.id}>
{props.columns.map((column) => (
<td key={column.key} className={column.className}>
{column.render(row)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
export function DetailRecordFooter(props: TablePaginationProps) {
return <div className="vm-table-footer vm-table-footer--compact"><TablePagination {...props} /></div>;
}
```
Use common `TablePagination`, no local pagination skin.
- [ ] **Step 4: Add shared record CSS**
Add `.va-record-tab`, `.va-record-filter`, `.va-record-filter__grid`,
`.va-record-filter__actions`, `.va-record-table-card`, and wide-scroll rules.
Reuse `vm-table` and `vm-pagination` classes rather than duplicating them.
- [ ] **Step 5: Verify primitive tests**
Run the focused record test. Expected: primitive tests pass.
## Task 6: Implement the insurance record tab
**Files:**
- Create: `src/prototypes/vehicle-management/components/DetailInsuranceRecordsTab.tsx`
- Modify: `tests/vehicle-management/DetailRecordTabs.test.tsx`
- [ ] **Step 1: Add failing insurance contract tests**
Render with current record, insurance expiry map entry, and current purchase rows. Assert:
- five overview buttons: 交强险、商业险、超赔险、驾意险、货物险;
- selected insurance detail contains 保单号、保险公司、到期日期、保费;
- history headers exactly:
保险导入时间、操作人、类型、保单号、保险状态、保险公司、付款时间、到期日期、保单文件;
- unavailable JSON fields display `—`;
- preview/download buttons call the supplied `onToast` with the existing prototype wording.
- [ ] **Step 2: Verify RED**
Run the record test. Expected: insurance component import or field assertions fail.
- [ ] **Step 3: Implement the insurance tab**
API:
```tsx
interface DetailInsuranceRecordsTabProps {
record: VehicleRecord;
insurance?: VehicleInsuranceExpire;
rows: InsurancePurchaseRecord[];
onToast: (message: string) => void;
}
```
Do not add fields to `insurance-purchases.json`. Normalize missing operator, type,
policy number, company, payment date, premium, and files to `—`. Preserve current
purchase/expiry values. Only show file actions when a file exists; otherwise `—`.
Use clickable overview cards and the shared record table/footer.
- [ ] **Step 4: Verify insurance tests**
Run the focused record test. Expected: insurance tests pass.
## Task 7: Implement lease, accident, violation, movement, transfer, and annual tabs
**Files:**
- Create: `src/prototypes/vehicle-management/components/DetailRecordTabs.tsx`
- Modify: `tests/vehicle-management/DetailRecordTabs.test.tsx`
- [ ] **Step 1: Add failing field-contract tests**
For each exported component, render fixture rows and assert exact headers:
```ts
const leaseHeaders = ['合同编码','项目名称','客户名称','业务类型','交车日期','交车人','还车日期','还车人'];
const accidentHeaders = ['事故编码','事故时间','事故地点','事故类型','客户名称','我方定损金额','对方定损金额','责任划分','事故状态','结案时间'];
const violationHeaders = ['违法时间','违法地点','违法行为','扣分','罚款金额','缴费状态','是否处理','采集单位'];
const movementHeaders = ['异动开始日期','异动预计结束日期','异动状态','异动目的地','目的地名称','异动类型','预计异动里程 (km)','异动开始里程 (km)','异动开始电量 (kWh)','异动开始氢量','异动结束里程 (km)','异动结束电量 (kWh)','异动结束氢量','创建人','创建时间'];
const transferHeaders = ['调拨日期','调出人','接收人','出发区域','接收区域','调拨方式','出发停车场','接收停车场','接收日期'];
const annualHeaders = ['检验有效期至','检测服务站名称','检测费用','二保服务站名称','二保费用','整备服务站名称','整备费用','办理人','完成时间'];
```
Assert each tab exposes its approved filter labels and query/reset buttons.
- [ ] **Step 2: Verify RED**
Run the record test. Expected: component exports or header assertions fail.
- [ ] **Step 3: Implement explicit filter types and helpers**
In `DetailRecordTabs.tsx`, define one filter type/empty constant per tab. Use:
- `O2Input` for contract/project/customer/free-text fields;
- `O2Select` for enumerations;
- `O2DateRangePicker` for dates.
Each component maintains `pendingFilters`, `appliedFilters`, `page`, `pageSize`;
query copies pending to applied and resets page; reset restores the exact empty
constant and resets page. Filter only current in-memory rows.
- [ ] **Step 4: Implement exact columns and operations**
Use the JSON keys shown by the current data:
- lease: link contract/project/delivery/return cells to `onToast`;
- accident: link `accidentCode` to `onToast`;
- violation: link `violationBehavior` to `onToast`;
- movement: link `startDate` to `onToast`;
- transfer: link `transferDate` to `onToast`;
- annual: link `inspectionValidUntil` to `onToast`.
Use `Intl.NumberFormat('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })`
for monetary values, returning `—` for null/empty.
- [ ] **Step 5: Verify all six record tabs**
Run:
```bash
npm run test:run -- tests/vehicle-management/DetailRecordTabs.test.tsx
```
Expected: all header, filter, query/reset, and interaction tests pass.
## Task 8: Repair and integrate the fault-record tab
**Files:**
- Modify: `src/prototypes/vehicle-management/components/DetailFaultRecordsTab.tsx`
- Modify: `tests/vehicle-management/DetailRecordTabs.test.tsx`
- [ ] **Step 1: Add failing fault contract tests**
Assert the six filters and fourteen headers from the approved spec. Assert action
buttons are exactly `查看记录`, `编辑`, and optional `更多操作` only when more items exist.
Click both actions and verify callbacks receive the row.
- [ ] **Step 2: Verify RED**
Run the record test. Expected: current orphan component fails to import nonexistent
local `DateRangeFilterField`, `FilterPickerField`, `StatusTag`, `TablePagination`,
and `utils/faultRecords`.
- [ ] **Step 3: Make the component self-contained**
Replace missing imports with:
```tsx
import { O2DateRangePicker, O2Select, O2Input } from '../../../common/oneos-v2-form';
import { TablePagination } from '../../../common/TablePagination';
import { OperationActions } from '../../../common/OperationActions';
```
Read rows from the passed `rows` prop rather than a missing `faultRecords` utility.
Define the filter logic locally and use existing JSON keys.
Use global action semantics:
```tsx
<OperationActions
view={{ label: '查看记录', onClick: () => onViewFaultDetail?.(record, row) }}
edit={{ label: '编辑', onClick: () => onEditFaultRecord?.(record, row) }}
/>
```
Do not create a page-specific more menu.
- [ ] **Step 4: Verify fault tests**
Run the focused record test. Expected: all fault tests pass.
## Task 9: Integrate all tab components into DetailView
**Files:**
- Modify: `src/prototypes/vehicle-management/components/DetailView.tsx`
- Modify: `tests/vehicle-management/DetailView.tabs.test.tsx`
- [ ] **Step 1: Expand the integration test**
Click every tab from model through annual and assert a unique landmark:
- 型号参数: `型号参数(只读)`
- 证照信息: navigation `证照分类`
- 保险记录: text `车辆保险档案`
- 租赁记录: header `合同编码`
- 事故记录: header `事故编码`
- 故障记录: header `故障编号`
- 违章记录: header `违法行为`
- 异动记录: header `异动开始日期`
- 调拨记录: header `调拨日期`
- 年审记录: header `检验有效期至`
For each, assert aside is absent. Click lifecycle/basic and assert aside is present.
- [ ] **Step 2: Verify RED**
Run the detail-view test. Expected: license and record landmarks fail while inline
generic `RecordTable` remains.
- [ ] **Step 3: Replace inline rendering**
Import and render the focused components. Pass only the current vehicle rows already
computed in `plateRows`, plus existing `onToast` callbacks. Replace the license
placeholder, insurance blocks, and generic `RecordTable` branches.
Remove `RecordTable` from `DetailView`. Do not move child filter state into
`DetailView`.
- [ ] **Step 4: Verify the integrated detail view**
Run:
```bash
npm run test:run -- tests/vehicle-management/DetailView.tabs.test.tsx
```
Expected: all tab and layout assertions pass.
## Task 10: Responsive styling and visual QA
**Files:**
- Modify: `src/prototypes/vehicle-management/style.css`
- Verify: all new/modified components.
- [ ] **Step 1: Complete wide-layout CSS**
Add:
```css
.va-form-page-grid.is-wide-tab .va-form-main {
grid-column: 1 / -1;
}
.va-form-page-grid.is-wide-tab .va-form-card--workbench {
min-width: 0;
}
```
Keep summary/location cards unchanged. Ensure the workbench uses full width only
for model and later tabs.
- [ ] **Step 2: Add responsive rules**
At `max-width: 767px`:
- description grid one column;
- license cards one column;
- index pills horizontally scroll or wrap without clipping;
- record filters one column;
- buttons meet 44px touch targets;
- tables retain minimum widths and scroll inside `.vm-table-wrap`.
- [ ] **Step 3: Run focused and full tests**
Run:
```bash
npm run test:run -- \
tests/vehicle-management/DetailView.tabs.test.tsx \
tests/vehicle-management/DetailModelParamsTab.test.tsx \
tests/vehicle-management/DetailLicenseTab.test.tsx \
tests/vehicle-management/DetailRecordTabs.test.tsx
npm run test:run
```
Expected: all focused tests and the full Vitest suite pass.
- [ ] **Step 4: Run static checks**
Run:
```bash
npm run typecheck
npm run build
```
Expected for this workspace baseline:
- typecheck may remain blocked by missing `@types/react` / `@types/react-dom`;
- build may remain blocked first by
`src/prototypes/lease-contract-management/index.tsx` missing a default export.
Record the first fresh failure exactly. Investigate and fix any new diagnostic
attributable to vehicle-management.
- [ ] **Step 5: Browser QA at 1280px**
Open `/prototypes/vehicle-management`, enter the first vehicle, and verify:
- lifecycle selected initially;
- lifecycle/basic show aside;
- model onward full width;
- model uses unboxed description values;
- license index positions correctly and images preview only when present;
- all record filters/query/reset/pagination work;
- wide movement/fault tables scroll without page-level overflow.
Capture screenshots for model, license, insurance, and one wide record table.
- [ ] **Step 6: Browser QA at 390px**
Verify:
- no horizontal page overflow outside table scroll areas;
- tab strip remains usable;
- description/license layouts are single-column;
- filter controls and buttons are at least 44px;
- license index and preview remain accessible.
Reset the browser viewport after QA.
## Task 11: Final consistency review
**Files:**
- Verify: `src/prototypes/vehicle-management/.spec/2026-07-26-detail-tabs-v2-redesign.md`
- Verify: all Task 110 files.
- [ ] **Step 1: Scan for stale placeholders and old inline tables**
Run:
```bash
rg -n "已建档(只读演示)|function RecordTable|va-model-param-item__value" \
src/prototypes/vehicle-management/components \
src/prototypes/vehicle-management/style.css
```
Expected: no active old license placeholder, inline `DetailView` record table, or
input-like model-value style remains.
- [ ] **Step 2: Scan the approved field contract**
Run tests plus a source scan for all tab headers named in Tasks 68. Confirm no
reference-page field was silently dropped and no fake JSON field was added.
- [ ] **Step 3: Review deliverable scope**
List exact changed files. Exclude `.superpowers/brainstorm/` artifacts from the
deliverable. Because this workspace is not a Git repository, do not claim commits,
branches, or a clean Git diff.

View File

@@ -0,0 +1,128 @@
# OneOS V2 复杂台账列个性化设计说明
日期2026-07-26
状态:已完成产品需求与设计方案确认
## 1. 背景与目标
车辆资产页已经具备表格列宽调整、显示列选择和显示顺序调整能力,但 OneOS V2 设计规范尚未覆盖这些交互。本次将三项能力上升为所有复杂台账页面的通用规则,并以车辆资产页作为标准示例。
目标用户包括使用 OneOS V2 复杂台账的 PC/Web 用户,以及按规范设计、开发和验收台账页面的团队。用户的核心任务是根据当前工作内容调整列宽、选择显示列、排列列顺序,并在需要时恢复默认配置。
## 2. 范围
### 2.1 包含
- PC/Web 复杂台账表格的列宽拖拽调整。
- 可选业务列的显示与隐藏。
- 可选业务列的显示顺序调整。
- 单列宽度恢复和完整配置恢复默认。
- 按“用户 + 台账页面”保存和恢复配置。
- 鼠标、键盘和读屏操作规则。
- 配置加载、保存失败及表格字段变更时的兼容处理。
- 浅色和暗色主题下的状态表现。
### 2.2 不包含
- 不新增或改变车辆资产页的业务字段。
- 不改变复杂台账的筛选、分页、导入、导出或行内操作逻辑。
- H5 单列卡片列表不提供列宽、列显隐和列排序操作。
- 不跨台账复用列配置,也不在不同账号之间共享配置。
## 3. 规范组织
采用“分章主规则 + 总览摘要 + 交叉引用”的组织方式:
- `src/resources/design-system/DESIGN.md`:增加强制原则摘要,声明复杂台账必须提供列宽、列显隐、列排序和恢复默认能力。
- `src/resources/design-system/chapters/03-components.md`:承载完整组件与交互规则。
- `src/resources/design-system/chapters/06-accessibility.md`:承载焦点、键盘替代、读屏播报规则。
- `src/resources/design-system/chapters/02-layout.md`:说明 PC/Web 表格适用范围、H5 卡片排除规则及表格横向滚动关系。
- 车辆资产页作为标准实现示例被引用,本次不修改其业务功能。
## 4. 信息架构与组件关系
列个性化由两类入口组成:
1. 表头分隔线上的列宽调节把手,负责单列宽度调整和单列宽度恢复。
2. 台账工具栏中的“列设置”按钮,打开设置弹层,集中处理列显隐、列顺序和完整恢复默认。
列设置弹层包含标题、恢复默认入口、列项目列表、取消和确认操作。每个可选业务列包含显隐复选框、拖拽把手及键盘上移/下移操作;固定列显示固定标识和限制说明。
## 5. 列宽调整
- 表头分隔线提供约 `12px` 的可操作命中区。默认仅显示细分隔线,悬停、聚焦或拖动时用主题主色增强。
- 拖动过程实时预览列宽,鼠标释放时结束调整并触发保存。
- 普通业务列宽度限制为 `90480px`
- 首个核心识别列建议限制为 `120360px`;具体默认宽度由字段内容决定。
- 达到最小或最大边界后停止变化,不允许列宽无限缩小或撑宽。
- 双击调节把手只恢复当前列的默认宽度。
- 末尾操作列不提供调节把手,宽度由设计者依据操作组件规范固定。
- 表格容器仍遵守局部横向滚动规则,列宽个性化不得造成整页横向滚动。
## 6. 列显隐与列顺序
- “列设置”位于复杂台账工具栏,使用图标按钮并提供文字提示和无障碍名称。
- 设置弹层内,复选框控制列显隐,拖拽把手控制列顺序。
- 设置弹层中的修改先保存在临时状态;点击“确认”后一次应用并保存,点击“取消”不改变当前列表。
- “全选/全不选”只影响可选业务列,不影响固定列。
- 首个核心识别列必须显示、不可换序,但允许在规定范围内调宽。
- 末尾操作列必须显示、不可换序、不可调宽。
- 其他业务列可以显示或隐藏、调整顺序和调整列宽。
- 可选业务列只能在两端固定列之间排序,不能越过固定列。
## 7. 恢复默认
- 双击表头调节把手恢复当前列的默认宽度。
- 列设置弹层中的“恢复默认”同时恢复列宽、显隐和顺序。
- 完整恢复不使用危险确认弹窗。用户触发后先在弹层中预览默认布局,点击“确认”后应用并保存;点击“取消”保留原配置。
## 8. 保存、加载与兼容
- 保存范围为“用户 + 台账页面”,建议存储键包含 `userId + ledgerId + schemaVersion`
- 配置至少包含 `columnWidths``visibleColumns``columnOrder`
- 列宽在拖动结束后保存;列显隐和列顺序在弹层确认后保存。
- 保存过程不得阻塞台账浏览和操作。
- 再次进入同一台账时自动恢复该用户的配置。
- 初始化时先建立规范默认配置,再合并已保存配置,降低加载期间的布局跳动。
- 新增字段按设计者定义的默认显隐状态和默认位置插入。
- 已删除字段自动忽略,不能导致整份用户配置失效。
- 配置损坏、版本不兼容或加载失败时回退默认配置并轻提示,不阻塞台账数据加载。
- 保存失败时保留当前会话内的配置,并提示“当前设置暂未同步,下次进入可能恢复默认”。
- H5 卡片列表忽略 PC 列配置,但不得删除已经保存的 PC 配置。
## 9. 无障碍与键盘
- “列设置”按钮提供明确的 `aria-label`、展开状态和弹层关联。
- 打开弹层后焦点进入弹层,关闭后回到触发按钮;按 `Esc` 可关闭。
- 列宽调节把手可聚焦。方向键每次调整 `8px``Shift + 方向键` 每次调整 `24px``Home` 恢复当前列默认宽度。
- 读屏实时播报列名和当前宽度。
- 列排序除拖拽外,必须提供“上移/下移”按钮;到达边界或属于固定列时禁用并说明原因。
- 显隐复选框必须与列名关联。固定列控件禁用,并以文字说明不可隐藏。
- 拖动、排序、恢复默认、保存成功和保存失败均通过 `aria-live="polite"` 播报,不能只依赖颜色传达状态。
- 所有可聚焦控件使用主题 Focus Token视觉分隔线可以为 `23px`,但操作命中区不得随之缩小。
## 10. 视觉与主题
- 列宽把手、焦点、选中和拖拽目标状态全部消费 OneOS V2 主题 Token不使用硬编码品牌色。
- 浅色和暗色主题均需保持表头分隔线、焦点和拖拽状态的可辨识度。
- 固定列通过文字或图标加文字说明,不只使用灰色或禁用色表达限制。
- 列设置弹层沿用 OneOS V2 Web 浮层、复选框、按钮和间距规范,不新增独立视觉体系。
## 11. 验收标准
- 鼠标可在边界内实时调整所有允许调宽的列。
- 键盘可完成等价调宽,并能恢复单列默认宽度。
- 用户可显示或隐藏所有可选业务列。
- 用户可通过拖拽以及上移/下移按钮调整所有可选业务列顺序。
- 两类固定列始终遵守显示、换序和调宽限制。
- 取消设置不改变当前列表;确认后一次应用并保存。
- 双击把手只恢复当前列;“恢复默认”恢复列宽、显隐和顺序。
- 同一用户重新进入同一台账时恢复配置,不同用户或不同台账之间互不影响。
- 新增或删除字段后,历史配置仍可正常合并和使用。
- 配置加载或保存失败时台账仍可操作,并给出符合规范的轻提示。
- H5 卡片列表不出现任何列控制入口,且不会清除 PC 配置。
- 浅色、暗色主题以及键盘焦点、读屏播报均通过验收。
## 12. 标准示例
`src/prototypes/vehicle-management/components/ListView.tsx` 中的车辆资产台账作为交互参考,已经展示表头调宽把手、双击恢复、列设置入口、列显隐和列拖拽排序。规范落地时以本文确认的通用边界为准,尤其需要补足持久化、键盘替代、最大宽度和保存失败处理等通用要求。

View File

@@ -0,0 +1,113 @@
# OneOS V2 全局列表操作列调整设计
## 背景
车辆资产列表当前把全部行操作收进自建“更多”菜单,不符合 OneOS V2 使用公共 `OperationActions` 的要求。现行全局规范又将“查看详情”定义为低频浏览动作,默认收入“更多”,与本次确认的“双外置”方向不一致。
本次调整同时修正规范与车辆资产页:将“查看 / 详情”定义为稳定的首要入口,与常用工作流动作共同外置;低频管理、历史与危险操作保留在“更多”菜单。
## 目标与范围
### 目标用户
使用 OneOS V2 台账的运营人员、业务经办人和管理员。
### 核心任务
用户在扫描列表时,可以直接进入详情或执行最常用的工作流动作,不必先打开菜单。
### 本次范围
- 更新 OneOS V2 全局 `OperationActions` 规范。
- 同步公共组件说明、组件行为和相关设计系统引用。
- 将车辆资产列表迁移到公共 `OperationActions`
- 保留车辆资产页现有的查看详情、编辑和设置运维负责人能力。
### 不在范围内
- 不修改车辆资产的信息架构、筛选、列结构和详情页。
- 不新增业务权限或后端接口。
- 不批量迁移所有历史台账;全局规则用于后续迁移和新页面。
## 全局操作分级
操作列外置动作最多两个,固定按下列顺序显示:
1. 查看类入口:`查看``详情``查看详情`
2. 一个常用工作流动作:`编辑``处理``处置`
其余动作进入“更多”菜单:
- 低频管理:设置或修改负责人、转交等。
- 留痕与历史:查看记录、操作记录、历史日志等。
- 高风险操作:撤回、终止、删除等。
“查看记录”表示历史或留痕查询,继续视为低频动作;它不等同于进入当前业务对象的“查看详情”。
## 显示规则
```text
[查看/详情] [编辑/处理/处置] [⋮ 更多]
固定首位 最多一个 按需显示
```
- 只读台账仅外置“查看 / 详情”。
- 没有查看入口时,可外置最多两个常用工作流动作。
- 没有更多动作时,不显示省略号按钮。
- 没有任何可用操作时显示 `-`
- 权限过滤在布局分配前完成,避免空菜单或占位按钮。
- 操作列不换行PC 建议宽度为 `148184px`,按外置文案长度在页面级固定。
- H5 继续遵循 44×44px 最小触控热区。
## 车辆资产页落地
车辆资产列表每行使用:
- 外置一:`查看`,进入车辆详情页。
- 外置二:`编辑`,打开现有车辆编辑流程。
- 更多:`设置运维负责人`
操作顺序在所有行保持一致。无运维管理权限时,“更多”菜单不显示;若编辑权限也不可用,则只保留“查看”。
车辆页删除自建 `ActionsMoreMenu` 及其专属菜单皮肤,改用 `src/common/OperationActions.tsx``src/common/vm-operation-actions.css`。业务回调保持不变。
## 组件与文档同步
需要保持下列内容一致:
- `src/resources/design-system/DESIGN.md` §3.16
- `src/resources/design-system/chapters/03-components.md`
- `src/prototypes/vm-shared/DESIGN.md`
- `src/common/operation-actions-spec.md`
- `src/common/OperationActions.tsx`
- `src/common/vm-operation-actions.css`
公共组件应把详情类 `view` 动作放到外侧首位,把“查看记录 / 操作记录”等历史型动作分入更多菜单。`splitOperationActions` 需区分详情入口与历史记录,不能再把两者合并为同一低频类别。
## 交互与无障碍
- “更多”触发器使用 `MoreHorizontal` 图标,`aria-label="更多操作"`
- 菜单支持点击外部和 `Escape` 关闭,并沿用公共组件的键盘与焦点行为。
- 外置操作具有可见的悬停和键盘焦点状态。
- 冻结操作列中的菜单不得被表格行、滚动容器或相邻单元格遮挡。
- 危险操作需要保留公共组件的危险态表达,不与普通动作混排。
## 验收标准
- 全局主规范、章节规范、共享规范和公共组件说明不存在冲突表述。
- 公共组件默认按“查看在前、工作流动作在后、更多按需”渲染。
- 车辆资产页显示“查看、编辑、更多”,顺序和样式符合全局规范。
- 查看详情、编辑、设置运维负责人沿用原有业务行为。
- 权限受限时不显示未授权动作,也不出现空“更多”按钮。
- 操作列在桌面端不换行,菜单不被表格遮挡。
- 键盘可以聚焦并触发所有可见操作,`Escape` 可以关闭菜单。
- 相关自动化测试、类型检查和构建通过;通过浏览器检查桌面宽度及窄视口表现。
## 测试策略
- 为公共 `OperationActions` 补充或更新渲染顺序、只读状态、无更多动作和权限过滤测试。
-`splitOperationActions` 覆盖详情类与历史记录类的分类差异。
- 为车辆资产列表覆盖三个动作的存在性和回调触发。
- 运行项目现有测试、类型检查和构建。
- 在本地浏览器验证操作列宽度、冻结层级、菜单遮挡、悬停与键盘焦点。

View File

@@ -0,0 +1,49 @@
# 王冕数字分身 + 分身大脑 设计
- 日期2026-07-28初稿/ 2026-07-29双 Skill 落地)
- 状态:**已落地**
- 形态:两个个人 Cursor Skill
| Skill | 目录 | 中文名 |
|-------|------|--------|
| `wangmian-brain` | `~/.cursor/skills/wangmian-brain/` | 王冕的分身大脑 |
| `wangmian-twin` | `~/.cursor/skills/wangmian-twin/` | 王冕的数字分身 |
## 目标
本尊使用分身与大脑时按用户习惯UI/UX 审美、访谈与总结、工具、上下文)**自动产出需求**;业务真相来自 OneOS 知识库,不靠聊天流水账硬记。
## 决策摘要
| 项 | 选择 |
|----|------|
| 自动档位 | B 突击型 |
| 云效 | Y2本地完再问 |
| 触发 | T1 口令;分身内嵌可调大脑 |
| 称呼 | 我的本尊 |
| 分身口吻 | 产品日常;禁李云龙 |
| KB | `src/resources/oneos-knowledge-base/`(可回落 oneos-v2 绝对路径) |
## 文件
```text
wangmian-brain/
SKILL.md · retrieval.md · voice.md
wangmian-twin/
SKILL.md · persona.md · profile.md · habits.md · playbook.md
```
自我介绍:以 `profile.md` 为准(工作画像 + 话术模板)。
## 口令
- 大脑:`@王冕分身大脑` / `$wangmian-brain` / 「分身大脑」
- 分身:`@王冕分身` / `$wangmian-twin` / 「王冕的数字分身」
## 冒烟
新开对话(工作区建议 oneos-v2
1. `$wangmian-brain` 问一条业财门禁 → 应出大脑答复包
2. `$wangmian-twin` 扔一条小需求 → 应称「我的本尊」、引用大脑、无李云龙腔、末尾问云效