@agile-team/wl-skills-ui 1.9.6 → 1.9.8

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 CHANGED
@@ -4,6 +4,28 @@ 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.8] - 2026-07-29
8
+
9
+ ### Fixed
10
+
11
+ - 统一紧凑业务表单 label、输入值、选择值、placeholder、textarea 与数字值为 12px,并继续豁免登录页和显式定制区域。
12
+ - 修复 textarea 聚焦后品牌色边框不清晰,以及数字输入框在不同 wrapper DOM、hover + focus 组合态下边框消失或双描边的问题。
13
+ - 补强原生 Element Table 与 AG Grid 长文本单行省略样式,继续由 R039 / `defineColumns()` 负责悬停完整提示与业务例外。
14
+ - 修复 BaseToolbar 分裂下拉按钮的箭头独立色块与菜单项内嵌按钮描边;规则仅命中 `action-button-wrap` 和 `action-dropdown-menu`,不影响普通按钮组与普通下拉菜单。
15
+
16
+ ## [1.9.7] - 2026-07-28
17
+
18
+ ### Added
19
+
20
+ - 新增 R038 创建类主按钮门禁与自动修复:新增/新建/添加/创建类操作统一使用客户主题 `primary` 填充按钮,自动移除误用的 `plain`。
21
+ - 新增 R039 表格长文本门禁与自动修复:原生 `el-table-column` 普通数据列统一补齐 `show-overflow-tooltip`。
22
+ - `defineColumns()` 为 BaseTable/AG Grid 普通文本列递归补齐超长省略与悬停完整提示;结构列、自定义渲染列、换行列及显式配置保持业务声明。
23
+
24
+ ### Changed
25
+
26
+ - 表格样式仅对启用 overflow tooltip 的单元格补齐单行省略,避免影响 Tag、操作列和自定义内容。
27
+ - 列表页、按钮、Element Table、BaseTable 规范与模板同步声明主按钮和长文本交互契约。
28
+
7
29
  ## [1.9.6] - 2026-07-23
8
30
 
9
31
  ### Fixed
package/README.md CHANGED
@@ -200,7 +200,7 @@ yarn add @agile-team/wl-skills-ui
200
200
  |---|---|---|
201
201
  | `element-plus` | **`2.2.6-prod.3`** | 集团 jh- 定制版;EP 2.3.0 起引入 `.el-input__wrapper`,与 jh-ui 3.x 不兼容 |
202
202
  | `@jhlc/jh-ui` | **`3.1.0`** | SCSS 皮肤包,`.com-text` label 包裹 + `.has-colon` 冒号注入(wl-skills-ui 已统一屏蔽表单冒号) |
203
- | `@agile-team/wl-skills-ui` | `^1.9.6` | 已对齐上述组合的 DOM 假设、客户主题锁、圆角契约与定制页样式边界 |
203
+ | `@agile-team/wl-skills-ui` | `^1.9.8` | 已对齐上述组合的 DOM 假设、客户主题锁、圆角契约与定制页样式边界 |
204
204
 
205
205
  三种识别方式,任选其一:
206
206
 
@@ -214,8 +214,13 @@ yarn add @agile-team/wl-skills-ui
214
214
 
215
215
  ## 版本亮点
216
216
 
217
- 当前 v1.9.6
217
+ 当前 v1.9.8
218
218
 
219
+ - **主操作按钮不再漏色**:新增/新建/添加/创建类按钮强制使用客户主题 `primary` 填充样式,扫描器可识别并自动修复漏写 `type` 或误带 `plain` 的存量代码。
220
+ - **长文本交互统一**:普通数据列在空间不足时单行省略,悬停显示完整内容;Tag、操作列、自定义渲染和主动换行列不被机械覆盖。
221
+ - **紧凑表单字号闭环**:业务表单 label、输入值、选择值、placeholder、textarea 与数字输入统一为 12px;登录页和显式定制区域继续保持项目自身设计。
222
+ - **输入边框状态闭环**:textarea 聚焦始终显示品牌色边框;数字输入兼容 Element Plus 新旧及混合 wrapper DOM,默认、hover、focus、错误、禁用态均只保留一层正确边框。
223
+ - **分裂操作保持一体**:BaseToolbar 的主动作与下拉箭头共用外轮廓和状态色,下拉菜单中的业务按钮扁平化为菜单项;规则精准限定在平台工具栏结构,不污染普通按钮组和普通下拉菜单。
219
224
  - **Vite 4 运行时兼容**:浏览器 ESM 产物不再暴露可被开发转换误替换的 Node 环境变量成员链,预构建与直接加载均保持合法语法。
220
225
  - **登录页精准退出**:内置识别 `.lp-root` 与 `.session-login`,其内部表单、按钮、焦点和校验态保留登录页自身设计;业务页面仍由包高权重统一管控。
221
226
  - **通用定制页边界**:其他定制页在根节点增加 `.wl-ui-skin-exempt` 或 `data-wl-ui-skin="off"` 即可退出组件级强覆盖,不需要删除全局化妆包。
@@ -676,7 +681,7 @@ export const myRules = [
676
681
 
677
682
  ## 规范清单
678
683
 
679
- ### UI 规则(R001-R037,按 layer 自动分组)
684
+ ### UI 规则(R001-R039,按 layer 自动分组)
680
685
 
681
686
  | Rule | Layer | Vendor | 说明 |
682
687
  | ---- | ----- | --------- | ------------------------------------ |
@@ -707,6 +712,8 @@ export const myRules = [
707
712
  | R035 | L1 | element | el-upload 建议配置 tip/限制说明 |
708
713
  | R036 | L1 | element | el-steps 建议明确状态来源 |
709
714
  | R037 | L1 | element | 空/异常反馈建议统一操作入口 |
715
+ | R038 | L1 | element | 创建类主按钮缺 primary 填充主题色 |
716
+ | R039 | L1 | element | 普通数据列缺省略与悬停完整提示 |
710
717
 
711
718
  ### 工程规范
712
719
 
@@ -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.6` | 已对齐 jh-ui 3.1.0 + EP 2.2.6-prod.3 的 DOM 假设、客户主题锁、6px 基础圆角、定制页样式边界与 Vite 4 开发转换 |
14
+ | `@agile-team/wl-skills-ui` | `^1.9.8` | 已对齐 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.6 三方对齐。
38
+ - 集团内项目:**统一锚定 `element-plus@2.2.6-prod.3` + `@jhlc/jh-ui@3.1.0`**,与 wl-skills-ui v1.9.8 三方对齐。
39
39
  - 升级 `element-plus` 到 2.3+ 前,必须同步升级 `@jhlc/jh-ui` 到对应支持版本,否则 `.com-text` / `.el-input__inner` DOM 与 EP `__wrapper` 体系不兼容。
40
40
  - 新项目接入:见 README 「快速接入」章节,默认会按本表声明 peerDependency 范围。
41
41
 
@@ -279,24 +279,37 @@ function clearColumnAutoMap(name) {
279
279
  }
280
280
  function defineColumns(columns) {
281
281
  return columns.map((col) => {
282
- const fieldName = col.name ?? col.label ?? "";
282
+ const normalized = Array.isArray(col.children) ? {
283
+ ...col,
284
+ children: defineColumns(col.children)
285
+ } : col;
286
+ const fieldName = normalized.name ?? normalized.label ?? "";
283
287
  const preset = COLUMN_AUTO_MAP[fieldName];
284
- const hasRenderer = col.defaultNode !== void 0 || col.defaultSlot !== void 0;
285
- if (hasRenderer) return col;
286
- if (!preset && isDictColumn(col)) {
288
+ const hasRenderer = normalized.defaultNode !== void 0 || normalized.defaultSlot !== void 0;
289
+ if (hasRenderer) return normalized;
290
+ if (!preset && isDictColumn(normalized)) {
287
291
  return {
288
- ...col,
289
- defaultSlot: ({ row }) => renderDictClassifyTag(row?.[fieldName], String(col.logicValue))
292
+ ...normalized,
293
+ defaultSlot: ({ row }) => renderDictClassifyTag(
294
+ row?.[fieldName],
295
+ String(normalized.logicValue)
296
+ )
290
297
  };
291
298
  }
292
- if (!preset) return col;
293
- return { ...preset, ...col };
299
+ if (!preset) return withOverflowTooltip(normalized);
300
+ return withOverflowTooltip({ ...preset, ...normalized });
294
301
  });
295
302
  }
296
303
  function isDictColumn(col) {
297
304
  const logicType = String(col.logicType ?? "").toLowerCase();
298
305
  return Boolean(col.logicValue) && logicType.includes("dict");
299
306
  }
307
+ function withOverflowTooltip(col) {
308
+ if (!col.name || col.showOverflowTooltip !== void 0 || col.tooltipValueGetter !== void 0 || col.wrapText === true || col.autoHeight === true || ["selection", "index", "radio", "expand"].includes(String(col.type ?? "")) || col.defaultNode !== void 0 || col.defaultSlot !== void 0) {
309
+ return col;
310
+ }
311
+ return { ...col, showOverflowTooltip: true };
312
+ }
300
313
  registerColumnAutoMaps({
301
314
  enableStatus: {
302
315
  width: 80,
@@ -1,5 +1,5 @@
1
- import { renderTagNode, renderClassifyTag, installBrandThemeLock, registerColumnAutoMaps, renderRatingLevel, renderBadge } from './chunk-MPJ6QDYX.js';
2
- export { registerDictColorMap, registerDictColorMaps, renderDictClassifyTag, setDictResolver } from './chunk-MPJ6QDYX.js';
1
+ import { renderTagNode, renderClassifyTag, installBrandThemeLock, registerColumnAutoMaps, renderRatingLevel, renderBadge } from './chunk-XDGTJ7LE.js';
2
+ export { registerDictColorMap, registerDictColorMaps, renderDictClassifyTag, setDictResolver } from './chunk-XDGTJ7LE.js';
3
3
 
4
4
  // runtime/presets/common.ts
5
5
  var RISK_LEVEL_MAP = {
package/es/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { registerColumnAutoMaps } from './chunk-MPJ6QDYX.js';
2
- export { AUDIT_STATUS_MAP, BRAND_THEME_TOKENS, ENABLE_STATUS_MAP, RATING_LEVEL_COLORS, VERIFY_STATUS_MAP, clearColumnAutoMap, defineColumns, getColumnAutoMap, installBrandThemeLock, registerColumnAutoMap, registerColumnAutoMaps, registerDictColorMap, registerDictColorMaps, renderAuditStatus, renderBadge, renderClassifyTag, renderCountBadge, renderDangerText, renderDictClassifyTag, renderEnableStatus, renderOps, renderRatingLevel, renderTagNode, renderTagSlot, renderVerifyStatus, setDictResolver } from './chunk-MPJ6QDYX.js';
1
+ import { registerColumnAutoMaps } from './chunk-XDGTJ7LE.js';
2
+ export { AUDIT_STATUS_MAP, BRAND_THEME_TOKENS, ENABLE_STATUS_MAP, RATING_LEVEL_COLORS, VERIFY_STATUS_MAP, clearColumnAutoMap, defineColumns, getColumnAutoMap, installBrandThemeLock, registerColumnAutoMap, registerColumnAutoMaps, registerDictColorMap, registerDictColorMaps, renderAuditStatus, renderBadge, renderClassifyTag, renderCountBadge, renderDangerText, renderDictClassifyTag, renderEnableStatus, renderOps, renderRatingLevel, renderTagNode, renderTagSlot, renderVerifyStatus, setDictResolver } from './chunk-XDGTJ7LE.js';
3
3
 
4
4
  // runtime/presets/registry.ts
5
5
  var _installedPresets = /* @__PURE__ */ new Set();
@@ -1,4 +1,4 @@
1
- import { renderTagNode, installBrandThemeLock, registerDictColorMaps, registerColumnAutoMaps } from '../chunk-MPJ6QDYX.js';
1
+ import { renderTagNode, installBrandThemeLock, registerDictColorMaps, registerColumnAutoMaps } from '../chunk-XDGTJ7LE.js';
2
2
 
3
3
  // runtime/presets/security.ts
4
4
  var VIOLATION_STATUS_MAP = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agile-team/wl-skills-ui",
3
- "version": "1.9.6",
3
+ "version": "1.9.8",
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",
@@ -172,14 +172,46 @@ export function defineColumns<T = any>(
172
172
  columns: TableColumnDesc<T>[]
173
173
  ): TableColumnDesc<T>[] {
174
174
  return columns.map((col) => {
175
+ const normalized = Array.isArray(col.children)
176
+ ? ({
177
+ ...col,
178
+ children: defineColumns(col.children),
179
+ } as TableColumnDesc<T>)
180
+ : col;
175
181
  // 多选列:统一宽度 55px(页面已手写 width 时不覆盖)
176
- if ((col as any).type === "selection" && !(col as any).width) {
177
- return { width: 55, ...(col as any) } as unknown as TableColumnDesc<T>;
182
+ if ((normalized as any).type === "selection" && !(normalized as any).width) {
183
+ return {
184
+ width: 55,
185
+ ...(normalized as any),
186
+ } as unknown as TableColumnDesc<T>;
178
187
  }
179
- if (!col.name) return col;
180
- const auto = COLUMN_AUTO_MAP[col.name as string];
181
- if (!auto) return col;
188
+ if (!normalized.name) return normalized;
189
+ const auto = COLUMN_AUTO_MAP[normalized.name as string];
190
+ if (!auto) return withOverflowTooltip(normalized);
182
191
  // 自动配置 < 手写配置(手写的属性不被覆盖)
183
- return { ...auto, ...col } as TableColumnDesc<T>;
192
+ return withOverflowTooltip({
193
+ ...auto,
194
+ ...normalized,
195
+ } as TableColumnDesc<T>);
184
196
  });
185
197
  }
198
+
199
+ function withOverflowTooltip<T>(
200
+ col: TableColumnDesc<T>
201
+ ): TableColumnDesc<T> {
202
+ if (
203
+ !col.name ||
204
+ col.showOverflowTooltip !== undefined ||
205
+ col.tooltipValueGetter !== undefined ||
206
+ col.wrapText === true ||
207
+ col.autoHeight === true ||
208
+ ["selection", "index", "radio", "expand"].includes(
209
+ String((col as any).type ?? "")
210
+ ) ||
211
+ col.defaultNode !== undefined ||
212
+ col.defaultSlot !== undefined
213
+ ) {
214
+ return col;
215
+ }
216
+ return { ...col, showOverflowTooltip: true };
217
+ }
package/scanner/fix.mjs CHANGED
@@ -7,6 +7,8 @@
7
7
  * - el-input / el-select: 缺 size="small" → 添加
8
8
  * - el-date-picker: 缺 style="width:100%" → 添加
9
9
  * - el-table: 缺 empty-text="暂无数据" → 添加
10
+ * - 创建类主按钮: 缺 primary / 误用 plain → 纠正
11
+ * - el-table 普通数据列: 缺 overflow tooltip → 添加
10
12
  * - <style>/<template> 内可映射 hex 颜色 → 替换为 var(--el-color-*)
11
13
  */
12
14
  import { readFileSync, writeFileSync, readdirSync } from "node:fs";
@@ -14,6 +16,15 @@ import { join, relative, resolve } from "node:path";
14
16
  import { createSnapshot } from "./snapshot.mjs";
15
17
  import { TOKEN_MAP } from "./rules/_shared.mjs";
16
18
 
19
+ const CREATE_ACTION_LABEL =
20
+ /(?:新增|新建|添加|创建)(?:申请|记录|数据|客户|项目|任务|明细|行)?/;
21
+
22
+ function hasTrueBooleanAttr(tagText, attr) {
23
+ return new RegExp(
24
+ `(?:^|\\s)(?::${attr}\\s*=\\s*["']true["']|${attr}(?:\\s*=\\s*["'](?:true|)["'])?)(?=\\s|/?>)`,
25
+ ).test(tagText);
26
+ }
27
+
17
28
  const FIXES = {
18
29
  "el-input": [{ attr: "size", value: "small" }],
19
30
  "el-select": [{ attr: "size", value: "small" }],
@@ -37,6 +48,8 @@ export const FIXED_RULE_IDS = Object.freeze([
37
48
  "R016",
38
49
  "R017",
39
50
  "R021",
51
+ "R038",
52
+ "R039",
40
53
  ]);
41
54
 
42
55
  function parseTag(content, pos, tagName) {
@@ -57,10 +70,24 @@ function parseTag(content, pos, tagName) {
57
70
  return { text: content.slice(pos), end: content.length };
58
71
  }
59
72
 
60
- function addAttrIfMissing(tagText, tagName, attr, value, replaceStatic = false) {
73
+ function addAttrIfMissing(
74
+ tagText,
75
+ tagName,
76
+ attr,
77
+ value,
78
+ replaceStatic = false,
79
+ booleanAttr = false,
80
+ ) {
61
81
  // 已存在(含动态绑定 :attr=)则跳过
82
+ const escapedAttr = attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
83
+ if (
84
+ booleanAttr &&
85
+ new RegExp(`(?:^|\\s):?${escapedAttr}(?=\\s|=|/?>)`).test(tagText)
86
+ ) {
87
+ return { text: tagText, changed: false };
88
+ }
62
89
  const re = new RegExp(
63
- `:?${attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=`,
90
+ `:?${escapedAttr}\\s*=`,
64
91
  );
65
92
  if (re.test(tagText)) {
66
93
  if (!replaceStatic) return { text: tagText, changed: false };
@@ -71,6 +98,12 @@ function addAttrIfMissing(tagText, tagName, attr, value, replaceStatic = false)
71
98
  const text = tagText.replace(staticRe, `$1"${value}"`);
72
99
  return { text, changed: text !== tagText };
73
100
  }
101
+ if (booleanAttr) {
102
+ return {
103
+ text: tagText.replace(`<${tagName}`, `<${tagName} ${attr}`),
104
+ changed: true,
105
+ };
106
+ }
74
107
  const newTag = tagText.replace(
75
108
  `<${tagName}`,
76
109
  `<${tagName} ${attr}="${value}"`,
@@ -105,8 +138,26 @@ function fixTemplateAttrs(content) {
105
138
  if (tagName === "el-table-column" && text.includes('type="selection"')) {
106
139
  fixes.push({ attr: "header-align", value: "center" });
107
140
  }
108
- for (const { attr, value, replaceStatic } of fixes) {
109
- const r = addAttrIfMissing(text, tagName, attr, value, replaceStatic);
141
+ if (
142
+ tagName === "el-table-column" &&
143
+ /(?:^|\s):?prop\s*=/.test(text) &&
144
+ !/(?:^|\s)type\s*=\s*["'](?:selection|index|expand)["']/.test(text)
145
+ ) {
146
+ fixes.push({
147
+ attr: "show-overflow-tooltip",
148
+ value: "",
149
+ booleanAttr: true,
150
+ });
151
+ }
152
+ for (const { attr, value, replaceStatic, booleanAttr } of fixes) {
153
+ const r = addAttrIfMissing(
154
+ text,
155
+ tagName,
156
+ attr,
157
+ value,
158
+ replaceStatic,
159
+ booleanAttr,
160
+ );
110
161
  text = r.text;
111
162
  if (r.changed) changes++;
112
163
  }
@@ -116,6 +167,44 @@ function fixTemplateAttrs(content) {
116
167
  return { content: result, changes };
117
168
  }
118
169
 
170
+ function fixPrimaryActionButtons(content) {
171
+ let changes = 0;
172
+ const result = content.replace(
173
+ /<el-button\b[\s\S]*?<\/el-button>/g,
174
+ (button) => {
175
+ const openEnd = button.indexOf(">");
176
+ if (openEnd < 0) return button;
177
+ let opening = button.slice(0, openEnd + 1);
178
+ const label = button.slice(openEnd + 1, button.lastIndexOf("</el-button>"));
179
+ if (!CREATE_ACTION_LABEL.test(label)) return button;
180
+ if (/:type\s*=/.test(opening)) return button;
181
+ if (hasTrueBooleanAttr(opening, "link") || hasTrueBooleanAttr(opening, "text"))
182
+ return button;
183
+
184
+ const typeResult = addAttrIfMissing(
185
+ opening,
186
+ "el-button",
187
+ "type",
188
+ "primary",
189
+ true,
190
+ );
191
+ opening = typeResult.text;
192
+ if (typeResult.changed) changes++;
193
+
194
+ const withoutPlain = opening
195
+ .replace(/\s+:plain\s*=\s*["']true["']/g, "")
196
+ .replace(/\s+plain\s*=\s*["'](?:true|)["']/g, "")
197
+ .replace(/\s+plain(?=\s|\/?>)/g, "");
198
+ if (withoutPlain !== opening) {
199
+ opening = withoutPlain;
200
+ changes++;
201
+ }
202
+ return opening + button.slice(openEnd + 1);
203
+ },
204
+ );
205
+ return { content: result, changes };
206
+ }
207
+
119
208
  function fixHexColors(content) {
120
209
  let changes = 0;
121
210
  let result = content;
@@ -180,9 +269,12 @@ export function runFix({
180
269
  const r1 = fixTemplateAttrs(content);
181
270
  content = r1.content;
182
271
  changes += r1.changes;
183
- const r2 = fixHexColors(content);
272
+ const r2 = fixPrimaryActionButtons(content);
184
273
  content = r2.content;
185
274
  changes += r2.changes;
275
+ const r3 = fixHexColors(content);
276
+ content = r3.content;
277
+ changes += r3.changes;
186
278
  if (changes > 0 && content !== original) {
187
279
  pending.push({ filePath, original, content, changes });
188
280
  changedAbsPaths.push(filePath);
@@ -63,6 +63,8 @@ const RULE_NAME = {
63
63
  R018: "<script> 块硬编码 hex 颜色",
64
64
  R021: 'BaseTable 缺少 render-type="agGrid"',
65
65
  R022: "BaseTable 缺少唯一 cid",
66
+ R038: "创建类主操作缺少 primary 填充主题色",
67
+ R039: "普通数据列缺少超长省略与悬停完整提示",
66
68
  R031: "详情/统计卡片建议使用统一场景 class",
67
69
  R032: "el-tabs 建议明确页面场景",
68
70
  R033: "el-descriptions 建议使用 bordered 或统一容器",
@@ -1,6 +1,20 @@
1
- /** scanner/rules/button.mjs — 按钮规则:R004 R005 R015 */
1
+ /** scanner/rules/button.mjs — 按钮规则:R004 R005 R015 R038 */
2
2
  import { lineOf, issue, findTags } from "./_shared.mjs";
3
3
 
4
+ const CREATE_ACTION_LABEL = /(?:新增|新建|添加|创建)(?:申请|记录|数据|客户|项目|任务|明细|行)?/;
5
+
6
+ function buttonContent(template, tag) {
7
+ const start = tag.index + tag.text.length;
8
+ const end = template.indexOf("</el-button>", start);
9
+ return end < 0 ? "" : template.slice(start, end);
10
+ }
11
+
12
+ function hasTrueBooleanAttr(tagText, attr) {
13
+ return new RegExp(
14
+ `(?:^|\\s)(?::${attr}\\s*=\\s*["']true["']|${attr}(?:\\s*=\\s*["'](?:true|)["'])?)(?=\\s|/?>)`,
15
+ ).test(tagText);
16
+ }
17
+
4
18
  export const buttonRules = [
5
19
  // R004: 操作列文字按钮
6
20
  {
@@ -59,6 +73,46 @@ export const buttonRules = [
59
73
  },
60
74
  },
61
75
 
76
+ // R038: 新增/新建/添加/创建类主操作必须使用填充主题色
77
+ {
78
+ id: "R038",
79
+ category: "button",
80
+ severity: "error",
81
+ name: "创建类主操作缺少 primary 填充主题色",
82
+ check(template, file, lineOffset) {
83
+ const issues = [];
84
+ for (const tag of findTags(template, "el-button")) {
85
+ const label = buttonContent(template, tag);
86
+ if (!CREATE_ACTION_LABEL.test(label)) continue;
87
+ if (/:type\s*=/.test(tag.text)) continue;
88
+ if (
89
+ hasTrueBooleanAttr(tag.text, "link") ||
90
+ hasTrueBooleanAttr(tag.text, "text")
91
+ ) {
92
+ continue;
93
+ }
94
+ const isPrimary = /(?:^|\s)type\s*=\s*["']primary["']/.test(
95
+ tag.text,
96
+ );
97
+ const isPlain = hasTrueBooleanAttr(tag.text, "plain");
98
+ if (!isPrimary || isPlain) {
99
+ issues.push(
100
+ issue(
101
+ file,
102
+ lineOf(template, tag.index, lineOffset),
103
+ "R038",
104
+ "button",
105
+ "error",
106
+ "新增/新建/添加/创建类主操作必须是 primary 填充按钮",
107
+ '设置 type="primary" 并移除 plain;行内次级操作应显式使用 link/text',
108
+ ),
109
+ );
110
+ }
111
+ }
112
+ return issues;
113
+ },
114
+ },
115
+
62
116
  // R015: 弹窗嵌套表格操作列用 el-button link
63
117
  {
64
118
  id: "R015",
@@ -1,4 +1,4 @@
1
- /** scanner/rules/table.mjs — 表格相关规则:R001 R002 R003 R014 R021 R022 */
1
+ /** scanner/rules/table.mjs — 表格相关规则:R001 R002 R003 R014 R021 R022 R039 */
2
2
  import { lineOf, issue, findTags } from "./_shared.mjs";
3
3
 
4
4
  export const tableRules = [
@@ -35,6 +35,41 @@ export const tableRules = [
35
35
  },
36
36
  },
37
37
 
38
+ // R039: 原生 el-table 普通数据列必须支持超长省略与悬停完整提示
39
+ {
40
+ id: "R039",
41
+ category: "table",
42
+ severity: "warning",
43
+ name: "el-table 普通数据列缺少 show-overflow-tooltip",
44
+ check(template, file, lineOffset) {
45
+ const issues = [];
46
+ for (const tag of findTags(template, "el-table-column")) {
47
+ if (!/(?:^|\s):?prop\s*=/.test(tag.text)) continue;
48
+ if (
49
+ /(?:^|\s)type\s*=\s*["'](?:selection|index|expand)["']/.test(
50
+ tag.text,
51
+ )
52
+ ) {
53
+ continue;
54
+ }
55
+ if (/(?:^|\s):?show-overflow-tooltip(?:\s|=|\/?>)/.test(tag.text))
56
+ continue;
57
+ issues.push(
58
+ issue(
59
+ file,
60
+ lineOf(template, tag.index, lineOffset),
61
+ "R039",
62
+ "table",
63
+ "warning",
64
+ "普通数据列缺少超长省略与悬停完整内容提示",
65
+ "添加 show-overflow-tooltip;BaseTable/AG Grid 列统一使用 defineColumns() 自动补齐",
66
+ ),
67
+ );
68
+ }
69
+ return issues;
70
+ },
71
+ },
72
+
38
73
  // R002: el-table 缺少 empty-text
39
74
  {
40
75
  id: "R002",
@@ -59,6 +59,18 @@ el-date-picker 默认宽度不会自动撑满 el-form-item,必须显式设置
59
59
 
60
60
  ---
61
61
 
62
+ ## 表单字号与状态边框一致性 【中危】
63
+
64
+ - 紧凑业务表单的 label、输入值、选择值、picker、textarea、数字值和 placeholder
65
+ 统一使用 `--wk-form-font-size: 12px`;不要在某一种组件上单独保留 13px/14px。
66
+ - textarea focus 必须是清晰的品牌色实线与轻外环,不能只改 `border-color` 或依赖
67
+ 平台默认阴影。
68
+ - 数字输入框必须同时检查社区版“wrapper 子节点”和 jh-ui“wrapper 与
69
+ input-number 同节点”两种 DOM,并确保只存在一层可见边框。
70
+ - 登录页和显式定制页继续使用自身样式,不套用上述高权重规则。
71
+
72
+ ---
73
+
62
74
  ## 表单列数与输入宽度 【中危】
63
75
 
64
76
  - 全宽业务表单默认 4 列,严禁使用 5~8 列把输入框压缩到无法阅读或填写。
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: |
3
3
  表格组件规范 Skill — el-table / BaseTable / el-table-column 的对齐、空状态、selection 列标准。
4
- 覆盖规则:R001 R002 R003 R014。
4
+ 覆盖规则:R001 R002 R003 R014 R039
5
5
  applyTo: "**/*.vue"
6
6
  ---
7
7
 
@@ -55,6 +55,19 @@ applyTo: "**/*.vue"
55
55
 
56
56
  ---
57
57
 
58
+ ## R039 — 普通数据列必须超长省略并悬停显示完整内容 【中危】
59
+
60
+ ```diff
61
+ - <el-table-column prop="customerName" label="客户名称" align="center" />
62
+ + <el-table-column prop="customerName" label="客户名称" align="center" show-overflow-tooltip />
63
+ ```
64
+
65
+ BaseTable / AG Grid 使用 `defineColumns()` 后由 runtime 自动为普通文本列补齐
66
+ `showOverflowTooltip: true`。自定义渲染、结构列、换行列不强制;需要主动关闭时
67
+ 显式声明 `showOverflowTooltip: false`。
68
+
69
+ ---
70
+
58
71
  ## 完整标准写法示例
59
72
 
60
73
  ```html
@@ -67,7 +80,13 @@ applyTo: "**/*.vue"
67
80
  header-align="center"
68
81
  />
69
82
  <el-table-column type="index" align="center" width="60" label="序号" />
70
- <el-table-column align="center" prop="name" label="名称" min-width="120" />
83
+ <el-table-column
84
+ align="center"
85
+ prop="name"
86
+ label="名称"
87
+ min-width="120"
88
+ show-overflow-tooltip
89
+ />
71
90
  <el-table-column align="center" prop="status" label="状态" width="90">
72
91
  <template #default="{ row }">
73
92
  <!-- 使用 renderTagSlot 渲染彩色 Tag,见 tag-status/SKILL.md -->
@@ -53,6 +53,8 @@ applyTo: "**/*.vue"
53
53
 
54
54
  - R001 ~ R003:表格列对齐 + empty-text
55
55
  - R005:工具栏按钮缺 icon
56
+ - R038:新增/新建/添加/创建类主按钮缺 primary 填充主题色
57
+ - R039:普通数据列缺少省略号与悬停完整内容提示
56
58
  - R006:搜索区输入框缺 `size="small"`
57
59
  - R011:分页在 footer 内(严重)
58
60
 
@@ -97,6 +99,10 @@ applyTo: "**/*.vue"
97
99
  </template>
98
100
  ```
99
101
 
102
+ “新增”类按钮必须保持 `type="primary"` 且不得带 `plain`;生成完成后运行
103
+ `wl-ui scan` / `wl-ui fix` 复核 R038。表格列统一由 `defineColumns()` 包裹,
104
+ 普通文本列的 R039 行为由 runtime 自动补齐。
105
+
100
106
  ### SCSS 对应(由 styles/layouts/\_list-page.scss 提供)
101
107
 
102
108
  ```scss
@@ -28,12 +28,14 @@ applyTo: "**/*.vue"
28
28
  - ❌ 用 `<el-button type="primary">` 当操作按钮(应用 `is-text` 文字按钮,沿用 R004)
29
29
  - ❌ 列头未居中(同 R001)
30
30
  - ❌ 缺 `empty-text="暂无数据"`(同 R003)
31
+ - ❌ 普通文本列未通过 `defineColumns()` 获得超长省略与悬停完整提示(同 R039)
31
32
  - ❌ 在 `<BaseTable>` 外层包裹自定义 padding 容器,破坏整体节奏
32
33
 
33
34
  ## Repair(修复)
34
35
 
35
36
  ### A 类(自动修)
36
37
  - 缺失 `empty-text` → 补 `empty-text="暂无数据"`
38
+ - 普通文本列统一经 `defineColumns()` 自动补 `showOverflowTooltip: true`
37
39
  - 操作列按钮 `type="primary"` → 改为 `link` + `is-text`
38
40
  - 内联 hex → CSS 变量
39
41
 
@@ -51,7 +53,8 @@ applyTo: "**/*.vue"
51
53
 
52
54
  ## 全局样式来源
53
55
 
54
- `styles/vendors/_base-table.scss` — 已注入容器圆角、表头颜色、操作按钮间距。
56
+ `styles/vendors/_base-table.scss` — 已注入容器圆角、表头颜色、操作按钮间距;
57
+ `runtime.defineColumns()` 统一控制普通文本列的 overflow tooltip。
55
58
  项目侧无需重复写样式,遵循 DOM 约定即可。
56
59
 
57
60
  ## 未来源码迁移
@@ -107,6 +107,17 @@
107
107
  "scanner": "scanner/rules/button.mjs",
108
108
  "skills": ["layouts/list-page"]
109
109
  },
110
+ {
111
+ "id": "R038",
112
+ "category": "button",
113
+ "severity": "error",
114
+ "title": "新增/新建/添加/创建类主操作必须使用 primary 填充按钮",
115
+ "rationale": "创建类动作是列表页首要操作,必须稳定使用客户主题深蓝填充,禁止漏写 type 或误用 plain 退化为线框。",
116
+ "appliesTo": ["el-button[label~=新增|新建|添加|创建]"],
117
+ "autoFixable": true,
118
+ "scanner": "scanner/rules/button.mjs",
119
+ "skills": ["layouts/list-page"]
120
+ },
110
121
  {
111
122
  "id": "R006",
112
123
  "category": "form",
@@ -313,6 +324,20 @@
313
324
  "scanner": "scanner/rules/table.mjs",
314
325
  "skills": ["vendors/base-table"]
315
326
  },
327
+ {
328
+ "id": "R039",
329
+ "category": "table",
330
+ "severity": "warning",
331
+ "title": "普通数据列必须支持超长省略与悬停完整提示",
332
+ "rationale": "列宽不足时统一单行省略,悬停显示完整内容,避免长名称撑破布局或信息不可读。",
333
+ "appliesTo": [
334
+ "el-table-column[prop]",
335
+ "BaseTable defineColumns ordinary data column"
336
+ ],
337
+ "autoFixable": true,
338
+ "scanner": "scanner/rules/table.mjs",
339
+ "skills": ["element/el-table", "vendors/base-table"]
340
+ },
316
341
  {
317
342
  "id": "R031",
318
343
  "category": "family",
@@ -32,6 +32,27 @@
32
32
 
33
33
  ---
34
34
 
35
+ ## 规则 R039:长文本统一省略并悬停显示完整内容
36
+
37
+ 原生 Element Plus 普通数据列必须声明 `show-overflow-tooltip`:
38
+
39
+ ```vue
40
+ <el-table-column
41
+ prop="customerName"
42
+ label="客户名称"
43
+ min-width="160"
44
+ align="center"
45
+ show-overflow-tooltip
46
+ />
47
+ ```
48
+
49
+ BaseTable / AG Grid 列必须通过 `defineColumns()` 声明。普通文本列会自动补
50
+ `showOverflowTooltip: true`;自定义渲染列、selection/index/expand、`wrapText`
51
+ 或 `autoHeight` 列不会被强制。业务可显式设置 `showOverflowTooltip: false`
52
+ 退出。
53
+
54
+ ---
55
+
35
56
  ## 规则 R004:操作列使用 renderOps / jh-op-btn
36
57
 
37
58
  操作列按钮**不得**直接使用 `<el-button>` 或裸文本,必须通过 `renderOps` 渲染:
@@ -11,6 +11,11 @@
11
11
 
12
12
  ### 类型规则
13
13
 
14
+ **R038 强制约束**:凡按钮文案表达“新增 / 新建 / 添加 / 创建”主操作,
15
+ 必须使用 `type="primary"` 的填充样式,不得漏写 `type`,也不得同时设置
16
+ `plain`。只有明确属于行内次级交互时,才允许显式改用 `link` / `text`。
17
+ `wl-ui scan` 会报错,`wl-ui fix` 可机械纠正静态按钮。
18
+
14
19
  主按钮色值严格遵循客户 UI 规范,禁止使用 `#2254F4`、`#4368FF` 或 Element Plus 默认蓝替代:
15
20
 
16
21
  | 状态 | Token | 规范色值 |
@@ -36,7 +41,7 @@
36
41
 
37
42
  ```vue
38
43
  <!-- ✅ 工具栏按钮组 -->
39
- <el-button type="primary" @click="handleCreate">新增</el-button>
44
+ <el-button type="primary" icon="Plus" @click="handleCreate">新增</el-button>
40
45
  <el-button type="warning" plain icon="Edit" @click="handleEdit">修改</el-button>
41
46
  <el-button type="success" icon="Save" @click="handleSave">保存</el-button>
42
47
  <el-button icon="Download" @click="handleExport">导出</el-button>
@@ -44,6 +49,21 @@
44
49
 
45
50
  ---
46
51
 
52
+ ## BaseToolbar 分裂下拉按钮
53
+
54
+ 当一个工具栏动作同时包含主动作 `onClick` 与子动作 `children` 时,BaseToolbar
55
+ 会生成 split-button。它在交互语义上仍是两个点击区域,但视觉上必须表现为一个
56
+ 完整按钮:
57
+
58
+ - 主动作与箭头段共用同一背景色、hover/focus/active 状态和外轮廓。
59
+ - 箭头段不得拥有独立圆角、外边距、阴影或 Element Plus 默认分割线。
60
+ - `action-dropdown-menu` 中的 jh-button 必须扁平为菜单内容,边框和背景由
61
+ `el-dropdown-menu__item` 统一承担。
62
+ - 上述规则不得扩散到普通 `el-button-group`、普通 `el-dropdown-menu` 或页面
63
+ 自定义编辑器工具栏。
64
+
65
+ ---
66
+
47
67
  ## 操作列按钮(表格行内)
48
68
 
49
69
  统一使用 `renderOps` 图标按钮系统,**禁止** `<el-button>` 在行内使用:
@@ -67,6 +67,31 @@ textarea 不强制 26px 高度,只继承统一圆角、字体和状态样式
67
67
 
68
68
  ---
69
69
 
70
+ ## 规则:紧凑业务表单字号统一为 12px
71
+
72
+ 页面正文仍使用 14px;业务表单的 label、输入值、选择值、picker、textarea、
73
+ 数字输入框和 placeholder 统一使用:
74
+
75
+ ```scss
76
+ --wk-form-font-size: 12px;
77
+ ```
78
+
79
+ 该规则同时覆盖 Element Plus 2.2 直挂 input DOM、2.3+ wrapper DOM 与 jh-* 封装,
80
+ 登录页及 `.wl-ui-skin-exempt` / `data-wl-ui-skin="off"` 定制页不参与覆盖。
81
+
82
+ ---
83
+
84
+ ## 规则:textarea 与数字输入框只保留一层状态边框
85
+
86
+ - textarea 默认使用中性 1px 边框,hover 使用 hover 边框色,focus 强制使用品牌色
87
+ 1px 实线与轻外环,不能只依赖平台已有 box-shadow。
88
+ - 数字输入框兼容 `.el-input-number > .el-input__wrapper` 和
89
+ `.el-input-number.el-input__wrapper` 两种 DOM;由 wrapper 层绘制唯一边框,
90
+ 内部 input 不得再画第二层边框。
91
+ - error / disabled 状态继续分别使用危险色和禁用色,不得被 focus 覆盖。
92
+
93
+ ---
94
+
70
95
  ## 规则:表单 label 不强制冒号
71
96
 
72
97
  Element Plus 原生控件、picker 类控件和 jh-\* 封装控件的 label 后不强制追加 `:`。`@jhlc/jh-ui` 的 `.has-colon .com-text:after` / `.text-line-2:after` 冒号注入由 wl-skills-ui 统一屏蔽,避免 input / select / picker 之间出现有的带冒号、有的不带冒号。
@@ -5,6 +5,7 @@
5
5
  html body {
6
6
  --wk-form-control-radius: var(--el-border-radius-base, 6px);
7
7
  --wk-form-control-height: 26px;
8
+ --wk-form-font-size: 12px !important;
8
9
  --wk-form-label-control-gap: 16px;
9
10
  --wk-form-label-line-height: 22px;
10
11
  }
@@ -24,6 +25,31 @@ html body {
24
25
  padding-right: var(--wk-form-label-control-gap) !important;
25
26
  }
26
27
 
28
+ // ── 紧凑业务表单字号统一 ──────────────────────────────────────────────────
29
+ // 正文仍使用 14px;这里只治理表单 label、控件值与 placeholder。
30
+ &.el-form-item__label,
31
+ &.el-form-item__label > .com-text,
32
+ &.el-form-item__label .com-text,
33
+ &.el-form-item__label > .text-line-2,
34
+ &.el-form-item__label .text-line-2,
35
+ &.el-input__inner,
36
+ &.el-textarea__inner,
37
+ &.el-select__selected-item,
38
+ &.el-select__placeholder,
39
+ &.el-select__tags-text,
40
+ &.el-date-editor .el-range-input,
41
+ &.el-date-editor .el-range-separator,
42
+ &.el-radio__label,
43
+ &.el-checkbox__label {
44
+ font-size: var(--wk-form-font-size, 12px) !important;
45
+ }
46
+
47
+ &.el-input__inner::placeholder,
48
+ &.el-textarea__inner::placeholder,
49
+ &.el-date-editor .el-range-input::placeholder {
50
+ font-size: var(--wk-form-font-size, 12px) !important;
51
+ }
52
+
27
53
  &.el-input__wrapper,
28
54
  &.el-select__wrapper,
29
55
  &.el-select .el-select__wrapper,
@@ -47,9 +73,10 @@ html body {
47
73
  line-height: calc(var(--wk-form-control-height) - 2px) !important;
48
74
  }
49
75
 
50
- // common-core InputNumberComponent 即使传入 controls=false,部分版本仍会
51
- // 输出 controls-right 步进按钮。数值框保留 Element Plus 的 wrapper 作为唯一
52
- // 可见边框,根节点仅负责尺寸,避免 wrapper 聚焦阴影与根边框叠成双层描边。
76
+ // common-core / jh-ui 有两种数字框 DOM:
77
+ // A. .el-input-number > .el-input > .el-input__wrapper(社区版)
78
+ // B. .el-input-number.el-input__wrapper > .el-input(jh 同节点 wrapper
79
+ // 两种结构都只允许 wrapper 层绘制唯一可见边框。
53
80
  &.el-input-number,
54
81
  &.el-input-number:focus,
55
82
  &.el-input-number:focus-within {
@@ -61,6 +88,11 @@ html body {
61
88
  outline: none !important;
62
89
  box-sizing: border-box !important;
63
90
  background: transparent !important;
91
+ }
92
+
93
+ &.el-input-number:not(.el-input__wrapper),
94
+ &.el-input-number:not(.el-input__wrapper):focus,
95
+ &.el-input-number:not(.el-input__wrapper):focus-within {
64
96
  box-shadow: none !important;
65
97
  }
66
98
 
@@ -73,7 +105,8 @@ html body {
73
105
  box-shadow: none !important;
74
106
  }
75
107
 
76
- &.el-input-number .el-input__wrapper {
108
+ &.el-input-number .el-input__wrapper,
109
+ &.el-input-number.el-input__wrapper {
77
110
  width: 100% !important;
78
111
  height: 100% !important;
79
112
  min-height: 0 !important;
@@ -86,23 +119,35 @@ html body {
86
119
  transition: box-shadow 0.15s ease !important;
87
120
  }
88
121
 
89
- &.el-input-number:not(.is-disabled):hover .el-input__wrapper {
122
+ &.el-input-number:not(.is-disabled):hover .el-input__wrapper,
123
+ &.el-input-number.el-input__wrapper:not(.is-disabled):hover {
90
124
  box-shadow:
91
125
  0 0 0 1px var(--el-border-color-hover, #c0c4cc) inset !important;
92
126
  }
93
127
 
94
- &.el-input-number .el-input__wrapper.is-focus,
95
- &.el-input-number:focus-within .el-input__wrapper {
128
+ &.el-input-number:not(.is-disabled) .el-input__wrapper.is-focus,
129
+ &.el-input-number:not(.is-disabled):focus-within .el-input__wrapper,
130
+ &.el-input-number.el-input__wrapper:not(.is-disabled).is-focus,
131
+ &.el-input-number.el-input__wrapper:not(.is-disabled):focus,
132
+ &.el-input-number.el-input__wrapper:not(.is-disabled):focus-within {
96
133
  box-shadow:
97
134
  0 0 0 1.5px var(--el-color-primary, #002a8f) inset !important;
98
135
  }
99
136
 
100
- &.el-input-number.is-disabled .el-input__wrapper {
137
+ &.el-input-number.is-disabled .el-input__wrapper,
138
+ &.el-input-number.el-input__wrapper.is-disabled {
101
139
  background: var(--el-disabled-bg-color, #f5f7fa) !important;
102
140
  box-shadow:
103
141
  0 0 0 1px var(--el-disabled-border-color, #e4e7ed) inset !important;
104
142
  }
105
143
 
144
+ // 极少数过渡版本会同时输出“同节点 wrapper + 子 wrapper”;
145
+ // 此时由外层绘制边框,明确清掉内层,防止双描边。
146
+ &.el-input-number.el-input__wrapper > .el-input > .el-input__wrapper {
147
+ background: transparent !important;
148
+ box-shadow: none !important;
149
+ }
150
+
106
151
  &.el-input-number .el-input__inner,
107
152
  &.el-input-number .el-input__inner:focus,
108
153
  &.el-input-number .el-input__inner:focus-visible {
@@ -139,7 +184,35 @@ html body {
139
184
  -moz-appearance: textfield !important;
140
185
  }
141
186
 
142
- &.el-textarea__inner {
187
+ // ── textarea 默认 / hover / focus ─────────────────────────────────────────
188
+ // focus 使用“品牌色实线 + 轻外环”,不依赖平台原有 border-width/box-shadow。
189
+ &.el-textarea > .el-textarea__inner,
190
+ &.com-input > .el-textarea > .el-textarea__inner,
191
+ &.com-textarea > .el-textarea > .el-textarea__inner {
192
+ border: 1px solid var(--el-border-color, #dcdfe6) !important;
143
193
  border-radius: var(--wk-form-control-radius) !important;
194
+ box-shadow: none !important;
195
+ transition:
196
+ border-color 0.15s ease,
197
+ box-shadow 0.15s ease !important;
198
+ }
199
+
200
+ &.el-textarea:not(.is-disabled) > .el-textarea__inner:hover:not(:focus),
201
+ &.com-input > .el-textarea:not(.is-disabled) > .el-textarea__inner:hover:not(:focus),
202
+ &.com-textarea > .el-textarea:not(.is-disabled) > .el-textarea__inner:hover:not(:focus) {
203
+ border-color: var(--el-border-color-hover, #c0c4cc) !important;
204
+ }
205
+
206
+ &.el-textarea.is-focus > .el-textarea__inner,
207
+ &.el-textarea > .el-textarea__inner:focus,
208
+ &.el-textarea > .el-textarea__inner:focus-visible,
209
+ &.com-input > .el-textarea.is-focus > .el-textarea__inner,
210
+ &.com-input > .el-textarea > .el-textarea__inner:focus,
211
+ &.com-textarea > .el-textarea.is-focus > .el-textarea__inner,
212
+ &.com-textarea > .el-textarea > .el-textarea__inner:focus {
213
+ border: 1px solid var(--el-color-primary, #002a8f) !important;
214
+ outline: none !important;
215
+ box-shadow:
216
+ 0 0 0 2px rgba(var(--el-color-primary-rgb, 0, 42, 143), 0.12) !important;
144
217
  }
145
218
  }
@@ -12,6 +12,15 @@ html body .el-table {
12
12
  --el-table-tr-bg-color: var(--el-fill-color-lighter, #fafafa);
13
13
  }
14
14
 
15
+ // 仅命中 show-overflow-tooltip 生成的单元格,不压制 Tag/操作列等自定义内容。
16
+ .el-table__body .cell.el-tooltip {
17
+ min-width: 0;
18
+ max-width: 100%;
19
+ overflow: hidden;
20
+ text-overflow: ellipsis;
21
+ white-space: nowrap;
22
+ }
23
+
15
24
  .el-table__empty-block {
16
25
  display: flex;
17
26
  align-items: center;
@@ -119,6 +119,7 @@
119
119
  }
120
120
 
121
121
  .ag-cell-value {
122
+ min-width: 0;
122
123
  overflow: hidden;
123
124
  text-overflow: ellipsis;
124
125
  white-space: nowrap;
@@ -453,20 +453,11 @@
453
453
  &.el-textarea {
454
454
  .el-textarea__inner {
455
455
  border-radius: 6px !important;
456
- font-size: 13px !important;
456
+ font-size: var(--wk-form-font-size, 12px) !important;
457
457
  line-height: 1.65 !important;
458
458
  padding: 7px 10px !important;
459
459
  resize: vertical !important; // 只允许垂直拉伸,不允许水平
460
460
  transition: border-color 0.15s, box-shadow 0.15s;
461
-
462
- &:hover:not(:focus) {
463
- border-color: var(--el-border-color-hover, #c0c4cc) !important;
464
- }
465
-
466
- &:focus {
467
- border-color: var(--el-color-primary) !important;
468
- box-shadow: 0 0 0 2px rgba(var(--el-color-primary-rgb), 0.12) !important;
469
- }
470
461
  }
471
462
 
472
463
  // 字数统计:更小、更淡、不遮挡内容
@@ -495,8 +486,7 @@
495
486
  &.el-select__wrapper.is-focused,
496
487
  &.el-select__wrapper:focus-within,
497
488
  &.el-date-editor.el-input__wrapper.is-focus,
498
- &.el-date-editor.el-input__wrapper:focus-within,
499
- &.el-textarea__inner:focus {
489
+ &.el-date-editor.el-input__wrapper:focus-within {
500
490
  box-shadow: 0 0 0 1.5px var(--el-color-primary, #002a8f) inset !important;
501
491
  }
502
492
 
@@ -514,13 +504,13 @@
514
504
  &.el-form.el-form--label-left .el-form-item__label,
515
505
  &.el-form.el-form--label-right .el-form-item__label {
516
506
  align-items: baseline !important;
517
- font-size: 12px !important;
507
+ font-size: var(--wk-form-font-size, 12px) !important;
518
508
  line-height: var(--wk-form-label-line-height, 22px) !important;
519
509
  }
520
510
 
521
511
  &.el-form.el-form--label-left .el-form-item__label > .com-text,
522
512
  &.el-form.el-form--label-right .el-form-item__label > .com-text {
523
- font-size: 12px !important;
513
+ font-size: var(--wk-form-font-size, 12px) !important;
524
514
  line-height: var(--wk-form-label-line-height, 22px) !important;
525
515
  }
526
516
 
@@ -529,7 +519,7 @@
529
519
  &.el-form.el-form--label-right .el-form-item.is-required:not(.is-no-asterisk) > .el-form-item__label::before {
530
520
  content: "*" !important;
531
521
  color: var(--el-color-danger, #f56c6c) !important;
532
- font-size: 12px !important;
522
+ font-size: var(--wk-form-font-size, 12px) !important;
533
523
  font-weight: 400 !important;
534
524
  line-height: var(--wk-form-label-line-height, 22px) !important;
535
525
  display: inline-block !important;
@@ -545,7 +535,7 @@
545
535
  &.el-form.el-form--label-right .el-form-item.is-required:not(.is-no-asterisk) > .el-form-item__label > .com-text::before {
546
536
  content: "*" !important;
547
537
  color: var(--el-color-danger, #f56c6c) !important;
548
- font-size: 12px !important;
538
+ font-size: var(--wk-form-font-size, 12px) !important;
549
539
  font-weight: 400 !important;
550
540
  line-height: var(--wk-form-label-line-height, 22px) !important;
551
541
  display: inline-block !important;
@@ -572,7 +562,7 @@
572
562
  &.el-form.el-form--label-right .el-form-item.is-required:not(.is-no-asterisk).asterisk-right > .el-form-item__label::after {
573
563
  content: "*" !important;
574
564
  color: var(--el-color-danger, #f56c6c) !important;
575
- font-size: 12px !important;
565
+ font-size: var(--wk-form-font-size, 12px) !important;
576
566
  font-weight: 400 !important;
577
567
  line-height: var(--wk-form-label-line-height, 22px) !important;
578
568
  display: inline-block !important;
@@ -606,6 +596,20 @@
606
596
  .el-textarea__inner {
607
597
  box-shadow: 0 0 0 1px var(--el-color-danger, #f56c6c) inset !important;
608
598
  }
599
+
600
+ // InputNumber 的 focus 兼容规则权重较高;非禁用 error 必须最终覆盖 focus。
601
+ .el-input-number:not(.is-disabled) .el-input__wrapper,
602
+ .el-input-number.el-input__wrapper:not(.is-disabled) {
603
+ box-shadow:
604
+ 0 0 0 1px var(--el-color-danger, #f56c6c) inset !important;
605
+ }
606
+
607
+ // 过渡版双 wrapper 由外层表达 error,内层不得出现第二道危险色边框。
608
+ .el-input-number.el-input__wrapper:not(.is-disabled)
609
+ > .el-input
610
+ > .el-input__wrapper {
611
+ box-shadow: none !important;
612
+ }
609
613
  }
610
614
  }
611
615
 
@@ -354,7 +354,7 @@
354
354
  cursor: not-allowed !important;
355
355
  }
356
356
 
357
- // ── 下拉菜单按钮(工具栏内的 el-dropdown 触发器)────────────────────────────
357
+ // ── 工具栏下拉 / 分裂按钮 ──────────────────────────────────────────────────
358
358
  .base-toolbar-box .el-dropdown > .el-button,
359
359
  .base-toolbar-box .el-button-group .el-button {
360
360
  height: 24px !important;
@@ -363,25 +363,68 @@
363
363
  font-weight: 500;
364
364
  }
365
365
 
366
- .base-toolbar-box .el-button-group {
366
+ // BaseToolbar 仅在“主动作有 onClick 且还有 children”时生成 split-button
367
+ // 精确限定 action-button-wrap,避免影响页面自行编排的普通 el-button-group。
368
+ .base-toolbar-box
369
+ .action-button-wrap
370
+ > .el-dropdown
371
+ > .el-button-group {
367
372
  display: inline-flex !important;
368
373
  align-items: center !important;
369
374
  vertical-align: middle !important;
375
+ gap: 0 !important;
376
+ overflow: hidden;
377
+ border-radius: var(--el-border-radius-base) !important;
370
378
 
371
- .el-button {
379
+ > .el-button {
372
380
  border-radius: 0 !important;
373
381
  margin: 0 !important;
382
+ box-shadow: none !important;
374
383
  }
375
384
 
376
- .el-button:first-child {
385
+ > .el-button:first-child {
377
386
  border-top-left-radius: var(--el-border-radius-base) !important;
378
387
  border-bottom-left-radius: var(--el-border-radius-base) !important;
388
+ border-top-right-radius: 0 !important;
389
+ border-bottom-right-radius: 0 !important;
390
+ border-right-color: transparent !important;
379
391
  }
380
392
 
381
- .el-button:last-child {
393
+ > .el-button.el-dropdown__caret-button {
394
+ width: 24px !important;
395
+ min-width: 24px !important;
396
+ padding: 0 !important;
397
+ margin-left: 0 !important;
398
+ border-left: 0 !important;
399
+ border-top-left-radius: 0 !important;
400
+ border-bottom-left-radius: 0 !important;
382
401
  border-top-right-radius: var(--el-border-radius-base) !important;
383
402
  border-bottom-right-radius: var(--el-border-radius-base) !important;
384
- margin-left: -1px !important;
403
+
404
+ // Element Plus 默认用 ::before 画分割线,会让箭头看起来像独立色块。
405
+ &::before {
406
+ display: none !important;
407
+ content: none !important;
408
+ }
409
+ }
410
+
411
+ // 鼠标或键盘进入任一分段时,两段同步使用同一状态色,视觉保持一个按钮。
412
+ &:hover > .el-button:not(.is-disabled):not([disabled]),
413
+ &:focus-within > .el-button:not(.is-disabled):not([disabled]) {
414
+ color: var(--el-button-hover-text-color) !important;
415
+ border-color: var(--el-button-hover-border-color) !important;
416
+ background-color: var(--el-button-hover-bg-color) !important;
417
+ }
418
+
419
+ &:hover > .el-button:first-child:not(.is-disabled):not([disabled]),
420
+ &:focus-within > .el-button:first-child:not(.is-disabled):not([disabled]) {
421
+ border-right-color: transparent !important;
422
+ }
423
+
424
+ &:active > .el-button:not(.is-disabled):not([disabled]) {
425
+ color: var(--el-button-active-text-color, #ffffff) !important;
426
+ border-color: var(--el-button-active-border-color) !important;
427
+ background-color: var(--el-button-active-bg-color) !important;
385
428
  }
386
429
  }
387
430
 
@@ -393,23 +436,84 @@
393
436
  margin: 0 !important;
394
437
  }
395
438
 
396
- .el-dropdown-menu .el-dropdown-menu__item {
439
+ // BaseToolbar V1 会在 action-dropdown-menu 的 item 内再渲染一个 jh-button。
440
+ // 只扁平化这个已知结构,不影响普通 el-dropdown 和业务自定义下拉。
441
+ html body .el-dropdown-menu.action-dropdown-menu .el-dropdown-menu__item {
397
442
  display: flex;
398
443
  align-items: center;
399
444
  gap: 6px;
400
445
  min-height: 28px;
401
446
  line-height: 1.3;
447
+ padding: 0 !important;
402
448
  color: var(--el-text-color-regular, #606266);
403
449
  }
404
450
 
405
- .el-dropdown-menu .el-dropdown-menu__item:hover,
406
- .el-dropdown-menu .el-dropdown-menu__item:focus {
451
+ html
452
+ body
453
+ .el-dropdown-menu.action-dropdown-menu
454
+ .el-dropdown-menu__item
455
+ > .el-button {
456
+ width: 100% !important;
457
+ height: 28px !important;
458
+ min-height: 28px !important;
459
+ justify-content: flex-start !important;
460
+ margin: 0 !important;
461
+ padding: 0 10px !important;
462
+ border: 0 !important;
463
+ border-radius: inherit !important;
464
+ color: inherit !important;
465
+ background: transparent !important;
466
+ box-shadow: none !important;
467
+ font-size: 12px !important;
468
+ font-weight: 400 !important;
469
+ line-height: 1.3 !important;
470
+ transform: none !important;
471
+ }
472
+
473
+ html
474
+ body
475
+ .el-dropdown-menu.action-dropdown-menu
476
+ .el-dropdown-menu__item
477
+ > .el-button:hover,
478
+ html
479
+ body
480
+ .el-dropdown-menu.action-dropdown-menu
481
+ .el-dropdown-menu__item
482
+ > .el-button:focus,
483
+ html
484
+ body
485
+ .el-dropdown-menu.action-dropdown-menu
486
+ .el-dropdown-menu__item
487
+ > .el-button:active {
488
+ color: inherit !important;
489
+ border: 0 !important;
490
+ background: transparent !important;
491
+ box-shadow: none !important;
492
+ transform: none !important;
493
+ }
494
+
495
+ html
496
+ body
497
+ .el-dropdown-menu.action-dropdown-menu
498
+ .el-dropdown-menu__item:not(.is-disabled):hover,
499
+ html
500
+ body
501
+ .el-dropdown-menu.action-dropdown-menu
502
+ .el-dropdown-menu__item:not(.is-disabled):focus {
407
503
  color: var(--el-color-primary, #002a8f) !important;
408
504
  background-color: rgba(var(--el-color-primary-rgb), 0.08) !important;
409
505
  }
410
506
 
411
- .el-dropdown-menu .el-dropdown-menu__item .el-icon,
412
- .el-dropdown-menu .el-dropdown-menu__item svg {
507
+ html
508
+ body
509
+ .el-dropdown-menu.action-dropdown-menu
510
+ .el-dropdown-menu__item
511
+ .el-icon,
512
+ html
513
+ body
514
+ .el-dropdown-menu.action-dropdown-menu
515
+ .el-dropdown-menu__item
516
+ svg {
413
517
  color: currentColor !important;
414
518
  fill: currentColor;
415
519
  flex: 0 0 auto;
@@ -24,7 +24,7 @@ columnsDef(): TableColumnDesc<any>[] {
24
24
 
25
25
  // ── 数据列(已注册字段自动映射,无需 defaultNode)──
26
26
  { name: 'riskNo', label: '编号', width: 100 }, // → renderBadge
27
- { name: 'name', label: '名称', minWidth: 150 },
27
+ { name: 'name', label: '名称', minWidth: 150 }, // → 自动省略 + 悬停完整提示
28
28
  { name: 'riskLevel', label: '风险分级', width: 90 }, // → renderRiskLevel Tag
29
29
  { name: 'riskStatus', label: '状态', width: 90 }, // → renderRiskStatus Tag
30
30
 
@@ -74,3 +74,4 @@ toolbarDef() {
74
74
  2. 操作列用 `renderOps([...])` — 图标按钮 + 自动 stopPropagation
75
75
  3. selection 列必须同时设置 `align: 'center'` + `headerAlign: 'center'`(R014)
76
76
  4. 分类字段(type/level)用 `renderClassifyTag`,状态字段用 `renderTagNode`
77
+ 5. `defineColumns()` 自动为普通文本列补 `showOverflowTooltip: true`;自定义渲染、结构列和换行列除外
@@ -107,6 +107,7 @@ const modal = ref();
107
107
  function columnsDef() {
108
108
  return defineColumns([
109
109
  { type: "index", label: "序号", width: 60, align: "center" },
110
+ // 普通文本列由 defineColumns 自动补 showOverflowTooltip: true
110
111
  { name: "name", label: "名称", minWidth: 120 },
111
112
  { name: "status", label: "状态", width: 80 }, // enableStatus 已自动映射
112
113
  { name: "remark", label: "备注", minWidth: 160 },
@@ -141,3 +142,5 @@ async function handleDel(id: string) {
141
142
  3. `enableStatus` 字段在 `installCommonPreset()` 后自动渲染为彩色 Tag,无需手写 defaultNode
142
143
  4. 工具栏按钮必须带 `icon`(R005 规则)
143
144
  5. 标准顺序固定为“查询区 → 工具栏 → 列表标题 → 表格 → 分页器”;工具栏不得放到标题右侧或标题下方
145
+ 6. 新增/新建/添加/创建类主按钮必须 `type="primary"` 且不得使用 `plain`(R038)
146
+ 7. 普通文本列由 `defineColumns()` 自动启用省略号与悬停完整提示;原生 `el-table-column` 必须加 `show-overflow-tooltip`(R039)