@agile-team/wl-skills-kit 2.1.1 → 2.1.3

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.
Files changed (80) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +6 -5
  3. package/bin/wl-skills.js +35 -1
  4. package/files/.github/guides/README.md +13 -13
  5. package/files/.github/guides/architecture.md +555 -555
  6. package/files/.github/guides/usage.md +166 -166
  7. package/files/.github/reports/README.md +65 -65
  8. package/files/.github/reports/SYS_DICT_INFO.md +19 -19
  9. package/files/.github/reports/SYS_MENU_INFO.md +247 -247
  10. package/files/.github/reports/SYS_PERMISSION_INFO.md +20 -20
  11. package/files/.github/reports//347/273/204/344/273/266/346/217/220/345/217/226/345/273/272/350/256/256.md +33 -33
  12. package/files/.github/reports//350/247/204/350/214/203/345/256/241/346/237/245/346/212/245/345/221/212.md +44 -44
  13. package/files/.github/skills/_compat/README.md +108 -108
  14. package/files/.github/skills/_compat/headers/agents.txt +8 -8
  15. package/files/.github/skills/_compat/headers/claude-code.txt +7 -7
  16. package/files/.github/skills/_compat/headers/cline.txt +7 -7
  17. package/files/.github/skills/_compat/headers/cursor-mdc.txt +16 -16
  18. package/files/.github/skills/_compat/headers/cursor-rules.txt +7 -7
  19. package/files/.github/skills/_compat/headers/github-copilot.txt +1 -1
  20. package/files/.github/skills/_compat/headers/kiro.txt +10 -10
  21. package/files/.github/skills/_compat/headers/trae.txt +11 -11
  22. package/files/.github/skills/_compat/headers/windsurf.txt +7 -7
  23. package/files/.github/skills/_registry.md +81 -81
  24. package/files/.github/skills/core/api-contract/SKILL.md +344 -344
  25. package/files/.github/skills/core/api-contract/USAGE.md +110 -110
  26. package/files/.github/skills/core/convention-audit/SKILL.md +189 -189
  27. package/files/.github/skills/core/convention-audit/USAGE.md +99 -99
  28. package/files/.github/skills/core/page-codegen/SKILL.md +973 -973
  29. package/files/.github/skills/core/page-codegen/USAGE.md +102 -102
  30. package/files/.github/skills/core/page-codegen/templates/_index.md +46 -46
  31. package/files/.github/skills/core/page-codegen/templates/domains/_CONTRIBUTING.md +107 -107
  32. package/files/.github/skills/core/page-codegen/templates/domains/produce/TPL-OPERATION-STATION.md +442 -442
  33. package/files/.github/skills/core/page-codegen/templates/domains/sale/README.md +26 -26
  34. package/files/.github/skills/core/page-codegen/templates/universal/TPL-CHANGE-HISTORY.md +276 -276
  35. package/files/.github/skills/core/page-codegen/templates/universal/TPL-DETAIL-TABS.md +1145 -1145
  36. package/files/.github/skills/core/page-codegen/templates/universal/TPL-DRIVEN.md +124 -124
  37. package/files/.github/skills/core/page-codegen/templates/universal/TPL-FORM-ROUTE.md +436 -436
  38. package/files/.github/skills/core/page-codegen/templates/universal/TPL-LIST.md +191 -191
  39. package/files/.github/skills/core/page-codegen/templates/universal/TPL-MASTER-DETAIL.md +148 -148
  40. package/files/.github/skills/core/page-codegen/templates/universal/TPL-RECORD-FORM.md +371 -371
  41. package/files/.github/skills/core/page-codegen/templates/universal/TPL-TREE-LIST.md +186 -186
  42. package/files/.github/skills/core/prototype-scan/SKILL.md +498 -498
  43. package/files/.github/skills/core/prototype-scan/USAGE.md +95 -95
  44. package/files/.github/skills/core/template-extract/SKILL.md +139 -139
  45. package/files/.github/skills/core/template-extract/USAGE.md +93 -93
  46. package/files/.github/skills/domain/README.md +51 -51
  47. package/files/.github/skills/ops/code-fix/SKILL.draft.md +108 -108
  48. package/files/.github/skills/sync/dict-sync/SKILL.draft.md +100 -100
  49. package/files/.github/skills/sync/menu-sync/SKILL.md +258 -258
  50. package/files/.github/skills/sync/menu-sync/USAGE.md +104 -104
  51. package/files/.github/skills/sync/menu-sync/env/env.local.json +6 -6
  52. package/files/.github/skills/sync/menu-sync/env/guide.md +83 -83
  53. package/files/.github/skills/sync/permission-sync/SKILL.draft.md +91 -91
  54. package/files/.github/standards/01-toolchain.md +57 -57
  55. package/files/.github/standards/02-code-structure.md +111 -111
  56. package/files/.github/standards/03-comments.md +53 -53
  57. package/files/.github/standards/04-coding-basics.md +33 -33
  58. package/files/.github/standards/05-logging.md +38 -38
  59. package/files/.github/standards/06-security.md +44 -44
  60. package/files/.github/standards/07-config.md +52 -52
  61. package/files/.github/standards/08-git.md +60 -60
  62. package/files/.github/standards/09-typescript.md +71 -71
  63. package/files/.github/standards/10-pinia.md +57 -57
  64. package/files/.github/standards/11-form-validation.md +81 -81
  65. package/files/.github/standards/12-base-table.md +116 -116
  66. package/files/.github/standards/13-platform-components.md +123 -123
  67. package/files/.github/standards/index.md +89 -89
  68. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/data.ts +196 -196
  69. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/index.scss +150 -150
  70. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/index.vue +79 -79
  71. package/files/docs/jh-date-range.md +257 -257
  72. package/files/docs/jh-date.md +222 -222
  73. package/files/docs/jh-dept-picker.md +190 -190
  74. package/files/docs/jh-drag-row.md +590 -590
  75. package/files/docs/jh-file-upload.md +216 -216
  76. package/files/docs/jh-picker.md +218 -218
  77. package/files/docs/jh-select.md +148 -148
  78. package/files/docs/jh-text.md +248 -248
  79. package/files/docs/jh-user-picker.md +197 -197
  80. package/package.json +3 -5
@@ -1,26 +1,26 @@
1
- # 销售域专属模板(待沉淀)
2
-
3
- 本目录用于沉淀**销售域**特有的页面模板,目前为空。
4
-
5
- ---
6
-
7
- ## 待提取候选
8
-
9
- 如果销售域出现 3+ 页面遵循的非标准交互模式,可考虑提取。当前观察到的可能候选:
10
-
11
- - 国内贸易订单 + 期货订单 + 国外订单 → 是否存在共性"订单录入页"模式?
12
- - 客户档案 + 信用档案 + 资信档案 → 是否存在共性"多档案合一管理"模式?
13
-
14
- ---
15
-
16
- ## 如何提取
17
-
18
- 使用 `skills/core/template-extract/SKILL.md` 自动提取。详见 `templates/domains/_CONTRIBUTING.md`。
19
-
20
- ---
21
-
22
- ## 提交模板时
23
-
24
- 1. 文件命名 `TPL-{NAME}.md` 放在本目录
25
- 2. 同步更新 `templates/_index.md` 注册条目
26
- 3. 删除本说明文件(README.md),让本目录成为正式模板目录
1
+ # 销售域专属模板(待沉淀)
2
+
3
+ 本目录用于沉淀**销售域**特有的页面模板,目前为空。
4
+
5
+ ---
6
+
7
+ ## 待提取候选
8
+
9
+ 如果销售域出现 3+ 页面遵循的非标准交互模式,可考虑提取。当前观察到的可能候选:
10
+
11
+ - 国内贸易订单 + 期货订单 + 国外订单 → 是否存在共性"订单录入页"模式?
12
+ - 客户档案 + 信用档案 + 资信档案 → 是否存在共性"多档案合一管理"模式?
13
+
14
+ ---
15
+
16
+ ## 如何提取
17
+
18
+ 使用 `skills/core/template-extract/SKILL.md` 自动提取。详见 `templates/domains/_CONTRIBUTING.md`。
19
+
20
+ ---
21
+
22
+ ## 提交模板时
23
+
24
+ 1. 文件命名 `TPL-{NAME}.md` 放在本目录
25
+ 2. 同步更新 `templates/_index.md` 注册条目
26
+ 3. 删除本说明文件(README.md),让本目录成为正式模板目录
@@ -2,280 +2,280 @@
2
2
 
3
3
  > 见 SKILL.md 主文件(约束 + 按钮规则 + Mock 规范等共用规则)。
4
4
 
5
-
6
- > 适用场景:左侧为变更历史时间线列表,右侧为变更详情(复用业务域组件如 `c_customerTabs`,view 模式只读)。
7
- > 特征识别:原型中出现"变更记录"左面板 + "变更详情"右面板的双栏布局;左侧每条记录含类型(数据新增/数据变更)、时间、人员。
8
-
9
- #### 识别规则
10
-
11
- - 原型出现**左窄右宽双栏**布局
12
- - 左侧为**时间线 / 历史记录列表**(含彩色圆点 + 类型 + 时间 + 人员)
13
- - 右侧为**详情区域**(只读 view 模式),复用表单页的业务组件
14
- - 页面为**隐藏菜单**,由表单页跳转而来
15
-
16
- #### 文件结构
17
-
18
- ```
19
- [kebab-name]-change-history/
20
- ├── index.vue ← 双栏布局(左面板 + 右面板 + 业务组件)
21
- ├── data.ts ← useChangeHistory composable + mock 数据
22
- ├── index.scss ← 双栏 flex 布局样式
23
- └── api.md ← 接口约定
24
- ```
25
-
26
- #### data.ts 模板
27
-
28
- ```typescript
29
- import { getAction } from "@jhlc/common-core/src/api/action";
30
- import { useRouter } from "vue-router";
31
- import { create[Domain]MockData } from "@/components/local/c_[domainTabs]/data";
32
- import type { BasicInfoForm, BusinessInfoRow } from "@/components/local/c_[domainTabs]/data";
33
-
34
- export const API_CONFIG = {
35
- changeHistoryList: "/[服务缩写]/[资源]/changeHistory/list",
36
- getById: "/[服务缩写]/[资源]/changeHistory/getById",
37
- getDiffById: "/[服务缩写]/[资源]/changeHistory/getDiffById" // 获取旧版比对数据
38
- } as const;
39
-
40
- export interface HistoryRecord {
41
- id: string;
42
- changeType: string;
43
- changeTime: string;
44
- changePerson: string;
45
- }
46
-
47
- /** 变更历史记录 mock 数据(对齐原型截图) */
48
- function createHistoryListMock(): HistoryRecord[] {
49
- return [
50
- { id: "h001", changeType: "数据新增", changeTime: "2025/12/15 13:48:07", changePerson: "新增人姓名" },
51
- { id: "h003", changeType: "数据变更", changeTime: "2025/12/15 13:48:07", changePerson: "变更人姓名" },
52
- // ... 按原型增减
53
- ];
54
- }
55
-
56
- /** 变更比对 mock:旧版数据(体现字段级差异)
57
- * 修改目标字段使之与 createChangeMockData 产生差异,
58
- * c_domainTabs.loadDiffData() 会自动对比并高亮。
59
- */
60
- function createDiffMockData() {
61
- const current = create[Domain]MockData();
62
- return {
63
- basicInfo: { ...current.basicInfo, /* 修改需要比对的字段 */ },
64
- businessInfoList: current.businessInfoList.map((row, idx) => {
65
- if (idx === 0) return { ...row, /* 修改需要比对的字段 */ };
66
- return { ...row };
67
- })
68
- };
69
- }
70
-
71
- export function useChangeHistory(tabsRef: any) {
72
- const router = useRouter();
73
- const loading = ref(false);
74
- const historyLoading = ref(false);
75
- const historyList = ref<HistoryRecord[]>([]);
76
- const selectedId = ref<string>("");
77
- const isMockMode = ref(false);
78
-
79
- // ── Mock 模式(后端接口未就绪时使用,关闭 mock 后切换到 loadHistoryList) ──
80
- function loadMockData() {
81
- isMockMode.value = true;
82
- // 数据变更记录排在首位,确保页面加载时立即可见 diff 比对效果
83
- historyList.value = createHistoryListMock();
84
- if (historyList.value.length > 0) {
85
- nextTick(() => selectMockDetail(historyList.value[0].id));
86
- }
87
- }
88
-
89
- function selectMockDetail(id: string) {
90
- selectedId.value = id;
91
- const record = historyList.value.find(r => r.id === id);
92
- tabsRef.value?.loadData(create[Domain]MockData());
93
- if (record?.changeType.includes("变更")) {
94
- tabsRef.value?.loadDiffData(createDiffMockData());
95
- } else {
96
- tabsRef.value?.clearDiffData();
97
- }
98
- }
99
-
100
- // ── 真实接口模式(有 id 时使用) ──
101
- async function loadHistoryList(applyId: string) {
102
- historyLoading.value = true;
103
- try {
104
- const res = await getAction(API_CONFIG.changeHistoryList, { applyId });
105
- historyList.value = res?.data?.length ? res.data : createHistoryListMock();
106
- } catch {
107
- historyList.value = createHistoryListMock();
108
- } finally {
109
- historyLoading.value = false;
110
- if (historyList.value.length > 0) {
111
- await loadHistoryDetail(historyList.value[0].id);
112
- }
113
- }
114
- }
115
-
116
- async function loadHistoryDetail(id: string) {
117
- selectedId.value = id;
118
- loading.value = true;
119
- try {
120
- const res = await getAction(API_CONFIG.getById, { id });
121
- tabsRef.value?.loadData(res?.data || create[Domain]MockData());
122
- const diffRes = await getAction(API_CONFIG.getDiffById, { id }).catch(() => null);
123
- if (diffRes?.data) {
124
- tabsRef.value?.loadDiffData(diffRes.data);
125
- } else {
126
- tabsRef.value?.clearDiffData();
127
- }
128
- } catch {
129
- tabsRef.value?.loadData(create[Domain]MockData());
130
- tabsRef.value?.clearDiffData();
131
- } finally {
132
- loading.value = false;
133
- }
134
- }
135
-
136
- function handleSelectHistory(item: HistoryRecord) {
137
- if (item.id === selectedId.value) return;
138
- if (isMockMode.value) {
139
- selectMockDetail(item.id);
140
- } else {
141
- loadHistoryDetail(item.id);
142
- }
143
- }
144
-
145
- function handleCancel() { router.back(); }
146
-
147
- return { loading, historyLoading, historyList, selectedId, loadHistoryList, loadMockData, handleSelectHistory, handleCancel };
148
- }
149
- ```
150
-
151
- #### index.vue 模板
152
-
153
- ```vue
154
- <template>
155
- <div class="app-container [page-class]">
156
- <!-- 左侧:变更历史记录面板 -->
157
- <div class="history-panel" v-loading="historyLoading">
158
- <div class="history-panel__header">变更记录</div>
159
- <div class="history-panel__list">
160
- <div v-for="item in historyList" :key="item.id" class="history-card"
161
- :class="{ 'is-active': item.id === selectedId }" @click="handleSelectHistory(item)">
162
- <span class="history-card__dot"
163
- :class="item.changeType.includes('新增') ? 'is-add' : 'is-change'"></span>
164
- <div class="history-card__content">
165
- <div class="history-card__type">{{ item.changeType }}</div>
166
- <div class="history-card__date">{{ item.changeTime }}</div>
167
- <div class="history-card__person">{{ item.changePerson }}</div>
168
- </div>
169
- </div>
170
- <div v-if="!historyList.length && !historyLoading" class="history-empty">暂无变更记录</div>
171
- </div>
172
- </div>
173
- <!-- 右侧:变更详情面板 -->
174
- <div class="detail-panel" v-loading="loading">
175
- <div class="page-header">
176
- <span class="page-title">[实体]变更详情</span>
177
- <span class="page-tag page-tag--change">变更</span>
178
- <span class="page-tag page-tag--status">未审核</span>
179
- </div>
180
- <div class="page-toolbar">
181
- <el-button @click="handleCancel">取消</el-button>
182
- </div>
183
- <div class="detail-panel__body">
184
- <c_[domainTabs] ref="tabsRef" mode="view" />
185
- </div>
186
- </div>
187
- </div>
188
- </template>
189
-
190
- <script setup lang="ts">
191
- import { useRoute } from "vue-router";
192
- import { useChangeHistory } from "./data";
193
- import c_[domainTabs] from "@/components/local/c_[domainTabs]/index.vue";
194
-
195
- const tabsRef = ref();
196
- const route = useRoute();
197
- const { loading, historyLoading, historyList, selectedId, loadHistoryList, loadMockData, handleSelectHistory, handleCancel } = useChangeHistory(tabsRef);
198
-
199
- onMounted(() => {
200
- const id = route.query.id as string;
201
- if (id) {
202
- loadHistoryList(id); // 真实接口:有 id 时加载
203
- } else {
204
- loadMockData(); // Mock 模式:无 id 时纯 mock,零接口请求
205
- }
206
- });
207
- </script>
208
-
209
- <style scoped lang="scss">
210
- @import "./index.scss";
211
- </style>
212
- ```
213
-
214
- #### index.scss 模板
215
-
216
- ```scss
217
- .[page-class] {
218
- display: flex !important;
219
- padding: 0 !important;
220
- height: 100%;
221
- overflow: hidden;
222
-
223
- .history-panel {
224
- width: 200px; flex-shrink: 0;
225
- border-right: 1px solid #e4e7ed;
226
- display: flex; flex-direction: column; overflow: hidden; background: #fff;
227
- &__header { padding: 12px 16px; font-size: 14px; font-weight: 600; border-bottom: 1px solid #e4e7ed; flex-shrink: 0; }
228
- &__list { flex: 1; overflow-y: auto; padding: 4px 0; }
229
- }
230
-
231
- .history-card {
232
- display: flex; align-items: flex-start; padding: 10px 16px; cursor: pointer;
233
- border-left: 3px solid transparent; transition: background 0.15s; gap: 8px;
234
- &:hover { background: #f5f7fa; }
235
- &.is-active { background: #ecf5ff; border-left-color: #409eff; }
236
- &__dot { width: 8px; height: 8px; border-radius: 50%; margin-top: 5px; flex-shrink: 0;
237
- &.is-add { background: #409eff; }
238
- &.is-change { background: #e6a23c; }
239
- }
240
- &__content { flex: 1; min-width: 0; }
241
- &__type { font-size: 13px; font-weight: 500; color: #303133; line-height: 18px; }
242
- &__date { font-size: 12px; color: #909399; line-height: 18px; }
243
- &__person { font-size: 12px; color: #606266; line-height: 18px; }
244
- }
245
-
246
- .history-empty { padding: 32px 16px; text-align: center; color: #909399; font-size: 13px; }
247
-
248
- .detail-panel {
249
- flex: 1; overflow: hidden; display: flex; flex-direction: column; background: #fff;
250
- &__body { flex: 1; overflow-y: auto; padding: 0 16px 16px; }
251
- }
252
-
253
- .page-header {
254
- display: flex; align-items: center; padding: 10px 16px; flex-shrink: 0;
255
- .page-title { font-size: 16px; font-weight: 600; margin-right: 10px; }
256
- .page-tag { display: inline-block; padding: 1px 8px; font-size: 12px; border-radius: 4px; margin-right: 8px;
257
- &--change { background-color: var(--el-color-primary-light-9); color: var(--el-color-primary); border: 1px solid var(--el-color-primary-light-7); }
258
- &--status { background-color: var(--el-color-warning-light-9); color: var(--el-color-warning); border: 1px solid var(--el-color-warning-light-7); }
259
- }
260
- }
261
-
262
- .page-toolbar { padding: 0 16px 8px; flex-shrink: 0; }
263
- }
264
- ```
265
-
266
- #### 导航方式
267
-
268
- 从表单页跳转到变更历史页(隐藏 → 隐藏),**必须使用 `location.href`**:
269
-
270
- ```typescript
271
- const HISTORY_ROUTE = "/[subModule]/[kebabName]ChangeHistory";
272
- function handleChangeHistory() {
273
- const router = envConfig()?.router;
274
- if (!router) { ElMessage.error("路由未初始化,请刷新页面重试"); return; }
275
- location.href = router.resolve({ path: HISTORY_ROUTE, query: { id: currentId.value } }).href;
276
- }
277
- ```
278
-
279
- > ⚠️ 不可使用 `useRouter().push()`,原因参见 §FORM_ROUTE 导航规则。
280
-
5
+
6
+ > 适用场景:左侧为变更历史时间线列表,右侧为变更详情(复用业务域组件如 `c_customerTabs`,view 模式只读)。
7
+ > 特征识别:原型中出现"变更记录"左面板 + "变更详情"右面板的双栏布局;左侧每条记录含类型(数据新增/数据变更)、时间、人员。
8
+
9
+ #### 识别规则
10
+
11
+ - 原型出现**左窄右宽双栏**布局
12
+ - 左侧为**时间线 / 历史记录列表**(含彩色圆点 + 类型 + 时间 + 人员)
13
+ - 右侧为**详情区域**(只读 view 模式),复用表单页的业务组件
14
+ - 页面为**隐藏菜单**,由表单页跳转而来
15
+
16
+ #### 文件结构
17
+
18
+ ```
19
+ [kebab-name]-change-history/
20
+ ├── index.vue ← 双栏布局(左面板 + 右面板 + 业务组件)
21
+ ├── data.ts ← useChangeHistory composable + mock 数据
22
+ ├── index.scss ← 双栏 flex 布局样式
23
+ └── api.md ← 接口约定
24
+ ```
25
+
26
+ #### data.ts 模板
27
+
28
+ ```typescript
29
+ import { getAction } from "@jhlc/common-core/src/api/action";
30
+ import { useRouter } from "vue-router";
31
+ import { create[Domain]MockData } from "@/components/local/c_[domainTabs]/data";
32
+ import type { BasicInfoForm, BusinessInfoRow } from "@/components/local/c_[domainTabs]/data";
33
+
34
+ export const API_CONFIG = {
35
+ changeHistoryList: "/[服务缩写]/[资源]/changeHistory/list",
36
+ getById: "/[服务缩写]/[资源]/changeHistory/getById",
37
+ getDiffById: "/[服务缩写]/[资源]/changeHistory/getDiffById" // 获取旧版比对数据
38
+ } as const;
39
+
40
+ export interface HistoryRecord {
41
+ id: string;
42
+ changeType: string;
43
+ changeTime: string;
44
+ changePerson: string;
45
+ }
46
+
47
+ /** 变更历史记录 mock 数据(对齐原型截图) */
48
+ function createHistoryListMock(): HistoryRecord[] {
49
+ return [
50
+ { id: "h001", changeType: "数据新增", changeTime: "2025/12/15 13:48:07", changePerson: "新增人姓名" },
51
+ { id: "h003", changeType: "数据变更", changeTime: "2025/12/15 13:48:07", changePerson: "变更人姓名" },
52
+ // ... 按原型增减
53
+ ];
54
+ }
55
+
56
+ /** 变更比对 mock:旧版数据(体现字段级差异)
57
+ * 修改目标字段使之与 createChangeMockData 产生差异,
58
+ * c_domainTabs.loadDiffData() 会自动对比并高亮。
59
+ */
60
+ function createDiffMockData() {
61
+ const current = create[Domain]MockData();
62
+ return {
63
+ basicInfo: { ...current.basicInfo, /* 修改需要比对的字段 */ },
64
+ businessInfoList: current.businessInfoList.map((row, idx) => {
65
+ if (idx === 0) return { ...row, /* 修改需要比对的字段 */ };
66
+ return { ...row };
67
+ })
68
+ };
69
+ }
70
+
71
+ export function useChangeHistory(tabsRef: any) {
72
+ const router = useRouter();
73
+ const loading = ref(false);
74
+ const historyLoading = ref(false);
75
+ const historyList = ref<HistoryRecord[]>([]);
76
+ const selectedId = ref<string>("");
77
+ const isMockMode = ref(false);
78
+
79
+ // ── Mock 模式(后端接口未就绪时使用,关闭 mock 后切换到 loadHistoryList) ──
80
+ function loadMockData() {
81
+ isMockMode.value = true;
82
+ // 数据变更记录排在首位,确保页面加载时立即可见 diff 比对效果
83
+ historyList.value = createHistoryListMock();
84
+ if (historyList.value.length > 0) {
85
+ nextTick(() => selectMockDetail(historyList.value[0].id));
86
+ }
87
+ }
88
+
89
+ function selectMockDetail(id: string) {
90
+ selectedId.value = id;
91
+ const record = historyList.value.find(r => r.id === id);
92
+ tabsRef.value?.loadData(create[Domain]MockData());
93
+ if (record?.changeType.includes("变更")) {
94
+ tabsRef.value?.loadDiffData(createDiffMockData());
95
+ } else {
96
+ tabsRef.value?.clearDiffData();
97
+ }
98
+ }
99
+
100
+ // ── 真实接口模式(有 id 时使用) ──
101
+ async function loadHistoryList(applyId: string) {
102
+ historyLoading.value = true;
103
+ try {
104
+ const res = await getAction(API_CONFIG.changeHistoryList, { applyId });
105
+ historyList.value = res?.data?.length ? res.data : createHistoryListMock();
106
+ } catch {
107
+ historyList.value = createHistoryListMock();
108
+ } finally {
109
+ historyLoading.value = false;
110
+ if (historyList.value.length > 0) {
111
+ await loadHistoryDetail(historyList.value[0].id);
112
+ }
113
+ }
114
+ }
115
+
116
+ async function loadHistoryDetail(id: string) {
117
+ selectedId.value = id;
118
+ loading.value = true;
119
+ try {
120
+ const res = await getAction(API_CONFIG.getById, { id });
121
+ tabsRef.value?.loadData(res?.data || create[Domain]MockData());
122
+ const diffRes = await getAction(API_CONFIG.getDiffById, { id }).catch(() => null);
123
+ if (diffRes?.data) {
124
+ tabsRef.value?.loadDiffData(diffRes.data);
125
+ } else {
126
+ tabsRef.value?.clearDiffData();
127
+ }
128
+ } catch {
129
+ tabsRef.value?.loadData(create[Domain]MockData());
130
+ tabsRef.value?.clearDiffData();
131
+ } finally {
132
+ loading.value = false;
133
+ }
134
+ }
135
+
136
+ function handleSelectHistory(item: HistoryRecord) {
137
+ if (item.id === selectedId.value) return;
138
+ if (isMockMode.value) {
139
+ selectMockDetail(item.id);
140
+ } else {
141
+ loadHistoryDetail(item.id);
142
+ }
143
+ }
144
+
145
+ function handleCancel() { router.back(); }
146
+
147
+ return { loading, historyLoading, historyList, selectedId, loadHistoryList, loadMockData, handleSelectHistory, handleCancel };
148
+ }
149
+ ```
150
+
151
+ #### index.vue 模板
152
+
153
+ ```vue
154
+ <template>
155
+ <div class="app-container [page-class]">
156
+ <!-- 左侧:变更历史记录面板 -->
157
+ <div class="history-panel" v-loading="historyLoading">
158
+ <div class="history-panel__header">变更记录</div>
159
+ <div class="history-panel__list">
160
+ <div v-for="item in historyList" :key="item.id" class="history-card"
161
+ :class="{ 'is-active': item.id === selectedId }" @click="handleSelectHistory(item)">
162
+ <span class="history-card__dot"
163
+ :class="item.changeType.includes('新增') ? 'is-add' : 'is-change'"></span>
164
+ <div class="history-card__content">
165
+ <div class="history-card__type">{{ item.changeType }}</div>
166
+ <div class="history-card__date">{{ item.changeTime }}</div>
167
+ <div class="history-card__person">{{ item.changePerson }}</div>
168
+ </div>
169
+ </div>
170
+ <div v-if="!historyList.length && !historyLoading" class="history-empty">暂无变更记录</div>
171
+ </div>
172
+ </div>
173
+ <!-- 右侧:变更详情面板 -->
174
+ <div class="detail-panel" v-loading="loading">
175
+ <div class="page-header">
176
+ <span class="page-title">[实体]变更详情</span>
177
+ <span class="page-tag page-tag--change">变更</span>
178
+ <span class="page-tag page-tag--status">未审核</span>
179
+ </div>
180
+ <div class="page-toolbar">
181
+ <el-button @click="handleCancel">取消</el-button>
182
+ </div>
183
+ <div class="detail-panel__body">
184
+ <c_[domainTabs] ref="tabsRef" mode="view" />
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </template>
189
+
190
+ <script setup lang="ts">
191
+ import { useRoute } from "vue-router";
192
+ import { useChangeHistory } from "./data";
193
+ import c_[domainTabs] from "@/components/local/c_[domainTabs]/index.vue";
194
+
195
+ const tabsRef = ref();
196
+ const route = useRoute();
197
+ const { loading, historyLoading, historyList, selectedId, loadHistoryList, loadMockData, handleSelectHistory, handleCancel } = useChangeHistory(tabsRef);
198
+
199
+ onMounted(() => {
200
+ const id = route.query.id as string;
201
+ if (id) {
202
+ loadHistoryList(id); // 真实接口:有 id 时加载
203
+ } else {
204
+ loadMockData(); // Mock 模式:无 id 时纯 mock,零接口请求
205
+ }
206
+ });
207
+ </script>
208
+
209
+ <style scoped lang="scss">
210
+ @import "./index.scss";
211
+ </style>
212
+ ```
213
+
214
+ #### index.scss 模板
215
+
216
+ ```scss
217
+ .[page-class] {
218
+ display: flex !important;
219
+ padding: 0 !important;
220
+ height: 100%;
221
+ overflow: hidden;
222
+
223
+ .history-panel {
224
+ width: 200px; flex-shrink: 0;
225
+ border-right: 1px solid #e4e7ed;
226
+ display: flex; flex-direction: column; overflow: hidden; background: #fff;
227
+ &__header { padding: 12px 16px; font-size: 14px; font-weight: 600; border-bottom: 1px solid #e4e7ed; flex-shrink: 0; }
228
+ &__list { flex: 1; overflow-y: auto; padding: 4px 0; }
229
+ }
230
+
231
+ .history-card {
232
+ display: flex; align-items: flex-start; padding: 10px 16px; cursor: pointer;
233
+ border-left: 3px solid transparent; transition: background 0.15s; gap: 8px;
234
+ &:hover { background: #f5f7fa; }
235
+ &.is-active { background: #ecf5ff; border-left-color: #409eff; }
236
+ &__dot { width: 8px; height: 8px; border-radius: 50%; margin-top: 5px; flex-shrink: 0;
237
+ &.is-add { background: #409eff; }
238
+ &.is-change { background: #e6a23c; }
239
+ }
240
+ &__content { flex: 1; min-width: 0; }
241
+ &__type { font-size: 13px; font-weight: 500; color: #303133; line-height: 18px; }
242
+ &__date { font-size: 12px; color: #909399; line-height: 18px; }
243
+ &__person { font-size: 12px; color: #606266; line-height: 18px; }
244
+ }
245
+
246
+ .history-empty { padding: 32px 16px; text-align: center; color: #909399; font-size: 13px; }
247
+
248
+ .detail-panel {
249
+ flex: 1; overflow: hidden; display: flex; flex-direction: column; background: #fff;
250
+ &__body { flex: 1; overflow-y: auto; padding: 0 16px 16px; }
251
+ }
252
+
253
+ .page-header {
254
+ display: flex; align-items: center; padding: 10px 16px; flex-shrink: 0;
255
+ .page-title { font-size: 16px; font-weight: 600; margin-right: 10px; }
256
+ .page-tag { display: inline-block; padding: 1px 8px; font-size: 12px; border-radius: 4px; margin-right: 8px;
257
+ &--change { background-color: var(--el-color-primary-light-9); color: var(--el-color-primary); border: 1px solid var(--el-color-primary-light-7); }
258
+ &--status { background-color: var(--el-color-warning-light-9); color: var(--el-color-warning); border: 1px solid var(--el-color-warning-light-7); }
259
+ }
260
+ }
261
+
262
+ .page-toolbar { padding: 0 16px 8px; flex-shrink: 0; }
263
+ }
264
+ ```
265
+
266
+ #### 导航方式
267
+
268
+ 从表单页跳转到变更历史页(隐藏 → 隐藏),**必须使用 `location.href`**:
269
+
270
+ ```typescript
271
+ const HISTORY_ROUTE = "/[subModule]/[kebabName]ChangeHistory";
272
+ function handleChangeHistory() {
273
+ const router = envConfig()?.router;
274
+ if (!router) { ElMessage.error("路由未初始化,请刷新页面重试"); return; }
275
+ location.href = router.resolve({ path: HISTORY_ROUTE, query: { id: currentId.value } }).href;
276
+ }
277
+ ```
278
+
279
+ > ⚠️ 不可使用 `useRouter().push()`,原因参见 §FORM_ROUTE 导航规则。
280
+
281
281
  ---