@qilitt-mickey/vue3-temp-skill 1.1.47 → 1.1.49
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/mapping/component/overlay.json +16 -0
- package/mapping/scenes/admin/content.json +106 -10
- package/mapping/scenes/admin/layout.json +53 -7
- package/package.json +1 -1
- package/parser/fixtures/changeset-fullpage-restore.json +1 -1
- package/parser/fixtures/registry-sample.json +2 -2
- package/parser/index.js +464 -18
- package/references/design-apply.md +62 -5
|
@@ -58,6 +58,22 @@
|
|
|
58
58
|
"hover_color": "--el-pagination-hover-color"
|
|
59
59
|
},
|
|
60
60
|
"custom_style_map": {}
|
|
61
|
+
},
|
|
62
|
+
"component.tooltip.base": {
|
|
63
|
+
"type": "global_component",
|
|
64
|
+
"component": "ElTooltip(原生)/ v-tippy(指令)",
|
|
65
|
+
"style_scope": ":root",
|
|
66
|
+
"css_vars": {
|
|
67
|
+
"bg_color": "--el-tooltip-bg-color",
|
|
68
|
+
"text_color": "--el-tooltip-color"
|
|
69
|
+
},
|
|
70
|
+
"custom_style_map": {
|
|
71
|
+
"radius": "border-radius",
|
|
72
|
+
"padding": "padding",
|
|
73
|
+
"font_size": "font-size",
|
|
74
|
+
"max_width": "max-width"
|
|
75
|
+
},
|
|
76
|
+
"$note": "全局提示气泡。项目内提示有**两条来路**——Element Plus 原生 ElTooltip(继承 --el-tooltip-* 令牌,如 ReAvatars 的 el-tooltip)与 v-tippy 指令(ReTableBar / ReCollapsible / ReAnchor / lay-tag 等处使用,走 tippy 主题,**不自动继承 --el-* 令牌**)。本条目面向 ElTooltip;若设计要求覆盖 v-tippy 气泡,须先确认 tippy 主题变量与本节令牌的对应关系,未映射时按「补通道」补主题变量并在交付说明单列申报(**不得假设自动生效**)。本条目为补建通道——此前登记表标 pending。"
|
|
61
77
|
}
|
|
62
78
|
}
|
|
63
79
|
}
|
|
@@ -73,13 +73,26 @@
|
|
|
73
73
|
},
|
|
74
74
|
"admin.content.table.operation_bar": {
|
|
75
75
|
"type": "element",
|
|
76
|
-
"selector": ".vts-
|
|
77
|
-
"file": "
|
|
76
|
+
"selector": ".vts-page-card .flex-between > .flex-end(ReTableBar 内部工具栏行右侧容器,bar.vue JSX 实证 div.flex-end)",
|
|
77
|
+
"file": "条件通道实证(2026-09-20 复核):.vts-action-bar 类**全项目无模板挂载**(仅 element-plus.scss 有样式定义、无消费者)——历史 selector 锚定 .vts-action-bar 属「应该在」而非「实际在」,导致设计层规则全部落空(工具钮裸排事故)。本 selector 改锚真实出厂 DOM;.vts-action-bar 作为可选增强通道保留在 element-plus.scss,页面按需自行挂载后设计层规则自动命中。右侧工具三件套源码实证:刷新 svg-icon + el-dropdown 密度(宽松/默认/紧凑)+ el-popover 列设置",
|
|
78
78
|
"style_scope": ".vts-page-card",
|
|
79
79
|
"forms": {
|
|
80
80
|
"with_tools": "工具三件套(刷新/密度/列设置)齐全——设计规范 list-page §4 要求三件套缺一不可(ReTableBar 出厂已具备,此为默认形态)",
|
|
81
|
+
"tools_labeled": "边框文字钮形态:三件套从「纯图标 + 竖分隔线」换装为「图标 + 文案」的 1px 边框钮(高/圆角/字号走 tools_* 字段族,hover 文字与边框变主色)——出厂 svg-icon 结构保留,文案由业务侧 slots 或 AI 增量插入 span 补齐(结构增量,不删原节点);suppress 必含 tool_divider(出厂两条 el-divider direction=vertical 与边框钮观感互斥,不收口 = 图标钮与边框钮混排)",
|
|
81
82
|
"left_batch": "左侧批量操作区(批量删除红底 + 批量导出白底,未选中置灰 opacity 0.4 + cursor not-allowed)——经 slots.buttons 插入,出厂多数 demo 未用"
|
|
82
83
|
},
|
|
84
|
+
"suppress_rules": {
|
|
85
|
+
"tools_labeled": ["tool_divider"]
|
|
86
|
+
},
|
|
87
|
+
"field_forms": {
|
|
88
|
+
"tools_btn_height": ["tools_labeled"],
|
|
89
|
+
"tools_btn_radius": ["tools_labeled"],
|
|
90
|
+
"tools_font_size": ["tools_labeled"],
|
|
91
|
+
"tools_icon_size": ["tools_labeled"],
|
|
92
|
+
"tools_text_color": ["tools_labeled"],
|
|
93
|
+
"tools_hover_color": ["tools_labeled"],
|
|
94
|
+
"tools_btn_gap": ["tools_labeled"]
|
|
95
|
+
},
|
|
83
96
|
"css_vars": {
|
|
84
97
|
"gap": "--action-bar-gap",
|
|
85
98
|
"padding_y": "--action-bar-padding-y"
|
|
@@ -89,24 +102,34 @@
|
|
|
89
102
|
"btn_radius": "border-radius",
|
|
90
103
|
"btn_font_size": "font-size",
|
|
91
104
|
"batch_delete_bg": "background-color",
|
|
92
|
-
"batch_disabled_opacity": "opacity"
|
|
105
|
+
"batch_disabled_opacity": "opacity",
|
|
106
|
+
"tools_btn_height": "height",
|
|
107
|
+
"tools_btn_radius": "border-radius",
|
|
108
|
+
"tools_font_size": "font-size",
|
|
109
|
+
"tools_btn_gap": "gap"
|
|
93
110
|
},
|
|
94
111
|
"anchors": [
|
|
95
|
-
{ "kind": "class", "layer": "inner", "target": ".vts-
|
|
96
|
-
{ "kind": "class", "layer": "inner", "target": ".vts-
|
|
112
|
+
{ "kind": "class", "layer": "inner", "target": ".vts-page-card .flex-between > .flex-end" },
|
|
113
|
+
{ "kind": "class", "layer": "inner", "target": ".vts-page-card .flex-end .el-button" },
|
|
114
|
+
{ "kind": "class", "layer": "inner", "target": ".vts-page-card .flex-end .vts-tool-btn" },
|
|
97
115
|
{ "kind": "css_var", "target": "--action-bar-gap" },
|
|
98
116
|
{ "kind": "css_var", "target": "--action-bar-padding-y" }
|
|
99
117
|
],
|
|
118
|
+
"sub_selectors": {
|
|
119
|
+
"$comment": "工具三件套部件点位(bar.vue JSX 实证):刷新 svg-icon[name=refresh]、密度 el-dropdown>svg-icon[name=collapse]、列设置 el-popover reference 槽 svg-icon[name=tools];出厂右侧容器 = 工具栏行 .flex-between 的 .flex-end 子容器。form=tools_labeled 时按钮壳与文案按此定位增量插入",
|
|
120
|
+
"tools": ".vts-page-card .flex-between > .flex-end",
|
|
121
|
+
"tool_btn": ".vts-page-card .flex-end .vts-tool-btn"
|
|
122
|
+
},
|
|
100
123
|
"preconditions": [
|
|
101
|
-
"
|
|
102
|
-
"
|
|
124
|
+
"挂载点实证(2026-09-20 复核修正):出厂右侧容器是 .flex-between > .flex-end(真实存在),历史锚点 .vts-action-bar 全项目无模板挂载(仅样式定义、零消费者)——设计层规则写到 .vts-action-bar 上全部落空(工具钮裸排事故根因)。词典 selector 必须锚定源码实证存在的 DOM;「应该在」的点位未经 structure_ops 补挂 = 不存在。",
|
|
125
|
+
"本条目样式落点是 ReTableBar 内部工具栏行内容器——禁止把容器级样式(flex-direction 等)写到 ReTableBar 根节点:根节点是「工具栏行 + 表格 + 分页」的纵向复合容器,写根节点会让工具栏与表格并排、表格被挤压、tableHeight 实测失准(已发生的事故,勿复发)。",
|
|
103
126
|
"gap / padding 的 CSS 变量必须在项目样式中存在消费者(var() 引用),只定义不消费 = 死通道。--action-bar-padding-y 供 padding-block,最多两个值(如 20px 8px),给三个值整条声明即失效。"
|
|
104
127
|
],
|
|
105
128
|
"priority": {
|
|
106
129
|
"strategy": "scope_qualified",
|
|
107
|
-
"reason": "出厂 .
|
|
130
|
+
"reason": "出厂 .flex-end 类位 1(uno 约定类,无 scoped 竞争),作用域限定到 .vts-page-card 前缀即可覆盖;但不得把容器级样式写到 ReTableBar 根节点上。"
|
|
108
131
|
},
|
|
109
|
-
"$note": "设计意图(list-page §4/§11.25-11.36):flex 两端对齐、pad 20px 20px 8px、左侧批量(未选中置灰 opacity 0.4 + not-allowed)、右侧工具三件套缺一不可。分工:本条目管容器布局与置灰;批量删除红色按钮本体走 component.button.danger。demo
|
|
132
|
+
"$note": "设计意图(list-page §4/§11.25-11.36):flex 两端对齐、pad 20px 20px 8px、左侧批量(未选中置灰 opacity 0.4 + not-allowed)、右侧工具三件套缺一不可。分工:本条目管容器布局与置灰;批量删除红色按钮本体走 component.button.danger。demo 噪音按钮去留属业务层,交付说明申报后由项目侧业务闭环移除。⚠tools_labeled 形态(效果图口径):三件套为「图标16px+文案」32px 边框钮(border 1px、radius 6px、gap 6px、hover 文字/边框主色),与出厂「纯图标20px+竖分隔线」是**形态互斥**——声明 tools_labeled 必须同时 suppress tool_divider(隐藏出厂 el-divider),并按结构增量给每个 svg-icon 套 .vts-tool-btn 按钮壳 + 文案 span(结构施工,design-apply 底线二豁免)。"
|
|
110
133
|
},
|
|
111
134
|
"admin.content.table.operation_column": {
|
|
112
135
|
"type": "element",
|
|
@@ -123,6 +146,12 @@
|
|
|
123
146
|
"divider_color": "--re-op-divider-color",
|
|
124
147
|
"divider_gap": "--re-op-divider-gap"
|
|
125
148
|
},
|
|
149
|
+
"field_forms": {
|
|
150
|
+
"slot_width": ["text_link", "icon_text"]
|
|
151
|
+
},
|
|
152
|
+
"data_channels": {
|
|
153
|
+
"slot_width": "等宽槽宽度通道(⚠2026-09-20 语义重写·动态优先):**槽宽默认由组件按按钮文案动态自适应**(ReTableOperate 的 --re-op-slot-w 在 slotMinWidth 省略时走 resolveOperateSlotWidthPx 自动推算),变更集**不声明 slot_width、业务页不传 slot-min-width** ——设计只声明 divider 几何(width/height/margin_x),槽宽随内容走、divider_margin_x 对称由槽内居中兑现(历史事故①:声明 60px 钉死槽宽违反动态优先;历史事故②:槽内 flex-start 使短文案按钮残白全堆到分割线前侧 → 线前 40px/线后 8px 视觉不等距,第五轮截图实证)。仅当设计效果图确实要求**统一槽宽**(如所有操作列等宽对齐网格)时例外:变更集显式声明 slot_width → 施工落业务页 prop(结构通道),禁 CSS 仿冒;无此明确设计意图 = 保持动态。等宽槽本体是对齐机制(槽边界保证行间第 N 项对齐),机制本身禁 CSS 收口 width:auto"
|
|
154
|
+
},
|
|
126
155
|
"$note": "操作列分割线为**条件通道**:出厂组件当前**没有**分割线元素——声明 divider_color / divider_gap 时,必须先按 structure_ops 在 ReTableOperate 模板按钮间 add_node `<span class=\"re-table-operate__divider\">`(中性元素,几何与颜色全走令牌:width/height/gap/color 分别挂 --re-op-divider-width/height/gap/color,出厂缺省 0 不可见),并同步给 resolveOperateColumnWidth 公式补分割线占位((limit-1)×(width+2×gap)),否则最后按钮被裁——元素与公式缺一即申报补通道失败。列宽由组件公式推算,业务页禁止写死 width——列宽属业务层,设计侧不声明列宽(操作列除外)。",
|
|
127
156
|
"custom_style_map": {
|
|
128
157
|
"align": "text-align",
|
|
@@ -132,6 +161,7 @@
|
|
|
132
161
|
"divider_width": "width",
|
|
133
162
|
"divider_height": "height",
|
|
134
163
|
"divider_margin_x": "margin-inline",
|
|
164
|
+
"divider_color": "background-color",
|
|
135
165
|
"more_arrow_size": "font-size",
|
|
136
166
|
"more_arrow_gap": "gap",
|
|
137
167
|
"vertical_align": "vertical-align",
|
|
@@ -145,9 +175,17 @@
|
|
|
145
175
|
{ "kind": "class", "layer": "host", "target": ".vts-table-area .el-table__cell:has(.re-table-operate)" },
|
|
146
176
|
{ "kind": "class", "layer": "inner", "target": ".vts-table-area .el-table__cell:has(.re-table-operate) > .cell" }
|
|
147
177
|
],
|
|
178
|
+
"sub_selectors": {
|
|
179
|
+
"$comment": "divider_* 字段按前缀路由到分割线元素本体(出厂平铺按钮间 v-if=\"i > 0\" 已有 .re-table-operate__divider;「更多」槽前无元素须按 structure_ops 增量补挂)",
|
|
180
|
+
"divider": ".vts-page-card .re-table-operate .re-table-operate__divider"
|
|
181
|
+
},
|
|
148
182
|
"preconditions": [
|
|
149
183
|
"按钮之间的间距由分割线元素的左右 margin 承载,不是按钮自身 gap:必须先存在分割线元素(ReTableOperate 默认不渲染 divider,需按 structure_ops 补槽位),divider_width / divider_height / divider_margin_x 才有落点——这是「操作列按钮间距没还原」的根因。",
|
|
150
|
-
"「纯文字无图标」是数据侧结论:图标来自业务页列定义的 operateButtons[].icon,只写 form=text_link 而不移除数据里的 icon = 未落地(改 CSS 不算)。"
|
|
184
|
+
"「纯文字无图标」是数据侧结论:图标来自业务页列定义的 operateButtons[].icon,只写 form=text_link 而不移除数据里的 icon = 未落地(改 CSS 不算)。",
|
|
185
|
+
"「更多」折叠槽前必须有分割线:组件出厂模板只在平铺按钮之间渲染 divider(v-if=\"i > 0\"),平铺末位与「更多」槽之间无分割线元素——效果图口径是每个按钮之间都要有且等距,施工时须按 structure_ops 给「更多」槽前补 divider(或把「更多」并入 v-for 让 i 计数覆盖),缺线 = 最后一段间距失真(历史缺口:查看│编辑 更多 之间无线)。",
|
|
186
|
+
"等宽槽(--re-op-slot-w)是行间纵向对齐的唯一机制:设计层禁止覆写 .re-table-operate__slot 宽度为 auto,CSS 收口 auto = 各行按钮漂移、竖线间距不一致(历史事故:孔雀绿 160px 紧凑型被 CSS 收口 auto)。",
|
|
187
|
+
"⚠槽宽三态判定(2026-09-20 通用编排,与 data_channels.slot_width 配套):①**默认=动态自适应**——组件按最长文案自动推算槽宽,变更集不声明 slot_width、业务页不传 slot-min-width(设计只管 divider 几何;钉死固定值 = 短文案按钮残白堆到分割线前侧、线距视觉不等,60px 事故实证);②**例外=统一槽宽**——设计效果图明确所有操作列等宽网格时才声明 slot_width,施工落业务页 prop 禁 CSS 仿冒;③**机制不可 CSS 化**——无论哪态,禁在设计层覆写 __slot 宽度。判据一句话:**槽宽是内容推算的机制值,不是设计声明值;设计声明divider,不声明槽宽**。",
|
|
188
|
+
"⚠对齐语义两级契约(2026-09-20 第五轮截图复核,替代第四轮单级结论):**容器级 flex-start + 槽内 center**。①容器级(.re-table-operate)必须 justify-content: flex-start——按钮数不同的行(2 项 vs 3 项)整组从左起点排布,行组不漂移;容器级 center 是历史误结论,禁用(各行各自居中 → 槽边界漂移、竖线竖向错位)。②槽内级(.re-table-operate__slot 与槽内 .el-button)必须 justify-content: center——等宽槽宽按最长文案推算(如「更多」3 字=60px),2 字按钮(28px)靠左会把 32px 残白全堆到分割线前侧 → 线前 40px/线后 8px 视觉不等距(第五轮截图实证:竖线贴右按钮);居中后 divider_margin_x 两侧对称才成立。行间第 N 项对齐由**等宽槽边界**保证(槽宽全行统一),不受槽内对齐影响——第四轮「槽内也 flex-start」是把容器级结论过度泛化到槽级,勿复发。"
|
|
151
189
|
],
|
|
152
190
|
"priority": {
|
|
153
191
|
"strategy": "scope_qualified",
|
|
@@ -215,6 +253,7 @@
|
|
|
215
253
|
],
|
|
216
254
|
"preconditions": [
|
|
217
255
|
"出厂 element-plus.scss 已给 .el-table .el-table__cell 设了 padding(var(--vts-space-xs) 0),本条目字段必须落 host 层才能覆盖它;写在 inner 层只改内层文本盒,观感上「改了没反应」。",
|
|
256
|
+
"几何语境随值迁移:padding/padding_x/padding_y 落宿主 td 时必须同步声明 suppress_inner(内层 .cell padding 清零口径)——出厂内层 .cell 自带 padding 0 12px,不清零则两层之和 ≠ 设计值(28px/侧 → 列挤坏)。",
|
|
218
257
|
"行高由「单元格 padding + 内容行高」共同决定;声明行高档位时必须同时给出 padding 取值,只改其一会得到非预期行高。"
|
|
219
258
|
],
|
|
220
259
|
"priority": {
|
|
@@ -308,6 +347,63 @@
|
|
|
308
347
|
},
|
|
309
348
|
"$note": "设计意图(§11.108-11.109):列宽 72px 居中,缩略盒 40×40 圆角 6px。落地手段:列 className=col-thumb + el-image 40×40"
|
|
310
349
|
},
|
|
350
|
+
"admin.content.table.selection_column": {
|
|
351
|
+
"type": "element",
|
|
352
|
+
"selector": ".vts-table-area .el-table__cell.el-table-column--selection",
|
|
353
|
+
"file": "业务 views(el-table-column type=\"selection\";Element Plus 给选择列表头与单元格自动加 el-table-column--selection 类,无需业务自定义类名)",
|
|
354
|
+
"style_scope": ".vts-page-card",
|
|
355
|
+
"css_vars": {
|
|
356
|
+
"checked_bg": "--el-checkbox-checked-bg-color",
|
|
357
|
+
"checked_border": "--el-checkbox-checked-input-border-color",
|
|
358
|
+
"hover_border": "--el-checkbox-input-border-color-hover"
|
|
359
|
+
},
|
|
360
|
+
"custom_style_map": {
|
|
361
|
+
"text_align": "text-align",
|
|
362
|
+
"padding": "padding"
|
|
363
|
+
},
|
|
364
|
+
"anchors": [
|
|
365
|
+
{ "kind": "class", "layer": "host", "target": ".vts-table-area .el-table__cell.el-table-column--selection" },
|
|
366
|
+
{ "kind": "class", "layer": "inner", "target": ".vts-table-area .el-table__cell.el-table-column--selection > .cell" },
|
|
367
|
+
{ "kind": "css_var", "target": "--el-checkbox-checked-bg-color" }
|
|
368
|
+
],
|
|
369
|
+
"preconditions": [
|
|
370
|
+
"选择列由 el-table-column type=\"selection\" 渲染,Element Plus 自动在表头与单元格上加 el-table-column--selection 类——列未声明 type=selection 时本条目永不命中(项目源码实证:views/table/base/index.vue 已用 <el-table-column type=\"selection\" reserve-selection />)。",
|
|
371
|
+
"列宽与其他业务列同属**业务层**(设计侧不声明列宽);本条目只承载对齐与勾选态配色。"
|
|
372
|
+
],
|
|
373
|
+
"priority": {
|
|
374
|
+
"strategy": "scope_qualified",
|
|
375
|
+
"reason": "选择列无行内样式竞争,作用域限定到 .vts-page-card 即可覆盖。"
|
|
376
|
+
},
|
|
377
|
+
"$note": "设计意图(list-page §11.44 选择列):窄列、居中、勾选态用主题主色。本条目为**补建通道**——此前登记表标 pending(项目侧无承载点位),选择列样式无处可落;勾选色走 EP 原生 --el-checkbox-* 令牌,随主题切换。"
|
|
378
|
+
},
|
|
379
|
+
"admin.content.table.row_density": {
|
|
380
|
+
"type": "element",
|
|
381
|
+
"selector": ".vts-table-area .el-table .el-table__cell",
|
|
382
|
+
"file": "src/components/ReTableBar/src/bar.vue(密度下拉:宽松/默认/紧凑 → el-table size = large/default/small)+ src/styles/element-plus.scss(单元格 padding 承载行高)",
|
|
383
|
+
"style_scope": ".vts-page-card",
|
|
384
|
+
"forms": {
|
|
385
|
+
"density": ["loose", "default", "compact"]
|
|
386
|
+
},
|
|
387
|
+
"css_vars": {},
|
|
388
|
+
"custom_style_map": {
|
|
389
|
+
"row_height": "min-height",
|
|
390
|
+
"cell_padding_y": "padding-block"
|
|
391
|
+
},
|
|
392
|
+
"anchors": [
|
|
393
|
+
{ "kind": "class", "layer": "host", "target": ".vts-table-area .el-table .el-table__cell" },
|
|
394
|
+
{ "kind": "class", "layer": "inner", "target": ".vts-table-area .el-table .el-table__cell > .cell" }
|
|
395
|
+
],
|
|
396
|
+
"preconditions": [
|
|
397
|
+
"行高由「单元格 padding + 内容行高」共同决定(见 admin.content.table.cell)——声明行高档位时必须同时给出 padding 取值,只改其一会得到非预期行高。",
|
|
398
|
+
"几何语境随值迁移:padding_x 落宿主 th/td 时必须同步声明 suppress_inner(内层 .cell padding 清零口径)——出厂内层 .cell 自带 padding 0 12px,不清零则两层之和 ≠ 设计值。",
|
|
399
|
+
"出厂密度档位由 ReTableBar 的 size(large/default/small,源码实证 bar.vue 的宽松/默认/紧凑三项)驱动;设计侧只声明档位意图(form.density),具体 padding 由本项目按常规取值落定,不得照抄效果图某实例数据的行高。"
|
|
400
|
+
],
|
|
401
|
+
"priority": {
|
|
402
|
+
"strategy": "scope_qualified",
|
|
403
|
+
"reason": "单元格 padding 属全局样式,作用域限定到 .vts-page-card 即可覆盖。"
|
|
404
|
+
},
|
|
405
|
+
"$note": "设计意图(list-page §11.40 行密度):表格行高三档取一(宽松/默认/紧凑)。本条目为**补建通道**——此前登记表标 pending;语义档位 loose/default/compact ↔ 出厂 size large/default/small,项目侧负责映射。"
|
|
406
|
+
},
|
|
311
407
|
"admin.content.table.area": {
|
|
312
408
|
"type": "component",
|
|
313
409
|
"component": "ElTable(useTableSearch tableHeight 定高)",
|
|
@@ -55,10 +55,25 @@
|
|
|
55
55
|
"style_scope": ".sidebar-container",
|
|
56
56
|
"forms": {
|
|
57
57
|
"pill": "整块胶囊底色高亮(sidebar.scss 消费 --vts-theme-menu-active-bg,带 !important)。suppress 必含 right_indicator_legacy——出厂 element-plus.scss 的 .el-menu-item.is-active{border-right:var(--menu-active-border-right)} 是活通道,不收口会出现左右两根竖条并存",
|
|
58
|
-
"indicator_left": "
|
|
59
|
-
"indicator_right": "
|
|
60
|
-
"indicator_underline": "
|
|
61
|
-
"none": "无指示:胶囊与竖条均关闭,仅文字变色。suppress 必含 right_indicator_legacy"
|
|
58
|
+
"indicator_left": "仅左竖条指示、无胶囊:先在本作用域覆写 --vts-theme-menu-active-bg: transparent 关胶囊,再于设计层新建 ::after 竖条消费 --vts-theme-menu-active-before。suppress 必含 right_indicator_legacy",
|
|
59
|
+
"indicator_right": "仅右竖条指示、无胶囊:同上关胶囊后 ::after 挂 right:0。出厂右竖条通道 --menu-active-border-right(element-plus.scss 消费、design-tokens.scss 定义为 3px solid 品牌色)是活通道,可复用也可收口后自建,二选一必须显式声明",
|
|
60
|
+
"indicator_underline": "仅底部横线指示、无胶囊:关胶囊后 ::after 挂 bottom:0(宽取 100% 或指定宽度,厚取 indicator_thickness)。suppress 必含 right_indicator_legacy",
|
|
61
|
+
"none": "无指示:胶囊与竖条均关闭,仅文字变色。suppress 必含 right_indicator_legacy",
|
|
62
|
+
"pill_indicator_left": "胶囊底色 + 左竖条**并存**复合形态(设计实证:效果图 .menu-item.active 同时声明 background:var(--primary) 与 ::before 竖条):胶囊照 pill 走 --vts-theme-menu-active-bg;竖条照 indicator_left 走 --vts-theme-menu-active-before(设计层新建 ::after/::before 消费者)。suppress 必含 right_indicator_legacy(出厂右竖条必须收口)。⚠菜单项宿主盒必须声明 margin 横向内缩(custom_style_map margin_x),否则竖条负偏移落进容器外被 overflow 裁掉——竖条偏移值以「容器 padding − 竖条 left 偏移」为语境换算,禁止照搬外部效果图常量"
|
|
63
|
+
},
|
|
64
|
+
"suppress_rules": {
|
|
65
|
+
"pill": ["right_indicator_legacy"],
|
|
66
|
+
"indicator_left": ["right_indicator_legacy"],
|
|
67
|
+
"indicator_right": ["right_indicator_legacy"],
|
|
68
|
+
"indicator_underline": ["right_indicator_legacy"],
|
|
69
|
+
"none": ["right_indicator_legacy"],
|
|
70
|
+
"pill_indicator_left": ["right_indicator_legacy"]
|
|
71
|
+
},
|
|
72
|
+
"field_forms": {
|
|
73
|
+
"active_indicator_width": ["indicator_left", "indicator_right", "indicator_underline", "pill_indicator_left"],
|
|
74
|
+
"active_indicator_thickness": ["indicator_left", "indicator_right", "indicator_underline", "pill_indicator_left"],
|
|
75
|
+
"active_indicator_style": ["indicator_left", "indicator_right", "indicator_underline", "pill_indicator_left"],
|
|
76
|
+
"menu_active_bg": ["pill", "pill_indicator_left"]
|
|
62
77
|
},
|
|
63
78
|
"css_vars": {
|
|
64
79
|
"menu_bg": "--vts-theme-menu-bg",
|
|
@@ -78,6 +93,7 @@
|
|
|
78
93
|
"active_indicator_thickness": "height",
|
|
79
94
|
"active_indicator_style": "border-radius",
|
|
80
95
|
"padding_y": "padding-block",
|
|
96
|
+
"margin_x": "margin-inline",
|
|
81
97
|
"font_size": "font-size",
|
|
82
98
|
"font_weight": "font-weight",
|
|
83
99
|
"text_color": "color"
|
|
@@ -96,7 +112,7 @@
|
|
|
96
112
|
"strategy": "deep_qualified",
|
|
97
113
|
"reason": "出厂 sidebar.scss 的胶囊规则用 !important,设计层需以同等或更高权重覆盖。"
|
|
98
114
|
},
|
|
99
|
-
"$note": "侧栏配色挂载点为 html[data-theme='xxx'](theme.scss 8 套预设);设计输出深色侧栏 → Theme=default + Version++(useTheme 规则),浅色 → Theme=light。⚠文本完整性红线:菜单项名称必须完整显示(el-menu 自带 ellipsis 折叠溢出菜单),禁止改 menu 文本截断/隐藏策略;item_height 加大时确认行高不挤压文字。宽度类改动(--sidebar-width)后必须目视验证长菜单名显示。⚠激活态出厂中性基线(禁止把某张图的结论写进源码):出厂形态 = pill 胶囊高亮;--vts-theme-menu-active-before 出厂默认 transparent 且**全项目无消费者**(死通道)。因此 form=indicator_* 时必须在本条目 style_scope 内成对落地两件事才算施工完成——①
|
|
115
|
+
"$note": "侧栏配色挂载点为 html[data-theme='xxx'](theme.scss 8 套预设);设计输出深色侧栏 → Theme=default + Version++(useTheme 规则),浅色 → Theme=light。⚠文本完整性红线:菜单项名称必须完整显示(el-menu 自带 ellipsis 折叠溢出菜单),禁止改 menu 文本截断/隐藏策略;item_height 加大时确认行高不挤压文字。宽度类改动(--sidebar-width)后必须目视验证长菜单名显示。⚠激活态出厂中性基线(禁止把某张图的结论写进源码):出厂形态 = pill 胶囊高亮;--vts-theme-menu-active-before 出厂默认 transparent 且**全项目无消费者**(死通道)。因此 form=indicator_* / pill_indicator_left 时必须在本条目 style_scope 内成对落地两件事才算施工完成——① 竖条消费者(form 不含 pill 时另需覆写 --vts-theme-menu-active-bg: transparent 关胶囊);② 在设计层文件(不可写出厂 sidebar.scss)新建 ::after/::before 消费者引用 var(--vts-theme-menu-active-before)。缺任一项 = 「映射了却没效果」。所有几何参数(方位/粗细/长度/圆角/内缩)取自变更集 style 字段,禁止写死 left:0 / 3px 等常量。⚠field_forms 硬闸(parser G7 机检):style 声明的字段必须被当前 form 消费——如 active_indicator_* 只在 indicator_* / pill_indicator_left 形态下有消费者,声明 pill 却带竖条字段 = 字段被静默丢弃(历史事故:孔雀绿变更集声明 pill + active_indicator_*,设计层按 pill 施工把竖条令牌置透明,效果图要求的左竖条消失)。"
|
|
100
116
|
},
|
|
101
117
|
"admin.layout.sidebar.collapse_trigger": {
|
|
102
118
|
"type": "element",
|
|
@@ -107,6 +123,10 @@
|
|
|
107
123
|
"bottom_bar": "SidebarLeftCollapse(底部条,出厂默认)",
|
|
108
124
|
"center_edge": "SidebarCenterCollapse(侧缘中点,折叠/悬停时显示)"
|
|
109
125
|
},
|
|
126
|
+
"suppress_rules": {
|
|
127
|
+
"center_edge": ["bottom_bar"],
|
|
128
|
+
"bottom_bar": ["center_edge"]
|
|
129
|
+
},
|
|
110
130
|
"css_vars": {
|
|
111
131
|
"bar_height": "--sidebar-collapse-bar-height"
|
|
112
132
|
},
|
|
@@ -190,6 +210,9 @@
|
|
|
190
210
|
"icon_only": "出厂现状:纯图标触发器(.language-trigger > .language-icon),下拉菜单选语言",
|
|
191
211
|
"inline_switch": "并排切换形态(下拉项平铺为触发器旁的切换组)"
|
|
192
212
|
},
|
|
213
|
+
"suppress_rules": {
|
|
214
|
+
"inline_switch": ["icon_only"]
|
|
215
|
+
},
|
|
193
216
|
"css_vars": {
|
|
194
217
|
"icon_color": "--el-text-color-regular",
|
|
195
218
|
"hover_bg": "--el-fill-color-light"
|
|
@@ -216,6 +239,9 @@
|
|
|
216
239
|
"sidebar_bottom": "出厂现状:侧栏底部折叠条(SidebarLeftCollapse)",
|
|
217
240
|
"sidebar_center": "侧栏侧缘中点(SidebarCenterCollapse)"
|
|
218
241
|
},
|
|
242
|
+
"suppress_rules": {
|
|
243
|
+
"header_inline": ["sidebar_bottom", "sidebar_center"]
|
|
244
|
+
},
|
|
219
245
|
"css_vars": {
|
|
220
246
|
"icon_color": "--el-text-color-regular",
|
|
221
247
|
"hover_bg": "--el-fill-color-light"
|
|
@@ -273,6 +299,11 @@
|
|
|
273
299
|
"avatar_only": "仅头像",
|
|
274
300
|
"name_only": "仅用户名"
|
|
275
301
|
},
|
|
302
|
+
"suppress_rules": {
|
|
303
|
+
"greeting_name": ["avatar_name"],
|
|
304
|
+
"avatar_only": ["avatar_name"],
|
|
305
|
+
"name_only": ["avatar_name"]
|
|
306
|
+
},
|
|
276
307
|
"sub_selectors": {
|
|
277
308
|
"$comment": "avatar/name 为点位内部部件,不设独立语义 ID(符合 4 段 ID 上限);样式字段按前缀路由到对应选择器。form 含 greeting 时新增节点统一用 class 前缀 vts-header-greeting / vts-header-username(插入前先查重名)",
|
|
278
309
|
"avatar": ".navbar-right .el-dropdown-link img",
|
|
@@ -315,6 +346,18 @@
|
|
|
315
346
|
"card": "卡片页签(ShowModel=card):圆角卡片底 + --vts-tag-card-* 整组令牌 + 关闭钮 hover 才显现(出厂 opacity:0)",
|
|
316
347
|
"flat": "平面页签(设计规范口径,出厂无此形态):ShowModel 仍设 card(复用 card 结构 = 图标槽 + 关闭钮 + resolveCardTabIcon 数据驱动),平面观感全部由设计层覆写达成 —— ① --vts-tag-card-idle-bg/hover-bg/active-bg 置 transparent;② --vts-tag-card-radius 与 --vts-tag-card-gap 置 0;③ 激活态仅文字/图标变主色 + font-weight 500,不加 border-bottom / box-shadow;④ 关闭钮 opacity 覆写为常显值(出厂 card 为 0);⑤ 首页页签项宽=高=--vts-tag-card-height 居中呈方块;⑥ 页签间 1px×100% 贯穿分割线(card 无该元素,需设计层 ::after/::before 新建)。几何与色值一律取自变更集 style 字段,禁止写死常量"
|
|
317
348
|
},
|
|
349
|
+
"suppress_rules": {
|
|
350
|
+
"flat": [
|
|
351
|
+
"card_idle_bg",
|
|
352
|
+
"card_active_bg",
|
|
353
|
+
"card_hover_bg",
|
|
354
|
+
"card_radius",
|
|
355
|
+
"card_gap",
|
|
356
|
+
"card_active_text",
|
|
357
|
+
"card_idle_text",
|
|
358
|
+
"card_close_color"
|
|
359
|
+
]
|
|
360
|
+
},
|
|
318
361
|
"css_vars": {
|
|
319
362
|
"height": "--tabs-height",
|
|
320
363
|
"chrome_inset_top": "--vts-tag-chrome-inset-top",
|
|
@@ -339,13 +382,16 @@
|
|
|
339
382
|
{ "kind": "class", "layer": "inner", "target": ".tags-view .scroll-container" }
|
|
340
383
|
],
|
|
341
384
|
"preconditions": [
|
|
342
|
-
"容器内边距出厂由组件样式给出(非 0),设计要求「内边距归零、首页页签与折叠按钮左对齐」时必须显式声明 padding_inline,不声明即保留出厂值(这是「首页页签不贴左缘」的根因)。"
|
|
385
|
+
"容器内边距出厂由组件样式给出(非 0),设计要求「内边距归零、首页页签与折叠按钮左对齐」时必须显式声明 padding_inline,不声明即保留出厂值(这是「首页页签不贴左缘」的根因)。",
|
|
386
|
+
"⚠ShowModel 运行时缓存坑:showModel 经 responsive-storage 持久化于 localStorage(命名空间 responsive-),platform-config.json 改 ShowModel 后**旧缓存仍生效**——项目侧施工 ShowModel 时必须同步清 localStorage(或 Version++ 触发响应式存储失效),否则模板代码已换、运行时仍渲染旧形态,页签「样式完全不对」多半是缓存残留而非映射失败。施工后验证口径:浏览器 DevTools Application→Local Storage 清 responsive-* 键后刷新再比对。",
|
|
387
|
+
"⚠ShowModel 三分支运行时兜底(2026-09-20 截图复核):useTags.ts 实证 showModel 读取链 = localStorage → fallback **'smart'**(platform-config 的 ShowModel 只在 storage 初始化时生效);而模板 DOM 只实现 chrome / card 两分支(v-if / v-else-if),smart 落入 v-else 裸分支——**无 .card-item / .card-tab 类**,锚定 card 结构的设计层规则全部不命中,仅锚定 .scroll-item 的规则(分割线等)命中 → 同一页签栏「半新半旧」混合观感(截图实证:底色块残留 + 分割线在)。因此 form=flat 施工必须**三分支全覆盖**:① 锚定 .card-item / .card-tab 的规则之外,同步给出锚定 .scroll-item 直系(不限形态类)的兜底规则(盒几何/底色透明/激活态),或在词典 preconditions 声明「ShowModel 须确保运行时为 card」(清缓存 + platform-config 双写)——二选一必须做,只写 card 锚点 = smart 残余分支裸奔。",
|
|
388
|
+
"⚠ShowModel 三件套硬闸(2026-09-20 复盘升级,G4 T 通道施工必须项):仅改 platform-config.ShowModel **不生效**——showModel 读 localStorage,而 localStorage 只在 platform-config.Version 变化时经 responsive.ts invalidateStorageIfConfigVersionChanged 重灌(历史事故:孔雀绿还原只改了 ShowModel=card,Version 停留 0.0.5,浏览器残留 chrome 形态缓存 → card/smart 兜底规则双双不命中、页签零变化)。form 涉及页签形态时施工三件套缺一不可:① platform-config.ShowModel = form 对应值(flat→card);② platform-config.Version 自增一位(0.0.5→0.0.6,触发缓存失效重灌);③ 渲染取证断言 .scroll-item.card-item 实际挂载(DevTools 或 DOM 断言),不许只看代码改没改。漏②=代码对但运行时仍旧形态(本轮实证);漏③=无渲染证据自嗨绿。"
|
|
343
389
|
],
|
|
344
390
|
"priority": {
|
|
345
391
|
"strategy": "deep_qualified",
|
|
346
392
|
"reason": "容器内边距规则写在组件 scoped 内,与设计层同权重,需显式提权。"
|
|
347
393
|
},
|
|
348
|
-
"$note": "showModel 由 platform-config.json ShowModel
|
|
394
|
+
"$note": "showModel 由 platform-config.json ShowModel 字段声明、但运行时实值由 localStorage 决定(Version++ 才重灌)——施工按 preconditions 三件套执行;smart/line 模板未实现,映射层按 chrome/card 两形态处理"
|
|
349
395
|
},
|
|
350
396
|
"admin.layout.tabs.home_tab": {
|
|
351
397
|
"type": "element",
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
{ "id": "admin.layout.header", "style": { "bg": "#FFFFFF" } },
|
|
29
|
-
{ "id": "admin.layout.header.lang_switch", "form": "inline_switch" },
|
|
29
|
+
{ "id": "admin.layout.header.lang_switch", "form": "inline_switch", "suppress": ["icon_only"] },
|
|
30
30
|
{
|
|
31
31
|
"id": "admin.layout.header.user_profile",
|
|
32
32
|
"style": { "avatar_width": "28px", "avatar_height": "28px", "avatar_radius": "全圆角", "name_font_size": "14px", "name_font_weight": "中等" }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "回归夹具 · 登记表样例(不是真实交付件)。6 条登记项示范粒度与字段口径:4 条 always(恒存在,必登)、
|
|
2
|
+
"$comment": "回归夹具 · 登记表样例(不是真实交付件)。6 条登记项示范粒度与字段口径:4 条 always(恒存在,必登)、2 条条件项。用于 M/N 两步:M 校验覆盖率重算能报出欠声明并 exit 1,N 校验通道核验能跑通。⚠ selection_column / cell 等点位词典已补建通道,本夹具不再示范 pending 标注(pending 演示见 registry-bad.json 的硬缺口反例)。",
|
|
3
3
|
"schema": "design.restore-registry/1.0",
|
|
4
4
|
"design_scheme": "default",
|
|
5
5
|
"scene": "admin",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{ "rule": "sample#2", "group": "样例·表格区", "sid": "admin.content.table.cell", "dims": ["padding", "padding_y"], "channel": ["style", "layer"], "source": "both", "always": true },
|
|
10
10
|
{ "rule": "sample#3", "group": "样例·金额列", "sid": "admin.content.table.cell_amount", "dims": ["align"], "channel": ["style", "structure_ops"], "source": "spec", "always": true },
|
|
11
11
|
{ "rule": "sample#4", "group": "样例·状态列", "sid": "admin.content.table.cell_status", "dims": ["form", "bg"], "channel": ["form", "structure_ops"], "source": "spec", "always": true },
|
|
12
|
-
{ "rule": "sample#5", "group": "样例·选择列", "sid": "admin.content.table.selection_column", "dims": ["
|
|
12
|
+
{ "rule": "sample#5", "group": "样例·选择列", "sid": "admin.content.table.selection_column", "dims": ["text_align", "padding"], "channel": ["style"], "source": "spec", "always": false },
|
|
13
13
|
{ "rule": "sample#6", "group": "样例·状态列", "sid": "component.tag.status", "dims": ["bg_color"], "channel": ["style"], "source": "spec", "always": false }
|
|
14
14
|
]
|
|
15
15
|
}
|
package/parser/index.js
CHANGED
|
@@ -45,6 +45,13 @@ const COMPOSITE_VALUE_RE = /\s/;
|
|
|
45
45
|
const LAYER_VALUES = ["host", "inner"];
|
|
46
46
|
/** 语义 ID 严格格式(apply_via 锚点名须符合) */
|
|
47
47
|
const ID_RE_STRICT = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){1,3}$/;
|
|
48
|
+
/** 内层盒属性族(changeset-schema §layer):这些属性落在 inner 才生效(G5 几何语境闸使用) */
|
|
49
|
+
const INNER_ONLY_PROPS = new Set(["white_space", "text_overflow", "overflow", "word_break", "text_align", "line_height", "vertical_align", "nowrap", "text_align_last"]);
|
|
50
|
+
/** 几何字段(宿主内边距/外边距族):落 host 时与出厂内层盒自带 padding 叠加,必须同步清零内层(G5) */
|
|
51
|
+
const GEOMETRY_PAD_RE = /^(padding|padding_x|padding_y|padding_inline|padding_block|margin|margin_x|margin_y|margin_inline|margin_block)$/;
|
|
52
|
+
/** 登记表里唯一允许的「非词典点位」:全局令牌通道(对应变更集顶层 global 节点,无 css selector 故不入词典)。
|
|
53
|
+
* ⚠ 必须在 main() 调用前初始化(registry/match 命令运行时即访问;下移会触发 TDZ ReferenceError) */
|
|
54
|
+
const GLOBAL_PSEUDO_ID = "global";
|
|
48
55
|
|
|
49
56
|
/** audit:审计时跳过的目录(依赖 / 产物 / 缓存) */
|
|
50
57
|
const AUDIT_SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".output", ".vite", ".nuxt", "coverage", "public"]);
|
|
@@ -84,7 +91,7 @@ function main() {
|
|
|
84
91
|
case "audit":
|
|
85
92
|
return cmdAudit(args, dataset);
|
|
86
93
|
case "verify":
|
|
87
|
-
return cmdVerify(args);
|
|
94
|
+
return cmdVerify(args, dataset);
|
|
88
95
|
case "registry":
|
|
89
96
|
return cmdRegistry(args, dataset);
|
|
90
97
|
default:
|
|
@@ -104,6 +111,8 @@ function printHelp() {
|
|
|
104
111
|
node parser/index.js audit --project <dir> [<cs.json…>] 挂载点存在性 + 死通道静态审计(需目标项目源码)
|
|
105
112
|
node parser/index.js verify --oracle <o.json> --actual <a.json> [--sids a,b]
|
|
106
113
|
渲染态逐点验收:computed style 期望 vs 实测 diff
|
|
114
|
+
node parser/index.js verify --oracle <o.json> --resolve [--sids a,b]
|
|
115
|
+
实测端定位计划:sid → 词典 selector(项目页不打 data-sid 时按此抽取)
|
|
107
116
|
node parser/index.js verify … --from-theme 钴蓝 --to-theme 橘橙
|
|
108
117
|
同构异色复用:按语义槽位把效果图色值归一为本主题色值再比(配合 oracle.theme_mapping)
|
|
109
118
|
node parser/index.js registry <registry.json> [--verbose]
|
|
@@ -269,6 +278,7 @@ function matchOne(file, dataset, batchSummary, opts = {}) {
|
|
|
269
278
|
const plan = [];
|
|
270
279
|
const pending = [];
|
|
271
280
|
const fieldPending = []; // 字段级待补充:ID 命中但 style 字段两层映射均未命中
|
|
281
|
+
const gateViolations = []; // 词典驱动硬闸:suppress 收口(G1)+ 几何语境(G5),违规 = 协议违规同级阻断
|
|
272
282
|
|
|
273
283
|
// global 令牌翻译(design-apply 第 3 步:走 basic/ 词典,主色另需 setEpThemeColor 链路)
|
|
274
284
|
if (changeset.global && Object.keys(changeset.global).length > 0) {
|
|
@@ -347,12 +357,18 @@ function matchOne(file, dataset, batchSummary, opts = {}) {
|
|
|
347
357
|
if (p.level === 1) {
|
|
348
358
|
console.log(`\n[L1 完整命中] ${p.id}`);
|
|
349
359
|
checkFormEnum(p.entry, p.change);
|
|
360
|
+
checkSuppress(p.change, p.entry, p.id, gateViolations);
|
|
361
|
+
checkGeometryContext(p.change, p.entry, p.id, gateViolations);
|
|
362
|
+
checkFieldForms(p.change, p.entry, p.id, gateViolations);
|
|
350
363
|
printApplyPlan(p.entry, p.change, p.id, index);
|
|
351
364
|
printStyleTranslation(p.entry, p.change, p.id, fieldPending, valueDict);
|
|
352
365
|
}
|
|
353
366
|
else if (p.level === 2) {
|
|
354
367
|
console.log(`\n[L2 父模块命中] ${p.id} → 继承 ${p.parent}(${p.entry.selector ?? p.entry.component ?? "?"})`);
|
|
355
368
|
console.log(` 结构:定位父容器按相对位置增量插入;样式:复用父模块映射 + 全局组件映射`);
|
|
369
|
+
checkSuppress(p.change, p.entry, p.id, gateViolations);
|
|
370
|
+
checkGeometryContext(p.change, p.entry, p.id, gateViolations);
|
|
371
|
+
checkFieldForms(p.change, p.entry, p.id, gateViolations);
|
|
356
372
|
printApplyPlan(p.entry, p.change, `${p.id}(继承 ${p.parent})`, index);
|
|
357
373
|
printStyleTranslation(p.entry, p.change, `${p.id}(继承 ${p.parent})`, fieldPending, valueDict);
|
|
358
374
|
}
|
|
@@ -381,6 +397,18 @@ function matchOne(file, dataset, batchSummary, opts = {}) {
|
|
|
381
397
|
const l3 = plan.filter(p => p.level === 3).length;
|
|
382
398
|
console.log(`\n汇总:L1=${l1}(还原度 100%) L2=${l2}(85-90%) L3=${l3}(70-80%,补映射后可达 100%)`);
|
|
383
399
|
|
|
400
|
+
// 词典驱动硬闸(suppress 收口 / 几何语境):违规 = 交付不完整,与禁词同级阻断
|
|
401
|
+
if (gateViolations.length > 0) {
|
|
402
|
+
console.error(`\n✖ 词典硬闸违规(共 ${gateViolations.length} 处 → 拒绝执行):`);
|
|
403
|
+
for (const v of gateViolations)
|
|
404
|
+
console.error(` ✘ ${v}`);
|
|
405
|
+
console.error(` 处置:回变更集补齐 suppress / suppress_inner / layer 声明后重跑;判据均来自词典条目自身,不接受「我观感上对了」。`);
|
|
406
|
+
batchSummary?.invalid.push(path.basename(file));
|
|
407
|
+
if (!batchSummary)
|
|
408
|
+
process.exit(1);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
384
412
|
// 汇入跨文件统计(项目级批量模式)
|
|
385
413
|
if (batchSummary) {
|
|
386
414
|
batchSummary.l1 += l1;
|
|
@@ -532,6 +560,10 @@ function printStyleTranslation(entry, change, idLabel, fieldPending, valueDict)
|
|
|
532
560
|
const layerLabel = spot.layer ? ` · ${spot.layer} 层` : "";
|
|
533
561
|
lines.push(` . ${field}: ${valueLabel} → 自定义 ${entry.custom_style_map[field]}(@ ${spot.target}${layerLabel})`);
|
|
534
562
|
}
|
|
563
|
+
else if (entry.data_channels && entry.data_channels[field]) {
|
|
564
|
+
// 数据/结构通道:值走业务页 prop / 数据 / 模板(禁 CSS 仿冒),非缺口
|
|
565
|
+
lines.push(` . ${field}: ${valueLabel} → 数据/结构通道(${entry.data_channels[field].slice(0, 60)}…)`);
|
|
566
|
+
}
|
|
535
567
|
else {
|
|
536
568
|
unmatched.push(field);
|
|
537
569
|
continue;
|
|
@@ -703,6 +735,25 @@ function checkOracle(changeset, plan, valueDict) {
|
|
|
703
735
|
const hiddenOnly = new Set(
|
|
704
736
|
plan.filter(p => p.change && p.change.visible === false).map(p => p.id),
|
|
705
737
|
);
|
|
738
|
+
// 实物闭合兜底(G3):带登记表时由 checkRegistryCoverage 做权威实物对账;
|
|
739
|
+
// 未带登记表时此处直接打开 mockup 数 data-sid,防止「sids 自报全绿、实物基线为空」。
|
|
740
|
+
if (!changeset.coverage?.registry && mockup) {
|
|
741
|
+
const cands = [path.resolve(mockup), path.resolve(ROOT, mockup), path.resolve(process.cwd(), mockup)];
|
|
742
|
+
let physical = null;
|
|
743
|
+
for (const c of cands) {
|
|
744
|
+
try {
|
|
745
|
+
const stripped = readFileSync(c, "utf-8").replace(/<!--[\s\S]*?-->/g, "");
|
|
746
|
+
physical = new Set([...stripped.matchAll(/data-sid\s*=\s*["']([^"']+)["']/g)].map(m => m[1]));
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
catch { /* 试下一个候选路径 */ }
|
|
750
|
+
}
|
|
751
|
+
if (physical) {
|
|
752
|
+
const sidNoPhysical = sids.filter(s => !physical.has(s) && !hiddenOnly.has(s));
|
|
753
|
+
if (sidNoPhysical.length > 0)
|
|
754
|
+
issues.push(`实物缺口 ${sidNoPhysical.length} 处:oracle.sids 声明了、效果图 HTML 实物里无对应 data-sid(剥注释后数点)→ 基线抽不出来。\n ${sidNoPhysical.join("、")}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
706
757
|
const sidNotDeclared = sids.filter(s => !declaredIds.includes(s));
|
|
707
758
|
const declaredWithoutSid = declaredIds.filter(id => !sidSet.has(id) && !hiddenOnly.has(id));
|
|
708
759
|
if (hiddenOnly.size > 0)
|
|
@@ -794,9 +845,6 @@ function registrySids(item) {
|
|
|
794
845
|
return item?.sid ? [item.sid] : [];
|
|
795
846
|
}
|
|
796
847
|
|
|
797
|
-
/** 登记表里唯一允许的「非词典点位」:全局令牌通道(对应变更集顶层 global 节点,无 css selector 故不入词典) */
|
|
798
|
-
const GLOBAL_PSEUDO_ID = "global";
|
|
799
|
-
|
|
800
848
|
/**
|
|
801
849
|
* 登记表覆盖率(子项级细闸,)
|
|
802
850
|
* 组级台账(restore-checklist)只能防「整组忘光」;组内子项是否被静默丢失,必须由登记表
|
|
@@ -844,7 +892,29 @@ function checkRegistryCoverage(changeset, plan, opts = {}) {
|
|
|
844
892
|
}
|
|
845
893
|
if (changeset.global && Object.keys(changeset.global).length > 0)
|
|
846
894
|
declared.add(GLOBAL_PSEUDO_ID);
|
|
847
|
-
const
|
|
895
|
+
const declaredSids = new Set(Array.isArray(changeset.oracle?.sids) ? changeset.oracle.sids : []);
|
|
896
|
+
|
|
897
|
+
// 实物闭合:打开 oracle.mockup 这个 HTML,数真实存在的 data-sid(不只看设计侧自报的 oracle.sids)
|
|
898
|
+
const mockupRef = changeset.oracle?.mockup;
|
|
899
|
+
let physicalSids = null;
|
|
900
|
+
let mockupFile = null;
|
|
901
|
+
if (typeof mockupRef === "string" && mockupRef.trim()) {
|
|
902
|
+
const cands = [path.resolve(mockupRef), path.resolve(ROOT, mockupRef), path.resolve(process.cwd(), mockupRef)];
|
|
903
|
+
for (const c of cands) {
|
|
904
|
+
try {
|
|
905
|
+
const html = readFileSync(c, "utf-8");
|
|
906
|
+
// 先剥离 HTML 注释再数 data-sid:注释内的同形串是说明文字、不是点位。
|
|
907
|
+
// 不剥离 → 注释可「兜住」一个实际缺失的标注,闸门全绿而基线为空(详见 mockup-extraction-spec.md §7.1)。
|
|
908
|
+
const stripped = html.replace(/<!--[\s\S]*?-->/g, "");
|
|
909
|
+
physicalSids = new Set([...stripped.matchAll(/data-sid\s*=\s*["']([^"']+)["']/g)].map(m => m[1]));
|
|
910
|
+
mockupFile = c;
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
catch { /* 该候选路径不可读,试下一个 */ }
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
// 比对基准:优先效果图实物;实物读不到时退化为自报清单(并在下方告警)
|
|
917
|
+
const mockupSids = physicalSids || declaredSids;
|
|
848
918
|
|
|
849
919
|
const applicable = items.filter(it => it.always === true || registrySids(it).some(s => mockupSids.has(s)));
|
|
850
920
|
const gaps = applicable.filter(it => !registrySids(it).every(s => declared.has(s)));
|
|
@@ -853,6 +923,23 @@ function checkRegistryCoverage(changeset, plan, opts = {}) {
|
|
|
853
923
|
console.log(` 登记表:${registryPath}(登记项 R=${items.length})`);
|
|
854
924
|
console.log(` 适用 A=${applicable.length} / 已兑现 C=${applicable.length - gaps.length} / 缺口 ${gaps.length}`);
|
|
855
925
|
|
|
926
|
+
// 实物校验:标注缺口的权威来源是效果图 HTML 里的 data-sid,而不是自报的 oracle.sids
|
|
927
|
+
let physicalGap = false;
|
|
928
|
+
if (physicalSids) {
|
|
929
|
+
console.log(` 效果图实物:${mockupFile}(实测 data-sid ${physicalSids.size} 个)`);
|
|
930
|
+
const declaredNoPhysical = [...declaredSids].filter(s => !physicalSids.has(s));
|
|
931
|
+
const physicalNoDeclared = [...physicalSids].filter(s => !declaredSids.has(s));
|
|
932
|
+
if (declaredNoPhysical.length > 0) {
|
|
933
|
+
console.log(` ⚠ oracle.sids 声明了 ${declaredNoPhysical.length} 个点位,但效果图实物里没有对应 data-sid(无基线):${declaredNoPhysical.join("、")}`);
|
|
934
|
+
physicalGap = true;
|
|
935
|
+
}
|
|
936
|
+
if (physicalNoDeclared.length > 0)
|
|
937
|
+
console.log(` ℹ 效果图实物另有 ${physicalNoDeclared.length} 个 data-sid 未列入 oracle.sids(自报清单不全,不阻塞):${physicalNoDeclared.join("、")}`);
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
console.log(` ⚠ 未能打开效果图实物(oracle.mockup 不可读)→ 本轮退化为按自报 oracle.sids 校验,实物闭合未生效`);
|
|
941
|
+
}
|
|
942
|
+
|
|
856
943
|
// 效果图标注完整性:恒存在的子项必须能在效果图上找到同名 data-sid,否则基线根本抽不出来
|
|
857
944
|
const unannotated = items.filter(it => it.always === true && !registrySids(it).some(s => mockupSids.has(s)));
|
|
858
945
|
if (unannotated.length > 0) {
|
|
@@ -863,13 +950,20 @@ function checkRegistryCoverage(changeset, plan, opts = {}) {
|
|
|
863
950
|
}
|
|
864
951
|
console.log(` ⚠ 效果图标注缺口 ${unannotated.length} 条:这些子项恒存在,却无同名 data-sid → 基线抽取不全`);
|
|
865
952
|
console.log(` 涉及点位:${[...bySid.entries()].map(([s, n]) => `${s}(${n})`).join("、")}`);
|
|
953
|
+
physicalGap = true;
|
|
866
954
|
}
|
|
867
955
|
|
|
868
|
-
if (gaps.length === 0) {
|
|
869
|
-
console.log(` ✔ 覆盖率通过:C = A
|
|
956
|
+
if (gaps.length === 0 && !physicalGap) {
|
|
957
|
+
console.log(` ✔ 覆盖率通过:C = A,无子项被静默丢失;效果图实物标注闭合`);
|
|
870
958
|
return 0;
|
|
871
959
|
}
|
|
872
960
|
|
|
961
|
+
if (gaps.length === 0 && physicalGap) {
|
|
962
|
+
console.log(`\n✘ 效果图实物标注不闭合 → exit 1(oracle.sids 与效果图 HTML 的 data-sid 不一致 / 恒存在子项缺标注)`);
|
|
963
|
+
console.log(` 处置:给效果图对应元素补 data-sid,或把该登记项降级 source: spec(见 mockup-extraction-spec.md §7)`);
|
|
964
|
+
return 1;
|
|
965
|
+
}
|
|
966
|
+
|
|
873
967
|
const byGroup = new Map();
|
|
874
968
|
for (const it of gaps) {
|
|
875
969
|
const g = it.group || "(未分组)";
|
|
@@ -1083,7 +1177,125 @@ function checkApplyFields(change, label, violations) {
|
|
|
1083
1177
|
}
|
|
1084
1178
|
}
|
|
1085
1179
|
|
|
1086
|
-
// ----
|
|
1180
|
+
// ---- 词典驱动硬闸:suppress 收口(G1)+ 几何语境(G5) ----
|
|
1181
|
+
// 与 checkApplyFields(协议自洽性校验)不同,这两个闸的判据来自**词典条目自身**:
|
|
1182
|
+
// · suppress_rules:条目声明某 form 时必须收口的出厂互斥通道(声明 form = 换掉旧形态,不是叠加)
|
|
1183
|
+
// · preconditions 含「几何语境随值迁移」:几何值落宿主时必须同步清零内层盒同向属性
|
|
1184
|
+
// 缺失即「新旧形态叠加 / 两层 padding 相加」——值都对、观感错,是「还原度连乘公式」里
|
|
1185
|
+
// 通道正确率的天花板级杀手,因此与禁词同级(阻断,不是提示)。
|
|
1186
|
+
|
|
1187
|
+
/** 提取条目 suppress_rules:优先结构化字段;回落解析 forms 描述与 $note 中的「suppress 必含 xxx」内联标记 */
|
|
1188
|
+
function extractSuppressRules(entry) {
|
|
1189
|
+
const rules = {};
|
|
1190
|
+
if (entry && typeof entry.suppress_rules === "object" && entry.suppress_rules !== null) {
|
|
1191
|
+
for (const [form, channels] of Object.entries(entry.suppress_rules)) {
|
|
1192
|
+
rules[form] = Array.isArray(channels) ? channels.filter(c => typeof c === "string") : [];
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
// 兜底:从 forms 描述文本提取「suppress 必含 名1/名2」标记(历史条目未结构化时仍能机检)
|
|
1196
|
+
const forms = entry?.forms;
|
|
1197
|
+
if (forms && typeof forms === "object" && !Array.isArray(forms)) {
|
|
1198
|
+
for (const [form, desc] of Object.entries(forms)) {
|
|
1199
|
+
if (typeof desc !== "string")
|
|
1200
|
+
continue;
|
|
1201
|
+
// 通道名 = 小写下划线标识符(可多项、以 、/, 分隔);遇非标识符字符(——、。等)即止
|
|
1202
|
+
for (const m of desc.matchAll(/suppress\s*必含\s*([a-z][a-z0-9_]*(?:[、,/][a-z][a-z0-9_]*)*)/gi)) {
|
|
1203
|
+
const channels = m[1].split(/[、,/]/).map(s => s.trim()).filter(Boolean);
|
|
1204
|
+
rules[form] = [...new Set([...(rules[form] || []), ...channels])];
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
return rules;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/** G1:声明 form 时,词典 suppress_rules 要求的出厂互斥通道必须逐项声明在条目 suppress 数组 */
|
|
1212
|
+
function checkSuppress(change, entry, idLabel, violations) {
|
|
1213
|
+
if (typeof change.form !== "string" || !change.form || !entry)
|
|
1214
|
+
return;
|
|
1215
|
+
const rules = extractSuppressRules(entry);
|
|
1216
|
+
const required = rules[change.form];
|
|
1217
|
+
if (!required || required.length === 0)
|
|
1218
|
+
return;
|
|
1219
|
+
const declared = Array.isArray(change.suppress) ? change.suppress : [];
|
|
1220
|
+
const missing = required.filter(ch => !declared.includes(ch));
|
|
1221
|
+
if (missing.length > 0) {
|
|
1222
|
+
violations.push(
|
|
1223
|
+
`${idLabel}.suppress 缺口:词典要求 form="${change.form}" 必须收口出厂互斥通道 [${required.join("、")}],`
|
|
1224
|
+
+ `实际声明 [${declared.join("、") || "无"}],缺 [${missing.join("、")}]`
|
|
1225
|
+
+ ` —— 声明 form = 换掉旧形态而不是叠加;不收口 = 出厂旧装饰残留(历史事故:侧栏 pill 未收口右竖条 → 双竖条并存)。`
|
|
1226
|
+
+ `通道名取该条目 forms/css_vars 的键名。`,
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* G5:几何语境自检 —— 宿主 + 内层之和必须等于设计值。
|
|
1233
|
+
* 词典 preconditions 含「几何语境随值迁移」标记的条目:style 声明几何字段且 layer 未显式
|
|
1234
|
+
* 指到 inner(即默认落 host)时,必须同步声明 suppress_inner(内层清零口径,纯语义描述),
|
|
1235
|
+
* 否则实际内边距 = 设计值 + 出厂内层自带值(历史事故:th 16px + EP .cell 0 12px = 28px/侧 → 列挤坏)。
|
|
1236
|
+
*/
|
|
1237
|
+
function checkGeometryContext(change, entry, idLabel, violations) {
|
|
1238
|
+
if (!entry || !Array.isArray(entry.preconditions))
|
|
1239
|
+
return;
|
|
1240
|
+
if (!entry.preconditions.some(p => typeof p === "string" && p.includes("几何语境随值迁移")))
|
|
1241
|
+
return;
|
|
1242
|
+
const style = change.style && typeof change.style === "object" ? change.style : null;
|
|
1243
|
+
if (!style)
|
|
1244
|
+
return;
|
|
1245
|
+
const layer = change.layer && typeof change.layer === "object" && !Array.isArray(change.layer) ? change.layer : {};
|
|
1246
|
+
const geometryFields = Object.keys(style).filter(k => GEOMETRY_PAD_RE.test(k));
|
|
1247
|
+
if (geometryFields.length === 0)
|
|
1248
|
+
return;
|
|
1249
|
+
// 内层属性族字段(white-space/text-align 等)已逐字段声明 inner → 施工方已知内层结构,不重复拦截
|
|
1250
|
+
const hasInnerAware = Object.keys(layer).some(f => layer[f] === "inner") || geometryFields.every(f => layer[f] === "inner");
|
|
1251
|
+
if (hasInnerAware)
|
|
1252
|
+
return;
|
|
1253
|
+
const hostGeometry = geometryFields.filter(f => layer[f] !== "inner");
|
|
1254
|
+
if (hostGeometry.length === 0)
|
|
1255
|
+
return;
|
|
1256
|
+
const declaredInner = typeof change.suppress_inner === "string" ? change.suppress_inner.trim() : "";
|
|
1257
|
+
if (!declaredInner) {
|
|
1258
|
+
violations.push(
|
|
1259
|
+
`${idLabel}.suppress_inner 缺口:条目 preconditions 标记「几何语境随值迁移」(出厂内层文本盒自带内边距),`
|
|
1260
|
+
+ `style 几何字段 [${hostGeometry.join("、")}] 落宿主时必须同步声明 suppress_inner(内层清零口径)`
|
|
1261
|
+
+ ` —— 否则实际间距 = 设计值 + 出厂内层值,两层之和 ≠ 设计值(历史事故:表头 16px + 出厂内层 12px/侧 = 28px/侧 → 列宽被挤、标题截断)。`,
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* G7:field_forms 声明-消费闭合 —— style 字段必须被当前 form 消费。
|
|
1268
|
+
* 词典条目声明 field_forms(字段 → 合法 form 枚举)后:变更集 style 里的字段若不在
|
|
1269
|
+
* 当前 form 的合法集内 = 字段无消费者、将被静默丢弃(历史事故:变更集声明 pill +
|
|
1270
|
+
* active_indicator_* 三字段,设计层按 pill 施工把竖条令牌置透明,效果图要求的左竖条消失——
|
|
1271
|
+
* 「声明了却不消费」此前无任何闸门拦截)。修复路径:换 form(含组合形态)或删字段。
|
|
1272
|
+
*/
|
|
1273
|
+
function checkFieldForms(change, entry, idLabel, violations) {
|
|
1274
|
+
if (!entry || typeof entry.field_forms !== "object" || entry.field_forms === null)
|
|
1275
|
+
return;
|
|
1276
|
+
const style = change.style && typeof change.style === "object" ? change.style : null;
|
|
1277
|
+
if (!style)
|
|
1278
|
+
return;
|
|
1279
|
+
const form = typeof change.form === "string" && change.form ? change.form : null;
|
|
1280
|
+
// 无 form 声明时跳过(纯样式条目,字段消费不依赖形态);无该字段映射时跳过
|
|
1281
|
+
if (!form)
|
|
1282
|
+
return;
|
|
1283
|
+
for (const [field, legalForms] of Object.entries(entry.field_forms)) {
|
|
1284
|
+
if (!(field in style))
|
|
1285
|
+
continue;
|
|
1286
|
+
if (!Array.isArray(legalForms) || legalForms.length === 0)
|
|
1287
|
+
continue;
|
|
1288
|
+
if (!legalForms.includes(form)) {
|
|
1289
|
+
violations.push(
|
|
1290
|
+
`${idLabel}.field_forms 闭合违规:style 字段「${field}」仅在 form [${legalForms.join("、")}] 下有消费者,`
|
|
1291
|
+
+ `当前 form="${form}" 不消费该字段 → 声明将被静默丢弃`
|
|
1292
|
+
+ ` —— 修复:换用消费该字段的 form(含组合形态),或删除该字段。禁止「声明字段 + 互斥 form」的组合(历史事故:pill + active_indicator_* → 设计层置透明,左竖条消失)。`,
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
|
|
1087
1299
|
// 本节的模块级常量(AUDIT_SKIP_DIRS / STRUCT_EXT / STYLE_EXT / DESIGN_LAYER_RE /
|
|
1088
1300
|
// BUILTIN_BARE_CLASSES)已在文件顶部声明——模块级常量必须在 main() 调用前初始化,勿下移。
|
|
1089
1301
|
|
|
@@ -1171,8 +1383,9 @@ function findVariableConsumer(varName, sources) {
|
|
|
1171
1383
|
function judgeMount(token, where, hit) {
|
|
1172
1384
|
if (hit.struct.length > 0)
|
|
1173
1385
|
return [];
|
|
1386
|
+
const fmt = arr => arr.map(h => (typeof h === "string" ? h : `${h.file}(${(h.reasons || []).join(";")})`)).slice(0, 2).join("、");
|
|
1174
1387
|
if (hit.style.length > 0)
|
|
1175
|
-
return [`挂载点可疑:${where} 的「${token}」只出现在样式文件(${hit.style
|
|
1388
|
+
return [`挂载点可疑:${where} 的「${token}」只出现在样式文件(${fmt(hit.style)}),未在模板 / 脚本里挂载 —— 规则可能永不命中(历史缺口:列未绑 class-name,金额列右对齐 / 缩略图尺寸失效)`];
|
|
1176
1389
|
if (hit.designLayer.length > 0)
|
|
1177
1390
|
return [`挂载点缺失:${where} 的「${token}」只出现在设计层样式(${hit.designLayer.slice(0, 2).join("、")}),项目里既无结构挂载也无出厂对应 —— 规则不会生效`];
|
|
1178
1391
|
return [`挂载点缺失:${where} 的「${token}」在项目中未找到任何出现位置(选择器永不命中)`];
|
|
@@ -1181,8 +1394,16 @@ function judgeMount(token, where, hit) {
|
|
|
1181
1394
|
/** 单条目审计:selector / anchors 挂载点 + css_var 消费者 */
|
|
1182
1395
|
function auditEntry(entry, sources) {
|
|
1183
1396
|
const issues = [];
|
|
1184
|
-
|
|
1185
|
-
|
|
1397
|
+
const miss = (token) => {
|
|
1398
|
+
// 设计层已承载同类名 → 挂载点可疑降级为提示(不阻断):C 通道由 design-shell.scss 兜底
|
|
1399
|
+
if (hasDesignLayerTakeover(token, sources))
|
|
1400
|
+
return null;
|
|
1401
|
+
return true;
|
|
1402
|
+
};
|
|
1403
|
+
for (const t of extractMountTokens(entry.selector)) {
|
|
1404
|
+
if (miss(t))
|
|
1405
|
+
issues.push(...judgeMount(t, "selector", findMount(t, sources)));
|
|
1406
|
+
}
|
|
1186
1407
|
|
|
1187
1408
|
const anchors = Array.isArray(entry.anchors) ? entry.anchors : [];
|
|
1188
1409
|
for (const a of anchors) {
|
|
@@ -1190,17 +1411,47 @@ function auditEntry(entry, sources) {
|
|
|
1190
1411
|
continue;
|
|
1191
1412
|
if (a.kind === "css_var") {
|
|
1192
1413
|
const consumers = findVariableConsumer(a.target, sources);
|
|
1193
|
-
if (consumers.length === 0)
|
|
1194
|
-
|
|
1414
|
+
if (consumers.length === 0) {
|
|
1415
|
+
// 两类合法豁免(防误报):
|
|
1416
|
+
// ① --el-* 组件库变量:消费者在 element-plus 主题库(node_modules 不在扫描范围),
|
|
1417
|
+
// 项目源码无 var() 属正常 —— EP runtime 必然消费。
|
|
1418
|
+
// ② 设计层已接管变量定义(design-shell 显式赋值):suppress / 接管属故意行为
|
|
1419
|
+
// (词典 preconditions 有明示的条目,如 --vts-theme-menu-active-before)。
|
|
1420
|
+
if (a.target.startsWith("--el-")) {
|
|
1421
|
+
console.log(` ℹ ${a.target} 项目源码无 var() 消费者 —— 由组件库运行时消费(node_modules 不在扫描范围),不按死通道处理`);
|
|
1422
|
+
}
|
|
1423
|
+
else if (designLayerDefines(a.target, sources)) {
|
|
1424
|
+
console.log(` ℹ ${a.target} 项目源码无 var() 消费者 —— 设计层已接管该变量定义(suppress / 接管属故意行为),不按死通道处理`);
|
|
1425
|
+
}
|
|
1426
|
+
else {
|
|
1427
|
+
issues.push(`死通道:CSS 变量 ${a.target} 在项目中无 var() 消费者(只定义不消费 = 通道死路,翻译再准也不生效)`);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1195
1430
|
}
|
|
1196
1431
|
else {
|
|
1197
|
-
for (const t of extractMountTokens(a.target))
|
|
1198
|
-
|
|
1432
|
+
for (const t of extractMountTokens(a.target)) {
|
|
1433
|
+
if (miss(t))
|
|
1434
|
+
issues.push(...judgeMount(t, `anchors(${a.layer || "未标层级"})`, findMount(t, sources)));
|
|
1435
|
+
}
|
|
1199
1436
|
}
|
|
1200
1437
|
}
|
|
1201
1438
|
return [...new Set(issues)];
|
|
1202
1439
|
}
|
|
1203
1440
|
|
|
1441
|
+
/** 判定「设计层已承载」的豁免:条目 anchors/selector 的类词元在设计层文件出现时,
|
|
1442
|
+
* 挂载点可疑可降级为提示 —— 设计层覆写本身就是合法落点(design-shell.scss 是 C 通道终点)。 */
|
|
1443
|
+
function hasDesignLayerTakeover(token, sources) {
|
|
1444
|
+
const needle = token.startsWith("[") ? token.slice(1, -1) : token;
|
|
1445
|
+
const re = new RegExp(`(^|[^\\w-])${escapeRe(needle)}([^\\w-]|$)`);
|
|
1446
|
+
return sources.some(s => DESIGN_LAYER_RE.test(s.file) && re.test(s.text));
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/** 设计层是否显式定义了该 CSS 变量(suppress / 接管动作),定义在非设计层的令牌宿主不算 */
|
|
1450
|
+
function designLayerDefines(varName, sources) {
|
|
1451
|
+
const re = new RegExp(`${escapeRe(varName)}\\s*:`);
|
|
1452
|
+
return sources.some(s => DESIGN_LAYER_RE.test(s.file) && re.test(s.text));
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1204
1455
|
function cmdAudit(args, dataset) {
|
|
1205
1456
|
const projIdx = args.indexOf("--project");
|
|
1206
1457
|
const projectRoot = projIdx > -1 ? args[projIdx + 1] : null;
|
|
@@ -1228,6 +1479,7 @@ function cmdAudit(args, dataset) {
|
|
|
1228
1479
|
|
|
1229
1480
|
// 给了变更集 → 只审被引用的点位(含 apply_via 指向的锚点来源);否则审词典全量
|
|
1230
1481
|
let targetIds;
|
|
1482
|
+
const loadedChangesets = []; // G2/G4:保留完整变更集供施工证据与 T 通道对账
|
|
1231
1483
|
if (files.length > 0) {
|
|
1232
1484
|
const wanted = new Set();
|
|
1233
1485
|
for (const f of files) {
|
|
@@ -1239,6 +1491,7 @@ function cmdAudit(args, dataset) {
|
|
|
1239
1491
|
console.error(`✖ 变更集读取失败 ${f}:${e.message}`);
|
|
1240
1492
|
process.exit(1);
|
|
1241
1493
|
}
|
|
1494
|
+
loadedChangesets.push(cs);
|
|
1242
1495
|
for (const c of cs.changes || []) {
|
|
1243
1496
|
if (c.id)
|
|
1244
1497
|
wanted.add(c.id);
|
|
@@ -1256,6 +1509,113 @@ function cmdAudit(args, dataset) {
|
|
|
1256
1509
|
console.log(`源码文件:${sources.length}(已剔除 node_modules / dist 等产物目录)`);
|
|
1257
1510
|
console.log(`检查条目:${targetIds.length}${files.length > 0 ? "(仅变更集引用点位)" : "(词典全量)"}`);
|
|
1258
1511
|
|
|
1512
|
+
// ---- G2:structure_ops 施工证据(台账落地断言)----
|
|
1513
|
+
// 数据 / 结构驱动点位必须在项目源码里找到施工痕迹,纯 CSS 落点声明 structure_ops = 冒充施工:
|
|
1514
|
+
// add_node/remove_node/move_node/render_branch/bind_source → 按条目 selector/anchors 的语义类名
|
|
1515
|
+
// 在**非设计层**源码(含模板与数据脚本)里查证该节点/绑定的存在性(结构文件命中才算兑现)。
|
|
1516
|
+
// 历史事故:操作列分割线 add_node 声明了多轮从未施工,CSS 覆写冒充结构改动 → 按钮间距 / 列宽永不达标。
|
|
1517
|
+
let structIssues = 0;
|
|
1518
|
+
if (loadedChangesets.length > 0) {
|
|
1519
|
+
console.log(`\n=== structure_ops 施工证据(G2 · 模板 diff 取证)===`);
|
|
1520
|
+
for (const cs of loadedChangesets) {
|
|
1521
|
+
for (const [i, c] of (cs.changes || []).entries()) {
|
|
1522
|
+
const ops = Array.isArray(c.structure_ops) ? c.structure_ops : [];
|
|
1523
|
+
if (ops.length === 0 || !c.id)
|
|
1524
|
+
continue;
|
|
1525
|
+
const entry = index[c.id];
|
|
1526
|
+
const probes = collectStructProbes(entry, c);
|
|
1527
|
+
const evidence = probes
|
|
1528
|
+
.map(p => ({ probe: p, hits: findStructEvidence(p, sources) }))
|
|
1529
|
+
.filter(x => x.hits.length > 0);
|
|
1530
|
+
if (probes.length === 0) {
|
|
1531
|
+
console.log(` ⚠ changes[${i}] ${c.id}:structure_ops ${ops.length} 项,但词典条目无 selector/anchors 可提取证据探针 → 人工核验施工台账(design-apply §施工台账)`);
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
if (evidence.length === 0) {
|
|
1535
|
+
console.log(` ✘ ${c.id}:structure_ops 声明 [${ops.map(o => o.op).join("/")}],但源码中找不到任何施工证据探针 [${probes.join("、")}](非设计层结构文件零命中)`);
|
|
1536
|
+
console.log(` → 纯 CSS 覆写不能兑现结构操作;须改模板/数据并在交付说明附施工台账,或从变更集移除该声明`);
|
|
1537
|
+
structIssues++;
|
|
1538
|
+
}
|
|
1539
|
+
else {
|
|
1540
|
+
const files1 = [...new Set(evidence.flatMap(x => x.hits.map(h => h.file)))];
|
|
1541
|
+
console.log(` ✔ ${c.id}:structure_ops 证据命中 ${evidence.length}/${probes.length} 探针(${files1.slice(0, 3).join("、")})`);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
if (structIssues === 0)
|
|
1546
|
+
console.log(` ✔ 全部 structure_ops 均有源码施工证据`);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// ---- G4:T 通道对账(运行时令牌 ↔ 变更集主题/形态声明)----
|
|
1550
|
+
// T 通道(platform-config.json / 运行时令牌)优先级最高、也最易被遗忘:
|
|
1551
|
+
// 主题色不一致 → 全局主色错;页签 ShowModel 与 form 叠加 → 新旧形态并存。
|
|
1552
|
+
let tIssues = 0;
|
|
1553
|
+
if (loadedChangesets.length > 0) {
|
|
1554
|
+
console.log(`\n=== T 通道对账(G4 · platform-config ↔ 变更集声明)===`);
|
|
1555
|
+
const pcPath = path.join(root, "public", "platform-config.json");
|
|
1556
|
+
let pc = null;
|
|
1557
|
+
try {
|
|
1558
|
+
pc = JSON.parse(readFileSync(pcPath, "utf-8"));
|
|
1559
|
+
console.log(` platform-config:${pcPath}`);
|
|
1560
|
+
}
|
|
1561
|
+
catch {
|
|
1562
|
+
console.log(` ⚠ 未能读取 ${pcPath}(无 public/platform-config.json)→ 跳过 T 通道对账`);
|
|
1563
|
+
}
|
|
1564
|
+
if (pc) {
|
|
1565
|
+
const tChecks = [];
|
|
1566
|
+
// ① 主题色:global.brand_primary(语义值经译表解析为 hex)↔ EpThemeColor
|
|
1567
|
+
const declaredBrand = loadedChangesets.map(cs => cs.global?.brand_primary).find(Boolean);
|
|
1568
|
+
if (declaredBrand && typeof declaredBrand === "string") {
|
|
1569
|
+
const allDict = buildSemanticValueIndex(dataset);
|
|
1570
|
+
const hex = resolveSemanticValue(declaredBrand, allDict) || (/^#/.test(declaredBrand) ? declaredBrand : null);
|
|
1571
|
+
if (hex && pc.EpThemeColor && String(pc.EpThemeColor).toLowerCase() !== String(hex).toLowerCase()) {
|
|
1572
|
+
tChecks.push(`✘ 主题色不一致:变更集 brand_primary=${declaredBrand}(解析 ${hex}),platform-config.EpThemeColor=${pc.EpThemeColor} → 改 platform-config 或运行时令牌持久化(T 通道先于 C 通道)`);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
// ② 页签形态:tabs form ↔ ShowModel(出厂 flat/chrome 形态都运行在 ShowModel=card 结构上)
|
|
1576
|
+
const tabsForm = loadedChangesets
|
|
1577
|
+
.flatMap(cs => (cs.changes || []).filter(c => c.id === "admin.layout.tabs" && typeof c.form === "string").map(c => c.form))
|
|
1578
|
+
.find(Boolean);
|
|
1579
|
+
if (tabsForm && pc.ShowModel && !["flat", "chrome", "card"].includes(pc.ShowModel) === false) {
|
|
1580
|
+
// flat 与 card 共用 card 结构,chrome 形态要求 ShowModel=chrome
|
|
1581
|
+
if (tabsForm === "chrome" && pc.ShowModel !== "chrome") {
|
|
1582
|
+
tChecks.push(`✘ 页签形态不一致:变更集 form=chrome 需 ShowModel=chrome,platform-config.ShowModel=${pc.ShowModel}`);
|
|
1583
|
+
}
|
|
1584
|
+
else if (tabsForm !== "chrome" && pc.ShowModel === "chrome") {
|
|
1585
|
+
tChecks.push(`✘ 页签形态不一致:变更集 form=${tabsForm}(card/flat 结构),platform-config.ShowModel=chrome → 运行时仍是梯形页签,设计层覆写不改变结构形态`);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
// ③ 显隐类 D 通道开关:变更集 visible:false 的点位若词典标了 visible_prop,必须核对开关
|
|
1589
|
+
const visibleProps = [];
|
|
1590
|
+
for (const cs of loadedChangesets) {
|
|
1591
|
+
for (const c of (cs.changes || [])) {
|
|
1592
|
+
if (c.visible !== false || !c.id)
|
|
1593
|
+
continue;
|
|
1594
|
+
const entry = index[c.id] || index[String(c.id).split(".").slice(0, -1).join(".")];
|
|
1595
|
+
if (entry?.visible_prop)
|
|
1596
|
+
visibleProps.push({ id: c.id, prop: entry.visible_prop });
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
for (const vp of visibleProps) {
|
|
1600
|
+
const m = /^(\w+)(platform-config\.json)$/.exec(vp.prop) || /^(\w+)$/.exec(vp.prop.split("(")[0].trim());
|
|
1601
|
+
const key = m ? m[1] : null;
|
|
1602
|
+
if (key && key in pc && pc[key] === true) {
|
|
1603
|
+
tChecks.push(`✘ 显隐不一致:${vp.id} 声明 visible:false,但词典 visible_prop=${vp.prop} 且 platform-config.${key}=${pc[key]} → 开关未关,点位仍渲染(D 通道先于 S/C 通道)`);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
if (tChecks.length === 0) {
|
|
1607
|
+
console.log(` ✔ T/D 通道一致:主题色、页签形态、显隐开关均与变更集声明对齐`);
|
|
1608
|
+
}
|
|
1609
|
+
else {
|
|
1610
|
+
for (const t of tChecks) {
|
|
1611
|
+
console.log(` ${t}`);
|
|
1612
|
+
tIssues++;
|
|
1613
|
+
}
|
|
1614
|
+
console.log(` 处置:T 通道(platform-config / 运行时令牌持久化)优先级最高,先清 T 再谈 S/C;存量 localStorage 持久化值须一并断言清零。`);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1259
1619
|
let problems = 0;
|
|
1260
1620
|
const clean = [];
|
|
1261
1621
|
for (const id of targetIds) {
|
|
@@ -1286,6 +1646,48 @@ function cmdAudit(args, dataset) {
|
|
|
1286
1646
|
else {
|
|
1287
1647
|
console.log(`✔ 无挂载 / 通道问题(渲染态是否一致仍需 §验收比对确认:静态审计查不出「值对了、位置不对」)`);
|
|
1288
1648
|
}
|
|
1649
|
+
// G2/G4 违规同样置退出码(与挂载/通道问题同级 —— 施工没兑现、T 通道没清零 = 交付不完整)
|
|
1650
|
+
if (structIssues > 0) {
|
|
1651
|
+
console.log(`✖ structure_ops 施工证据缺失 ${structIssues} 处(G2)`);
|
|
1652
|
+
process.exitCode = 1;
|
|
1653
|
+
}
|
|
1654
|
+
if (tIssues > 0) {
|
|
1655
|
+
console.log(`✖ T 通道对账不一致 ${tIssues} 处(G4)`);
|
|
1656
|
+
process.exitCode = 1;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/** G2 辅助:从词典条目与 structure_ops 声明中提取「施工证据探针」(语义类名 / 节点名,不含技术词)。
|
|
1661
|
+
* 探针来源优先级:条目 anchors 的 class target 词元 → selector 词元 → op.node 语义名。 */
|
|
1662
|
+
function collectStructProbes(entry, change) {
|
|
1663
|
+
const probes = new Set();
|
|
1664
|
+
const pushTokens = (sel) => {
|
|
1665
|
+
if (typeof sel !== "string")
|
|
1666
|
+
return;
|
|
1667
|
+
for (const m of sel.matchAll(/\.([a-zA-Z][\w-]*__(?:divider|tag|icon|close|title|badge|arrow)[\w-]*|[a-zA-Z][\w-]*-divider[\w-]*|vts-[\w-]+|re-table-operate__[\w-]+)/g))
|
|
1668
|
+
probes.add(m[1]);
|
|
1669
|
+
};
|
|
1670
|
+
if (entry) {
|
|
1671
|
+
for (const a of (Array.isArray(entry.anchors) ? entry.anchors : [])) {
|
|
1672
|
+
if (a && a.kind === "class" && typeof a.target === "string")
|
|
1673
|
+
pushTokens(a.target);
|
|
1674
|
+
}
|
|
1675
|
+
pushTokens(entry.selector);
|
|
1676
|
+
}
|
|
1677
|
+
// op.node 语义名(如 tab_divider):既可能是设计层伪元素名(tab_divider → ::before 不可探测),
|
|
1678
|
+
// 也可能是真实类名(status_tag);转词元时保留下划线形式,匹配源码任意出现(类名/常量名/注释均可)
|
|
1679
|
+
for (const op of (Array.isArray(change.structure_ops) ? change.structure_ops : [])) {
|
|
1680
|
+
if (op && typeof op.node === "string" && op.node.trim())
|
|
1681
|
+
probes.add(op.node.trim());
|
|
1682
|
+
}
|
|
1683
|
+
return [...probes];
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
/** G2 辅助:在非设计层源码中查找探针出现(结构文件命中 = 施工痕迹;仅样式文件命中不算) */
|
|
1687
|
+
function findStructEvidence(probe, sources) {
|
|
1688
|
+
const re = new RegExp(`(?:class|className|classList|node|name|id|label|key|prop)["']?\\s*[:=]\\s*["']${escapeRe(probe)}["']|${escapeRe(probe)}`);
|
|
1689
|
+
return sources.filter(h => h.kind === "struct" && !DESIGN_LAYER_RE.test(h.file) && re.test(h.text))
|
|
1690
|
+
.map(h => ({ file: h.file }));
|
|
1289
1691
|
}
|
|
1290
1692
|
|
|
1291
1693
|
// ---- verify:渲染态逐点验收( 补丁 3 · 闭环验收回路) ----
|
|
@@ -1296,9 +1698,10 @@ function cmdAudit(args, dataset) {
|
|
|
1296
1698
|
* 本命令不做抽取,只做归一化 + 逐点 diff + 三类归因。
|
|
1297
1699
|
*
|
|
1298
1700
|
* CLI:node parser/index.js verify --oracle <oracle.json> --actual <actual.json> [--sids a,b,c]
|
|
1701
|
+
* 实测端定位计划:node parser/index.js verify --oracle <oracle.json> --resolve [--sids a,b,c]
|
|
1299
1702
|
* 退出码:存在缺口 → 1;全部命中 → 0。
|
|
1300
1703
|
*/
|
|
1301
|
-
function cmdVerify(args) {
|
|
1704
|
+
function cmdVerify(args, dataset) {
|
|
1302
1705
|
// 几何 / 盒模型属性:值对不上 = 语境错位(宿主盒 / 内层盒模型 / 层叠上下文不同)
|
|
1303
1706
|
const GEOMETRY_PROPS = new Set([
|
|
1304
1707
|
"display", "position", "top", "right", "bottom", "left", "box-sizing",
|
|
@@ -1322,8 +1725,10 @@ function cmdVerify(args) {
|
|
|
1322
1725
|
};
|
|
1323
1726
|
const oraclePath = get("--oracle");
|
|
1324
1727
|
const actualPath = get("--actual");
|
|
1325
|
-
|
|
1728
|
+
const resolveOnly = args.includes("--resolve");
|
|
1729
|
+
if (!oraclePath || (!resolveOnly && !actualPath)) {
|
|
1326
1730
|
console.error("✖ 缺少参数:node parser/index.js verify --oracle <oracle.json> --actual <actual.json> [--sids a,b,c]");
|
|
1731
|
+
console.error(" 或输出实测端定位计划:node parser/index.js verify --oracle <oracle.json> --resolve [--sids a,b,c]");
|
|
1327
1732
|
process.exit(1);
|
|
1328
1733
|
}
|
|
1329
1734
|
const sidsArg = get("--sids");
|
|
@@ -1356,12 +1761,53 @@ function cmdVerify(args) {
|
|
|
1356
1761
|
};
|
|
1357
1762
|
|
|
1358
1763
|
const oracle = readDump(oraclePath, "效果图期望");
|
|
1359
|
-
const actual = readDump(actualPath, "项目实测");
|
|
1360
1764
|
|
|
1361
1765
|
let sids = Object.keys(oracle);
|
|
1362
1766
|
if (sidFilter)
|
|
1363
1767
|
sids = sids.filter(s => sidFilter.has(s));
|
|
1364
1768
|
|
|
1769
|
+
// 实测端定位闭合:--resolve 输出「sid → 词典 selector」抽取计划。
|
|
1770
|
+
// 效果图端用 data-sid(设计侧自控);项目页端不要求打 data-sid —— 浏览器按本计划的 selector 抽取。
|
|
1771
|
+
if (resolveOnly) {
|
|
1772
|
+
const index = buildMergedIndex(dataset || {});
|
|
1773
|
+
console.log(`\n=== 实测端定位计划(sid → 词典 selector)===`);
|
|
1774
|
+
console.log(`用途:项目页不要求打 data-sid —— 浏览器抽取脚本按下列 selector 定位元素取 computed style 生成 actual.json`);
|
|
1775
|
+
console.log(`selector 挂载点存在性由 audit 静态校验;本计划只回答「按什么定位」。\n`);
|
|
1776
|
+
let missing = 0;
|
|
1777
|
+
for (const sid of sids) {
|
|
1778
|
+
const entry = index[sid];
|
|
1779
|
+
if (!entry) {
|
|
1780
|
+
console.log(` ✘ ${sid} —— 词典无此条目(L3 新点位,先落地并回写临时映射)`);
|
|
1781
|
+
missing++;
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
const sels = [];
|
|
1785
|
+
if (Array.isArray(entry.anchors)) {
|
|
1786
|
+
for (const a of entry.anchors) {
|
|
1787
|
+
if (a && a.kind === "class" && a.target)
|
|
1788
|
+
sels.push(`[${a.layer || "host"}] ${a.target}`);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
if (sels.length === 0 && entry.selector)
|
|
1792
|
+
sels.push(`[host] ${entry.selector}`);
|
|
1793
|
+
if (sels.length === 0) {
|
|
1794
|
+
console.log(` ✘ ${sid} —— 词典条目无 selector / anchors,无法定位`);
|
|
1795
|
+
missing++;
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1798
|
+
console.log(` · ${sid}`);
|
|
1799
|
+
for (const s of sels)
|
|
1800
|
+
console.log(` ${s}`);
|
|
1801
|
+
}
|
|
1802
|
+
console.log(`\n汇总:${sids.length} 点位,${missing} 个无法定位`);
|
|
1803
|
+
console.log(`抽取属性白名单两端必须一致(padding / font-size / font-weight / text-align / color / background-color / border-radius / gap 等,见 mockup-extraction-spec §3);同构异色复用再加 --from-theme/--to-theme 做色值归一。`);
|
|
1804
|
+
if (missing > 0)
|
|
1805
|
+
process.exitCode = 1;
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
const actual = readDump(actualPath, "项目实测");
|
|
1810
|
+
|
|
1365
1811
|
const normProp = p => String(p).trim().toLowerCase().replace(/_/g, "-");
|
|
1366
1812
|
const normValue = v => {
|
|
1367
1813
|
let s = String(v == null ? "" : v).trim().toLowerCase().replace(/\s+/g, " ");
|
|
@@ -103,11 +103,52 @@ parser match(确定性翻译)→ 施工坐标 → AI 按坐标改代码 →
|
|
|
103
103
|
|
|
104
104
|
## 执行流程(按序,禁止跳步)
|
|
105
105
|
|
|
106
|
-
### 第 0 步 · 版本与场景检查
|
|
106
|
+
### 第 0 步 · 版本与场景检查 · 对接匹配(适配前置)· 运行时清场
|
|
107
|
+
|
|
108
|
+
**0.1 版本与场景检查**
|
|
107
109
|
|
|
108
110
|
- 变更集 `schema` 版本不在支持范围 → 停止,向上游(用户/设计侧)报告版本不匹配,不降级猜测。
|
|
109
111
|
- `scene` 在 `parser list` 场景清单中无对应域 → 报告并列出支持的场景,等待确认。
|
|
110
112
|
|
|
113
|
+
**0.2 对接匹配(适配前置)——把散落的匹配收拢为一个前置阶段**
|
|
114
|
+
|
|
115
|
+
在施工前,把「设计意图 ⇄ 模板能力」整体对一遍,输出匹配结论,供**生成前**决策(而不是落地时才发现对不上)。三步判定:
|
|
116
|
+
|
|
117
|
+
| 级 | 判定 | 依据 |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| **布局级** | `layout_type` 命中标准枚举? | 命中 → 切布局模板池(§5.2);未命中 → 变体派生 / 定制生成骨架(§5.3 / §5.4) |
|
|
120
|
+
| **点位级** | 逐 id 三级匹配 | 一级完整命中 / 二级父级命中 / 三级未命中(§4.1) |
|
|
121
|
+
| **形态级** | `form` 在条目 `forms` 枚举内?`structure_ops` 所需 DOM 钩子是否存在? | 词典 `forms` / 模板源码 / `audit` |
|
|
122
|
+
|
|
123
|
+
**输出三类结论,处置全在项目侧**:
|
|
124
|
+
|
|
125
|
+
| 结论 | 判据 | 处置 |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| 完全匹配 | L1 ∧ form 在枚举 ∧ 无需结构 | 直接施工 |
|
|
128
|
+
| 可派生 | L2 / 变体布局 / form 可映射 | 派生骨架或继承父级后施工 |
|
|
129
|
+
| 需适配 | L3 / 定制布局 / 缺通道 / 需新 DOM | **先补通道 / 结构增量 / 生成骨架,再生成**;交付说明单列申报 |
|
|
130
|
+
|
|
131
|
+
> **总原则(红线):设计为主,项目侧适配。** 设计效果是目标态,模板是待适配的一方。"设计 ↔ 模板对不上"的出口**永远在项目侧**——**不回流设计侧改设计**。只有「设计侧未给值 / 值非法」(**意图缺口**)才回流设计侧。严禁以「模板不支持」为由要求设计改图。
|
|
132
|
+
|
|
133
|
+
**0.3 运行时主题清场(T 通道先于 D/S/C)**
|
|
134
|
+
|
|
135
|
+
主色 / 主题走的是**运行时通道**:`setEpThemeColor()` 把 `--el-color-primary` 写成 `documentElement` 的**内联样式**并持久化到 localStorage(读取优先级 = 浏览器持久化 ?? `platform-config.json`)。**内联样式 + 运行时持久化 ⇒ 设计层 CSS 与词典 selector 都赢不了。**
|
|
136
|
+
|
|
137
|
+
- 落地前先读 localStorage(键 `responsive-layout`)的 `theme` / `epThemeColor`,与本变更集主题**不一致时清 key 或显式重设**;
|
|
138
|
+
- 清场动作与结果写入交付说明(可回溯);
|
|
139
|
+
- **不清场则颜色类差异全部失真**——归因前必须先确认起点已对齐,否则会把「环境残留」误判成「落地失败」。
|
|
140
|
+
- **机检对账(G4)**:`parser audit` 会自动比对 `global.brand_primary` ↔ `platform-config.EpThemeColor`、页签 form ↔ `ShowModel`、`visible:false` ↔ 显隐开关,不一致即 `exit 1`——人工清场 + 机检对账双保险。
|
|
141
|
+
|
|
142
|
+
**0.3b T 通道持久化三件套(页签形态 / 主题类字段施工必做)**
|
|
143
|
+
|
|
144
|
+
运行时配置读 localStorage,而 localStorage 只在 `platform-config.Version` 变化时经 `responsive.ts invalidateStorageIfConfigVersionChanged` 重灌。凡施工涉及 `ShowModel` / `Theme` / `EpThemeColor` 等 T 通道字段,**三件套缺一不可**:
|
|
145
|
+
|
|
146
|
+
1. 改 `platform-config.json` 实值(如 `ShowModel: "card"`);
|
|
147
|
+
2. **`Version` 自增一位**(如 0.0.6 → 0.0.7)——漏此步 = 代码对但运行时仍旧形态(历史事故:只改 ShowModel 未动 Version,页签零变化);
|
|
148
|
+
3. **渲染取证**:断言新形态类实际挂载(如 `.scroll-item.card-item` 出现在 DOM)——不许只看代码改没改。
|
|
149
|
+
|
|
150
|
+
词典 `admin.layout.tabs.preconditions` 有同款硬闸描述;本三件套对任意 T 通道字段通用。
|
|
151
|
+
|
|
111
152
|
### 第 1 步 · parser 确定性匹配
|
|
112
153
|
|
|
113
154
|
```bash
|
|
@@ -145,6 +186,15 @@ node parser/index.js match --batch <changesets-dir>
|
|
|
145
186
|
| `▸ 前置条件 preconditions` | 值成立依赖的宿主前提 | 施工前必须满足,否则「值对了也不生效」 |
|
|
146
187
|
| `▸ 覆写优先级 priority` | 该点位的提权策略 | 按策略取胜,禁止逐点硬堆 `!important` |
|
|
147
188
|
|
|
189
|
+
**词典硬闸(match 期机检,违规 = 拒绝执行)**:
|
|
190
|
+
|
|
191
|
+
| 闸 | 触发条件 | 判据来源 | 违规输出 |
|
|
192
|
+
|---|---|---|---|
|
|
193
|
+
| **G1 suppress 收口** | 条目声明某 `form`,词典 `suppress_rules[form]`(或 forms 描述「suppress 必含 x」内联标记)要求收口的出厂互斥通道未全部出现在该条目 `suppress[]` | 词典条目 `suppress_rules` | `suppress 缺口:…必须收口出厂互斥通道 [x],缺 [x]` |
|
|
194
|
+
| **G5 几何语境** | 词典 preconditions 含「几何语境随值迁移」标记 ∧ `style` 含 padding/margin 族几何字段落宿主 ∧ 未声明 `suppress_inner` | 词典条目 preconditions 标记句 | `suppress_inner 缺口:…两层之和 ≠ 设计值` |
|
|
195
|
+
|
|
196
|
+
两闸与禁词校验同级阻断(`exit 1`)——「新旧形态叠加」「两层 padding 相加」都是值全对、观感全错的历史事故源(双竖条、28px 列挤压),不允许靠观感兜底。
|
|
197
|
+
|
|
148
198
|
**验收基线输出**:转换计划末尾另打印 `=== 验收基线 oracle ===`(效果图路径 + 待比对点位)与 `=== 覆盖率 coverage ===`(登记项 R / 适用 A / 已兑现 C)。有 `oracle` → 落地后必须跑第 6 步 `verify`;缺 `oracle` / `coverage` → 输出提示,把「补基线 / 补登记覆盖率」回流设计侧(场景还原应给)。
|
|
149
199
|
**覆盖率校验**:`coverage.covered` 必须等于 `coverage.applicable`、`gaps` 为空,否则打印缺口清单——`C < A` 即**欠声明**(有规范子项未落进 `changes[]`/`visible`/`pending`),属设计侧交付不完整,必须回流,不得在项目侧"猜着补"。
|
|
150
200
|
|
|
@@ -159,10 +209,12 @@ node parser/index.js audit --project <目标项目根目录> [<changeset.json…
|
|
|
159
209
|
| 检查 | 抓什么 | 对应历史缺口 |
|
|
160
210
|
|---|---|---|
|
|
161
211
|
| **挂载点缺失 / 可疑** | `selector` / `anchors.target` 里的类名在项目里**只在样式文件出现、模板没挂**,或**完全找不到** | 列未绑 `class-name` → 金额列右对齐 / 缩略图尺寸永不生效;操作栏规则有定义无引用 |
|
|
162
|
-
| **死通道** | `anchors` 里的 CSS 变量在项目中**无 `var()`
|
|
212
|
+
| **死通道** | `anchors` 里的 CSS 变量在项目中**无 `var()` 消费者**(两类豁免:`--el-*` 组件库变量由 EP 运行时消费;设计层已显式接管定义的变量属故意 suppress) | 变量有定义无引用,翻译再准也不生效 |
|
|
163
213
|
| **词典无此条目** | 变更集引用了词典未建的 ID | L3 新增点位须当场落地并回写临时映射 |
|
|
214
|
+
| **G2 structure_ops 施工证据** | 变更集声明 `structure_ops` 的点位,按词典 anchors/selector 语义类名 + `op.node` 提取探针,在**非设计层结构源码**中查证施工痕迹;零命中 = 纯 CSS 冒充结构施工 | 操作列分割线 `add_node` 声明多轮从未施工 |
|
|
215
|
+
| **G4 T 通道对账** | `global.brand_primary`(语义值解析 hex)↔ `platform-config.EpThemeColor`;页签 `form` ↔ `ShowModel`;`visible:false` 点位的 `visible_prop` 开关 ↔ platform-config 实值 | 变更集孔雀绿、platform-config 残留旧主题;flat 页签叠加 chrome 结构 |
|
|
164
216
|
|
|
165
|
-
|
|
217
|
+
挂载点疑似但**设计层文件已承载该类名** → 降级不阻断(C 通道由 design-shell 兜底是合法落点)。G2 / G4 违规与挂载点问题同置 `exitCode 1`。审计报错 = **能力缺口**:当场按 `structure_ops` 补挂载 / 补消费者,交付说明单列申报;不得以「词典没建」为由跳过。审计通过 ≠ 还原达标——**静态审计查不出「值对了、位置不对」**,那要靠第 6 步渲染态验收。
|
|
166
218
|
|
|
167
219
|
### 第 1 步附加(之二)· 登记表覆盖率(子项级细闸 · )
|
|
168
220
|
|
|
@@ -347,6 +399,7 @@ html .navbar .navbar-breadcrumb-wrap {
|
|
|
347
399
|
|
|
348
400
|
### 第 4 步 · L3 新模块处理(分级布局)
|
|
349
401
|
|
|
402
|
+
- **验收对象闸(角色集合必须齐)**:验收页面的**角色集合**必须覆盖变更集声明的角色。缺角色的,按第 0.2 步「对接匹配 → 需适配」处置——**项目侧补齐该角色的骨架 / 结构**(走 `crud-pages` 样板 + L3 落地并申报),**不是要求设计侧换页面**。不得用出厂 demo 页充数:demo 页**结构上就没有**标题区 / 批量按钮 / 状态 Tag 这些角色 → 这类差异**无论怎么调 CSS 都消不掉**(不是样式问题,是角色集合不匹配)。
|
|
350
403
|
- **列宽口径(业务层 / 设计层边界)**:表格**业务列宽度属业务层**,设计侧不声明、项目侧不按效果图硬套——效果图里的列宽只是其示例数据下的参考值。**唯一例外是操作列**:宽度由组件公式推算(`resolveOperateColumnWidth`,已含分割线占位),业务页禁止写死。**固定列自查(落完后必做,防「符合规范但不合常规」)**:操作列按钮不被裁、不换行、分割线居中可见;状态列标签完整;表头/单元格标题不被省略号截断——任何一项不满足即返工,禁止用「设计值就是对的」搪塞。
|
|
351
404
|
|
|
352
405
|
- **L3 是施工指令,不是待办**:词典未命中的点位一律当场落地、交付说明申报「补映射」,不留待确认。新模块按「定制布局」三步走:① 基于全局规范生成结构(继承命名/作用域/组件规范)② 原子层字段(色彩/圆角/组件级)从 `basic/` + `component/` 词典借力 ③ 场景专属点位生成后**立即写入 mapping/ 对应场景域**(临时映射),累计复用 ≥3 次建议升级标准(由人工评审)。
|
|
@@ -398,8 +451,12 @@ html .navbar .navbar-breadcrumb-wrap {
|
|
|
398
451
|
**链路(抽取由浏览器完成,diff 由 parser 完成)**:
|
|
399
452
|
|
|
400
453
|
1. **抽取期望值**:在浏览器打开效果图(`oracle.mockup`,元素已带 `data-sid`),对 `oracle.sids` 中每个 sid 取其代表性元素的 computed style,导出为 `{ "<sid>": { "<css属性>": "<值>" } }` → `oracle.json`。
|
|
401
|
-
2.
|
|
402
|
-
|
|
454
|
+
2. **抽取实测值**:在浏览器打开已落地页面,**按词典 `selector` / `anchors.target` 定位元素**(**项目页不要求打 `data-sid`**)做抽取 → `actual.json`。
|
|
455
|
+
- 先跑 `node parser/index.js verify --oracle oracle.json --resolve` 得到「sid → 词典 selector」定位计划;
|
|
456
|
+
- 抽取脚本(Playwright / 浏览器控制台)按计划 selector 取 `getComputedStyle(el)` 的白名单属性(padding / font-size / font-weight / text-align / color / background-color / border-radius / gap 等);
|
|
457
|
+
- **期望端(效果图)用 `data-sid`、实测端(项目页)用词典 selector——两侧定位方式不必对称**:效果图是设计侧自控产物,项目页是消费方、不感知设计坐标。selector 挂载点错位由 `audit` 先于 `verify` 报出,责任链回到词典;
|
|
458
|
+
- 期望值**优先直接消费设计侧交付的 `oracle.json` 抽取产物**(期望 / 实测同源同口径);无产物时再从 `oracle.mockup` 现场抽取;
|
|
459
|
+
- 两个页面的属性白名单必须一致,否则出现假缺口。
|
|
403
460
|
3. **逐点 diff**:
|
|
404
461
|
```bash
|
|
405
462
|
node parser/index.js verify --oracle oracle.json --actual actual.json
|