@agile-team/wl-skills-ui 1.9.11 → 1.9.12
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 +16 -0
- package/README.md +37 -3
- package/docs/compat-matrix.md +2 -2
- package/docs/composite-component-contracts.md +43 -0
- package/docs/visual-regression.md +49 -0
- package/package.json +9 -3
- package/scanner/rules/componentStructure.mjs +63 -0
- package/scanner/rules/index.mjs +2 -0
- package/skills/vendors/jh-components/SKILL.md +2 -0
- package/standards/component-structures.json +220 -0
- package/standards/rules.json +20 -0
- package/standards/ui/03-form.md +4 -0
- package/styles/vendors/_jh-ui.scss +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ 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.9.12] - 2026-08-06
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 新增 `standards/component-structures.json` 复合控件结构契约,首批登记 common-core 多标签、人员/部门/树选择、多选、混合数字输入与 BaseToolbar 分裂按钮的边框所有者、高度策略、状态、内部无描边层和 Teleport 出口。
|
|
12
|
+
- 新增 R040 未知复合结构审查规则与真实 DOM fixtures;已登记结构正常通过,疑似新结构要求人工核对后再增加精准适配,不执行机械修复。
|
|
13
|
+
- 新增 Chromium 浏览器视觉回归及发布门禁,覆盖主题防反覆盖、按钮/圆角、textarea focus、数字输入单描边、复合输入自然增高、长文本提示、表格行状态和定制区域豁免。
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `prepublishOnly` 升级为完整 `release:check`,npm 发布前必须同时通过代码、文档、构建、包内容及真实浏览器视觉回归;测试页和截图不进入 npm 包。
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- 真实浏览器回归发现并修复复合多标签内部 editor 仍继承 Element Plus 默认 inset 的问题;仅在已登记的 `.com-input-multi-tag-wrap` 结构中清除子 wrapper 描边,普通输入与其他 picker 不受影响。
|
|
22
|
+
|
|
7
23
|
## [1.9.11] - 2026-08-06
|
|
8
24
|
|
|
9
25
|
### Fixed
|
package/README.md
CHANGED
|
@@ -115,6 +115,7 @@ wl-skills-ui/
|
|
|
115
115
|
│ │ ├── table.mjs # R001 R002 R003 R014 R039
|
|
116
116
|
│ │ ├── form.mjs # R006 R007 R008
|
|
117
117
|
│ │ ├── button.mjs # R004 R005 R015 R038
|
|
118
|
+
│ │ ├── componentStructure.mjs # R040 复合结构登记审查
|
|
118
119
|
│ │ ├── tag.mjs # R009 R010 R012
|
|
119
120
|
│ │ ├── dialog.mjs # R011
|
|
120
121
|
│ │ ├── color.mjs # R016 R017 R018
|
|
@@ -166,9 +167,12 @@ wl-skills-ui/
|
|
|
166
167
|
│ └── ag-grid-page/TPL-AG-GRID.md
|
|
167
168
|
│
|
|
168
169
|
├── standards/ # 团队规范文档
|
|
170
|
+
│ ├── component-structures.json # 复合控件结构契约单一事实源
|
|
169
171
|
│ ├── ui/ # UI 规范(01-table / 02-button / ...)
|
|
170
172
|
│ └── engineering/ # 工程规范(import 顺序 / 命名 / SCSS 结构)
|
|
171
173
|
│
|
|
174
|
+
├── tests/visual/ # Chromium 真实浏览器视觉回归与基准图
|
|
175
|
+
│
|
|
172
176
|
├── bin/ # CLI
|
|
173
177
|
│ └── wl-ui.js # 统一入口(init/update/diff/clean/doctor/prompts/scan/fix/add-preset)
|
|
174
178
|
│
|
|
@@ -208,7 +212,7 @@ yarn add @agile-team/wl-skills-ui
|
|
|
208
212
|
|---|---|---|
|
|
209
213
|
| `element-plus` | **`2.2.6-prod.3`** | 集团 jh- 定制版;EP 2.3.0 起引入 `.el-input__wrapper`,与 jh-ui 3.x 不兼容 |
|
|
210
214
|
| `@jhlc/jh-ui` | **`3.1.0`** | SCSS 皮肤包,`.com-text` label 包裹 + `.has-colon` 冒号注入(wl-skills-ui 已统一屏蔽表单冒号) |
|
|
211
|
-
| `@agile-team/wl-skills-ui` | `^1.9.
|
|
215
|
+
| `@agile-team/wl-skills-ui` | `^1.9.12` | 已对齐上述组合的 DOM 假设、客户主题锁、圆角契约、复合输入、表格长文本兜底与定制页样式边界 |
|
|
212
216
|
|
|
213
217
|
三种识别方式,任选其一:
|
|
214
218
|
|
|
@@ -222,7 +226,13 @@ yarn add @agile-team/wl-skills-ui
|
|
|
222
226
|
|
|
223
227
|
## 版本亮点
|
|
224
228
|
|
|
225
|
-
当前 v1.9.
|
|
229
|
+
当前 v1.9.12:
|
|
230
|
+
|
|
231
|
+
- **复合结构有清单**:`standards/component-structures.json` 登记边框所有者、内部无描边层、高度策略、状态与 Teleport 出口;首批覆盖 common-core 多标签、人员/部门/树选择、多选、混合数字框及 BaseToolbar 分裂按钮。
|
|
232
|
+
- **未知结构先评审**:新增 R040,对疑似复合 Element wrapper 只报告、不自动修复,要求先核对真实 DOM 并登记契约,禁止继续放大普通 Element 选择器。
|
|
233
|
+
- **发版前真实浏览器门禁**:新增 Chromium 视觉回归,验证客户主题防反覆盖、按钮与圆角、textarea focus、数字框单描边、复合输入自然增高、长文本省略/提示、表格行状态和定制区域豁免;截图基准不进入 npm 产物。
|
|
234
|
+
|
|
235
|
+
上一版 v1.9.11:
|
|
226
236
|
|
|
227
237
|
- **复合输入不再误伤**:common-core 多标签/人员选择器不再被普通 input 的固定高度压缩;无标签时保持 26px 紧凑基线,标签换行时容器自然增高。
|
|
228
238
|
- **视觉契约保持统一**:复合输入外层继续使用统一 6px 圆角及默认、hover、品牌 focus、error、disabled 边框,内部编辑 input 不重复描边;普通 input/select/date/input-number 原有规则不变。
|
|
@@ -717,7 +727,7 @@ export const myRules = [
|
|
|
717
727
|
|
|
718
728
|
## 规范清单
|
|
719
729
|
|
|
720
|
-
### UI 规则(R001-
|
|
730
|
+
### UI 规则(R001-R040,按 layer 自动分组)
|
|
721
731
|
|
|
722
732
|
| Rule | Layer | Vendor | 说明 |
|
|
723
733
|
| ---- | ----- | --------- | ------------------------------------ |
|
|
@@ -750,6 +760,30 @@ export const myRules = [
|
|
|
750
760
|
| R037 | L1 | element | 空/异常反馈建议统一操作入口 |
|
|
751
761
|
| R038 | L1 | element | 创建类主按钮缺 primary 填充主题色 |
|
|
752
762
|
| R039 | L1 | element | 普通数据列缺省略与悬停完整提示 |
|
|
763
|
+
| R040 | L2 | common-core | 未登记复合控件结构需人工评审 |
|
|
764
|
+
|
|
765
|
+
### 维护者防回归
|
|
766
|
+
|
|
767
|
+
复合控件必须先登记结构契约,再增加精准样式;包发版必须经过 Chromium 基准图对比:
|
|
768
|
+
|
|
769
|
+
```bash
|
|
770
|
+
# Windows 默认使用已安装的 Edge Chromium;非 Windows 首次安装一次
|
|
771
|
+
pnpm exec playwright install chromium
|
|
772
|
+
|
|
773
|
+
# 日常验证
|
|
774
|
+
pnpm test
|
|
775
|
+
pnpm test:visual
|
|
776
|
+
|
|
777
|
+
# 仅在设计变更已经人工确认时更新基准图
|
|
778
|
+
pnpm test:visual:update
|
|
779
|
+
|
|
780
|
+
# 完整发版门禁(含 lint、契约、SCSS、文档、构建、包校验、视觉回归)
|
|
781
|
+
pnpm release:check
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
机制与准入细则见 [复合控件结构契约](docs/composite-component-contracts.md) 和
|
|
785
|
+
[浏览器视觉回归](docs/visual-regression.md)。视觉基准依赖字体、浏览器与操作系统渲染,
|
|
786
|
+
必须在相同环境中比较;当前项目基准由 Windows Edge Chromium 生成。
|
|
753
787
|
|
|
754
788
|
### 工程规范
|
|
755
789
|
|
package/docs/compat-matrix.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
| `element-plus` | **`2.2.6-prod.3`** | 集团 jh- 定制版,搭配 `@jhlc/jh-ui` 使用,DOM 仍为 `.el-input > .el-input__inner` 直挂结构(**EP 2.3.0 起才引入 `.el-input__wrapper`**) |
|
|
12
12
|
| `@jhlc/jh-ui` | **`3.1.0`** | SCSS 皮肤包,`.com-text` label 包裹、`.has-colon ::after` 冒号注入、`.el-form-item--default { margin-bottom: 24px }` 等强约束 |
|
|
13
13
|
| `@jhlc/common-core` | `3.1.0` 或 `3.1.0-prod.x` | 基础 util/types,`@jhlc/jh-ui` 间接依赖 |
|
|
14
|
-
| `@agile-team/wl-skills-ui` | `^1.9.
|
|
14
|
+
| `@agile-team/wl-skills-ui` | `^1.9.12` | 已对齐 jh-ui 3.1.0 + EP 2.2.6-prod.3 的 DOM 假设、客户主题锁、6px 基础圆角、紧凑表单、复合结构契约、表格行状态、分裂按钮、包级长文本提示、定制页样式边界与 Vite 4 开发转换 |
|
|
15
15
|
|
|
16
16
|
## 项目集群当前实测
|
|
17
17
|
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
## 适配建议
|
|
37
37
|
|
|
38
|
-
- 集团内项目:**统一锚定 `element-plus@2.2.6-prod.3` + `@jhlc/jh-ui@3.1.0`**,与 wl-skills-ui v1.9.
|
|
38
|
+
- 集团内项目:**统一锚定 `element-plus@2.2.6-prod.3` + `@jhlc/jh-ui@3.1.0`**,与 wl-skills-ui v1.9.12 三方对齐。
|
|
39
39
|
- Skin 项目除全局样式外,在 `main.ts` 引入一次 `@agile-team/wl-skills-ui/runtime/auto`;Native 项目的 `installCommonPreset()` 已包含相同包级保护,不要重复安装。
|
|
40
40
|
- 升级 `element-plus` 到 2.3+ 前,必须同步升级 `@jhlc/jh-ui` 到对应支持版本,否则 `.com-text` / `.el-input__inner` DOM 与 EP `__wrapper` 体系不兼容。
|
|
41
41
|
- 新项目接入:见 README 「快速接入」章节,默认会按本表声明 peerDependency 范围。
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# 复合控件结构契约
|
|
2
|
+
|
|
3
|
+
## 目标
|
|
4
|
+
|
|
5
|
+
复合控件经常同时包含 Element Plus 外壳、内部输入编辑器、标签容器以及 Teleport
|
|
6
|
+
弹层。普通 `.el-input` / `.el-input__wrapper` 选择器无法判断谁负责边框和高度,直接扩大
|
|
7
|
+
覆盖会导致固定高度裁切、边框消失或双描边。
|
|
8
|
+
|
|
9
|
+
`standards/component-structures.json` 是这类结构的单一事实源。它不修改 common-core,
|
|
10
|
+
只记录 wl-skills-ui 已核对和允许治理的 DOM 契约。
|
|
11
|
+
|
|
12
|
+
## 每项必须声明
|
|
13
|
+
|
|
14
|
+
- `rootSelector`:组件结构边界。
|
|
15
|
+
- `ownerSelector`:唯一负责默认、hover、focus、error、disabled 边框的节点。
|
|
16
|
+
- `innerBorderlessSelectors`:必须保持无描边的内部编辑器。
|
|
17
|
+
- `heightPolicy`:固定单行高度、最小高度自然增长或内容驱动。
|
|
18
|
+
- `states`:必须回归的交互状态。
|
|
19
|
+
- `portalSelectors`:下拉、Tooltip、Dialog 等 Teleport 出口;没有则可省略。
|
|
20
|
+
- `fixture`:从真实 DOM 最小化得到的结构样本。
|
|
21
|
+
- `knownRootClasses`:供 R040 识别已登记结构。
|
|
22
|
+
|
|
23
|
+
## 新复合控件准入流程
|
|
24
|
+
|
|
25
|
+
1. 在浏览器中复制真实 DOM,确认 Element Plus / jh-ui / common-core 版本。
|
|
26
|
+
2. 提取最小 fixture,保留会影响选择器匹配的 class、父子层级和 Teleport 出口。
|
|
27
|
+
3. 在 `standards/component-structures.json` 登记边框所有者、高度策略与状态。
|
|
28
|
+
4. 运行 `pnpm test`;结构测试会校验 selector 唯一命中和 fixture 完整性。
|
|
29
|
+
5. 只为已登记结构增加精准 SCSS,并在 `tests/visual` 补充状态断言或截图。
|
|
30
|
+
6. 运行 `pnpm release:check` 后方可发布。
|
|
31
|
+
|
|
32
|
+
R040 只做审查提示,不自动改业务代码。已登记根类不会重复报告;发现疑似未知复合
|
|
33
|
+
wrapper 时,应先按上述流程确认,不能直接扩大全局 Element 选择器。
|
|
34
|
+
|
|
35
|
+
## 当前首批结构
|
|
36
|
+
|
|
37
|
+
- common-core 多标签输入与人员选择多选。
|
|
38
|
+
- 部门选择、树选择和 Element 多选封装。
|
|
39
|
+
- 同节点 wrapper + 子 wrapper 的混合数字输入。
|
|
40
|
+
- BaseToolbar 分裂按钮及其动作菜单 Teleport 出口。
|
|
41
|
+
|
|
42
|
+
组件结构变化时,测试会先在 fixture/selector 契约处失败,从而把风险阻断在发版前,
|
|
43
|
+
不会等十余个业务项目升级后再发现。
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# 浏览器视觉回归
|
|
2
|
+
|
|
3
|
+
## 覆盖目标
|
|
4
|
+
|
|
5
|
+
`tests/visual` 使用真实 Vue、Element Plus、Skin SCSS 和 `runtime/auto`,在 Chromium 中
|
|
6
|
+
验证最终计算样式与关键区域截图。它补充现有 Node DOM/SCSS 契约测试,专门发现
|
|
7
|
+
“选择器存在但浏览器层叠后没生效”以及“修一处却破坏另一处”的问题。
|
|
8
|
+
|
|
9
|
+
当前覆盖:
|
|
10
|
+
|
|
11
|
+
- 客户主色被平台 JS 改写后由主题锁恢复。
|
|
12
|
+
- 主按钮、禁用态、功能色和统一圆角。
|
|
13
|
+
- textarea focus 品牌边框与轻焦点环。
|
|
14
|
+
- 数字输入只有一个边框所有者。
|
|
15
|
+
- common-core 复合多标签外壳自然增高、内层无双描边。
|
|
16
|
+
- Element Table 长文本真实溢出、省略号与按需完整提示。
|
|
17
|
+
- 表格选中行柔和状态色、BaseToolbar 分裂按钮与定制区域豁免。
|
|
18
|
+
|
|
19
|
+
## 使用
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Windows 默认复用已安装的 Edge Chromium;非 Windows 首次安装一次
|
|
23
|
+
pnpm exec playwright install chromium
|
|
24
|
+
|
|
25
|
+
# 对比现有基准
|
|
26
|
+
pnpm test:visual
|
|
27
|
+
|
|
28
|
+
# 调试
|
|
29
|
+
pnpm test:visual:ui
|
|
30
|
+
|
|
31
|
+
# 设计变更经评审确认后才更新基准
|
|
32
|
+
pnpm test:visual:update
|
|
33
|
+
|
|
34
|
+
# 发版完整门禁
|
|
35
|
+
pnpm release:check
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
截图基准按 Playwright 的浏览器/平台规则保存,不随 npm 包发布。Windows 默认使用企业
|
|
39
|
+
环境已有的 Edge Chromium,也可通过 `WL_UI_BROWSER_CHANNEL` 显式指定 Playwright 通道。
|
|
40
|
+
视觉像素受操作系统、浏览器版本和字体渲染影响,生成与比较必须使用相同环境;当前
|
|
41
|
+
基准为 Windows Edge Chromium。
|
|
42
|
+
升级 Playwright、Chromium 或字体后,应单独提交基准变化并人工审图,不能把更新截图当成
|
|
43
|
+
消除失败的常规手段。
|
|
44
|
+
|
|
45
|
+
## 增加场景
|
|
46
|
+
|
|
47
|
+
优先向现有固定尺寸区域加入最小状态,不复制整套业务页面。每个缺陷至少提供一个
|
|
48
|
+
计算样式断言;只有颜色、边框、布局关系需要肉眼整体判断时再补截图。动态时间、随机数、
|
|
49
|
+
动画和远程资源不得进入基准页。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agile-team/wl-skills-ui",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.12",
|
|
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",
|
|
@@ -48,6 +48,9 @@
|
|
|
48
48
|
"prepare": "npm run build",
|
|
49
49
|
"sync:tokens": "node -e \"import('fs').then(fs=>fs.copyFileSync('design/tokens/base.css','dist/tokens.css'))\"",
|
|
50
50
|
"test": "node --test scanner/__tests__/*.test.mjs",
|
|
51
|
+
"test:visual": "playwright test --config tests/visual/playwright.config.ts",
|
|
52
|
+
"test:visual:ui": "playwright test --config tests/visual/playwright.config.ts --ui",
|
|
53
|
+
"test:visual:update": "playwright test --config tests/visual/playwright.config.ts --update-snapshots",
|
|
51
54
|
"check:scss": "node scripts/check-scss.mjs",
|
|
52
55
|
"docs:check": "node scripts/check-docs.mjs",
|
|
53
56
|
"scan": "node scanner/index.mjs scan",
|
|
@@ -60,8 +63,8 @@
|
|
|
60
63
|
"verify:package": "node scripts/verify-package.mjs",
|
|
61
64
|
"verify": "npm run lint && npm test && npm run check:scss && npm run docs:check && npm run build && npm run verify:package",
|
|
62
65
|
"pack:dry": "npm pack --dry-run --ignore-scripts",
|
|
63
|
-
"release:check": "npm run verify && npm pack --dry-run --ignore-scripts",
|
|
64
|
-
"prepublishOnly": "npm run
|
|
66
|
+
"release:check": "npm run verify && npm run test:visual && npm pack --dry-run --ignore-scripts",
|
|
67
|
+
"prepublishOnly": "npm run release:check"
|
|
65
68
|
},
|
|
66
69
|
"files": [
|
|
67
70
|
"dist",
|
|
@@ -92,8 +95,10 @@
|
|
|
92
95
|
"@commitlint/cli": "^20.5.3",
|
|
93
96
|
"@commitlint/config-conventional": "^20.5.3",
|
|
94
97
|
"@element-plus/icons-vue": "^2.3.0",
|
|
98
|
+
"@playwright/test": "^1.62.1",
|
|
95
99
|
"@typescript-eslint/eslint-plugin": "^8.59.2",
|
|
96
100
|
"@typescript-eslint/parser": "^8.59.2",
|
|
101
|
+
"@vitejs/plugin-vue": "^6.0.8",
|
|
97
102
|
"@vue/eslint-config-typescript": "^14.7.0",
|
|
98
103
|
"commitizen": "^4.3.1",
|
|
99
104
|
"cz-customizable": "^7.5.4",
|
|
@@ -106,6 +111,7 @@
|
|
|
106
111
|
"sass": "1.77.8",
|
|
107
112
|
"tsup": "^8.3.0",
|
|
108
113
|
"typescript": "^5.4.0",
|
|
114
|
+
"vite": "^8.2.0",
|
|
109
115
|
"vue": "^3.4.0"
|
|
110
116
|
},
|
|
111
117
|
"peerDependencies": {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { issue, lineOf } from "./_shared.mjs";
|
|
3
|
+
|
|
4
|
+
const registry = JSON.parse(
|
|
5
|
+
readFileSync(
|
|
6
|
+
new URL("../../standards/component-structures.json", import.meta.url),
|
|
7
|
+
"utf8",
|
|
8
|
+
),
|
|
9
|
+
);
|
|
10
|
+
const knownRootClasses = new Set(
|
|
11
|
+
registry.contracts.flatMap((contract) => contract.knownRootClasses || []),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const CLASS_ATTRIBUTE = /class\s*=\s*["']([^"']+)["']/g;
|
|
15
|
+
const COMPOSITE_NAME = /(?:multi|multiple|picker|tags?|input-number|split)/i;
|
|
16
|
+
|
|
17
|
+
export function findUnknownCompositeRoots(template) {
|
|
18
|
+
const findings = [];
|
|
19
|
+
let match;
|
|
20
|
+
while ((match = CLASS_ATTRIBUTE.exec(template)) !== null) {
|
|
21
|
+
const classValue = match[1];
|
|
22
|
+
const classes = classValue.split(/\s+/).filter(Boolean);
|
|
23
|
+
if (classes.some((name) => knownRootClasses.has(name))) continue;
|
|
24
|
+
|
|
25
|
+
const isHybridElementWrapper =
|
|
26
|
+
classes.includes("el-input") && classes.includes("el-input__wrapper");
|
|
27
|
+
const looksComposite = COMPOSITE_NAME.test(classValue);
|
|
28
|
+
const nearby = template.slice(match.index, match.index + 1600);
|
|
29
|
+
const containsInputStructure =
|
|
30
|
+
/<el-(?:input|select|autocomplete|cascader)\b|el-(?:input|select)__wrapper/.test(
|
|
31
|
+
nearby,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (isHybridElementWrapper || (looksComposite && containsInputStructure)) {
|
|
35
|
+
findings.push({ index: match.index, classValue });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
CLASS_ATTRIBUTE.lastIndex = 0;
|
|
39
|
+
return findings;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const componentStructureRules = [
|
|
43
|
+
{
|
|
44
|
+
id: "R040",
|
|
45
|
+
category: "composite",
|
|
46
|
+
severity: "review",
|
|
47
|
+
name: "未知复合控件结构必须先登记边框与高度契约",
|
|
48
|
+
check(template, file, lineOffset) {
|
|
49
|
+
return findUnknownCompositeRoots(template).map((finding) =>
|
|
50
|
+
issue(
|
|
51
|
+
file,
|
|
52
|
+
lineOf(template, finding.index, lineOffset),
|
|
53
|
+
"R040",
|
|
54
|
+
"composite",
|
|
55
|
+
"review",
|
|
56
|
+
`发现未登记的疑似复合控件结构:class="${finding.classValue}"`,
|
|
57
|
+
"先核对真实 DOM,明确 root/边框所有者/内部无描边层/高度策略/Teleport 出口,再登记到 standards/component-structures.json;禁止直接扩大普通 Element 选择器。",
|
|
58
|
+
{ layer: "L2", vendor: "common-core" },
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
];
|
package/scanner/rules/index.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { colorRules } from "./color.mjs";
|
|
|
17
17
|
import { dialogRules } from "./dialog.mjs";
|
|
18
18
|
import { tagRules } from "./tag.mjs";
|
|
19
19
|
import { componentFamilyRules } from "./componentFamily.mjs";
|
|
20
|
+
import { componentStructureRules } from "./componentStructure.mjs";
|
|
20
21
|
import { semanticRules } from "./semantic.mjs";
|
|
21
22
|
|
|
22
23
|
export const BUILT_IN_RULES = [
|
|
@@ -27,6 +28,7 @@ export const BUILT_IN_RULES = [
|
|
|
27
28
|
...dialogRules,
|
|
28
29
|
...tagRules,
|
|
29
30
|
...componentFamilyRules,
|
|
31
|
+
...componentStructureRules,
|
|
30
32
|
...semanticRules,
|
|
31
33
|
];
|
|
32
34
|
|
|
@@ -34,6 +34,7 @@ applyTo: "**/*.vue"
|
|
|
34
34
|
- EP 2.2.x `.el-input__inner` 26px 高度、圆角 / focus / error 三态
|
|
35
35
|
- jh-picker / reference-picker 的 `.com-picker` / `.com-reference-picker` 输入高度兜底
|
|
36
36
|
- common-core 多标签/人员选择器的 `.com-input-multi-tag-wrap` 使用 26px 最小高度并允许标签换行自然增高;只由外层绘制统一状态边框,内部编辑 input 不重复描边
|
|
37
|
+
- common-core 复合结构统一登记在 `standards/component-structures.json`;R040 遇到未登记的疑似 picker/multiple/tag 混合 wrapper 时只提示人工核对,不允许直接放大通用 Element 选择器
|
|
37
38
|
- jh-select / jh-date-picker 的 `.el-select.is-focus` / `.el-input.is-focus` 兼容
|
|
38
39
|
- 必填星号在 inline-flex label 下显式声明颜色
|
|
39
40
|
|
|
@@ -52,6 +53,7 @@ applyTo: "**/*.vue"
|
|
|
52
53
|
- ❌ jh-pagination 未对齐到右侧(同 R011)
|
|
53
54
|
- ❌ jh-form 内不用 `size="small"` 控件(同 R006)
|
|
54
55
|
- ❌ 发现新的复杂 `<jh-*>` 组件后只在页面局部写补丁,而不沉淀到 L2 Project Vendors
|
|
56
|
+
- ❌ 新复合结构未登记边框所有者、高度策略、内部无描边层与 Teleport 出口就直接写全局覆盖(R040)
|
|
55
57
|
|
|
56
58
|
## Repair
|
|
57
59
|
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"description": "复合/组合控件结构契约单一事实源。登记边框所有者、高度策略、内部无描边层和 Teleport 出口,防止普通 Element 选择器误识别。",
|
|
4
|
+
"heightPolicies": [
|
|
5
|
+
"fixed-control-height",
|
|
6
|
+
"auto-with-control-min-height",
|
|
7
|
+
"content-driven"
|
|
8
|
+
],
|
|
9
|
+
"contracts": [
|
|
10
|
+
{
|
|
11
|
+
"id": "common-core.multi-tag-input",
|
|
12
|
+
"vendor": "common-core",
|
|
13
|
+
"family": "form-composite",
|
|
14
|
+
"source": "lowcode-engine/packages/common/common-lowcode/components/src/schema-component/base-input-group/input/com/multi-tag-input.vue",
|
|
15
|
+
"fixture": "scanner/__tests__/component-fixtures/common-core-multi-tag-input.html",
|
|
16
|
+
"rootSelector": ".com-input-multi-tag-wrap.el-input.el-input__wrapper",
|
|
17
|
+
"ownerSelector": ".com-input-multi-tag-wrap.el-input__wrapper",
|
|
18
|
+
"innerBorderlessSelectors": [
|
|
19
|
+
".com-input-multi-tag-wrap .el-input__wrapper"
|
|
20
|
+
],
|
|
21
|
+
"knownRootClasses": [
|
|
22
|
+
"com-input-multi-tag-wrap"
|
|
23
|
+
],
|
|
24
|
+
"heightPolicy": "auto-with-control-min-height",
|
|
25
|
+
"states": [
|
|
26
|
+
"default",
|
|
27
|
+
"hover",
|
|
28
|
+
"focus",
|
|
29
|
+
"error",
|
|
30
|
+
"disabled"
|
|
31
|
+
],
|
|
32
|
+
"assertions": [
|
|
33
|
+
{
|
|
34
|
+
"target": "root",
|
|
35
|
+
"selector": ".el-input__wrapper:not(.com-input-multi-tag-wrap)",
|
|
36
|
+
"matches": false
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"target": "inner",
|
|
40
|
+
"selector": ".com-input-multi-tag-wrap .el-input__wrapper",
|
|
41
|
+
"matches": true
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"risk": "外壳同时携带 el-input 与 el-input__wrapper,内部又包含真实 input wrapper;误套固定高度会裁切标签,内外同时描边会出现双边框。"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "common-core.user-picker-multiple",
|
|
48
|
+
"vendor": "common-core",
|
|
49
|
+
"family": "picker-composite",
|
|
50
|
+
"source": "lowcode-engine/packages/common/common-lowcode/components/src/schema-component/base-input-group/user-picker/com/multiple-picker.vue",
|
|
51
|
+
"fixture": "scanner/__tests__/component-fixtures/common-core-user-picker-multiple.html",
|
|
52
|
+
"rootSelector": ".com-userPicker.picker-wrap",
|
|
53
|
+
"ownerSelector": ".com-userPicker .com-input-multi-tag-wrap.el-input__wrapper",
|
|
54
|
+
"innerBorderlessSelectors": [
|
|
55
|
+
".com-userPicker .com-input-multi-tag-wrap .el-input__wrapper"
|
|
56
|
+
],
|
|
57
|
+
"portalSelectors": [
|
|
58
|
+
".user-picker-popper",
|
|
59
|
+
".com-userPicker-dialog"
|
|
60
|
+
],
|
|
61
|
+
"knownRootClasses": [
|
|
62
|
+
"com-userPicker"
|
|
63
|
+
],
|
|
64
|
+
"heightPolicy": "auto-with-control-min-height",
|
|
65
|
+
"states": [
|
|
66
|
+
"default",
|
|
67
|
+
"hover",
|
|
68
|
+
"focus",
|
|
69
|
+
"error",
|
|
70
|
+
"disabled",
|
|
71
|
+
"open"
|
|
72
|
+
],
|
|
73
|
+
"risk": "多标签外壳、Tooltip 和 append-to-body Dialog 同时存在,页面内样式与 Teleport 浮层必须分别验证。"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"id": "common-core.dept-picker-multiple",
|
|
77
|
+
"vendor": "common-core",
|
|
78
|
+
"family": "picker-composite",
|
|
79
|
+
"source": "lowcode-engine/packages/common/common-lowcode/components/src/schema-component/base-input-group/dept-picker/com/multiple-picker.vue",
|
|
80
|
+
"fixture": "scanner/__tests__/component-fixtures/common-core-dept-picker-multiple.html",
|
|
81
|
+
"rootSelector": ".com-deptPicker.picker-wrap",
|
|
82
|
+
"ownerSelector": ".com-deptPicker .el-input__wrapper",
|
|
83
|
+
"innerBorderlessSelectors": [],
|
|
84
|
+
"portalSelectors": [
|
|
85
|
+
".com-deptPicker-dialog"
|
|
86
|
+
],
|
|
87
|
+
"knownRootClasses": [
|
|
88
|
+
"com-deptPicker"
|
|
89
|
+
],
|
|
90
|
+
"heightPolicy": "fixed-control-height",
|
|
91
|
+
"states": [
|
|
92
|
+
"default",
|
|
93
|
+
"hover",
|
|
94
|
+
"focus",
|
|
95
|
+
"error",
|
|
96
|
+
"disabled",
|
|
97
|
+
"open"
|
|
98
|
+
],
|
|
99
|
+
"risk": "输入触发器与 append-to-body Dialog 分离,作用域或弹层规则调整时容易只覆盖一侧。"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"id": "common-core.tree-picker",
|
|
103
|
+
"vendor": "common-core",
|
|
104
|
+
"family": "picker-composite",
|
|
105
|
+
"source": "lowcode-engine/packages/common/common-lowcode/components/src/schema-component/base-input-group/tree-picker/com-pc/tree-picker.vue",
|
|
106
|
+
"fixture": "scanner/__tests__/component-fixtures/common-core-tree-picker.html",
|
|
107
|
+
"rootSelector": ".com-treePicker.picker-wrap",
|
|
108
|
+
"ownerSelector": ".com-treePicker-input .el-input__wrapper",
|
|
109
|
+
"innerBorderlessSelectors": [],
|
|
110
|
+
"portalSelectors": [
|
|
111
|
+
".com-treePicker-dialog"
|
|
112
|
+
],
|
|
113
|
+
"knownRootClasses": [
|
|
114
|
+
"com-treePicker"
|
|
115
|
+
],
|
|
116
|
+
"heightPolicy": "fixed-control-height",
|
|
117
|
+
"states": [
|
|
118
|
+
"default",
|
|
119
|
+
"hover",
|
|
120
|
+
"focus",
|
|
121
|
+
"error",
|
|
122
|
+
"disabled",
|
|
123
|
+
"open"
|
|
124
|
+
],
|
|
125
|
+
"risk": "外部触发输入、树弹窗和弹窗内搜索输入属于不同层级,通用输入规则不能把三者视为一个边框所有者。"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"id": "common-core.select-multiple",
|
|
129
|
+
"vendor": "common-core",
|
|
130
|
+
"family": "select-composite",
|
|
131
|
+
"source": "lowcode-engine/packages/common/common-lowcode/components/src/schema-component/base-input-group/select/com/select.vue",
|
|
132
|
+
"fixture": "scanner/__tests__/component-fixtures/common-core-select-multiple.html",
|
|
133
|
+
"rootSelector": ".el-select.com-select-multiple",
|
|
134
|
+
"ownerSelector": ".el-select.com-select-multiple .el-select__wrapper",
|
|
135
|
+
"innerBorderlessSelectors": [
|
|
136
|
+
".el-select.com-select-multiple .el-select__input"
|
|
137
|
+
],
|
|
138
|
+
"portalSelectors": [
|
|
139
|
+
".el-select__popper"
|
|
140
|
+
],
|
|
141
|
+
"knownRootClasses": [
|
|
142
|
+
"com-select-multiple"
|
|
143
|
+
],
|
|
144
|
+
"heightPolicy": "auto-with-control-min-height",
|
|
145
|
+
"states": [
|
|
146
|
+
"default",
|
|
147
|
+
"hover",
|
|
148
|
+
"focus",
|
|
149
|
+
"error",
|
|
150
|
+
"disabled",
|
|
151
|
+
"open"
|
|
152
|
+
],
|
|
153
|
+
"risk": "Tag、过滤输入和 Teleport 下拉层共存;必须保留多选自然增高并避免内部 editor 重复描边。"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"id": "common-core.input-number-mixed-wrapper",
|
|
157
|
+
"vendor": "common-core",
|
|
158
|
+
"family": "number-composite",
|
|
159
|
+
"source": "lowcode-engine/packages/common/common-lowcode/components/src/schema-component/base-input-group/input-number/com/input-number.vue",
|
|
160
|
+
"fixture": "scanner/__tests__/component-fixtures/common-core-input-number-mixed.html",
|
|
161
|
+
"rootSelector": ".el-input-number.el-input__wrapper",
|
|
162
|
+
"ownerSelector": ".el-input-number.el-input__wrapper",
|
|
163
|
+
"innerBorderlessSelectors": [
|
|
164
|
+
".el-input-number.el-input__wrapper > .el-input > .el-input__wrapper"
|
|
165
|
+
],
|
|
166
|
+
"knownRootClasses": [
|
|
167
|
+
"el-input-number"
|
|
168
|
+
],
|
|
169
|
+
"heightPolicy": "fixed-control-height",
|
|
170
|
+
"states": [
|
|
171
|
+
"default",
|
|
172
|
+
"hover",
|
|
173
|
+
"focus",
|
|
174
|
+
"error",
|
|
175
|
+
"disabled"
|
|
176
|
+
],
|
|
177
|
+
"assertions": [
|
|
178
|
+
{
|
|
179
|
+
"target": "root",
|
|
180
|
+
"selector": ".el-input-number.el-input__wrapper",
|
|
181
|
+
"matches": true
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"target": "inner",
|
|
185
|
+
"selector": ".el-input-number.el-input__wrapper > .el-input > .el-input__wrapper",
|
|
186
|
+
"matches": true
|
|
187
|
+
}
|
|
188
|
+
],
|
|
189
|
+
"risk": "根节点本身是 wrapper,内部仍含 Element input wrapper;只能由根节点绘制唯一状态边框。"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"id": "common-core.base-toolbar-split-button",
|
|
193
|
+
"vendor": "common-core",
|
|
194
|
+
"family": "action-composite",
|
|
195
|
+
"source": "common-core BaseToolbar split-button rendered DOM",
|
|
196
|
+
"fixture": "scanner/__tests__/component-fixtures/common-core-base-toolbar-split.html",
|
|
197
|
+
"rootSelector": ".base-toolbar-box .action-button-wrap > .el-dropdown > .el-button-group",
|
|
198
|
+
"ownerSelector": ".base-toolbar-box .action-button-wrap > .el-dropdown > .el-button-group",
|
|
199
|
+
"innerBorderlessSelectors": [
|
|
200
|
+
".base-toolbar-box .action-button-wrap > .el-dropdown > .el-button-group > .el-dropdown__caret-button"
|
|
201
|
+
],
|
|
202
|
+
"portalSelectors": [
|
|
203
|
+
".el-dropdown-menu.action-dropdown-menu"
|
|
204
|
+
],
|
|
205
|
+
"knownRootClasses": [
|
|
206
|
+
"action-button-wrap"
|
|
207
|
+
],
|
|
208
|
+
"heightPolicy": "content-driven",
|
|
209
|
+
"states": [
|
|
210
|
+
"default",
|
|
211
|
+
"hover",
|
|
212
|
+
"focus",
|
|
213
|
+
"active",
|
|
214
|
+
"disabled",
|
|
215
|
+
"open"
|
|
216
|
+
],
|
|
217
|
+
"risk": "主动作和箭头是两个交互区但必须共享一个视觉外轮廓;下拉菜单中的业务按钮需扁平化且不能污染普通菜单。"
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
}
|
package/standards/rules.json
CHANGED
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
"layout": {
|
|
45
45
|
"label": "页面骨架 / 操作组合",
|
|
46
46
|
"standardsDoc": "skills/layouts/list-page/SKILL.md"
|
|
47
|
+
},
|
|
48
|
+
"composite": {
|
|
49
|
+
"label": "复合控件结构契约",
|
|
50
|
+
"standardsDoc": "standards/component-structures.json"
|
|
47
51
|
}
|
|
48
52
|
},
|
|
49
53
|
"rules": [
|
|
@@ -464,6 +468,22 @@
|
|
|
464
468
|
"manualReason": "需按组件语义选择 base/small/round 等圆角 token",
|
|
465
469
|
"scanner": "scanner/rules/semantic.mjs",
|
|
466
470
|
"skills": ["tokens/index"]
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
"id": "R040",
|
|
474
|
+
"category": "composite",
|
|
475
|
+
"severity": "review",
|
|
476
|
+
"title": "未知复合控件结构必须先登记边框与高度契约",
|
|
477
|
+
"rationale": "复合输入可能同时包含 Element 外壳、内部 editor 与 Teleport 浮层;直接套用普通 input 规则会造成固定高度裁切、边框消失或双描边。",
|
|
478
|
+
"appliesTo": [
|
|
479
|
+
"project-owned composite input wrapper",
|
|
480
|
+
".el-input.el-input__wrapper",
|
|
481
|
+
"picker/multiple/tag wrapper containing Element input"
|
|
482
|
+
],
|
|
483
|
+
"autoFixable": false,
|
|
484
|
+
"manualReason": "必须核对真实 DOM,明确边框所有者、高度策略、内部无描边层与 Teleport 出口",
|
|
485
|
+
"scanner": "scanner/rules/componentStructure.mjs",
|
|
486
|
+
"skills": ["vendors/jh-components"]
|
|
467
487
|
}
|
|
468
488
|
]
|
|
469
489
|
}
|
package/standards/ui/03-form.md
CHANGED
|
@@ -70,6 +70,10 @@ textarea 不强制 26px 高度,只继承统一圆角、字体和状态样式
|
|
|
70
70
|
复合控件只能由最外层 wrapper 绘制默认、hover、focus、error、disabled 边框;内部编辑 input
|
|
71
71
|
必须保持无描边,避免边框消失、内容裁切或出现双描边。
|
|
72
72
|
|
|
73
|
+
所有非薄封装复合控件必须登记到 `standards/component-structures.json`,明确根节点、边框
|
|
74
|
+
所有者、内部无描边层、高度策略、状态与 Teleport 出口,并提供真实 DOM fixture。R040
|
|
75
|
+
发现疑似未知结构时只要求人工评审,不自动改写;禁止在结构未确认前扩大全局 Element 选择器。
|
|
76
|
+
|
|
73
77
|
---
|
|
74
78
|
|
|
75
79
|
## 规则:紧凑业务表单字号统一为 12px
|
|
@@ -186,6 +186,17 @@
|
|
|
186
186
|
0 0 0 1px var(--el-disabled-border-color, #e4e7ed) inset !important;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// 内部 editor 只负责文字录入。Element Plus 默认会给子 wrapper 再画一层 inset,
|
|
190
|
+
// 必须在已登记的复合结构内精准清除;不触碰普通 input 或其他 picker。
|
|
191
|
+
&.com-input-multi-tag-wrap.el-input__wrapper
|
|
192
|
+
.el-input
|
|
193
|
+
> .el-input__wrapper {
|
|
194
|
+
border: 0 !important;
|
|
195
|
+
outline: none !important;
|
|
196
|
+
background: transparent !important;
|
|
197
|
+
box-shadow: none !important;
|
|
198
|
+
}
|
|
199
|
+
|
|
189
200
|
&.com-picker .el-input > .el-input__inner,
|
|
190
201
|
&.com-reference-picker .el-input > .el-input__inner,
|
|
191
202
|
&.com-reference-picker-input .el-input > .el-input__inner,
|