@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,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scanner/rules/_shared.mjs — 规则引擎公共工具
|
|
3
|
+
* 所有规则文件均从此处 import 工具函数和 TOKEN_MAP
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** 计算匹配位置的行号 */
|
|
7
|
+
export function lineOf(text, matchIndex, lineOffset = 0) {
|
|
8
|
+
return text.slice(0, matchIndex).split("\n").length + lineOffset;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** 构造 issue 对象(自动推断 layer/vendor,规则可通过 meta 覆盖) */
|
|
12
|
+
export function issue(
|
|
13
|
+
file,
|
|
14
|
+
line,
|
|
15
|
+
rule,
|
|
16
|
+
category,
|
|
17
|
+
severity,
|
|
18
|
+
description,
|
|
19
|
+
suggestion,
|
|
20
|
+
meta = {},
|
|
21
|
+
) {
|
|
22
|
+
const inferred = inferMeta(category);
|
|
23
|
+
return {
|
|
24
|
+
file,
|
|
25
|
+
line,
|
|
26
|
+
rule,
|
|
27
|
+
category,
|
|
28
|
+
severity,
|
|
29
|
+
description,
|
|
30
|
+
suggestion,
|
|
31
|
+
layer: meta.layer ?? inferred.layer,
|
|
32
|
+
vendor: meta.vendor ?? inferred.vendor,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 根据 category 推断 layer + vendor 默认值
|
|
38
|
+
* - color/token → L0 / —
|
|
39
|
+
* - table/form/dialog/button/tag → L1 / element
|
|
40
|
+
* - vendor-base / vendor-jh / vendor-c / vendor-custom / vendor-ag → L2 / 对应 vendor
|
|
41
|
+
* - layout-* → L3 / —
|
|
42
|
+
* - runtime-* → L4 / —
|
|
43
|
+
*/
|
|
44
|
+
export function inferMeta(category) {
|
|
45
|
+
if (!category) return { layer: "L1", vendor: "element" };
|
|
46
|
+
if (category === "color" || category === "token" || category === "style")
|
|
47
|
+
return { layer: "L0", vendor: null };
|
|
48
|
+
if (
|
|
49
|
+
[
|
|
50
|
+
"table",
|
|
51
|
+
"form",
|
|
52
|
+
"dialog",
|
|
53
|
+
"button",
|
|
54
|
+
"tag",
|
|
55
|
+
"pagination",
|
|
56
|
+
"input",
|
|
57
|
+
"card",
|
|
58
|
+
"tabs",
|
|
59
|
+
"descriptions",
|
|
60
|
+
"tree",
|
|
61
|
+
"drawer",
|
|
62
|
+
"upload",
|
|
63
|
+
"steps",
|
|
64
|
+
"overlay",
|
|
65
|
+
"navigation",
|
|
66
|
+
"feedback",
|
|
67
|
+
].includes(category)
|
|
68
|
+
)
|
|
69
|
+
return { layer: "L1", vendor: "element" };
|
|
70
|
+
if (category.startsWith("vendor-")) {
|
|
71
|
+
return { layer: "L2", vendor: category.replace("vendor-", "") };
|
|
72
|
+
}
|
|
73
|
+
if (category.startsWith("layout-")) {
|
|
74
|
+
return { layer: "L3", vendor: null };
|
|
75
|
+
}
|
|
76
|
+
if (category.startsWith("runtime-")) {
|
|
77
|
+
return { layer: "L4", vendor: null };
|
|
78
|
+
}
|
|
79
|
+
return { layer: "L1", vendor: "element" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 查找完整 HTML/Vue 标签(处理多行、引号嵌套) */
|
|
83
|
+
export function findTags(template, tagName) {
|
|
84
|
+
const results = [];
|
|
85
|
+
const pattern = new RegExp(`<${tagName}(?=[\\s/>])`, "g");
|
|
86
|
+
let m;
|
|
87
|
+
while ((m = pattern.exec(template)) !== null) {
|
|
88
|
+
const start = m.index;
|
|
89
|
+
let i = start + tagName.length + 1;
|
|
90
|
+
let inSingle = false,
|
|
91
|
+
inDouble = false;
|
|
92
|
+
while (i < template.length) {
|
|
93
|
+
const ch = template[i];
|
|
94
|
+
if (ch === '"' && !inSingle) inDouble = !inDouble;
|
|
95
|
+
else if (ch === "'" && !inDouble) inSingle = !inSingle;
|
|
96
|
+
else if (!inSingle && !inDouble) {
|
|
97
|
+
if (template.slice(i, i + 2) === "/>") {
|
|
98
|
+
i += 2;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
if (ch === ">") {
|
|
102
|
+
i += 1;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
results.push({ text: template.slice(start, i), index: start });
|
|
109
|
+
}
|
|
110
|
+
return results;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* CSS Token 映射表(hex → CSS 变量)
|
|
115
|
+
* 供 R016 / R017 / R018 共同使用
|
|
116
|
+
*/
|
|
117
|
+
export const TOKEN_MAP = {
|
|
118
|
+
"#409eff": "var(--el-color-primary)",
|
|
119
|
+
"#3a7afe": "var(--el-color-primary)",
|
|
120
|
+
"#4368ff": "var(--el-color-primary)",
|
|
121
|
+
"#002a8f": "var(--el-color-primary)",
|
|
122
|
+
"#1890ff": "var(--el-color-primary)",
|
|
123
|
+
"#fb2323": "var(--el-color-danger)",
|
|
124
|
+
"#f56c6c": "var(--el-color-danger)",
|
|
125
|
+
"#bb2d3f": "var(--el-color-danger)",
|
|
126
|
+
"#f5222d": "var(--el-color-danger)",
|
|
127
|
+
"#0cc859": "var(--el-color-success)",
|
|
128
|
+
"#67c23a": "var(--el-color-success)",
|
|
129
|
+
"#52c41a": "var(--el-color-success)",
|
|
130
|
+
"#ffaf27": "var(--el-color-warning)",
|
|
131
|
+
"#e6a23c": "var(--el-color-warning)",
|
|
132
|
+
"#faad14": "var(--el-color-warning)",
|
|
133
|
+
"#ecf5ff": "var(--el-color-primary-light-9)",
|
|
134
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** scanner/rules/button.mjs — 按钮规则:R004 R005 R015 */
|
|
2
|
+
import { lineOf, issue, findTags } from "./_shared.mjs";
|
|
3
|
+
|
|
4
|
+
export const buttonRules = [
|
|
5
|
+
// R004: 操作列文字按钮
|
|
6
|
+
{
|
|
7
|
+
id: "R004",
|
|
8
|
+
category: "button",
|
|
9
|
+
severity: "error",
|
|
10
|
+
name: "操作列使用旧格式文字按钮而非 renderOps 图标系统",
|
|
11
|
+
check(template, file, lineOffset) {
|
|
12
|
+
const issues = [];
|
|
13
|
+
const btnPattern =
|
|
14
|
+
/<el-button\b(?![^>]*jh-op-btn)[^>]*@click="handle(?:Edit|Delete|View|Remove|Cancel|Audit|Verify)[^"]*"[^>]*>[^<]*<\/el-button>/g;
|
|
15
|
+
let m;
|
|
16
|
+
while ((m = btnPattern.exec(template)) !== null)
|
|
17
|
+
issues.push(
|
|
18
|
+
issue(
|
|
19
|
+
file,
|
|
20
|
+
lineOf(template, m.index, lineOffset),
|
|
21
|
+
"R004",
|
|
22
|
+
"button",
|
|
23
|
+
"error",
|
|
24
|
+
"操作列使用文字 el-button,应改为 renderOps 图标按钮系统",
|
|
25
|
+
'defaultSlot: ({ row }) => renderOps([{ type: "edit", onClick: () => ... }])',
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
return issues;
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// R005: 工具栏按钮缺少 icon
|
|
33
|
+
{
|
|
34
|
+
id: "R005",
|
|
35
|
+
category: "button",
|
|
36
|
+
severity: "warning",
|
|
37
|
+
name: "工具栏按钮缺少 icon",
|
|
38
|
+
check(template, file, lineOffset) {
|
|
39
|
+
const issues = [];
|
|
40
|
+
for (const tag of findTags(template, "el-button")) {
|
|
41
|
+
if (!/type=["'](primary|success|warning|danger)["']/.test(tag.text))
|
|
42
|
+
continue;
|
|
43
|
+
if (/\blink\b/.test(tag.text)) continue;
|
|
44
|
+
if (/jh-op-btn/.test(tag.text)) continue;
|
|
45
|
+
if (!/:?icon=/.test(tag.text))
|
|
46
|
+
issues.push(
|
|
47
|
+
issue(
|
|
48
|
+
file,
|
|
49
|
+
lineOf(template, tag.index, lineOffset),
|
|
50
|
+
"R005",
|
|
51
|
+
"button",
|
|
52
|
+
"warning",
|
|
53
|
+
"工具栏 el-button 缺少 icon 属性",
|
|
54
|
+
'添加 icon="Plus / Edit / Search / Refresh" 等语义图标',
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return issues;
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// R015: 弹窗嵌套表格操作列用 el-button link
|
|
63
|
+
{
|
|
64
|
+
id: "R015",
|
|
65
|
+
category: "button",
|
|
66
|
+
severity: "error",
|
|
67
|
+
name: "弹窗嵌套表格操作列使用文字 el-button(应改为 jh-op-btn 图标按钮)",
|
|
68
|
+
check(template, file, lineOffset) {
|
|
69
|
+
const issues = [];
|
|
70
|
+
if (!file.endsWith("modal.vue")) return issues;
|
|
71
|
+
const pattern = /<el-button[\s\S]*?link[\s\S]*?<\/el-button>/g;
|
|
72
|
+
let m;
|
|
73
|
+
while ((m = pattern.exec(template)) !== null) {
|
|
74
|
+
const btnText = m[0];
|
|
75
|
+
if (/取消|确认|确定|关闭|保存|提交/.test(btnText)) continue;
|
|
76
|
+
if (
|
|
77
|
+
!/<el-table-column/.test(
|
|
78
|
+
template.slice(Math.max(0, m.index - 500), m.index),
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
continue;
|
|
82
|
+
issues.push(
|
|
83
|
+
issue(
|
|
84
|
+
file,
|
|
85
|
+
lineOf(template, m.index, lineOffset),
|
|
86
|
+
"R015",
|
|
87
|
+
"button",
|
|
88
|
+
"error",
|
|
89
|
+
"弹窗嵌套表格操作列使用 el-button link,风格不统一",
|
|
90
|
+
'改为 <button class="jh-op-btn jh-op-del/view/edit"> + <el-icon>图标</el-icon>',
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return issues;
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
];
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** scanner/rules/color.mjs — 颜色 Token 规则:R016 R017 R018 */
|
|
2
|
+
import { lineOf, issue, TOKEN_MAP } from "./_shared.mjs";
|
|
3
|
+
|
|
4
|
+
export const colorRules = [
|
|
5
|
+
// R016: <style> 块硬编码 hex
|
|
6
|
+
{
|
|
7
|
+
id: "R016",
|
|
8
|
+
category: "style",
|
|
9
|
+
severity: "warning",
|
|
10
|
+
name: "<style> 块存在硬编码 hex 颜色(应替换为 CSS Token 变量)",
|
|
11
|
+
check() {
|
|
12
|
+
return [];
|
|
13
|
+
},
|
|
14
|
+
checkStyle(styleBlock, file, lineOffset = 0) {
|
|
15
|
+
const issues = [];
|
|
16
|
+
styleBlock.split("\n").forEach((line, idx) => {
|
|
17
|
+
if (/^\s*(\/\/|\*|\/\*)/.test(line)) return;
|
|
18
|
+
const hexPattern = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g;
|
|
19
|
+
let m;
|
|
20
|
+
while ((m = hexPattern.exec(line)) !== null) {
|
|
21
|
+
const hex = m[0].toLowerCase();
|
|
22
|
+
const suggested = TOKEN_MAP[hex];
|
|
23
|
+
if (suggested)
|
|
24
|
+
issues.push(
|
|
25
|
+
issue(
|
|
26
|
+
file,
|
|
27
|
+
lineOffset + idx + 1,
|
|
28
|
+
"R016",
|
|
29
|
+
"style",
|
|
30
|
+
"warning",
|
|
31
|
+
`<style> 块硬编码颜色 "${hex}",应使用 CSS Token`,
|
|
32
|
+
`将 "${hex}" 替换为 ${suggested}`,
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return issues;
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// R017: <template> 内硬编码 hex
|
|
42
|
+
{
|
|
43
|
+
id: "R017",
|
|
44
|
+
category: "style",
|
|
45
|
+
severity: "warning",
|
|
46
|
+
name: "<template> 块存在硬编码 hex 颜色(应替换为 CSS Token 变量)",
|
|
47
|
+
check(template, file, lineOffset) {
|
|
48
|
+
const issues = [];
|
|
49
|
+
const patterns = [
|
|
50
|
+
/\bcolor\s*=\s*"(#[0-9a-fA-F]{3,8})"/g,
|
|
51
|
+
/\bcolor\s*=\s*'(#[0-9a-fA-F]{3,8})'/g,
|
|
52
|
+
/:style\s*=\s*"[^"]*?(#[0-9a-fA-F]{3,8})[^"]*?"/g,
|
|
53
|
+
/\bstyle\s*=\s*"[^"]*?(#[0-9a-fA-F]{3,8})[^"]*?"/g,
|
|
54
|
+
];
|
|
55
|
+
for (const p of patterns) {
|
|
56
|
+
let m;
|
|
57
|
+
while ((m = p.exec(template)) !== null) {
|
|
58
|
+
const hex = m[1].toLowerCase();
|
|
59
|
+
const suggested = TOKEN_MAP[hex];
|
|
60
|
+
if (suggested)
|
|
61
|
+
issues.push(
|
|
62
|
+
issue(
|
|
63
|
+
file,
|
|
64
|
+
lineOf(template, m.index, lineOffset),
|
|
65
|
+
"R017",
|
|
66
|
+
"style",
|
|
67
|
+
"warning",
|
|
68
|
+
`template 内硬编码颜色 "${hex}",应使用 CSS Token`,
|
|
69
|
+
`将 "${hex}" 替换为 ${suggested}`,
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return issues;
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// R018: <script> 块硬编码 hex(ECharts 等)
|
|
79
|
+
{
|
|
80
|
+
id: "R018",
|
|
81
|
+
category: "style",
|
|
82
|
+
severity: "warning",
|
|
83
|
+
name: "<script> 块存在硬编码 hex 颜色(ECharts/inline 配色应统一项目色系)",
|
|
84
|
+
check() {
|
|
85
|
+
return [];
|
|
86
|
+
},
|
|
87
|
+
checkScript(scriptBlock, file, lineOffset = 0) {
|
|
88
|
+
const SCRIPT_TOKEN_MAP = {
|
|
89
|
+
...TOKEN_MAP,
|
|
90
|
+
"#409eff": "var(--el-color-primary) / CHART_COLORS.primary",
|
|
91
|
+
"#4368ff": "var(--el-color-primary)",
|
|
92
|
+
};
|
|
93
|
+
const issues = [];
|
|
94
|
+
scriptBlock.split("\n").forEach((line, idx) => {
|
|
95
|
+
if (/^\s*(\/\/|\*|\/\*)/.test(line)) return;
|
|
96
|
+
const hexPattern = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g;
|
|
97
|
+
let m;
|
|
98
|
+
while ((m = hexPattern.exec(line)) !== null) {
|
|
99
|
+
const hex = m[0].toLowerCase();
|
|
100
|
+
const suggested = SCRIPT_TOKEN_MAP[hex];
|
|
101
|
+
if (suggested)
|
|
102
|
+
issues.push(
|
|
103
|
+
issue(
|
|
104
|
+
file,
|
|
105
|
+
lineOffset + idx + 1,
|
|
106
|
+
"R018",
|
|
107
|
+
"style",
|
|
108
|
+
"warning",
|
|
109
|
+
`<script> 块硬编码颜色 "${hex}",应改用项目色系常量`,
|
|
110
|
+
`${hex} → ${suggested}(建议统一使用 src/util/chart-colors.ts CHART_COLORS)`,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return issues;
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
];
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { lineOf, issue, findTags } from "./_shared.mjs";
|
|
2
|
+
|
|
3
|
+
function hasAttr(tagText, attr) {
|
|
4
|
+
return new RegExp(`\\s:?${attr}\\s*=`).test(tagText);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function hasClassLike(template, names) {
|
|
8
|
+
return names.some((name) => new RegExp(`class=["'][^"']*${name}[^"']*["']`).test(template));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const componentFamilyRules = [
|
|
12
|
+
{
|
|
13
|
+
id: "R031",
|
|
14
|
+
category: "card",
|
|
15
|
+
severity: "info",
|
|
16
|
+
name: "详情/统计卡片建议使用统一场景 class",
|
|
17
|
+
check(template, file, lineOffset) {
|
|
18
|
+
const issues = [];
|
|
19
|
+
if (hasClassLike(template, ["detail-card", "stat-card", "wk-card"])) return issues;
|
|
20
|
+
for (const tag of findTags(template, "el-card")) {
|
|
21
|
+
issues.push(
|
|
22
|
+
issue(
|
|
23
|
+
file,
|
|
24
|
+
lineOf(template, tag.index, lineOffset),
|
|
25
|
+
"R031",
|
|
26
|
+
"card",
|
|
27
|
+
"info",
|
|
28
|
+
"el-card 未标识业务场景 class,跨项目卡片密度和标题区可能不一致",
|
|
29
|
+
"建议按场景添加 detail-card/stat-card/wk-card,或确认已加载 wk-skills-ui Element 样式覆盖",
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return issues;
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "R032",
|
|
38
|
+
category: "tabs",
|
|
39
|
+
severity: "info",
|
|
40
|
+
name: "el-tabs 建议明确页面场景",
|
|
41
|
+
check(template, file, lineOffset) {
|
|
42
|
+
const issues = [];
|
|
43
|
+
if (hasClassLike(template, ["detail-tabs", "config-tabs", "wk-tabs"])) return issues;
|
|
44
|
+
for (const tag of findTags(template, "el-tabs")) {
|
|
45
|
+
issues.push(
|
|
46
|
+
issue(
|
|
47
|
+
file,
|
|
48
|
+
lineOf(template, tag.index, lineOffset),
|
|
49
|
+
"R032",
|
|
50
|
+
"tabs",
|
|
51
|
+
"info",
|
|
52
|
+
"el-tabs 未标识 detail/config 等业务场景,复杂详情页 Tab 风格可能漂移",
|
|
53
|
+
"建议添加 detail-tabs/config-tabs/wk-tabs,或通过 wk-skills-ui tabs 覆盖层统一视觉",
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return issues;
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "R033",
|
|
62
|
+
category: "descriptions",
|
|
63
|
+
severity: "info",
|
|
64
|
+
name: "el-descriptions 建议使用 bordered 或统一容器",
|
|
65
|
+
check(template, file, lineOffset) {
|
|
66
|
+
const issues = [];
|
|
67
|
+
for (const tag of findTags(template, "el-descriptions")) {
|
|
68
|
+
if (hasAttr(tag.text, "border") || hasClassLike(template, ["detail-page", "detail-card"])) continue;
|
|
69
|
+
issues.push(
|
|
70
|
+
issue(
|
|
71
|
+
file,
|
|
72
|
+
lineOf(template, tag.index, lineOffset),
|
|
73
|
+
"R033",
|
|
74
|
+
"descriptions",
|
|
75
|
+
"info",
|
|
76
|
+
"el-descriptions 未使用 border/bordered,也未处于统一详情容器,字段展示易出现视觉不齐",
|
|
77
|
+
"建议添加 border 或放入 detail-page/detail-card,并加载 wk-skills-ui descriptions 覆盖层",
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return issues;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "R034",
|
|
86
|
+
category: "drawer",
|
|
87
|
+
severity: "warning",
|
|
88
|
+
name: "el-drawer 建议明确 size",
|
|
89
|
+
check(template, file, lineOffset) {
|
|
90
|
+
const issues = [];
|
|
91
|
+
for (const tag of findTags(template, "el-drawer")) {
|
|
92
|
+
if (hasAttr(tag.text, "size")) continue;
|
|
93
|
+
issues.push(
|
|
94
|
+
issue(
|
|
95
|
+
file,
|
|
96
|
+
lineOf(template, tag.index, lineOffset),
|
|
97
|
+
"R034",
|
|
98
|
+
"drawer",
|
|
99
|
+
"warning",
|
|
100
|
+
"el-drawer 未明确 size,不同页面抽屉宽度可能不统一",
|
|
101
|
+
"建议按场景设置 size,例如详情 480px、编辑 560px,或通过业务 preset 统一",
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return issues;
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "R035",
|
|
110
|
+
category: "upload",
|
|
111
|
+
severity: "warning",
|
|
112
|
+
name: "el-upload 建议配置 tip/限制说明",
|
|
113
|
+
check(template, file, lineOffset) {
|
|
114
|
+
const issues = [];
|
|
115
|
+
const hasTip = /el-upload__tip|#tip|<template\s+#tip\b/.test(template);
|
|
116
|
+
for (const tag of findTags(template, "el-upload")) {
|
|
117
|
+
if (hasTip || hasAttr(tag.text, "limit") || hasAttr(tag.text, "accept")) continue;
|
|
118
|
+
issues.push(
|
|
119
|
+
issue(
|
|
120
|
+
file,
|
|
121
|
+
lineOf(template, tag.index, lineOffset),
|
|
122
|
+
"R035",
|
|
123
|
+
"upload",
|
|
124
|
+
"warning",
|
|
125
|
+
"el-upload 未提供上传限制或提示说明,附件场景交互不完整",
|
|
126
|
+
"建议配置 accept/limit,并补充 el-upload__tip 或 #tip 说明",
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return issues;
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "R036",
|
|
135
|
+
category: "steps",
|
|
136
|
+
severity: "info",
|
|
137
|
+
name: "el-steps 审批/流程建议明确状态来源",
|
|
138
|
+
check(template, file, lineOffset) {
|
|
139
|
+
const issues = [];
|
|
140
|
+
for (const tag of findTags(template, "el-steps")) {
|
|
141
|
+
if (hasAttr(tag.text, "active") || hasAttr(tag.text, "process-status") || hasAttr(tag.text, "finish-status")) continue;
|
|
142
|
+
issues.push(
|
|
143
|
+
issue(
|
|
144
|
+
file,
|
|
145
|
+
lineOf(template, tag.index, lineOffset),
|
|
146
|
+
"R036",
|
|
147
|
+
"steps",
|
|
148
|
+
"info",
|
|
149
|
+
"el-steps 未配置 active/process-status/finish-status,流程状态表达可能不稳定",
|
|
150
|
+
"建议绑定 active 并明确 process-status/finish-status",
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return issues;
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "R037",
|
|
159
|
+
category: "feedback",
|
|
160
|
+
severity: "info",
|
|
161
|
+
name: "空/异常反馈建议统一操作入口",
|
|
162
|
+
check(template, file, lineOffset) {
|
|
163
|
+
const issues = [];
|
|
164
|
+
const feedbackTags = ["el-empty", "el-result", "el-alert"];
|
|
165
|
+
for (const tagName of feedbackTags) {
|
|
166
|
+
for (const tag of findTags(template, tagName)) {
|
|
167
|
+
if (/#default|#extra|<el-button\b/.test(template)) continue;
|
|
168
|
+
issues.push(
|
|
169
|
+
issue(
|
|
170
|
+
file,
|
|
171
|
+
lineOf(template, tag.index, lineOffset),
|
|
172
|
+
"R037",
|
|
173
|
+
"feedback",
|
|
174
|
+
"info",
|
|
175
|
+
`${tagName} 未提供统一操作入口,空/异常状态可恢复性不足`,
|
|
176
|
+
"建议根据业务补充操作按钮或 extra/default slot,并加载 wk-skills-ui feedback 覆盖层",
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return issues;
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** scanner/rules/dialog.mjs — 弹窗分页规则:R011 */
|
|
2
|
+
import { lineOf, issue } from "./_shared.mjs";
|
|
3
|
+
|
|
4
|
+
export const dialogRules = [
|
|
5
|
+
// R011: pagination 放在 #footer 外面
|
|
6
|
+
{
|
|
7
|
+
id: "R011",
|
|
8
|
+
category: "dialog",
|
|
9
|
+
severity: "error",
|
|
10
|
+
name: "弹窗内分页器应放在 #footer slot 中",
|
|
11
|
+
check(template, file, lineOffset) {
|
|
12
|
+
const issues = [];
|
|
13
|
+
// 找到 #footer slot 的范围
|
|
14
|
+
const footerMatch = /<template\s+#footer\b/;
|
|
15
|
+
const footerIdx = template.search(footerMatch);
|
|
16
|
+
// 找 el-pagination
|
|
17
|
+
const paginationPattern = /<el-pagination\b/g;
|
|
18
|
+
let m;
|
|
19
|
+
while ((m = paginationPattern.exec(template)) !== null) {
|
|
20
|
+
// 是否在 el-dialog 里?
|
|
21
|
+
const before = template.slice(0, m.index);
|
|
22
|
+
const dialogOpens = (before.match(/<el-dialog\b/g) || []).length;
|
|
23
|
+
const dialogCloses = (before.match(/<\/el-dialog>/g) || []).length;
|
|
24
|
+
if (dialogOpens <= dialogCloses) continue; // 不在 dialog 内
|
|
25
|
+
// 是否在 footer 之外?
|
|
26
|
+
if (footerIdx === -1 || m.index < footerIdx)
|
|
27
|
+
issues.push(
|
|
28
|
+
issue(
|
|
29
|
+
file,
|
|
30
|
+
lineOf(template, m.index, lineOffset),
|
|
31
|
+
"R011",
|
|
32
|
+
"dialog",
|
|
33
|
+
"error",
|
|
34
|
+
"弹窗内 el-pagination 未放在 <template #footer> 中",
|
|
35
|
+
'将 el-pagination 移至 <template #footer><div class="dialog-footer">...</div></template>',
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return issues;
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
];
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** scanner/rules/form.mjs — 表单控件规则:R006 R007 R008 */
|
|
2
|
+
import { lineOf, issue, findTags } from "./_shared.mjs";
|
|
3
|
+
|
|
4
|
+
export const formRules = [
|
|
5
|
+
// R006: el-input / el-select 缺少 size="small"
|
|
6
|
+
{
|
|
7
|
+
id: "R006",
|
|
8
|
+
category: "form",
|
|
9
|
+
severity: "warning",
|
|
10
|
+
name: 'el-input / el-select 缺少 size="small"',
|
|
11
|
+
check(template, file, lineOffset) {
|
|
12
|
+
const issues = [];
|
|
13
|
+
for (const tagName of ["el-input", "el-select"]) {
|
|
14
|
+
for (const tag of findTags(template, tagName)) {
|
|
15
|
+
if (!/:?size\s*=/.test(tag.text))
|
|
16
|
+
issues.push(
|
|
17
|
+
issue(
|
|
18
|
+
file,
|
|
19
|
+
lineOf(template, tag.index, lineOffset),
|
|
20
|
+
"R006",
|
|
21
|
+
"form",
|
|
22
|
+
"warning",
|
|
23
|
+
`<${tagName}> 缺少 size="small"`,
|
|
24
|
+
'添加 size="small"',
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return issues;
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// R007: el-date-picker 缺少 width:100%
|
|
34
|
+
{
|
|
35
|
+
id: "R007",
|
|
36
|
+
category: "form",
|
|
37
|
+
severity: "warning",
|
|
38
|
+
name: 'el-date-picker 缺少 style="width:100%"',
|
|
39
|
+
check(template, file, lineOffset) {
|
|
40
|
+
const issues = [];
|
|
41
|
+
for (const tag of findTags(template, "el-date-picker")) {
|
|
42
|
+
if (!tag.text.includes("style=") && !tag.text.includes(":style="))
|
|
43
|
+
issues.push(
|
|
44
|
+
issue(
|
|
45
|
+
file,
|
|
46
|
+
lineOf(template, tag.index, lineOffset),
|
|
47
|
+
"R007",
|
|
48
|
+
"form",
|
|
49
|
+
"warning",
|
|
50
|
+
"el-date-picker 缺少宽度样式",
|
|
51
|
+
'添加 style="width:100%"',
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return issues;
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// R008: el-form labelWidth < 150px
|
|
60
|
+
{
|
|
61
|
+
id: "R008",
|
|
62
|
+
category: "form",
|
|
63
|
+
severity: "info",
|
|
64
|
+
name: "el-form labelWidth 偏小(< 150px)",
|
|
65
|
+
check(template, file, lineOffset) {
|
|
66
|
+
const issues = [];
|
|
67
|
+
const pattern = /labelWidth="(\d+)px"/g;
|
|
68
|
+
let m;
|
|
69
|
+
while ((m = pattern.exec(template)) !== null) {
|
|
70
|
+
if (parseInt(m[1]) < 150)
|
|
71
|
+
issues.push(
|
|
72
|
+
issue(
|
|
73
|
+
file,
|
|
74
|
+
lineOf(template, m.index, lineOffset),
|
|
75
|
+
"R008",
|
|
76
|
+
"form",
|
|
77
|
+
"info",
|
|
78
|
+
`labelWidth="${m[1]}px" 偏小,长标签(≥8字)可能换行`,
|
|
79
|
+
'建议改为 labelWidth="150px"(需人工确认)',
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return issues;
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
];
|