@agile-team/wl-skills-kit 2.3.3 → 2.3.5

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 (91) hide show
  1. package/CHANGELOG.md +24 -23
  2. package/README.md +15 -146
  3. package/files/.cursor/mcp.json +8 -0
  4. package/files/.github/guides/README.md +13 -13
  5. package/files/.github/guides/architecture.md +555 -555
  6. package/files/.github/guides/mcp-setup.md +109 -0
  7. package/files/.github/guides/usage.md +184 -176
  8. package/files/.github/reports/README.md +65 -65
  9. package/files/.github/reports/SYS_DICT_INFO.md +50 -50
  10. package/files/.github/reports/SYS_MENU_INFO.md +247 -247
  11. package/files/.github/reports/SYS_PERMISSION_INFO.md +20 -20
  12. 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
  13. 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
  14. package/files/.github/skills/_compat/README.md +108 -108
  15. package/files/.github/skills/_compat/editors.json +7 -0
  16. package/files/.github/skills/_compat/headers/agents.txt +8 -8
  17. package/files/.github/skills/_compat/headers/claude-code.txt +7 -7
  18. package/files/.github/skills/_compat/headers/cline.txt +7 -7
  19. package/files/.github/skills/_compat/headers/cursor-mdc.txt +16 -16
  20. package/files/.github/skills/_compat/headers/cursor-rules.txt +7 -7
  21. package/files/.github/skills/_compat/headers/github-copilot.txt +1 -1
  22. package/files/.github/skills/_compat/headers/kiro.txt +10 -10
  23. package/files/.github/skills/_compat/headers/qoder.txt +8 -0
  24. package/files/.github/skills/_compat/headers/trae.txt +11 -11
  25. package/files/.github/skills/_compat/headers/windsurf.txt +7 -7
  26. package/files/.github/skills/_registry.md +81 -81
  27. package/files/.github/skills/core/api-contract/SKILL.md +344 -344
  28. package/files/.github/skills/core/api-contract/USAGE.md +110 -110
  29. package/files/.github/skills/core/convention-audit/SKILL.md +189 -189
  30. package/files/.github/skills/core/convention-audit/USAGE.md +99 -99
  31. package/files/.github/skills/core/page-codegen/SKILL.md +973 -973
  32. package/files/.github/skills/core/page-codegen/USAGE.md +102 -102
  33. package/files/.github/skills/core/page-codegen/templates/_index.md +46 -46
  34. package/files/.github/skills/core/page-codegen/templates/domains/_CONTRIBUTING.md +107 -107
  35. package/files/.github/skills/core/page-codegen/templates/domains/produce/TPL-OPERATION-STATION.md +442 -442
  36. package/files/.github/skills/core/page-codegen/templates/domains/sale/README.md +26 -26
  37. package/files/.github/skills/core/page-codegen/templates/universal/TPL-CHANGE-HISTORY.md +276 -276
  38. package/files/.github/skills/core/page-codegen/templates/universal/TPL-DETAIL-TABS.md +1145 -1145
  39. package/files/.github/skills/core/page-codegen/templates/universal/TPL-DRIVEN.md +309 -309
  40. package/files/.github/skills/core/page-codegen/templates/universal/TPL-FORM-ROUTE.md +436 -436
  41. package/files/.github/skills/core/page-codegen/templates/universal/TPL-LIST.md +191 -191
  42. package/files/.github/skills/core/page-codegen/templates/universal/TPL-MASTER-DETAIL.md +148 -148
  43. package/files/.github/skills/core/page-codegen/templates/universal/TPL-RECORD-FORM.md +376 -376
  44. package/files/.github/skills/core/page-codegen/templates/universal/TPL-TREE-LIST.md +186 -186
  45. package/files/.github/skills/core/prototype-scan/SKILL.md +498 -498
  46. package/files/.github/skills/core/prototype-scan/USAGE.md +95 -95
  47. package/files/.github/skills/core/template-extract/SKILL.md +139 -139
  48. package/files/.github/skills/core/template-extract/USAGE.md +93 -93
  49. package/files/.github/skills/domain/README.md +51 -51
  50. package/files/.github/skills/sync/menu-sync/SKILL.md +263 -263
  51. package/files/.github/skills/sync/menu-sync/USAGE.md +104 -104
  52. package/files/.github/skills/sync/menu-sync/env/env.local.json +7 -7
  53. package/files/.github/skills/sync/menu-sync/env/guide.md +99 -99
  54. package/files/.github/skills/sync/permission-sync/SKILL.draft.md +91 -91
  55. package/files/.github/standards/01-toolchain.md +57 -57
  56. package/files/.github/standards/02-code-structure.md +111 -111
  57. package/files/.github/standards/03-comments.md +53 -53
  58. package/files/.github/standards/04-coding-basics.md +33 -33
  59. package/files/.github/standards/05-logging.md +38 -38
  60. package/files/.github/standards/06-security.md +44 -44
  61. package/files/.github/standards/07-config.md +52 -52
  62. package/files/.github/standards/08-git.md +60 -60
  63. package/files/.github/standards/09-typescript.md +71 -71
  64. package/files/.github/standards/10-pinia.md +57 -57
  65. package/files/.github/standards/11-form-validation.md +81 -81
  66. package/files/.github/standards/12-base-table.md +153 -153
  67. package/files/.github/standards/13-platform-components.md +123 -123
  68. package/files/.github/standards/index.md +89 -89
  69. package/files/.kiro/settings/mcp.json +8 -0
  70. package/files/.mcp.json +8 -0
  71. package/files/.vscode/mcp.json +9 -0
  72. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/data.ts +196 -196
  73. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/index.scss +150 -150
  74. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/index.vue +79 -79
  75. package/files/docs/jh-date-range.md +257 -257
  76. package/files/docs/jh-date.md +222 -222
  77. package/files/docs/jh-dept-picker.md +190 -190
  78. package/files/docs/jh-drag-row.md +590 -590
  79. package/files/docs/jh-file-upload.md +216 -216
  80. package/files/docs/jh-picker.md +218 -218
  81. package/files/docs/jh-select.md +148 -148
  82. package/files/docs/jh-text.md +248 -248
  83. package/files/docs/jh-user-picker.md +197 -197
  84. package/files/src/components/global/C_RightToolbar/data.ts +228 -228
  85. package/files/src/components/global/C_RightToolbar/index.scss +44 -44
  86. package/files/src/components/global/C_Splitter/index.scss +61 -61
  87. package/files/src/components/global/C_SvgIcon/index.scss +15 -15
  88. package/files/src/components/global/C_TagStatus/index.scss +20 -20
  89. package/files/src/components/global/C_Tree/data.ts +61 -61
  90. package/files/src/components/local/c_listModal/index.scss +4 -4
  91. package/package.json +1 -1
@@ -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
  ---