@agile-team/wl-skills-ui 1.8.1 → 1.8.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.
- package/CHANGELOG.md +21 -0
- package/README.md +9 -8
- package/docs/governance-long-term.md +25 -12
- package/package.json +3 -1
- package/scanner/__tests__/fixtures/r001-fail.vue +7 -0
- package/scanner/__tests__/fixtures/r001-pass.vue +7 -0
- package/scanner/__tests__/fixtures/r002-fail.vue +5 -0
- package/scanner/__tests__/fixtures/r002-pass.vue +5 -0
- package/scanner/__tests__/fixtures/r006-fail.vue +10 -0
- package/scanner/__tests__/fixtures/r006-pass.vue +10 -0
- package/scanner/__tests__/fixtures/r011-fail.vue +11 -0
- package/scanner/__tests__/fixtures/r011-pass.vue +14 -0
- package/scanner/__tests__/fixtures/r016-fail.vue +9 -0
- package/scanner/__tests__/fixtures/r016-pass.vue +9 -0
- package/scanner/__tests__/fixtures/r021-fail.vue +3 -0
- package/scanner/__tests__/fixtures/r021-pass.vue +3 -0
- package/scanner/__tests__/scan.test.mjs +145 -0
- package/scanner/index.mjs +129 -1
- package/scripts/check-scss.mjs +99 -0
- package/styles/vendors/_base-components.scss +17 -4
- package/styles/vendors/_base-query-toolbar.scss +10 -1
- package/styles/vendors/_jh-ui.scss +3 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ All notable changes to **@agile-team/wl-skills-ui** will be documented in this f
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [1.8.3] - 2026-05-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **scan --only / --skip**:支持规则级过滤,`--only R001,R016` 仅跑指定规则,`--skip R031-R037` 排除范围(支持连字符范围展开)。
|
|
12
|
+
- **exempt init 脚手架**:`wl-scan exempt init --target src` 智能扫描 src 下 big-screen/dashboard/chart 等个性化目录,自动生成 `.wl-exempt.json` 模板。
|
|
13
|
+
|
|
14
|
+
## [1.8.2] - 2026-05-13
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **搜索区字号不一致**:`_base-query-toolbar.scss` 统一 label / input / select / placeholder 为 **12px**,消除 13px→12px 混杂导致的视觉跳动。
|
|
19
|
+
- **必填星号重复 `* *`**:`_base-components.scss` 必填星号改为精确模式——默认只显示 `::before`,`asterisk-right` 仅显示 `::after`,避免 `display: inline-block !important` 同时作用于两个伪元素。`_jh-ui.scss` 同步精简为只声明颜色不控制 `display`。
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **scanner fixture 测试集**:`scanner/__tests__/` 覆盖 R001/R002/R006/R011/R016/R021 + drift 模块共 16 条自动化测试,`npm test` 一键跑。
|
|
24
|
+
- **SCSS 链路检查**:`scripts/check-scss.mjs` 递归验证 `styles/index.scss` 和 `presets/skin.scss` 的 `@forward/@use` 链路完整性。
|
|
25
|
+
- **scan --baseline 一步到位**:`wl-scan scan --target src --baseline .wl-baseline.json` 扫描后自动对比基线输出漂移报告,`--fail-on-error` 同时拦截新增违规。
|
|
26
|
+
- `package.json` 新增 `test` 和 `check:scss` 脚本。
|
|
27
|
+
|
|
7
28
|
## [1.8.1] - 2026-05-13
|
|
8
29
|
|
|
9
30
|
### Added
|
package/README.md
CHANGED
|
@@ -213,14 +213,15 @@ yarn add @agile-team/wl-skills-ui
|
|
|
213
213
|
|
|
214
214
|
## 版本亮点
|
|
215
215
|
|
|
216
|
-
当前 v1.8.
|
|
217
|
-
|
|
218
|
-
-
|
|
219
|
-
-
|
|
220
|
-
-
|
|
221
|
-
-
|
|
222
|
-
- `
|
|
223
|
-
-
|
|
216
|
+
当前 v1.8.3:
|
|
217
|
+
|
|
218
|
+
- `scan --only R001,R016` / `--skip R031-R037` 规则级过滤(支持范围展开)
|
|
219
|
+
- `exempt init --target src` 智能扫描个性化目录,自动生成 `.wl-exempt.json`
|
|
220
|
+
- **修复**搜索区字号统一 12px;必填星号 `* *` 去重
|
|
221
|
+
- **scanner fixture 测试集**(16 条,`npm test`)+ **SCSS 链路检查**(`npm run check:scss`)
|
|
222
|
+
- `scan --baseline` 一步漂移对比 + **drift.mjs** / MCP `wl_ui_drift`
|
|
223
|
+
- **R-rule 单一事实源** `standards/rules.json`(29 条),五向一致性守卫
|
|
224
|
+
- **长效治理方案** `docs/governance-long-term.md`
|
|
224
225
|
|
|
225
226
|
历史亮点(v1.7.1):
|
|
226
227
|
|
|
@@ -78,24 +78,37 @@ npx wl-ui scan --target src --baseline .wl-baseline.json --fail-on=new-issues
|
|
|
78
78
|
**目标**:让"逐渐变烂"可见,每次升级都能量化。
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
#
|
|
82
|
-
npx wl-ui
|
|
83
|
-
|
|
81
|
+
# 方式一:scan 一步到位(v1.8.2+)
|
|
82
|
+
npx wl-ui scan --target src --baseline .wl-baseline.json --fail-on-error
|
|
83
|
+
|
|
84
|
+
# 方式二:独立 drift 子命令
|
|
85
|
+
npx wl-ui scan --target src --output json --outFile .wl-current.json
|
|
86
|
+
npx wl-ui drift --baseline .wl-baseline.json --current .wl-current.json
|
|
84
87
|
```
|
|
85
88
|
|
|
86
|
-
|
|
89
|
+
输出示例:
|
|
87
90
|
|
|
88
91
|
```
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
╭──────────────────────────────────────╮
|
|
93
|
+
│ wl-skills-ui 漂移报告 │
|
|
94
|
+
╰──────────────────────────────────────╯
|
|
95
|
+
|
|
96
|
+
基线违规: 120 → 当前违规: 87 (-33)
|
|
97
|
+
|
|
98
|
+
🔴 gained +3 新增违规
|
|
99
|
+
🟢 fixed -36 消化历史
|
|
100
|
+
|
|
101
|
+
新增 Top 规则:
|
|
102
|
+
R017 +2
|
|
103
|
+
R001 +1
|
|
104
|
+
|
|
105
|
+
消化 Top 规则:
|
|
106
|
+
R009 -15
|
|
107
|
+
R004 -12
|
|
108
|
+
R016 -9
|
|
96
109
|
```
|
|
97
110
|
|
|
98
|
-
>
|
|
111
|
+
> 实现入口:`scanner/drift.mjs` 提供 `drift()` / `driftFromFiles()` / `formatDriftText()` / `formatDriftJson()` 四个 API。MCP 工具 `wl_ui_drift` 可供 AI 直接调用。
|
|
99
112
|
|
|
100
113
|
---
|
|
101
114
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agile-team/wl-skills-ui",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.3",
|
|
4
4
|
"description": "企业级 UI 风格对齐框架 — Vue + Element Plus 项目通用化妆/原生双模式(tokens / element / vendors / layouts / runtime / scanner / fixer / skills)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./es/index.js",
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"build": "tsup && npm run sync:tokens",
|
|
42
42
|
"prepare": "npm run build",
|
|
43
43
|
"sync:tokens": "node -e \"import('fs').then(fs=>fs.copyFileSync('design/tokens/base.css','dist/tokens.css'))\"",
|
|
44
|
+
"test": "node --test scanner/__tests__/scan.test.mjs",
|
|
45
|
+
"check:scss": "node scripts/check-scss.mjs",
|
|
44
46
|
"docs:check": "node scripts/check-docs.mjs",
|
|
45
47
|
"scan": "node scanner/index.mjs scan",
|
|
46
48
|
"check": "node scanner/index.mjs check",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-table :data="list" empty-text="暂无数据">
|
|
3
|
+
<el-table-column type="selection" align="center" header-align="center" width="55" />
|
|
4
|
+
<el-table-column prop="name" label="名称" align="center" />
|
|
5
|
+
<el-table-column prop="status" label="状态" align="center" />
|
|
6
|
+
</el-table>
|
|
7
|
+
</template>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-dialog v-model="visible" title="列表">
|
|
3
|
+
<el-table :data="list">
|
|
4
|
+
<el-table-column prop="name" label="名称" align="center" />
|
|
5
|
+
</el-table>
|
|
6
|
+
<template #footer>
|
|
7
|
+
<el-pagination :total="total" />
|
|
8
|
+
<el-button @click="visible = false">取消</el-button>
|
|
9
|
+
</template>
|
|
10
|
+
</el-dialog>
|
|
11
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-dialog v-model="visible" title="列表">
|
|
3
|
+
<el-table :data="list">
|
|
4
|
+
<el-table-column prop="name" label="名称" align="center" />
|
|
5
|
+
</el-table>
|
|
6
|
+
<div class="popup-pagination">
|
|
7
|
+
<el-pagination :total="total" />
|
|
8
|
+
</div>
|
|
9
|
+
<template #footer>
|
|
10
|
+
<el-button @click="visible = false">取消</el-button>
|
|
11
|
+
<el-button type="primary" @click="save">确定</el-button>
|
|
12
|
+
</template>
|
|
13
|
+
</el-dialog>
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scanner/__tests__/scan.test.mjs — scanner 规则 fixture 测试
|
|
3
|
+
*
|
|
4
|
+
* 用法:node --test scanner/__tests__/scan.test.mjs
|
|
5
|
+
*
|
|
6
|
+
* 约定:fixtures/<ruleId>-pass.vue → 0 violations of that rule
|
|
7
|
+
* fixtures/<ruleId>-fail.vue → ≥1 violations of that rule
|
|
8
|
+
*
|
|
9
|
+
* 每条测试只断言对应 rule 的命中数,不受其他规则干扰。
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it } from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
14
|
+
import { join, dirname } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { getRules } from "../rules/index.mjs";
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const FIXTURES = join(__dirname, "fixtures");
|
|
20
|
+
const rules = getRules();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 简易单文件扫描:跑所有规则,返回 issues 数组
|
|
24
|
+
*/
|
|
25
|
+
function scanFixture(filename) {
|
|
26
|
+
const content = readFileSync(join(FIXTURES, filename), "utf8");
|
|
27
|
+
const issues = [];
|
|
28
|
+
|
|
29
|
+
// extract template
|
|
30
|
+
const tmplMatch = content.match(/(<template[^>]*>)([\s\S]*?)(<\/template>)/);
|
|
31
|
+
const template = tmplMatch ? tmplMatch[0] : content;
|
|
32
|
+
const tmplOffset = tmplMatch
|
|
33
|
+
? content.slice(0, content.indexOf(tmplMatch[0])).split("\n").length - 1
|
|
34
|
+
: 0;
|
|
35
|
+
|
|
36
|
+
// extract style
|
|
37
|
+
const styleMatch = content.match(/<style[^>]*>([\s\S]*?)<\/style>/);
|
|
38
|
+
const styleBlock = styleMatch ? styleMatch[1] : null;
|
|
39
|
+
const styleOffset = styleMatch
|
|
40
|
+
? content.slice(0, content.indexOf(styleMatch[0])).split("\n").length
|
|
41
|
+
: 0;
|
|
42
|
+
|
|
43
|
+
// extract script
|
|
44
|
+
const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
45
|
+
const scriptBlock = scriptMatch ? scriptMatch[1] : null;
|
|
46
|
+
const scriptOffset = scriptMatch
|
|
47
|
+
? content.slice(0, content.indexOf(scriptMatch[0])).split("\n").length
|
|
48
|
+
: 0;
|
|
49
|
+
|
|
50
|
+
for (const rule of rules) {
|
|
51
|
+
if (typeof rule.check === "function") {
|
|
52
|
+
issues.push(...rule.check(template, filename, tmplOffset));
|
|
53
|
+
}
|
|
54
|
+
if (styleBlock && typeof rule.checkStyle === "function") {
|
|
55
|
+
issues.push(...rule.checkStyle(styleBlock, filename, styleOffset));
|
|
56
|
+
}
|
|
57
|
+
if (scriptBlock && typeof rule.checkScript === "function") {
|
|
58
|
+
issues.push(...rule.checkScript(scriptBlock, filename, scriptOffset));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return issues;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── 自动发现 fixtures 并生成测试 ────────────────────────────────────────────
|
|
65
|
+
const fixtureFiles = readdirSync(FIXTURES).filter((f) => f.endsWith(".vue"));
|
|
66
|
+
const grouped = new Map(); // ruleId → { pass: [], fail: [] }
|
|
67
|
+
|
|
68
|
+
for (const f of fixtureFiles) {
|
|
69
|
+
const m = f.match(/^(r\d+)-(pass|fail)\.vue$/i);
|
|
70
|
+
if (!m) continue;
|
|
71
|
+
const ruleId = m[1].toUpperCase();
|
|
72
|
+
const kind = m[2]; // pass | fail
|
|
73
|
+
if (!grouped.has(ruleId)) grouped.set(ruleId, { pass: [], fail: [] });
|
|
74
|
+
grouped.get(ruleId)[kind].push(f);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const [ruleId, { pass, fail }] of grouped) {
|
|
78
|
+
describe(ruleId, () => {
|
|
79
|
+
for (const f of pass) {
|
|
80
|
+
it(`${f} → 0 violations`, () => {
|
|
81
|
+
const issues = scanFixture(f).filter((i) => i.rule === ruleId);
|
|
82
|
+
assert.equal(
|
|
83
|
+
issues.length,
|
|
84
|
+
0,
|
|
85
|
+
`Expected 0 ${ruleId} issues but got ${issues.length}: ${JSON.stringify(issues, null, 2)}`,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
for (const f of fail) {
|
|
90
|
+
it(`${f} → ≥1 violations`, () => {
|
|
91
|
+
const issues = scanFixture(f).filter((i) => i.rule === ruleId);
|
|
92
|
+
assert.ok(issues.length > 0, `Expected ≥1 ${ruleId} issues but got 0`);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── drift 模块测试 ──────────────────────────────────────────────────────────
|
|
99
|
+
const { drift } = await import("../drift.mjs");
|
|
100
|
+
|
|
101
|
+
describe("drift", () => {
|
|
102
|
+
it("identical → 0 gained, 0 fixed", () => {
|
|
103
|
+
const data = { issues: [{ file: "a.vue", rule: "R001" }] };
|
|
104
|
+
const r = drift(data, data);
|
|
105
|
+
assert.equal(r.gained.count, 0);
|
|
106
|
+
assert.equal(r.fixed.count, 0);
|
|
107
|
+
assert.equal(r.netDelta, 0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("new issues → gained > 0", () => {
|
|
111
|
+
const base = { issues: [] };
|
|
112
|
+
const cur = { issues: [{ file: "a.vue", rule: "R001" }] };
|
|
113
|
+
const r = drift(base, cur);
|
|
114
|
+
assert.equal(r.gained.count, 1);
|
|
115
|
+
assert.equal(r.fixed.count, 0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("removed issues → fixed > 0", () => {
|
|
119
|
+
const base = { issues: [{ file: "a.vue", rule: "R001" }] };
|
|
120
|
+
const cur = { issues: [] };
|
|
121
|
+
const r = drift(base, cur);
|
|
122
|
+
assert.equal(r.gained.count, 0);
|
|
123
|
+
assert.equal(r.fixed.count, 1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("mixed → both gained and fixed", () => {
|
|
127
|
+
const base = {
|
|
128
|
+
issues: [
|
|
129
|
+
{ file: "a.vue", rule: "R001" },
|
|
130
|
+
{ file: "a.vue", rule: "R001" },
|
|
131
|
+
{ file: "b.vue", rule: "R016" },
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
const cur = {
|
|
135
|
+
issues: [
|
|
136
|
+
{ file: "a.vue", rule: "R001" },
|
|
137
|
+
{ file: "c.vue", rule: "R017" },
|
|
138
|
+
{ file: "c.vue", rule: "R017" },
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
const r = drift(base, cur);
|
|
142
|
+
assert.equal(r.gained.count, 2); // c.vue R017 ×2
|
|
143
|
+
assert.equal(r.fixed.count, 2); // a.vue R001 ×1 + b.vue R016 ×1
|
|
144
|
+
});
|
|
145
|
+
});
|
package/scanner/index.mjs
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* 用法:
|
|
6
6
|
* wl-scan scan --target <path> # 风格扫描
|
|
7
|
+
* wl-scan scan --target <path> --only R001,R016
|
|
8
|
+
* wl-scan scan --target <path> --skip R031-R037
|
|
7
9
|
* wl-scan scan --target <path> --outFile report.md
|
|
8
10
|
* wl-scan scan --target <path> --output json
|
|
9
11
|
* wl-scan check --project <path> # 接入完整性检查
|
|
@@ -12,6 +14,7 @@
|
|
|
12
14
|
* wl-scan all --project <path> # 接入检查 + 风格扫描 + 报告
|
|
13
15
|
* wl-scan init # 打印接入指引
|
|
14
16
|
* wl-scan drift --baseline <f> --current <f> # 漂移检测
|
|
17
|
+
* wl-scan exempt init --target <path> # 生成 .wl-exempt.json 豁免模板
|
|
15
18
|
* wl-scan snapshot list # 列出快照
|
|
16
19
|
* wl-scan snapshot rollback [--id <id>] # 回退到快照(默认最新)
|
|
17
20
|
* wl-scan snapshot diff [--id <id>] # 查看快照与当前差异
|
|
@@ -45,6 +48,7 @@ const SUBCOMMANDS = new Set([
|
|
|
45
48
|
"all",
|
|
46
49
|
"init",
|
|
47
50
|
"drift",
|
|
51
|
+
"exempt",
|
|
48
52
|
"snapshot",
|
|
49
53
|
]);
|
|
50
54
|
let subcommand = "scan";
|
|
@@ -71,6 +75,8 @@ const { values } = parseArgs({
|
|
|
71
75
|
exempt: { type: "string", default: "" },
|
|
72
76
|
baseline: { type: "string", default: "" },
|
|
73
77
|
current: { type: "string", default: "" },
|
|
78
|
+
only: { type: "string", default: "" },
|
|
79
|
+
skip: { type: "string", default: "" },
|
|
74
80
|
},
|
|
75
81
|
strict: false,
|
|
76
82
|
});
|
|
@@ -199,6 +205,24 @@ function runScan(targetDir, excludeDirs, exemptConfig) {
|
|
|
199
205
|
};
|
|
200
206
|
}
|
|
201
207
|
|
|
208
|
+
// ── 公共:规则范围展开(R031-R037 → R031,R032,...,R037)────────────────────
|
|
209
|
+
function expandRuleRange(input) {
|
|
210
|
+
const set = new Set();
|
|
211
|
+
for (const part of input.split(",").map((s) => s.trim())) {
|
|
212
|
+
const rangeMatch = part.match(/^(R)(\d+)-(R)?(\d+)$/i);
|
|
213
|
+
if (rangeMatch) {
|
|
214
|
+
const start = parseInt(rangeMatch[2]);
|
|
215
|
+
const end = parseInt(rangeMatch[4]);
|
|
216
|
+
for (let i = Math.min(start, end); i <= Math.max(start, end); i++) {
|
|
217
|
+
set.add("R" + String(i).padStart(3, "0"));
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
set.add(part.toUpperCase());
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return set;
|
|
224
|
+
}
|
|
225
|
+
|
|
202
226
|
// ── 公共:按 layer / vendor / mode 过滤 ─────────────────────────────────────
|
|
203
227
|
function applyFilters(issues) {
|
|
204
228
|
let out = issues;
|
|
@@ -216,6 +240,16 @@ function applyFilters(issues) {
|
|
|
216
240
|
} else if (values.mode === "native") {
|
|
217
241
|
// 原生模式:全部 layer
|
|
218
242
|
}
|
|
243
|
+
// --only R001,R016 → 仅保留指定规则
|
|
244
|
+
if (values.only) {
|
|
245
|
+
const allow = expandRuleRange(values.only);
|
|
246
|
+
out = out.filter((i) => allow.has(i.rule));
|
|
247
|
+
}
|
|
248
|
+
// --skip R031-R037 → 排除指定规则
|
|
249
|
+
if (values.skip) {
|
|
250
|
+
const deny = expandRuleRange(values.skip);
|
|
251
|
+
out = out.filter((i) => !deny.has(i.rule));
|
|
252
|
+
}
|
|
219
253
|
return out;
|
|
220
254
|
}
|
|
221
255
|
|
|
@@ -267,6 +301,76 @@ if (subcommand === "check") {
|
|
|
267
301
|
process.exit(values["fail-on-error"] && hasError ? 1 : 0);
|
|
268
302
|
}
|
|
269
303
|
|
|
304
|
+
if (subcommand === "exempt") {
|
|
305
|
+
const sub = args[0] || "init";
|
|
306
|
+
if (sub === "init") {
|
|
307
|
+
const { existsSync } = await import("node:fs");
|
|
308
|
+
const projectRoot = resolve(values.project);
|
|
309
|
+
const targetDir = resolve(values.target);
|
|
310
|
+
const outPath = join(projectRoot, ".wl-exempt.json");
|
|
311
|
+
if (existsSync(outPath)) {
|
|
312
|
+
console.log(`⚠️ ${outPath} 已存在,跳过生成。如需重新生成请先删除。`);
|
|
313
|
+
process.exit(0);
|
|
314
|
+
}
|
|
315
|
+
// 智能扫描目标目录下常见的个性化子目录
|
|
316
|
+
const EXEMPT_KEYWORDS = [
|
|
317
|
+
"big-screen",
|
|
318
|
+
"dashboard",
|
|
319
|
+
"map-view",
|
|
320
|
+
"topology",
|
|
321
|
+
"flow-designer",
|
|
322
|
+
"report-designer",
|
|
323
|
+
"chart",
|
|
324
|
+
"3d",
|
|
325
|
+
"canvas",
|
|
326
|
+
"screen",
|
|
327
|
+
"monitor",
|
|
328
|
+
"cockpit",
|
|
329
|
+
"visual",
|
|
330
|
+
];
|
|
331
|
+
const smartPaths = [];
|
|
332
|
+
function walkForExempt(dir, depth = 0) {
|
|
333
|
+
if (depth > 4) return;
|
|
334
|
+
try {
|
|
335
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
336
|
+
if (!entry.isDirectory()) continue;
|
|
337
|
+
if (["node_modules", "dist", ".git"].includes(entry.name)) continue;
|
|
338
|
+
const rel = relative(targetDir, join(dir, entry.name)).replace(
|
|
339
|
+
/\\/g,
|
|
340
|
+
"/",
|
|
341
|
+
);
|
|
342
|
+
if (
|
|
343
|
+
EXEMPT_KEYWORDS.some((kw) => entry.name.toLowerCase().includes(kw))
|
|
344
|
+
) {
|
|
345
|
+
smartPaths.push(rel + "/**");
|
|
346
|
+
} else {
|
|
347
|
+
walkForExempt(join(dir, entry.name), depth + 1);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
/* ignore permission errors */
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (existsSync(targetDir)) walkForExempt(targetDir);
|
|
355
|
+
const { generateExemptTemplate } = await import("./exempt.mjs");
|
|
356
|
+
const template = JSON.parse(generateExemptTemplate());
|
|
357
|
+
if (smartPaths.length > 0) {
|
|
358
|
+
template.exemptPaths = [
|
|
359
|
+
...new Set([...smartPaths, ...template.exemptPaths]),
|
|
360
|
+
];
|
|
361
|
+
template.description += `(自动扫描 ${targetDir} 发现 ${smartPaths.length} 个候选目录)`;
|
|
362
|
+
}
|
|
363
|
+
writeFileSync(outPath, JSON.stringify(template, null, 2) + "\n", "utf8");
|
|
364
|
+
console.log(`✅ 已生成 ${outPath}`);
|
|
365
|
+
if (smartPaths.length > 0) {
|
|
366
|
+
console.log(` 自动发现 ${smartPaths.length} 个候选豁免目录:`);
|
|
367
|
+
for (const p of smartPaths) console.log(` ${p}`);
|
|
368
|
+
}
|
|
369
|
+
console.log(` 请人工审核后提交至版本库。`);
|
|
370
|
+
}
|
|
371
|
+
process.exit(0);
|
|
372
|
+
}
|
|
373
|
+
|
|
270
374
|
if (subcommand === "drift") {
|
|
271
375
|
const { driftFromFiles, formatDriftText, formatDriftJson } =
|
|
272
376
|
await import("./drift.mjs");
|
|
@@ -423,6 +527,30 @@ if (subcommand === "all") {
|
|
|
423
527
|
} else {
|
|
424
528
|
console.log(report);
|
|
425
529
|
}
|
|
530
|
+
|
|
531
|
+
// ── baseline 漂移检测(--baseline <file>)────────────────────────────────
|
|
532
|
+
let driftResult = null;
|
|
533
|
+
if (values.baseline) {
|
|
534
|
+
const { existsSync: fileExists } = await import("node:fs");
|
|
535
|
+
const baselinePath = resolve(values.baseline);
|
|
536
|
+
if (fileExists(baselinePath)) {
|
|
537
|
+
const { drift, formatDriftText, formatDriftJson } =
|
|
538
|
+
await import("./drift.mjs");
|
|
539
|
+
const baseline = JSON.parse(readFileSync(baselinePath, "utf8"));
|
|
540
|
+
const currentData = { issues: filtered };
|
|
541
|
+
driftResult = drift(baseline, currentData);
|
|
542
|
+
console.error("");
|
|
543
|
+
if (values.output === "json") {
|
|
544
|
+
console.error(formatDriftJson(driftResult));
|
|
545
|
+
} else {
|
|
546
|
+
console.error(formatDriftText(driftResult));
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
console.error(`[wl-scan] 基线文件不存在: ${baselinePath},跳过漂移检测`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
426
553
|
const hasError = filtered.some((i) => i.severity === "error");
|
|
427
|
-
|
|
554
|
+
const hasDriftNew = driftResult && driftResult.gained.count > 0;
|
|
555
|
+
process.exit(values["fail-on-error"] && (hasError || hasDriftNew) ? 1 : 0);
|
|
428
556
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/check-scss.mjs — SCSS 链路完整性检查
|
|
4
|
+
*
|
|
5
|
+
* 递归验证 styles/index.scss 的 @forward/@use 链,
|
|
6
|
+
* 确保所有被引入的 SCSS partial 文件真实存在。
|
|
7
|
+
* 跳过 sass: 内置模块和包自引用(@agile-team/... / wl-skills-ui/...)。
|
|
8
|
+
*
|
|
9
|
+
* 用法:npm run check:scss
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
12
|
+
import { join, dirname, resolve, basename } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const ROOT = resolve(__dirname, "..");
|
|
17
|
+
const STYLES = join(ROOT, "styles");
|
|
18
|
+
|
|
19
|
+
let errors = 0;
|
|
20
|
+
|
|
21
|
+
/** 跳过包自引用和 sass 内置 */
|
|
22
|
+
function shouldSkip(ref) {
|
|
23
|
+
if (ref.startsWith("sass:")) return true;
|
|
24
|
+
if (ref.startsWith("~")) return true;
|
|
25
|
+
if (ref.startsWith("@agile-team/")) return true;
|
|
26
|
+
if (ref.startsWith("wl-skills-ui/")) return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 解析 SCSS @forward/@use 引用到实际文件路径 */
|
|
31
|
+
function resolveScssRef(dir, ref) {
|
|
32
|
+
// 已有 .scss 后缀
|
|
33
|
+
if (ref.endsWith(".scss")) {
|
|
34
|
+
const direct = join(dir, ref);
|
|
35
|
+
if (existsSync(direct)) return direct;
|
|
36
|
+
// _partial
|
|
37
|
+
const partial = join(dirname(direct), "_" + basename(direct));
|
|
38
|
+
if (existsSync(partial)) return partial;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
// 无后缀:SCSS partial 解析规则
|
|
42
|
+
const name = basename(ref);
|
|
43
|
+
const refDir = dirname(ref);
|
|
44
|
+
const base = join(dir, refDir);
|
|
45
|
+
const candidates = [
|
|
46
|
+
join(base, name + ".scss"),
|
|
47
|
+
join(base, "_" + name + ".scss"),
|
|
48
|
+
join(base, name, "index.scss"),
|
|
49
|
+
join(base, name, "_index.scss"),
|
|
50
|
+
];
|
|
51
|
+
return candidates.find((c) => existsSync(c)) || null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function checkImportChain(filePath, visited = new Set()) {
|
|
55
|
+
const norm = resolve(filePath);
|
|
56
|
+
if (visited.has(norm)) return;
|
|
57
|
+
visited.add(norm);
|
|
58
|
+
if (!existsSync(norm)) {
|
|
59
|
+
console.error(`❌ 文件不存在: ${norm}`);
|
|
60
|
+
errors++;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const content = readFileSync(norm, "utf8");
|
|
64
|
+
const re = /@(?:forward|use|import)\s+['"]([^'"]+)['"]/g;
|
|
65
|
+
let m;
|
|
66
|
+
while ((m = re.exec(content)) !== null) {
|
|
67
|
+
const ref = m[1];
|
|
68
|
+
if (shouldSkip(ref)) continue;
|
|
69
|
+
const resolved = resolveScssRef(dirname(norm), ref);
|
|
70
|
+
if (!resolved) {
|
|
71
|
+
console.error(`❌ ${norm} → "${ref}" 无法解析`);
|
|
72
|
+
errors++;
|
|
73
|
+
} else {
|
|
74
|
+
checkImportChain(resolved, visited);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 入口
|
|
80
|
+
const entryPoints = [
|
|
81
|
+
join(STYLES, "index.scss"),
|
|
82
|
+
join(STYLES, "presets", "skin.scss"),
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
for (const entry of entryPoints) {
|
|
86
|
+
if (!existsSync(entry)) {
|
|
87
|
+
console.error(`❌ 入口文件不存在: ${entry}`);
|
|
88
|
+
errors++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
checkImportChain(entry);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (errors > 0) {
|
|
95
|
+
console.error(`\n❌ SCSS 链路检查失败: ${errors} 个错误`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
} else {
|
|
98
|
+
console.log("✅ SCSS 链路检查通过");
|
|
99
|
+
}
|
|
@@ -508,13 +508,26 @@
|
|
|
508
508
|
word-break: keep-all;
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
.el-form-item.is-required:not(.is-no-asterisk)>.el-form-item__label::before
|
|
514
|
-
.el-form-item.is-required:not(.is-no-asterisk)>.el-form-item__label::after {
|
|
511
|
+
// 必填星号:只保留一颗 — 默认 asterisk-left(::before),asterisk-right 用 ::after
|
|
512
|
+
// 旧写法同时 display:inline-block 两个伪元素导致 * * 重复
|
|
513
|
+
.el-form-item.is-required:not(.is-no-asterisk)>.el-form-item__label::before {
|
|
515
514
|
color: var(--el-color-danger, #f56c6c) !important;
|
|
516
515
|
display: inline-block !important;
|
|
517
516
|
}
|
|
517
|
+
// asterisk-right 模式:显示 ::after,隐藏 ::before
|
|
518
|
+
.el-form-item.is-required:not(.is-no-asterisk).asterisk-right>.el-form-item__label::before {
|
|
519
|
+
display: none !important;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.el-form-item.is-required:not(.is-no-asterisk).asterisk-right>.el-form-item__label::after {
|
|
523
|
+
color: var(--el-color-danger, #f56c6c) !important;
|
|
524
|
+
display: inline-block !important;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 默认模式:隐藏 ::after 防止重复星号
|
|
528
|
+
.el-form-item.is-required:not(.is-no-asterisk):not(.asterisk-right)>.el-form-item__label::after {
|
|
529
|
+
display: none !important;
|
|
530
|
+
}
|
|
518
531
|
|
|
519
532
|
.el-form-item.is-error {
|
|
520
533
|
.el-form-item__label,
|
|
@@ -33,12 +33,21 @@
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// ── 搜索区字号统一 12px(紧凑密度,与 label / button 一致)────────────────
|
|
37
|
+
.el-form-item__label {
|
|
38
|
+
font-size: 12px !important;
|
|
39
|
+
}
|
|
36
40
|
.el-input__inner {
|
|
37
|
-
font-size:
|
|
41
|
+
font-size: 12px;
|
|
38
42
|
color: var(--el-text-color-primary);
|
|
39
43
|
&::placeholder { color: var(--el-text-color-placeholder); font-size: 12px; }
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
.el-select__placeholder,
|
|
47
|
+
.el-select__tags-text,
|
|
48
|
+
.el-select .el-select__selected-item {
|
|
49
|
+
font-size: 12px;
|
|
50
|
+
}
|
|
42
51
|
// ── 选择框 ────────────────────────────────────────────────────────────────
|
|
43
52
|
.el-select .el-select__wrapper {
|
|
44
53
|
border-radius: 6px !important;
|
|
@@ -91,12 +91,9 @@
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
// 6) 必填星号兼容 jh-ui label 结构
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
// `::before` 会作为 flex 项参与排版,必须显式声明颜色/显示。
|
|
94
|
+
// 颜色统一 danger 色;显示/隐藏由 _base-components.scss 总控,
|
|
95
|
+
// 此处只负责颜色,不设 display,避免与总控冲突导致 * * 重复。
|
|
97
96
|
.el-form-item.is-required:not(.is-no-asterisk) > .el-form-item__label::before,
|
|
98
|
-
.el-form-item.is-required:not(.is-no-asterisk)
|
|
99
|
-
.el-form-item.is-required:not(.is-no-asterisk) > .el-form-item__label > .com-text::before,
|
|
100
|
-
.el-form-item.is-required:not(.is-no-asterisk) > .el-form-item__label > .com-text::after {
|
|
97
|
+
.el-form-item.is-required:not(.is-no-asterisk)>.el-form-item__label>.com-text::before {
|
|
101
98
|
color: var(--el-color-danger, #f56c6c) !important;
|
|
102
99
|
}
|