@agile-team/wl-skills-kit 2.7.3 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,14 @@
1
1
  # TREE_LIST:树形+列表
2
2
 
3
3
  > 见 SKILL.md 主文件(约束 + 按钮规则 + Mock 规范等共用规则)。
4
+ > **布局硬约束**:严禁使用 `C_Splitter`(冻vnode导致响应式完全失效),必须用 `jh-drag-col`(详 standards/14-layout-containers.md)。
4
5
 
5
6
  #### index.vue
6
7
 
7
8
  ```vue
8
9
  <template>
9
- <div class="app-container app-page-container">
10
- <C_Splitter :left-width="220">
10
+ <div class="app-container app-page-container" style="height: 100%">
11
+ <jh-drag-col :leftWidth="220">
11
12
  <template #left>
12
13
  <C_Tree
13
14
  :tree-data="treeData"
@@ -40,7 +41,7 @@
40
41
  @size-change="select"
41
42
  />
42
43
  </template>
43
- </C_Splitter>
44
+ </jh-drag-col>
44
45
  </div>
45
46
  </template>
46
47
 
@@ -197,10 +198,8 @@ export function createPage(editModalRef?: any) {
197
198
 
198
199
  ```scss
199
200
  .app-page-container {
200
- // C_Splitter 需要父容器撑满高度
201
- :deep(.my-splitter-container) {
202
- height: 100%;
203
- }
201
+ // jh-drag-col 需要父容器擑满高度
202
+ height: 100%;
204
203
  }
205
204
  ```
206
205
 
@@ -144,7 +144,7 @@ AI 根据提取的信息,内部构建 page-spec JSON(**不输出给用户**
144
144
  | --------------- | --------------------- | ------------------------------------------------ |
145
145
  | `LIST` | 查询区 + 表格 + 分页 | BaseQuery + BaseTable + jh-pagination |
146
146
  | `MASTER_DETAIL` | 上方主表 + 下方明细表 | jh-drag-row(需 `.drager_row { height: 100% }`) |
147
- | `TREE_LIST` | 左侧树 + 右侧表格 | C_Splitter + C_Tree |
147
+ | `TREE_LIST` | 左侧树 + 右侧表格 | jh-drag-col + C_Tree |
148
148
  | `FORM_MODAL` | 弹窗中的表单 | el-dialog + el-form |
149
149
  | `COMPOSITE` | 多种组合 | 组合使用 |
150
150
 
@@ -195,7 +195,7 @@ AI 根据提取的信息,内部构建 page-spec JSON(**不输出给用户**
195
195
  | 表格 | BaseTable | 通过 `columnsDef()` 声明式配置 |
196
196
  | 分页 | jh-pagination | 固定用法,见 copilot-instructions.md |
197
197
  | 上下分栏 | jh-drag-row | 主从表必备,需设 `:top-height` |
198
- | 左右分割 | C_Splitter | 树形+列表必备,设 `:left-width` |
198
+ | 左右分割 | jh-drag-col | 树形+列表必备,设 `:leftWidth` + #left/#right slot |
199
199
  | 树形面板 | C_Tree | 含搜索+Tab 切换 |
200
200
  | 下拉选择 | jh-select | dict 属性自动加载字典数据 |
201
201
  | 日期选择 | jh-date / jh-date-range | 参见 `docs/jh-date.md` |
@@ -31,8 +31,8 @@
31
31
  | 下拉 / 选择器 | `jh-select` / `jh-picker` | ❌ el-select 手写 options |
32
32
  | 只读文本展示 | `jh-text` | ❌ span/div 直接渲染 |
33
33
  | 分页 | `jh-pagination` | ❌ el-pagination |
34
- | 上下分栏 | `jh-drag-row` | ❌ 手动 flex/grid |
35
- | 左右分割 | `C_Splitter` | 手动 flex/grid |
34
+ | 上下分栏 | `jh-drag-row` (#top/#bottom) | ❌ 手动 flex/grid |
35
+ | 左右分割 | `jh-drag-col` (#left/#right) | ❌️ `C_Splitter`(已废弃,onMounted 冻vnode,导致子树响应式完全失效) |
36
36
  | 树形面板 | `C_Tree` | ❌ el-tree 手写 |
37
37
  | 状态标签 | `C_TagStatus` | ❌ el-tag + 颜色映射 |
38
38
  | HTTP 请求 | `getAction/postAction/putAction/deleteAction` | ❌ axios / fetch 直接调用 |
@@ -0,0 +1,139 @@
1
+ # 14 — 布局容器规范(C_Splitter 禁用 + jh-drag-col/row 唯一推荐)
2
+
3
+ > **强制度**:🔴 必遵 + 阻断式(lint 命中即报错)。
4
+ > **背景**:2024 年 12 月一次真实事故,左树右表页面右侧面板永不刷新,最终定位为 `C_Splitter` 在 `onMounted` 中调用 `slots.default()` 冻结 vnode 快照,导致子树所有响应式绑定与父组件 ref 脱钩。
5
+ > **结论**:项目中**禁止再使用 `C_Splitter`**,所有左右/上下分栏一律用 `jh-drag-col` / `jh-drag-row`。
6
+
7
+ ---
8
+
9
+ ## 1. 强制对照
10
+
11
+ | 场景 | ✅ 必用 | ❌ 禁用 |
12
+ | ---------- | -------------------------------------- | ------------------- |
13
+ | 左右分栏 | `<jh-drag-col :leftWidth="240">` + `#left` / `#right` slot | `C_Splitter`、`el-aside`+`el-main` 手写 flex |
14
+ | 上下分栏 | `<jh-drag-row :topHeight="240">` + `#top` / `#bottom` slot | `C_Splitter direction="vertical"`、手写 flex |
15
+ | 嵌套分栏 | 多层 `jh-drag-col` / `jh-drag-row` 直接嵌套 | C_Splitter 嵌套(双倍 vnode 冻结) |
16
+
17
+ > `@jhlc/jh-ui` 的 `jh-drag-col` / `jh-drag-row` 使用 Vue 原生 `<slot />` 直接渲染,**不缓存 vnode**,子组件响应式与父组件 ref 完全连通。
18
+
19
+ ---
20
+
21
+ ## 2. C_Splitter 为什么必须废弃(根因)
22
+
23
+ `src/components/global/C_Splitter/index.vue` 内部实现:
24
+
25
+ ```js
26
+ onMounted(() => {
27
+ const defaultSlots = slots.default ? slots.default() : [];
28
+ // ...
29
+ vnodes.value = children; // 冻结 vnode 列表
30
+ });
31
+ // 模板里
32
+ // <component :is="item" />
33
+ ```
34
+
35
+ **反应链**:
36
+
37
+ 1. `slots.default()` 只在 `onMounted` 执行一次 → 拿到的是**当时**的 vnode 快照
38
+ 2. 模板用 `<component :is="item" />` 渲染快照 → 子组件的所有 props/slot props/ref 绑定**永远定格在 mount 那一刻**
39
+ 3. 父组件后续修改 ref(`activeModelId.value = "xxx"`)→ 子组件 v-if/v-show/插值**全部失效**
40
+ 4. 表现:点击树节点,右侧表格**永远显示初始数据**或空白;vue-devtools 看 ref 已变但 UI 不动
41
+
42
+ > 这是 Vue 3 slot 渲染模型的本质:`slots.default()` 是一次性的 render function 调用,**不能用 ref 缓存其结果当模板用**。任何"缓存 vnode 数组再 component is 渲染"的写法都有同样的 bug。
43
+
44
+ ---
45
+
46
+ ## 3. 标准用法
47
+
48
+ ### 3.1 左树右表(最常见)
49
+
50
+ ```vue
51
+ <template>
52
+ <div class="app-container app-page-container" style="height: 100%">
53
+ <jh-drag-col :leftWidth="240">
54
+ <template #left>
55
+ <C_Tree :data="treeData" @node-click="onNodeClick" />
56
+ </template>
57
+ <template #right>
58
+ <BaseQuery ... />
59
+ <BaseToolbar ... />
60
+ <BaseTable v-if="activeModelId" ... />
61
+ </template>
62
+ </jh-drag-col>
63
+ </div>
64
+ </template>
65
+ ```
66
+
67
+ ✅ `v-if`、`ref` 赋值、所有响应式都能正常驱动右侧重渲染。
68
+
69
+ ### 3.2 上表下详情(master-detail)
70
+
71
+ ```vue
72
+ <jh-drag-row :topHeight="320">
73
+ <template #top>
74
+ <BaseTable ... @row-click="onRowClick" />
75
+ </template>
76
+ <template #bottom>
77
+ <DetailPanel v-if="currentRow" :data="currentRow" />
78
+ </template>
79
+ </jh-drag-row>
80
+ ```
81
+
82
+ ### 3.3 上 Tab 表单 + 下子表
83
+
84
+ ```vue
85
+ <jh-drag-row :topHeight="280">
86
+ <template #top>
87
+ <el-tabs v-model="activeTab"> ... </el-tabs>
88
+ </template>
89
+ <template #bottom>
90
+ <BaseTable ... />
91
+ </template>
92
+ </jh-drag-row>
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 4. 兼容期处理
98
+
99
+ 若现存项目仍引用 `C_Splitter`:
100
+
101
+ 1. **当前版本**:保留组件文件,在 `onMounted` 顶部加 `console.warn("[C_Splitter 已废弃] ...")` 提示
102
+ 2. **下一版本**:删除 `src/components/global/C_Splitter/`,全量替换为 `jh-drag-col/row`
103
+ 3. **lint 规则**:`wl-skills validate` / `wl-skills doctor-ui` 命中 `import C_Splitter` 或 `<C_Splitter` 时报 ERROR
104
+
105
+ ---
106
+
107
+ ## 5. 自动迁移建议
108
+
109
+ ```bash
110
+ # 项目根目录执行
111
+ grep -rln "C_Splitter" src/views | while read f; do
112
+ echo "需要人工改造:$f"
113
+ done
114
+ ```
115
+
116
+ 迁移要点:
117
+
118
+ | 旧写法 | 新写法 |
119
+ | ------ | ------ |
120
+ | `<C_Splitter :left-width="220">` | `<jh-drag-col :leftWidth="220">` |
121
+ | `<C_Splitter direction="vertical">` | `<jh-drag-row :topHeight="...">` |
122
+ | 默认 slot 顺序:第一项 / 第二项 | 显式 `#left` `#right` / `#top` `#bottom` |
123
+ | 拖动配置:`min-left-width="200"` | `jh-drag-col` 内置阈值 |
124
+
125
+ ---
126
+
127
+ ## 6. lint / codegen 强制项
128
+
129
+ - `prototype-scan` / `page-codegen` 生成的模板**禁止**包含 `C_Splitter`
130
+ - TPL-TREE-LIST、TPL-DETAIL-TABS 等模板必须使用 `jh-drag-col` / `jh-drag-row`
131
+ - `wl-skills validate-page` 扫到 `C_Splitter` 直接 fail
132
+
133
+ ---
134
+
135
+ ## 关联
136
+
137
+ - `12-base-table.md` — BaseTable 内部高度撑满依赖父容器有明确高度,jh-drag-col/row 已正确给子区设 `height: 100%`
138
+ - `13-platform-components.md` — 平台组件对照表已同步移除 C_Splitter
139
+ - 真实场景案例:`demo/produce/aiflow/mmwr-customer-detail/`(master-detail 使用 jh-drag-row)
@@ -5,7 +5,7 @@
5
5
 
6
6
  ---
7
7
 
8
- ## 13 条规范清单
8
+ ## 14 条规范清单
9
9
 
10
10
  | 编号 | 文件 | 主题 | 强制度 |
11
11
  | ---- | --------------------------- | ---------------------- | -------------- |
@@ -22,6 +22,7 @@
22
22
  | 11 | `11-form-validation.md` | 表单与校验 | 🔴 必遵 |
23
23
  | 12 | `12-base-table.md` | BaseTable + AGGrid cid | 🔴 必遵 |
24
24
  | 13 | `13-platform-components.md` | 平台组件合规(核心) | 🔴 必遵 + 阻断 |
25
+ | 14 | `14-layout-containers.md` | 布局容器(禁用 C_Splitter) | 🔴 必遵 + 阻断 |
25
26
 
26
27
  ---
27
28
 
@@ -32,14 +33,14 @@
32
33
  ### 任务类型 A:生成新页面(page-codegen)
33
34
 
34
35
  ```
35
- 必读:01 / 02 / 04 / 12 / 13
36
+ 必读:01 / 02 / 04 / 12 / 13 / 14
36
37
  按需:09(TS 类型复杂时) / 10(涉及 Store) / 11(FORM_ROUTE 模板)
37
38
  ```
38
39
 
39
40
  ### 任务类型 B:修改/重构既有页面
40
41
 
41
42
  ```
42
- 必读:02 / 04 / 13
43
+ 必读:02 / 04 / 13 / 14
43
44
  按需:09 / 10 / 11
44
45
  ```
45
46
 
@@ -10,7 +10,7 @@
10
10
  display: flex;
11
11
  flex-direction: column;
12
12
 
13
- // C_Splitter 区域
13
+ // jh-drag-row 区域
14
14
  .main-splitter {
15
15
  flex: 1;
16
16
  overflow: hidden;
@@ -9,8 +9,9 @@
9
9
  -->
10
10
  <template>
11
11
  <div class="main-maintenance-container">
12
- <!-- C_Splitter 包裹表单区和项次信息 -->
13
- <C_Splitter direction="vertical" class="main-splitter">
12
+ <!-- jh-drag-row 包裹表单区和项次信息(上下拖拽) -->
13
+ <jh-drag-row :topHeight="480" class="main-splitter">
14
+ <template #top>
14
15
  <!-- 🆕 使用增强版组件(集成所有功能) -->
15
16
  <c_formSections
16
17
  :sections="sectionsConfig"
@@ -43,7 +44,9 @@
43
44
  </el-row>
44
45
  </template>
45
46
  </c_formSections>
47
+ </template>
46
48
 
49
+ <template #bottom>
47
50
  <el-card
48
51
  shadow="never"
49
52
  class="items-card"
@@ -88,7 +91,8 @@
88
91
  </div>
89
92
  </div>
90
93
  </el-card>
91
- </C_Splitter>
94
+ </template>
95
+ </jh-drag-row>
92
96
 
93
97
  <!-- 全屏模式 - 使用 Teleport 传送到 body -->
94
98
  <Teleport to="body">
@@ -136,7 +140,7 @@
136
140
  <script setup lang="ts">
137
141
  import { onMounted } from "vue";
138
142
  import { ArrowDown, Close } from "@element-plus/icons-vue";
139
- import C_Splitter from "@/components/global/C_Splitter/index.vue";
143
+ // jh-drag-row @jhlc/jh-ui 全局注册组件,无需 import
140
144
  import c_formSections from "@/components/local/c_formSections/index.vue";
141
145
  import {
142
146
  form,
@@ -220,7 +220,7 @@
220
220
 
221
221
  // 明细信息的表格容器(固定高度,给实验表格留空间)
222
222
  &[data-tab="detail"] {
223
- // 移除 table-container 固定高度,让 C_Splitter 控制
223
+ // 移除 table-container 固定高度,让 jh-drag-row 控制
224
224
  .detail-tables-splitter {
225
225
  flex: 1;
226
226
  }
@@ -151,7 +151,8 @@
151
151
 
152
152
  <!-- 选择节点后显示表格数据 -->
153
153
  <div v-else class="tables-content">
154
- <C_Splitter direction="vertical">
154
+ <jh-drag-row :topHeight="320">
155
+ <template #top>
155
156
  <div class="main-table-section">
156
157
  <BaseTable
157
158
  :key="updateKey"
@@ -163,6 +164,8 @@
163
164
  @row-click="handleRowClick"
164
165
  />
165
166
  </div>
167
+ </template>
168
+ <template #bottom>
166
169
  <div>
167
170
  <div class="experiment-section">
168
171
  <div class="experiment-header">
@@ -193,7 +196,8 @@
193
196
  </div>
194
197
  </div>
195
198
  </div>
196
- </C_Splitter>
199
+ </template>
200
+ </jh-drag-row>
197
201
  </div>
198
202
  </div>
199
203
  </div>
@@ -220,7 +224,7 @@
220
224
  import { onMounted, onUnmounted, watch, nextTick } from "vue";
221
225
  import { ArrowDown, ArrowLeft } from "@element-plus/icons-vue";
222
226
  import c_formModal from "@/components/local/c_formModal/index.vue";
223
- import C_Splitter from "@/components/global/C_Splitter/index.vue";
227
+ // jh-drag-row @jhlc/jh-ui 全局注册组件,无需 import
224
228
  import {
225
229
  // 状态
226
230
  activeTab,
@@ -0,0 +1,321 @@
1
+ # Mock 架构规范
2
+
3
+ > **适用范围**:所有基于 wl-skills-kit 的 Vue 3 业务子应用
4
+ > **技术依赖**:`vite-plugin-mock` + `mockjs`
5
+ > **源码参考**:wl-mdata 项目实践(v2.7.x 基线)
6
+
7
+ ---
8
+
9
+ ## 一、设计目标
10
+
11
+ | 目标 | 实现方式 |
12
+ |---|---|
13
+ | **与页面代码完全解耦** | Mock 文件独立放在 `mock/` 目录,不在 `src/views` 中 import 任何 mock |
14
+ | **开关零污染** | `ENV_MOCK=false` → vite-plugin-mock 整体不挂载,不拦截任何请求 |
15
+ | **按业务模块自治** | 每个模块一个 mock 文件,删除模块只需删除对应 mock 文件 |
16
+ | **共享工具复用** | `_utils.ts` 提供标准响应构造器,子模块 import 而非重复定义 |
17
+ | **会话内数据可见** | `let STORE = [...]` 可变数组,CRUD 操作直接修改内存,下次查询可见 |
18
+ | **跨会话重置** | dev server 重启时模块重新加载,STORE 恢复初始状态 |
19
+ | **真实接口无缝切换** | `API_CONFIG` 保持真实路径,mock 端点带 `/dev-api` 前缀,关闭 mock 后无需改代码 |
20
+
21
+ ---
22
+
23
+ ## 二、目录结构
24
+
25
+ ```
26
+ 项目根目录/
27
+ ├── .env.dev ← ENV_MOCK=true(Mock 总开关)
28
+ ├── vite.config.ts ← viteMockServe({ mockPath: "./mock", enable: ... })
29
+ ├── mock/
30
+ │ ├── _utils.ts ← 共享工具(pageResult / ok / paginate / nowStr / pick)
31
+ │ └── [业务域]/ ← 镜像 src/views 第一级目录
32
+ │ ├── [模块].ts ← 一个模块可覆盖多个相关页面
33
+ │ ├── [模块].ts
34
+ │ └── ...
35
+ └── src/
36
+ └── views/
37
+ └── [业务域]/
38
+ └── [模块]/
39
+ └── [页面]/
40
+ ├── index.vue
41
+ ├── data.ts ← API_CONFIG 保持真实路径
42
+ └── api.md
43
+ ```
44
+
45
+ **命名约定**:
46
+
47
+ | src/views 路径 | mock 文件路径 | 说明 |
48
+ |---|---|---|
49
+ | `src/views/mdata/model/` | `mock/mdata/model.ts` | 一个模块一个文件 |
50
+ | `src/views/mdata/management/` | `mock/mdata/master.ts` | 可按业务语义命名 |
51
+ | `src/views/sale/customer/` | `mock/sale/customer.ts` | 镜像第一级域 |
52
+ | `src/views/sale/contract/` | `mock/sale/contract.ts` | 同域不同模块 |
53
+
54
+ > **一个 mock 文件可覆盖同模块下多个页面的接口**,只要它们属于同一业务实体(如"客户管理"和"客户申请"可共用 `mock/sale/customer.ts`)。
55
+
56
+ ---
57
+
58
+ ## 三、`_utils.ts` 共享工具
59
+
60
+ > 该文件由 `wl-skills init` 自动写入 `mock/_utils.ts`。无 `export default`,vite-plugin-mock 会安全跳过。
61
+
62
+ ```typescript
63
+ import Mock from "mockjs";
64
+
65
+ export const Random = Mock.Random;
66
+ export const pick = <T>(arr: T[]): T => Random.pick(arr) as T;
67
+ export const bool = () => Random.boolean();
68
+
69
+ /** 标准分页响应 */
70
+ export function pageResult(records: any[], total: number) {
71
+ return { code: 2000, message: "操作成功", data: { records, total } };
72
+ }
73
+
74
+ /** 标准成功响应 */
75
+ export function ok(data: any = null) {
76
+ return { code: 2000, message: "操作成功", data };
77
+ }
78
+
79
+ /** 通用分页截取(兼容 pageNo/current、pageSize/size) */
80
+ export function paginate(pool: any[], query: any) {
81
+ const current = Number(query?.current || query?.pageNo) || 1;
82
+ const size = Number(query?.size || query?.pageSize) || 10;
83
+ const start = (current - 1) * size;
84
+ return pageResult(pool.slice(start, start + size), pool.length);
85
+ }
86
+
87
+ /** 当前时间字符串 */
88
+ export function nowStr() {
89
+ return new Date().toLocaleString("zh-CN", { hour12: false }).replace(/\//g, "-");
90
+ }
91
+ ```
92
+
93
+ 所有 mock 模块文件统一通过 `import { paginate, ok, pick, nowStr } from "../_utils"` 引入。
94
+
95
+ ---
96
+
97
+ ## 四、Mock 文件编写规范
98
+
99
+ ### 4.1 文件头注释
100
+
101
+ 每个 mock 文件顶部必须声明覆盖的业务模块和接口:
102
+
103
+ ```typescript
104
+ /**
105
+ * [业务模块名] Mock
106
+ * 覆盖接口:[接口前缀1] / [接口前缀2]
107
+ */
108
+ import type { MockMethod } from "vite-plugin-mock";
109
+ import Mock from "mockjs";
110
+ import { paginate, ok, pick, nowStr } from "../_utils";
111
+ ```
112
+
113
+ ### 4.2 STORE 模式
114
+
115
+ ```typescript
116
+ // ─── 数据生成器 ──────────────────────────────────────────────────
117
+ function genRecord() {
118
+ return {
119
+ id: Mock.Random.id(),
120
+ code: `MDM${Mock.Random.string("number", 8)}`,
121
+ name: Mock.Random.cword(4, 10),
122
+ status: pick(["ACTIVE", "DRAFT", "FROZEN"]),
123
+ createBy: Mock.Random.cname(),
124
+ createTime: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
125
+ };
126
+ }
127
+
128
+ // ─── 可变存储(dev server 会话内持久化)──────────────────────────
129
+ let STORE: any[] = Array.from({ length: 50 }, genRecord);
130
+ ```
131
+
132
+ **核心规则**:
133
+ - 用 `let`(非 `const`)声明 STORE,允许 splice/unshift/assign
134
+ - 生成器函数 `genRecord()` 封装单条数据的随机逻辑
135
+ - 初始数据量建议 20-100 条,支持分页验证
136
+
137
+ ### 4.3 端点编写
138
+
139
+ ```typescript
140
+ export default [
141
+ // ── 列表(分页)──
142
+ {
143
+ url: "/dev-api/[服务]/[资源]/list",
144
+ method: "get",
145
+ response: ({ query }: any) => paginate(STORE, query),
146
+ },
147
+
148
+ // ── 新增 ──
149
+ {
150
+ url: "/dev-api/[服务]/[资源]/save",
151
+ method: "post",
152
+ response: ({ body }: any) => {
153
+ STORE.unshift({ ...genRecord(), ...body, id: Mock.Random.id(), createTime: nowStr() });
154
+ return ok(null);
155
+ },
156
+ },
157
+
158
+ // ── 编辑 ──
159
+ {
160
+ url: "/dev-api/[服务]/[资源]/update",
161
+ method: "post",
162
+ response: ({ body }: any) => {
163
+ const idx = STORE.findIndex((r) => r.id === body?.id);
164
+ if (idx >= 0) Object.assign(STORE[idx], body, { updateTime: nowStr() });
165
+ return ok(null);
166
+ },
167
+ },
168
+
169
+ // ── 删除 ──
170
+ {
171
+ url: "/dev-api/[服务]/[资源]/remove",
172
+ method: "post",
173
+ response: ({ body }: any) => {
174
+ const ids = Array.isArray(body?.ids) ? body.ids : [body?.id];
175
+ ids.forEach((id: string) => {
176
+ const idx = STORE.findIndex((r) => r.id === id);
177
+ if (idx >= 0) STORE.splice(idx, 1);
178
+ });
179
+ return ok(null);
180
+ },
181
+ },
182
+
183
+ // ── 详情 ──
184
+ {
185
+ url: "/dev-api/[服务]/[资源]/getById",
186
+ method: "get",
187
+ response: ({ query }: any) => ok(STORE.find((r) => r.id === query?.id) || null),
188
+ },
189
+ ] as MockMethod[];
190
+ ```
191
+
192
+ ### 4.4 端点覆盖检查
193
+
194
+ **每个 `API_CONFIG` 的 key 都必须在 mock 文件中有对应端点**,零遗漏。
195
+
196
+ | 操作 | STORE 修改方式 |
197
+ |---|---|
198
+ | 删除 | `STORE.splice(idx, 1)` |
199
+ | 新增 | `STORE.unshift({ ...genRecord(), ...body, id })` |
200
+ | 编辑 | `Object.assign(STORE[idx], body)` |
201
+ | 启用/停用 | 修改 `item.status` |
202
+ | 提交/审批 | 修改 `item.approvalStatus` |
203
+ | 作废 | `STORE.splice(idx, 1)` 或修改状态 |
204
+
205
+ > ❌ 禁止端点只返回 `{ code: 2000 }` 不修改 STORE — 否则 `select()` 刷新后看不到变化。
206
+
207
+ ---
208
+
209
+ ## 五、开关机制
210
+
211
+ ### 5.1 环境变量
212
+
213
+ ```bash
214
+ # .env.dev
215
+ ENV_MOCK=true # true = 启用 mock,false = 关闭 mock 走真实接口
216
+ ```
217
+
218
+ ### 5.2 Vite 配置
219
+
220
+ ```typescript
221
+ // vite.config.ts
222
+ import { viteMockServe } from "vite-plugin-mock";
223
+
224
+ plugins: [
225
+ viteMockServe({
226
+ mockPath: "./mock",
227
+ enable: command === "serve" && config.ENV_MOCK !== "false",
228
+ logger: true,
229
+ }),
230
+ ]
231
+ ```
232
+
233
+ **关键**:`enable` 判断只在 `serve` 模式且 `ENV_MOCK` 不为 `"false"` 时挂载。生产构建永远不包含 mock。
234
+
235
+ ### 5.3 切换流程
236
+
237
+ ```
238
+ 开发阶段(Mock):ENV_MOCK=true → 所有接口走 mock,Network 可见
239
+ 联调阶段(真实):ENV_MOCK=false → mock 插件不加载,接口走 proxy → 后端
240
+ 部分联调: ENV_MOCK=false + 仅保留需要 mock 的端点(手动注释其余)
241
+ ```
242
+
243
+ > 切换时**无需修改任何页面代码**。`API_CONFIG` 始终保持真实路径,mock 端点的 `/dev-api` 前缀由 Vite proxy 统一处理。
244
+
245
+ ---
246
+
247
+ ## 六、一键清理
248
+
249
+ CLI 提供 `mock-clean` 命令,支持按模块清理或全量清理:
250
+
251
+ ```bash
252
+ # 清理指定业务域的 mock
253
+ npx @agile-team/wl-skills-kit mock-clean --domain mdata
254
+
255
+ # 清理全部 mock(保留 _utils.ts)
256
+ npx @agile-team/wl-skills-kit mock-clean --all
257
+
258
+ # 预览将要删除的文件(dry-run)
259
+ npx @agile-team/wl-skills-kit mock-clean --all --dry-run
260
+ ```
261
+
262
+ 清理后建议:
263
+ 1. 将 `.env.dev` 中 `ENV_MOCK` 改为 `false`
264
+ 2. 确认 `vite.config.ts` 中 proxy 已配置正确的后端地址
265
+ 3. 运行 `wl-skills validate` 确认页面无 mock 依赖残留
266
+
267
+ ---
268
+
269
+ ## 七、validate 检查项
270
+
271
+ `wl-skills validate` 对 mock 的检查(自动执行):
272
+
273
+ | 检查项 | 级别 | 说明 |
274
+ |---|---|---|
275
+ | `mock/_utils.ts` 是否存在 | warn | 缺失则提示 `wl-skills init` 补充 |
276
+ | API_CONFIG URL 是否有对应 mock 端点 | warn | 每个 URL 在 mock 文件中必须有 `/dev-api` 前缀的端点 |
277
+ | mock 文件是否 import `_utils` | info | 未使用共享工具则提示 |
278
+ | mock 文件是否按域分目录 | info | 扁平放在 `mock/` 根目录则提示迁移 |
279
+
280
+ ---
281
+
282
+ ## 八、常见问题
283
+
284
+ ### Q: mock 和真实接口返回格式不一致怎么办?
285
+
286
+ 确保 `_utils.ts` 中 `pageResult` / `ok` 的返回格式与后端统一:
287
+ ```typescript
288
+ // 如果后端用 code: 200
289
+ export function ok(data: any = null) {
290
+ return { code: 200, message: "操作成功", data };
291
+ }
292
+
293
+ // 如果后端用 code: 2000(平台默认)
294
+ export function ok(data: any = null) {
295
+ return { code: 2000, message: "操作成功", data };
296
+ }
297
+ ```
298
+
299
+ > 在 `_utils.ts` 中统一修改一处即可,无需改每个 mock 文件。
300
+
301
+ ### Q: 多个页面共享同一套数据怎么办?
302
+
303
+ 同一个 mock 模块文件中声明一个 STORE,多个端点共享。例如"客户列表"和"客户申请"可共享 `CUSTOMER_STORE`。
304
+
305
+ ### Q: 某个接口需要走真实后端,其余走 mock?
306
+
307
+ 方案一:删除该接口在 mock 文件中的端点,vite-plugin-mock 找不到匹配就会放行到 proxy。
308
+ 方案二:保持 `ENV_MOCK=true`,在 vite proxy 中对特定路径单独配置 target。
309
+
310
+ ---
311
+
312
+ ## 九、架构要点速查
313
+
314
+ | 能力 | 实现方式 |
315
+ |---|---|
316
+ | **新增数据可见** | 每个模块维护 `let STORE = [...]` 可变数组,save 用 `unshift` 置顶,下次 list 查询即可看到 |
317
+ | **关闭 mock 零污染** | `ENV_MOCK=false` → vite-plugin-mock 整体不挂载,不会有任何 mock handler 拦截真实请求 |
318
+ | **按模块独立** | 每个文件自治,未来删某业务模块 mock 只删对应文件 |
319
+ | **共享工具复用** | `_utils.ts` 提供 `pageResult` / `ok` / `paginate` / `nowStr` / `pick`,子模块 import 而非重复定义 |
320
+ | **跨会话重置** | dev server 重启时内存清空,mock 数据恢复初始状态(符合预期) |
321
+ | **URL 零修改切换** | `API_CONFIG` 保持真实路径,mock URL 带 `/dev-api` 前缀,关闭 mock 后业务代码无需任何改动 |