@agile-team/wl-skills-ui 1.6.2
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.
- package/CHANGELOG.md +207 -0
- package/README.md +623 -0
- package/SKILL.md +632 -0
- package/bin/wl-ui.js +794 -0
- package/design/spec/color.md +41 -0
- package/design/spec/spacing.md +28 -0
- package/design/spec/typography.md +29 -0
- package/design/tokens/base.css +144 -0
- package/design/tokens/index.css +8 -0
- package/dist/ag-grid-override.scss +1 -0
- package/dist/element.scss +1 -0
- package/dist/index.scss +1 -0
- package/dist/portal.scss +1 -0
- package/dist/style-override.scss +1 -0
- package/dist/tokens.css +144 -0
- package/es/chunk-GBB7LDKQ.js +261 -0
- package/es/chunk-MDI2SCUP.js +281 -0
- package/es/chunk-QDTNMM7W.js +286 -0
- package/es/chunk-YLNAFAWE.js +233 -0
- package/es/common-preset.d.ts +38 -0
- package/es/common-preset.js +157 -0
- package/es/index.d.ts +32 -0
- package/es/index.js +1 -0
- package/es/presets/security.d.ts +16 -0
- package/es/presets/security.js +99 -0
- package/es/renderers-BFRSp2BH.d.ts +51 -0
- package/es/renderers-DMZbi_Gs.d.ts +51 -0
- package/es/renderers-DWhUlI2y.d.ts +47 -0
- package/es/types-04qLKHMt.d.ts +62 -0
- package/es/types-BrCPjAH6.d.ts +62 -0
- package/es/types-QZ1gy9KX.d.ts +63 -0
- package/examples/migration-operations-to-renderOps.md +107 -0
- package/examples/wk-exempt.example.json +30 -0
- package/mcp/server.js +394 -0
- package/package.json +130 -0
- package/reference/SelectPopupCom.vue +634 -0
- package/reference/ag-cell-renders.ts +644 -0
- package/reference/define-columns.ts +185 -0
- package/reference/jhlc.d.ts +1 -0
- package/scanner/coverage.mjs +287 -0
- package/scanner/exempt.mjs +127 -0
- package/scanner/fix.mjs +216 -0
- package/scanner/index.mjs +403 -0
- package/scanner/integration.mjs +155 -0
- package/scanner/report.mjs +459 -0
- package/scanner/rules/_shared.mjs +134 -0
- package/scanner/rules/button.mjs +97 -0
- package/scanner/rules/color.mjs +118 -0
- package/scanner/rules/componentFamily.mjs +184 -0
- package/scanner/rules/dialog.mjs +42 -0
- package/scanner/rules/form.mjs +86 -0
- package/scanner/rules/index.mjs +58 -0
- package/scanner/rules/table.mjs +170 -0
- package/scanner/rules/tag.mjs +245 -0
- package/scanner/snapshot.mjs +198 -0
- package/skills/_flows/full-audit.md +65 -0
- package/skills/_flows/legacy-skin-align.md +71 -0
- package/skills/_flows/new-project-init.md +80 -0
- package/skills/_flows/progressive-migrate.md +49 -0
- package/skills/_meta/_compat/README.md +47 -0
- package/skills/_meta/_compat/editors.json +76 -0
- package/skills/_meta/_compat/headers/agents.txt +7 -0
- package/skills/_meta/_compat/headers/claude-code.txt +7 -0
- package/skills/_meta/_compat/headers/cline.txt +7 -0
- package/skills/_meta/_compat/headers/cursor-mdc.txt +5 -0
- package/skills/_meta/_compat/headers/github-copilot.txt +5 -0
- package/skills/_meta/_compat/headers/kiro.txt +4 -0
- package/skills/_meta/_compat/headers/qoder.txt +4 -0
- package/skills/_meta/_compat/headers/trae.txt +3 -0
- package/skills/_meta/_compat/headers/windsurf.txt +5 -0
- package/skills/_meta/_detection.md +81 -0
- package/skills/_meta/_registry.md +96 -0
- package/skills/element/component-family/SKILL.md +71 -0
- package/skills/element/el-dialog/SKILL.md +124 -0
- package/skills/element/el-form/SKILL.md +84 -0
- package/skills/element/el-table/SKILL.md +86 -0
- package/skills/element/el-tag/SKILL.md +127 -0
- package/skills/layouts/detail-page/SKILL.md +99 -0
- package/skills/layouts/form-dialog/SKILL.md +128 -0
- package/skills/layouts/list-page/SKILL.md +111 -0
- package/skills/layouts/tree-list/SKILL.md +108 -0
- package/skills/ops/audit/SKILL.md +109 -0
- package/skills/ops/fix/SKILL.md +52 -0
- package/skills/ops/fix/USAGE.md +24 -0
- package/skills/ops/migrate/SKILL.md +157 -0
- package/skills/ops/scan/SKILL.md +71 -0
- package/skills/ops/scan/USAGE.md +16 -0
- package/skills/runtime/design-tokens/SKILL.md +88 -0
- package/skills/runtime/design-tokens/USAGE.md +19 -0
- package/skills/runtime/migration/SKILL.md +106 -0
- package/skills/runtime/migration/USAGE.md +34 -0
- package/skills/runtime/style-align/SKILL.md +124 -0
- package/skills/runtime/style-align/USAGE.md +52 -0
- package/skills/vendors/ag-grid/SKILL.md +94 -0
- package/skills/vendors/base-table/SKILL.md +57 -0
- package/skills/vendors/c-components/SKILL.md +36 -0
- package/skills/vendors/custom-wrappers/SKILL.md +45 -0
- package/skills/vendors/jh-components/SKILL.md +42 -0
- package/skills/vendors/unknown-wrapper/SKILL.md +44 -0
- package/standards/engineering/01-import-order.md +46 -0
- package/standards/engineering/02-naming.md +54 -0
- package/standards/engineering/03-scss-structure.md +59 -0
- package/standards/ui/01-table.md +111 -0
- package/standards/ui/02-button.md +83 -0
- package/standards/ui/03-form.md +118 -0
- package/standards/ui/04-tag-status.md +110 -0
- package/standards/ui/05-dialog-pagination.md +118 -0
- package/styles/element/_card.scss +26 -0
- package/styles/element/_descriptions.scss +22 -0
- package/styles/element/_dialog.scss +43 -0
- package/styles/element/_drawer.scss +22 -0
- package/styles/element/_feedback.scss +52 -0
- package/styles/element/_form.scss +22 -0
- package/styles/element/_navigation.scss +36 -0
- package/styles/element/_overlay.scss +32 -0
- package/styles/element/_pagination.scss +16 -0
- package/styles/element/_steps.scss +32 -0
- package/styles/element/_table.scss +13 -0
- package/styles/element/_tabs.scss +40 -0
- package/styles/element/_tree.scss +27 -0
- package/styles/element/_upload.scss +34 -0
- package/styles/element/index.scss +17 -0
- package/styles/index.scss +21 -0
- package/styles/layouts/_detail-page.scss +27 -0
- package/styles/layouts/_form-dialog.scss +19 -0
- package/styles/layouts/_list-page.scss +47 -0
- package/styles/layouts/_tree-list.scss +33 -0
- package/styles/layouts/index.scss +6 -0
- package/styles/presets/element-only.scss +6 -0
- package/styles/presets/full.scss +12 -0
- package/styles/presets/security.scss +37 -0
- package/styles/presets/skin.scss +14 -0
- package/styles/presets/tokens-only.scss +7 -0
- package/styles/tokens/index.scss +79 -0
- package/styles/vendors/_ag-grid.scss +273 -0
- package/styles/vendors/_base-components.scss +480 -0
- package/styles/vendors/_base-query-toolbar.scss +253 -0
- package/styles/vendors/_base-table.scss +41 -0
- package/styles/vendors/_c-components.scss +34 -0
- package/styles/vendors/_custom-wrappers.scss +30 -0
- package/styles/vendors/_jh-drag-col.scss +63 -0
- package/styles/vendors/_jh-pagination.scss +81 -0
- package/styles/vendors/_jh-tree.scss +196 -0
- package/styles/vendors/_portal.scss +114 -0
- package/styles/vendors/index.scss +17 -0
- package/templates/ag-grid-page/TPL-AG-GRID.md +76 -0
- package/templates/form-dialog/TPL-FORM-DIALOG.md +165 -0
- package/templates/list-page/TPL-LIST.md +139 -0
- package/templates/tree-list/TPL-TREE-LIST.md +176 -0
- package/templates/ui-optimization-report/TPL-UI-OPTIM-REPORT.md +343 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scanner/rules/index.mjs — 规则注册中心
|
|
3
|
+
*
|
|
4
|
+
* 内置规则按类别拆分在各子文件中。
|
|
5
|
+
* 支持外部插件通过 addRules(rules) 动态注入自定义规则。
|
|
6
|
+
*
|
|
7
|
+
* 导出:
|
|
8
|
+
* - BUILT_IN_RULES 内置规则数组(只读)
|
|
9
|
+
* - addRules(rules) 注册外部规则(插件式扩展)
|
|
10
|
+
* - getRules() 获取当前全部规则(内置 + 外部)
|
|
11
|
+
* - getRuleById(id) 按 ID 查找规则
|
|
12
|
+
*/
|
|
13
|
+
import { tableRules } from "./table.mjs";
|
|
14
|
+
import { formRules } from "./form.mjs";
|
|
15
|
+
import { buttonRules } from "./button.mjs";
|
|
16
|
+
import { colorRules } from "./color.mjs";
|
|
17
|
+
import { dialogRules } from "./dialog.mjs";
|
|
18
|
+
import { tagRules } from "./tag.mjs";
|
|
19
|
+
import { componentFamilyRules } from "./componentFamily.mjs";
|
|
20
|
+
|
|
21
|
+
export const BUILT_IN_RULES = [
|
|
22
|
+
...tableRules,
|
|
23
|
+
...formRules,
|
|
24
|
+
...buttonRules,
|
|
25
|
+
...colorRules,
|
|
26
|
+
...dialogRules,
|
|
27
|
+
...tagRules,
|
|
28
|
+
...componentFamilyRules,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const _externalRules = [];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 注册外部自定义规则(插件式扩展)
|
|
35
|
+
* @param {Array} rules - 规则对象数组,每条规则需含 id / category / severity / check 方法
|
|
36
|
+
*/
|
|
37
|
+
export function addRules(rules) {
|
|
38
|
+
for (const r of rules) {
|
|
39
|
+
if (!r.id || typeof r.check !== "function")
|
|
40
|
+
throw new Error(
|
|
41
|
+
`[wk-scan] addRules: 规则 "${r.id}" 必须提供 id 和 check() 方法`,
|
|
42
|
+
);
|
|
43
|
+
_externalRules.push(r);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 返回全部规则(内置 + 外部插件) */
|
|
48
|
+
export function getRules() {
|
|
49
|
+
return [...BUILT_IN_RULES, ..._externalRules];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 按 ID 查找规则 */
|
|
53
|
+
export function getRuleById(id) {
|
|
54
|
+
return getRules().find((r) => r.id === id);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 兼容:直接 default 导出全部规则数组 */
|
|
58
|
+
export default getRules;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/** scanner/rules/table.mjs — 表格相关规则:R001 R002 R003 R014 R021 R022 */
|
|
2
|
+
import { lineOf, issue, findTags } from "./_shared.mjs";
|
|
3
|
+
|
|
4
|
+
export const tableRules = [
|
|
5
|
+
// R001: el-table-column 缺少 align="center"
|
|
6
|
+
{
|
|
7
|
+
id: "R001",
|
|
8
|
+
category: "table",
|
|
9
|
+
severity: "error",
|
|
10
|
+
name: 'el-table-column 缺少 align="center"',
|
|
11
|
+
check(template, file, lineOffset) {
|
|
12
|
+
const issues = [];
|
|
13
|
+
const hasCenter = (t) =>
|
|
14
|
+
/(?::?align)\s*=\s*["'](?:center|'center'|"center")["']/.test(t) ||
|
|
15
|
+
/align\s*=\s*["']center["']/.test(t);
|
|
16
|
+
const hasLeft = (t) => /align\s*=\s*["']left["']/.test(t);
|
|
17
|
+
for (const tag of findTags(template, "el-table-column")) {
|
|
18
|
+
if (!hasCenter(tag.text)) {
|
|
19
|
+
issues.push(
|
|
20
|
+
issue(
|
|
21
|
+
file,
|
|
22
|
+
lineOf(template, tag.index, lineOffset),
|
|
23
|
+
"R001",
|
|
24
|
+
"table",
|
|
25
|
+
"error",
|
|
26
|
+
hasLeft(tag.text)
|
|
27
|
+
? '有 align="left",应改为 align="center"'
|
|
28
|
+
: '缺少 align="center"',
|
|
29
|
+
'添加 align="center";selection列同时加 header-align="center"、width=55,index列width=60',
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return issues;
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// R002: el-table 缺少 empty-text
|
|
39
|
+
{
|
|
40
|
+
id: "R002",
|
|
41
|
+
category: "table",
|
|
42
|
+
severity: "warning",
|
|
43
|
+
name: 'el-table 缺少 empty-text="暂无数据"',
|
|
44
|
+
check(template, file, lineOffset) {
|
|
45
|
+
const issues = [];
|
|
46
|
+
for (const tag of findTags(template, "el-table")) {
|
|
47
|
+
if (!tag.text.includes("empty-text="))
|
|
48
|
+
issues.push(
|
|
49
|
+
issue(
|
|
50
|
+
file,
|
|
51
|
+
lineOf(template, tag.index, lineOffset),
|
|
52
|
+
"R002",
|
|
53
|
+
"table",
|
|
54
|
+
"warning",
|
|
55
|
+
"缺少 empty-text 属性",
|
|
56
|
+
'添加 empty-text="暂无数据"',
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return issues;
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// R003: BaseTable 缺少 empty-text
|
|
65
|
+
{
|
|
66
|
+
id: "R003",
|
|
67
|
+
category: "table",
|
|
68
|
+
severity: "warning",
|
|
69
|
+
name: 'BaseTable 缺少 empty-text="暂无数据"',
|
|
70
|
+
check(template, file, lineOffset) {
|
|
71
|
+
const issues = [];
|
|
72
|
+
for (const tag of findTags(template, "BaseTable")) {
|
|
73
|
+
if (!tag.text.includes("empty-text="))
|
|
74
|
+
issues.push(
|
|
75
|
+
issue(
|
|
76
|
+
file,
|
|
77
|
+
lineOf(template, tag.index, lineOffset),
|
|
78
|
+
"R003",
|
|
79
|
+
"table",
|
|
80
|
+
"warning",
|
|
81
|
+
"BaseTable 缺少 empty-text 属性",
|
|
82
|
+
'添加 empty-text="暂无数据"',
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return issues;
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// R014: selection 列缺少 header-align="center"
|
|
91
|
+
{
|
|
92
|
+
id: "R014",
|
|
93
|
+
category: "table",
|
|
94
|
+
severity: "warning",
|
|
95
|
+
name: 'selection 列缺少 header-align="center"',
|
|
96
|
+
check(template, file, lineOffset) {
|
|
97
|
+
const issues = [];
|
|
98
|
+
for (const tag of findTags(template, "el-table-column")) {
|
|
99
|
+
if (
|
|
100
|
+
tag.text.includes('type="selection"') &&
|
|
101
|
+
!tag.text.includes("header-align=")
|
|
102
|
+
)
|
|
103
|
+
issues.push(
|
|
104
|
+
issue(
|
|
105
|
+
file,
|
|
106
|
+
lineOf(template, tag.index, lineOffset),
|
|
107
|
+
"R014",
|
|
108
|
+
"table",
|
|
109
|
+
"warning",
|
|
110
|
+
'selection 列缺少 header-align="center",会导致表头复选框不居中',
|
|
111
|
+
'添加 header-align="center"',
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return issues;
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// R021: BaseTable 必须使用 AGGrid 渲染模式
|
|
120
|
+
{
|
|
121
|
+
id: "R021",
|
|
122
|
+
category: "table",
|
|
123
|
+
severity: "error",
|
|
124
|
+
name: 'BaseTable 缺少 render-type="agGrid"',
|
|
125
|
+
check(template, file, lineOffset) {
|
|
126
|
+
const issues = [];
|
|
127
|
+
for (const tag of findTags(template, "BaseTable")) {
|
|
128
|
+
if (!/render-type\s*=\s*["']agGrid["']/.test(tag.text))
|
|
129
|
+
issues.push(
|
|
130
|
+
issue(
|
|
131
|
+
file,
|
|
132
|
+
lineOf(template, tag.index, lineOffset),
|
|
133
|
+
"R021",
|
|
134
|
+
"table",
|
|
135
|
+
"error",
|
|
136
|
+
'BaseTable 必须显式配置 render-type="agGrid"',
|
|
137
|
+
'添加 render-type="agGrid"',
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return issues;
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// R022: BaseTable 必须有唯一 cid
|
|
146
|
+
{
|
|
147
|
+
id: "R022",
|
|
148
|
+
category: "table",
|
|
149
|
+
severity: "error",
|
|
150
|
+
name: "BaseTable 缺少 cid/:cid",
|
|
151
|
+
check(template, file, lineOffset) {
|
|
152
|
+
const issues = [];
|
|
153
|
+
for (const tag of findTags(template, "BaseTable")) {
|
|
154
|
+
if (!/(^|\s)(:cid|cid)\s*=/.test(tag.text))
|
|
155
|
+
issues.push(
|
|
156
|
+
issue(
|
|
157
|
+
file,
|
|
158
|
+
lineOf(template, tag.index, lineOffset),
|
|
159
|
+
"R022",
|
|
160
|
+
"table",
|
|
161
|
+
"error",
|
|
162
|
+
"BaseTable 必须配置全局唯一 cid",
|
|
163
|
+
'添加 cid="页面缩写-唯一后缀" 或 :cid="TABLE_CID"',
|
|
164
|
+
),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return issues;
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
];
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/** scanner/rules/tag.mjs — 标签/状态规则:R009 R010 R012 R017 R018 */
|
|
2
|
+
import { lineOf, issue, findTags } from "./_shared.mjs";
|
|
3
|
+
|
|
4
|
+
/** 状态类字段名关键字(动态流转状态) */
|
|
5
|
+
const STATUS_FIELD_PATTERN =
|
|
6
|
+
/(?:Status|State|Flag|status|state|flag)(?=\s*["']|:\s*["']|$)/;
|
|
7
|
+
|
|
8
|
+
/** 分类字段名关键字(组织/归档属性) */
|
|
9
|
+
const CLASSIFY_FIELD_PATTERN =
|
|
10
|
+
/(?:Type|Level|Kind|Class|Category|type|level|kind|class|category)(?=\s*["']|:\s*["']|$)/;
|
|
11
|
+
|
|
12
|
+
export const tagRules = [
|
|
13
|
+
// R009: 状态字段纯文本渲染(应使用 renderTagNode / ElTag)
|
|
14
|
+
{
|
|
15
|
+
id: "R009",
|
|
16
|
+
category: "tag",
|
|
17
|
+
severity: "warning",
|
|
18
|
+
name: "状态字段纯文本渲染(应使用 renderTagNode / ElTag)",
|
|
19
|
+
check(template, file, lineOffset) {
|
|
20
|
+
const issues = [];
|
|
21
|
+
// 检测 el-table-column 中 name/prop 含 Status/Level/State 但无 renderTagNode/ElTag/jh-tag/defaultSlot
|
|
22
|
+
for (const tag of findTags(template, "el-table-column")) {
|
|
23
|
+
const nameMatch = tag.text.match(/(?:name|prop)\s*=\s*["'](\w+)["']/);
|
|
24
|
+
if (!nameMatch) continue;
|
|
25
|
+
const fieldName = nameMatch[1];
|
|
26
|
+
if (
|
|
27
|
+
!STATUS_FIELD_PATTERN.test(fieldName) &&
|
|
28
|
+
!CLASSIFY_FIELD_PATTERN.test(fieldName)
|
|
29
|
+
)
|
|
30
|
+
continue;
|
|
31
|
+
// 有渲染器则跳过
|
|
32
|
+
if (
|
|
33
|
+
/renderTagNode|renderOps|ElTag|jh-tag|defaultSlot|defaultNode|renderClassifyTag|renderBadge/.test(
|
|
34
|
+
tag.text,
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
continue;
|
|
38
|
+
issues.push(
|
|
39
|
+
issue(
|
|
40
|
+
file,
|
|
41
|
+
lineOf(template, tag.index, lineOffset),
|
|
42
|
+
"R009",
|
|
43
|
+
"tag",
|
|
44
|
+
"warning",
|
|
45
|
+
`状态字段 "${fieldName}" 使用纯文本渲染,应改为 renderTagNode / ElTag`,
|
|
46
|
+
`使用 defineColumns() 包裹列定义,或手动添加 defaultSlot: ({ row }) => renderTagNode(row.${fieldName}, MAP)`,
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return issues;
|
|
51
|
+
},
|
|
52
|
+
// 脚本式列定义(columnsDef / columns 数组)中状态字段纯文本(含 logicType:dict 但无 defaultSlot)
|
|
53
|
+
checkScript(script, file, lineOffset) {
|
|
54
|
+
const issues = [];
|
|
55
|
+
const lines = script.split("\n");
|
|
56
|
+
for (let i = 0; i < lines.length; i++) {
|
|
57
|
+
const labelMatch = lines[i].match(/label:\s*["']([^"']+)["']/);
|
|
58
|
+
if (!labelMatch) continue;
|
|
59
|
+
const label = labelMatch[1];
|
|
60
|
+
if (!STATUS_FIELD_PATTERN.test(label) && !/状态|级别|类型/.test(label))
|
|
61
|
+
continue;
|
|
62
|
+
const blockStart = Math.max(0, i - 3);
|
|
63
|
+
const blockEnd = Math.min(lines.length, i + 8);
|
|
64
|
+
const block = lines.slice(blockStart, blockEnd).join("\n");
|
|
65
|
+
// 已有渲染器则跳过
|
|
66
|
+
if (
|
|
67
|
+
/defaultSlot|defaultNode|renderTag|renderClassify|renderDictClassify|renderEnable|renderBadge|ElTag/.test(
|
|
68
|
+
block,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
continue;
|
|
72
|
+
issues.push({
|
|
73
|
+
file,
|
|
74
|
+
line: lineOffset + i + 1,
|
|
75
|
+
rule: "R009",
|
|
76
|
+
category: "tag",
|
|
77
|
+
severity: "warning",
|
|
78
|
+
description: `状态/分类列 "${label}" 纯文本渲染,应使用 renderTagNode / renderDictClassifyTag`,
|
|
79
|
+
suggestion: `添加 defaultSlot: ({ row }) => renderDictClassifyTag(row.xxx, 'dictKey') 或 renderTagNode(row.xxx, MAP)`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return issues;
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// R010: 分类字段使用填充色 ElTag(应使用 effect="plain")
|
|
87
|
+
{
|
|
88
|
+
id: "R010",
|
|
89
|
+
category: "tag",
|
|
90
|
+
severity: "warning",
|
|
91
|
+
name: '分类字段使用填充色 ElTag(应使用 effect="plain" outline 风格)',
|
|
92
|
+
check(template, file, lineOffset) {
|
|
93
|
+
const issues = [];
|
|
94
|
+
// 检测含 Type/Level/Class/Category 的 el-tag 缺少 effect="plain"
|
|
95
|
+
for (const tag of findTags(template, "el-tag")) {
|
|
96
|
+
// 判断上下文是否是分类字段(前后 200 字符内含分类关键字)
|
|
97
|
+
const contextStart = Math.max(0, tag.index - 200);
|
|
98
|
+
const context = template.slice(
|
|
99
|
+
contextStart,
|
|
100
|
+
tag.index + tag.text.length,
|
|
101
|
+
);
|
|
102
|
+
if (!CLASSIFY_FIELD_PATTERN.test(context)) continue;
|
|
103
|
+
// 已有 effect="plain" 则跳过
|
|
104
|
+
if (/effect\s*=\s*["']plain["']/.test(tag.text)) continue;
|
|
105
|
+
// 有 effect="dark" 或无 effect 的分类 Tag → 建议改为 plain
|
|
106
|
+
if (
|
|
107
|
+
/effect\s*=\s*["']dark["']/.test(tag.text) ||
|
|
108
|
+
!/effect\s*=/.test(tag.text)
|
|
109
|
+
) {
|
|
110
|
+
issues.push(
|
|
111
|
+
issue(
|
|
112
|
+
file,
|
|
113
|
+
lineOf(template, tag.index, lineOffset),
|
|
114
|
+
"R010",
|
|
115
|
+
"tag",
|
|
116
|
+
"warning",
|
|
117
|
+
'分类字段 ElTag 未使用 effect="plain"(outline 风格),视觉权重过高',
|
|
118
|
+
'添加 effect="plain",或使用 renderClassifyTag() 自动渲染',
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return issues;
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// R017: 编号/工号/证件号列缺少 renderBadge(脚本式 columnsDef)
|
|
128
|
+
{
|
|
129
|
+
id: "R017",
|
|
130
|
+
category: "tag",
|
|
131
|
+
severity: "warning",
|
|
132
|
+
name: "编号/工号/证件号列缺少 renderBadge(脚本式 columnsDef)",
|
|
133
|
+
checkScript(script, file, lineOffset) {
|
|
134
|
+
const issues = [];
|
|
135
|
+
/** label 关键字:编号、工号、证件号、以及常见英文组合 */
|
|
136
|
+
const BADGE_LABEL_RE =
|
|
137
|
+
/编号|工号|证件号|驾驶员证件号|车主证件号|处置人工号/;
|
|
138
|
+
const lines = script.split("\n");
|
|
139
|
+
for (let i = 0; i < lines.length; i++) {
|
|
140
|
+
const labelMatch = lines[i].match(/label:\s*["']([^"']+)["']/);
|
|
141
|
+
if (!labelMatch) continue;
|
|
142
|
+
const label = labelMatch[1];
|
|
143
|
+
if (!BADGE_LABEL_RE.test(label)) continue;
|
|
144
|
+
const blockStart = Math.max(0, i - 2);
|
|
145
|
+
const blockEnd = Math.min(lines.length, i + 10);
|
|
146
|
+
const block = lines.slice(blockStart, blockEnd).join("\n");
|
|
147
|
+
if (/renderBadge|defaultSlot|defaultNode/.test(block)) continue;
|
|
148
|
+
const nameMatch = block.match(/name:\s*["']([^"']+)["']/);
|
|
149
|
+
const fieldName = nameMatch ? nameMatch[1] : "xxx";
|
|
150
|
+
issues.push({
|
|
151
|
+
file,
|
|
152
|
+
line: lineOffset + i + 1,
|
|
153
|
+
rule: "R017",
|
|
154
|
+
category: "tag",
|
|
155
|
+
severity: "warning",
|
|
156
|
+
description: `编号列 "${label}" 未使用 renderBadge,纯文本无视觉区分`,
|
|
157
|
+
suggestion: `添加 defaultSlot: ({ row }) => renderBadge(row.${fieldName}),并 import { renderBadge } from "@/util/ag-cell-renders"`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return issues;
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// R018: 脚本式列定义使用 logicType:dict 但缺少 defaultSlot(应改为 renderDictClassifyTag)
|
|
165
|
+
{
|
|
166
|
+
id: "R018",
|
|
167
|
+
category: "tag",
|
|
168
|
+
severity: "warning",
|
|
169
|
+
name: "dict列缺少 defaultSlot/renderDictClassifyTag(脚本式 columnsDef)",
|
|
170
|
+
checkScript(script, file, lineOffset) {
|
|
171
|
+
const issues = [];
|
|
172
|
+
const lines = script.split("\n");
|
|
173
|
+
for (let i = 0; i < lines.length; i++) {
|
|
174
|
+
if (!/logicType.*dict|BusLogicDataType\.dict/.test(lines[i])) continue;
|
|
175
|
+
const blockStart = Math.max(0, i - 5);
|
|
176
|
+
const blockEnd = Math.min(lines.length, i + 5);
|
|
177
|
+
const block = lines.slice(blockStart, blockEnd).join("\n");
|
|
178
|
+
if (
|
|
179
|
+
/defaultSlot|defaultNode|renderTag|renderClassify|renderDictClassify|renderEnable|renderBadge/.test(
|
|
180
|
+
block,
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
continue;
|
|
184
|
+
const labelMatch = block.match(/label:\s*["']([^"']+)["']/);
|
|
185
|
+
const label = labelMatch ? labelMatch[1] : "(unknown)";
|
|
186
|
+
const logicMatch = block.match(/logicValue:\s*["']([^"']+)["']/);
|
|
187
|
+
const dictKey = logicMatch ? logicMatch[1] : "dictKey";
|
|
188
|
+
const nameMatch = block.match(/name:\s*["']([^"']+)["']/);
|
|
189
|
+
const fieldName = nameMatch ? nameMatch[1] : "xxx";
|
|
190
|
+
issues.push({
|
|
191
|
+
file,
|
|
192
|
+
line: lineOffset + i + 1,
|
|
193
|
+
rule: "R018",
|
|
194
|
+
category: "tag",
|
|
195
|
+
severity: "warning",
|
|
196
|
+
description: `"${label}" 使用 logicType:dict 但无 defaultSlot,纯文本展示枚举值`,
|
|
197
|
+
suggestion: `改为 defaultSlot: ({ row }) => renderDictClassifyTag(row.${fieldName}, '${dictKey}'),删除 logicType/logicValue`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return issues;
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
// R012: 弹窗内 el-table 缺少空状态
|
|
205
|
+
{
|
|
206
|
+
id: "R012",
|
|
207
|
+
category: "tag",
|
|
208
|
+
severity: "warning",
|
|
209
|
+
name: "弹窗内 el-table 缺少 empty-text",
|
|
210
|
+
check(template, file, lineOffset) {
|
|
211
|
+
const issues = [];
|
|
212
|
+
// 检测 el-dialog 内的 el-table 缺少 empty-text
|
|
213
|
+
const dialogPattern = /<el-dialog\b/g;
|
|
214
|
+
let dm;
|
|
215
|
+
while ((dm = dialogPattern.exec(template)) !== null) {
|
|
216
|
+
// 找到 dialog 结束标签
|
|
217
|
+
const dialogStart = dm.index;
|
|
218
|
+
const afterDialog = template.slice(dialogStart);
|
|
219
|
+
const dialogCloseMatch = afterDialog.match(/<\/el-dialog>/);
|
|
220
|
+
if (!dialogCloseMatch) continue;
|
|
221
|
+
const dialogEnd =
|
|
222
|
+
dialogStart + dialogCloseMatch.index + "</el-dialog>".length;
|
|
223
|
+
const dialogContent = template.slice(dialogStart, dialogEnd);
|
|
224
|
+
|
|
225
|
+
// 在 dialog 内容中找 el-table
|
|
226
|
+
for (const tag of findTags(dialogContent, "el-table")) {
|
|
227
|
+
if (!tag.text.includes("empty-text=")) {
|
|
228
|
+
issues.push(
|
|
229
|
+
issue(
|
|
230
|
+
file,
|
|
231
|
+
lineOf(template, dialogStart + tag.index, lineOffset),
|
|
232
|
+
"R012",
|
|
233
|
+
"tag",
|
|
234
|
+
"warning",
|
|
235
|
+
"弹窗内 el-table 缺少 empty-text 属性",
|
|
236
|
+
'添加 empty-text="暂无数据"',
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return issues;
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
];
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scanner/snapshot.mjs — 皮肤层快照与回退机制
|
|
3
|
+
*
|
|
4
|
+
* 核心思路:
|
|
5
|
+
* fix 前自动对即将修改的文件做内容快照,写入一个 JSON manifest 文件。
|
|
6
|
+
* 回退时读取 manifest,一键还原所有文件内容。
|
|
7
|
+
* 不在项目里留备份文件,不影响性能,不污染 git。
|
|
8
|
+
*
|
|
9
|
+
* 快照格式(.wk-snapshot/<timestamp>.json):
|
|
10
|
+
* {
|
|
11
|
+
* "version": 1,
|
|
12
|
+
* "createdAt": "2025-05-02T17:30:00.000Z",
|
|
13
|
+
* "command": "fix",
|
|
14
|
+
* "targetDir": "/abs/path/src",
|
|
15
|
+
* "totalFiles": 12,
|
|
16
|
+
* "totalChanges": 38,
|
|
17
|
+
* "files": {
|
|
18
|
+
* "relative/path.vue": "<original content base64>"
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* CLI:
|
|
23
|
+
* wk-scan snapshot list [--project <path>] 列出所有快照
|
|
24
|
+
* wk-scan snapshot rollback [--id <timestamp>] 回退指定快照(默认最新)
|
|
25
|
+
* wk-scan snapshot clean [--keep <N>] 清理旧快照,保留最近 N 个
|
|
26
|
+
* wk-scan snapshot diff [--id <timestamp>] 查看快照与当前文件差异
|
|
27
|
+
*
|
|
28
|
+
* 集成:fix.mjs 调用 createSnapshot() 即可,无需改业务逻辑。
|
|
29
|
+
*/
|
|
30
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
31
|
+
import { join, relative, resolve } from "node:path";
|
|
32
|
+
|
|
33
|
+
const SNAPSHOT_DIR = ".wk-snapshot";
|
|
34
|
+
const SNAPSHOT_VERSION = 1;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 创建快照 — 在 fix 之前调用
|
|
38
|
+
* @param {object} opts
|
|
39
|
+
* @param {string} opts.projectRoot 项目根目录
|
|
40
|
+
* @param {string} opts.targetDir 扫描目标目录(绝对路径)
|
|
41
|
+
* @param {string[]} opts.filePaths 即将被修改的文件绝对路径列表
|
|
42
|
+
* @param {string} opts.command 触发命令(fix / apply 等)
|
|
43
|
+
* @returns {{ id: string, path: string, fileCount: number }}
|
|
44
|
+
*/
|
|
45
|
+
export function createSnapshot({ projectRoot, targetDir, filePaths, command = "fix" }) {
|
|
46
|
+
const dir = join(projectRoot, SNAPSHOT_DIR);
|
|
47
|
+
mkdirSync(dir, { recursive: true });
|
|
48
|
+
|
|
49
|
+
const id = new Date().toISOString().replace(/[:.]/g, "-");
|
|
50
|
+
const files = {};
|
|
51
|
+
|
|
52
|
+
for (const absPath of filePaths) {
|
|
53
|
+
if (!existsSync(absPath)) continue;
|
|
54
|
+
const rel = relative(projectRoot, absPath).replace(/\\/g, "/");
|
|
55
|
+
const content = readFileSync(absPath, "utf8");
|
|
56
|
+
// base64 编码存储,避免 JSON 转义问题
|
|
57
|
+
files[rel] = Buffer.from(content, "utf8").toString("base64");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const manifest = {
|
|
61
|
+
version: SNAPSHOT_VERSION,
|
|
62
|
+
createdAt: new Date().toISOString(),
|
|
63
|
+
command,
|
|
64
|
+
targetDir: relative(projectRoot, targetDir).replace(/\\/g, "/"),
|
|
65
|
+
totalFiles: Object.keys(files).length,
|
|
66
|
+
files,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const outPath = join(dir, `${id}.json`);
|
|
70
|
+
writeFileSync(outPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
71
|
+
|
|
72
|
+
return { id, path: outPath, fileCount: manifest.totalFiles };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 列出所有快照
|
|
77
|
+
* @param {string} projectRoot
|
|
78
|
+
* @returns {Array<{id, createdAt, command, targetDir, totalFiles, path}>}
|
|
79
|
+
*/
|
|
80
|
+
export function listSnapshots(projectRoot) {
|
|
81
|
+
const dir = join(projectRoot, SNAPSHOT_DIR);
|
|
82
|
+
if (!existsSync(dir)) return [];
|
|
83
|
+
|
|
84
|
+
return readdirSync(dir)
|
|
85
|
+
.filter(f => f.endsWith(".json"))
|
|
86
|
+
.sort()
|
|
87
|
+
.reverse()
|
|
88
|
+
.map(f => {
|
|
89
|
+
try {
|
|
90
|
+
const full = join(dir, f);
|
|
91
|
+
const data = JSON.parse(readFileSync(full, "utf8"));
|
|
92
|
+
return {
|
|
93
|
+
id: f.replace(".json", ""),
|
|
94
|
+
createdAt: data.createdAt,
|
|
95
|
+
command: data.command,
|
|
96
|
+
targetDir: data.targetDir,
|
|
97
|
+
totalFiles: data.totalFiles,
|
|
98
|
+
path: full,
|
|
99
|
+
};
|
|
100
|
+
} catch { return null; }
|
|
101
|
+
})
|
|
102
|
+
.filter(Boolean);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 回退快照 — 还原所有文件到快照时的内容
|
|
107
|
+
* @param {string} projectRoot
|
|
108
|
+
* @param {string} [snapshotId] 快照 ID(不传则回退最新)
|
|
109
|
+
* @param {boolean} [dryRun=false]
|
|
110
|
+
* @returns {{ restoredFiles: string[], skippedFiles: string[] }}
|
|
111
|
+
*/
|
|
112
|
+
export function rollbackSnapshot(projectRoot, snapshotId, dryRun = false) {
|
|
113
|
+
const snapshots = listSnapshots(projectRoot);
|
|
114
|
+
if (snapshots.length === 0) {
|
|
115
|
+
throw new Error("没有找到任何快照。请先执行 wk-scan fix 生成快照。");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let target;
|
|
119
|
+
if (snapshotId) {
|
|
120
|
+
target = snapshots.find(s => s.id === snapshotId);
|
|
121
|
+
if (!target) throw new Error(`快照 "${snapshotId}" 不存在。`);
|
|
122
|
+
} else {
|
|
123
|
+
target = snapshots[0]; // 最新
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const manifest = JSON.parse(readFileSync(target.path, "utf8"));
|
|
127
|
+
const restoredFiles = [];
|
|
128
|
+
const skippedFiles = [];
|
|
129
|
+
|
|
130
|
+
for (const [relPath, b64Content] of Object.entries(manifest.files)) {
|
|
131
|
+
const absPath = join(projectRoot, relPath);
|
|
132
|
+
const original = Buffer.from(b64Content, "base64").toString("utf8");
|
|
133
|
+
|
|
134
|
+
if (!existsSync(absPath)) {
|
|
135
|
+
skippedFiles.push(relPath);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!dryRun) {
|
|
140
|
+
writeFileSync(absPath, original, "utf8");
|
|
141
|
+
}
|
|
142
|
+
restoredFiles.push(relPath);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { snapshotId: target.id, restoredFiles, skippedFiles };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 查看快照与当前文件的差异摘要
|
|
150
|
+
* @param {string} projectRoot
|
|
151
|
+
* @param {string} [snapshotId]
|
|
152
|
+
* @returns {Array<{file, status}>} status: 'changed' | 'unchanged' | 'missing'
|
|
153
|
+
*/
|
|
154
|
+
export function diffSnapshot(projectRoot, snapshotId) {
|
|
155
|
+
const snapshots = listSnapshots(projectRoot);
|
|
156
|
+
let target;
|
|
157
|
+
if (snapshotId) {
|
|
158
|
+
target = snapshots.find(s => s.id === snapshotId);
|
|
159
|
+
} else {
|
|
160
|
+
target = snapshots[0];
|
|
161
|
+
}
|
|
162
|
+
if (!target) throw new Error("快照不存在。");
|
|
163
|
+
|
|
164
|
+
const manifest = JSON.parse(readFileSync(target.path, "utf8"));
|
|
165
|
+
const results = [];
|
|
166
|
+
|
|
167
|
+
for (const [relPath, b64Content] of Object.entries(manifest.files)) {
|
|
168
|
+
const absPath = join(projectRoot, relPath);
|
|
169
|
+
if (!existsSync(absPath)) {
|
|
170
|
+
results.push({ file: relPath, status: "missing" });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const current = readFileSync(absPath, "utf8");
|
|
174
|
+
const original = Buffer.from(b64Content, "base64").toString("utf8");
|
|
175
|
+
results.push({
|
|
176
|
+
file: relPath,
|
|
177
|
+
status: current === original ? "unchanged" : "changed",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return results;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 清理旧快照,保留最近 N 个
|
|
186
|
+
* @param {string} projectRoot
|
|
187
|
+
* @param {number} keep
|
|
188
|
+
* @returns {number} 删除数量
|
|
189
|
+
*/
|
|
190
|
+
export function cleanSnapshots(projectRoot, keep = 5) {
|
|
191
|
+
const snapshots = listSnapshots(projectRoot);
|
|
192
|
+
let removed = 0;
|
|
193
|
+
for (let i = keep; i < snapshots.length; i++) {
|
|
194
|
+
unlinkSync(snapshots[i].path);
|
|
195
|
+
removed++;
|
|
196
|
+
}
|
|
197
|
+
return removed;
|
|
198
|
+
}
|