@morya-ui/mcp 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/decisions.js CHANGED
@@ -1,3 +1,24 @@
1
+ function recipe(props, slots, events) {
2
+ const out = {
3
+ props: props.map(([zh]) => zh),
4
+ propsEn: props.map(([, en]) => en),
5
+ };
6
+ if (slots?.length) {
7
+ out.slots = slots.map(([zh]) => zh);
8
+ out.slotsEn = slots.map(([, en]) => en);
9
+ }
10
+ if (events?.length) {
11
+ out.events = events.map(([zh]) => zh);
12
+ out.eventsEn = events.map(([, en]) => en);
13
+ }
14
+ return out;
15
+ }
16
+ function anti(pairs) {
17
+ return {
18
+ antiPatterns: pairs.map(([zh]) => zh),
19
+ antiPatternsEn: pairs.map(([, en]) => en),
20
+ };
21
+ }
1
22
  export const componentDecisions = [
2
23
  {
3
24
  id: 'form-surface-choice',
@@ -21,6 +42,22 @@ export const componentDecisions = [
21
42
  ],
22
43
  avoidWhen: ['多步骤向导', '多分组长配置页'],
23
44
  avoidWhenEn: ['Multi-step wizards', 'Long multi-section configuration pages'],
45
+ recipe: recipe([
46
+ ['v-model / modelValue 控制开关(不要用 visible)', 'v-model / modelValue for open state (not visible)'],
47
+ ['header 或 title 设弹窗标题', 'header or title for the dialog title'],
48
+ ['width 约 32rem 适配短表单(见 form-in-dialog 黄金样例)', 'width ~32rem for short forms (see form-in-dialog golden)'],
49
+ ['表单字段用 fluid;主按钮 severity="primary"', 'Form fields use fluid; primary button severity="primary"'],
50
+ ], [
51
+ ['#footer 放取消/保存', '#footer for cancel / save'],
52
+ ['默认插槽放 MForm + MFormItem', 'Default slot: MForm + MFormItem'],
53
+ ], [
54
+ ['@close 重置表单模型', '@close to reset the form model'],
55
+ ['保存可用按钮 @click,或 MForm @submit + type="submit"', 'Save via button @click, or MForm @submit + type="submit"'],
56
+ ]),
57
+ ...anti([
58
+ ['用普通 Dialog 手写删除确认 → MConfirmDialog', 'Hand-rolled delete confirm in Dialog → MConfirmDialog'],
59
+ ['长多分组配置塞进 Dialog → Form 页或 Drawer', 'Long multi-section config in Dialog → Form page or Drawer'],
60
+ ]),
24
61
  },
25
62
  {
26
63
  component: 'Drawer',
@@ -28,6 +65,19 @@ export const componentDecisions = [
28
65
  whenEn: ['Form is longer than a dialog but list context matters', 'Detail plus edit in a side panel'],
29
66
  avoidWhen: ['字段极少的确认式录入'],
30
67
  avoidWhenEn: ['Tiny confirmation-style forms'],
68
+ recipe: recipe([
69
+ ['v-model / modelValue 控制开关', 'v-model / modelValue for open state'],
70
+ ['position 常用 right;较长表单可加大 size', 'position usually right; raise size for longer forms'],
71
+ ['header 标明新建/编辑对象', 'header names the create/edit subject'],
72
+ ['字段 fluid;底栏操作对齐 #footer', 'Fields fluid; actions in #footer'],
73
+ ], [
74
+ ['#footer 取消/保存', '#footer cancel / save'],
75
+ ['默认插槽 MForm', 'Default slot: MForm'],
76
+ ], [['@close 清理草稿', '@close to clear draft state']]),
77
+ ...anti([
78
+ ['极短 2–3 字段也用 Drawer → Dialog', 'Tiny 2–3 field form in Drawer → Dialog'],
79
+ ['危险删除只用 Drawer 无确认 → MConfirmDialog', 'Destructive delete in Drawer without confirm → MConfirmDialog'],
80
+ ]),
31
81
  },
32
82
  {
33
83
  component: 'Form',
@@ -38,6 +88,19 @@ export const componentDecisions = [
38
88
  ],
39
89
  avoidWhen: ['列表上的常规短 CRUD'],
40
90
  avoidWhenEn: ['Routine short CRUD from a list'],
91
+ recipe: recipe([
92
+ ['MPageContent width="narrow"', 'MPageContent width="narrow"'],
93
+ ['MPageSection variant="form" 包表单主体', 'MPageSection variant="form" wraps the form body'],
94
+ ['MPageSection variant="actions" 放底栏按钮', 'MPageSection variant="actions" for footer buttons'],
95
+ ['字段 v-model + fluid;校验用 invalid / errorMessage', 'Fields v-model + fluid; validation via invalid / errorMessage'],
96
+ ], [['默认插槽:MFormItem 字段', 'Default: MFormItem fields']], [
97
+ ['MForm @submit(或 @submit.prevent)', 'MForm @submit (or @submit.prevent)'],
98
+ ['提交按钮 type="submit" + severity="primary"', 'Submit button type="submit" + severity="primary"'],
99
+ ]),
100
+ ...anti([
101
+ ['列表短 CRUD 开独立路由 → form-in-dialog', 'Short list CRUD as a route → form-in-dialog'],
102
+ ['表单外再套装饰性 MCard → 用 PageSection form', 'Extra decorative MCard around form → PageSection form'],
103
+ ]),
41
104
  },
42
105
  ],
43
106
  },
@@ -47,41 +110,85 @@ export const componentDecisions = [
47
110
  titleEn: 'Choosing an overlay component',
48
111
  question: '这是确认、短任务、上下文操作,还是需要保留页面上下文的编辑?',
49
112
  questionEn: 'Is this a confirmation, short task, contextual action, or an edit that needs page context?',
50
- keywords: ['弹窗', '浮层', '模态', '侧栏', '确认', '编辑', 'dialog', 'drawer', 'popover', 'tooltip', 'modal', 'side editing', 'side panel', 'context'],
113
+ keywords: [
114
+ '弹窗',
115
+ '浮层',
116
+ '模态',
117
+ '侧栏',
118
+ '确认',
119
+ '编辑',
120
+ 'dialog',
121
+ 'drawer',
122
+ 'popover',
123
+ 'tooltip',
124
+ 'modal',
125
+ 'side editing',
126
+ 'side panel',
127
+ 'context',
128
+ ],
51
129
  options: [
52
130
  {
53
131
  component: 'Dialog',
54
132
  when: [
55
133
  '需要用户聚焦完成一项短到中等任务',
56
134
  '列表页上的新建/编辑短表单(约 ≤8 个字段)',
57
- '需要确认危险操作',
58
135
  '内容不适合直接放在页面流中',
59
136
  ],
60
137
  whenEn: [
61
138
  'The user should focus on a short or medium-sized task',
62
139
  'Create/edit short forms launched from a list (~≤8 fields)',
63
- 'A destructive action needs confirmation',
64
140
  'Content should not interrupt the page flow',
65
141
  ],
66
- avoidWhen: ['内容接近完整多分组页面', '用户需要持续查看底层页面上下文且表单很长'],
142
+ avoidWhen: ['内容接近完整多分组页面', '用户需要持续查看底层页面上下文且表单很长', '只是危险操作确认'],
67
143
  avoidWhenEn: [
68
144
  'The content is effectively a multi-section full page',
69
145
  'The user must continuously reference the underlying page and the form is long',
146
+ 'Only a destructive confirmation is needed',
70
147
  ],
148
+ recipe: recipe([
149
+ ['v-model 控制可见', 'v-model for visibility'],
150
+ ['header / title', 'header / title'],
151
+ ['modal 默认阻塞;任务型内容放默认插槽', 'modal blocks by default; task content in default slot'],
152
+ ['危险确认不要用 Dialog 冒充 → ConfirmDialog', 'Do not fake destructive confirm with Dialog → ConfirmDialog'],
153
+ ], [['#footer 主/次操作', '#footer primary / secondary actions']], [['@close / @update:modelValue', '@close / @update:modelValue']]),
154
+ ...anti([
155
+ ['Dialog + 手写「确定删除」→ MConfirmDialog', 'Dialog + hand-rolled Delete? → MConfirmDialog'],
156
+ ['v-model:visible → v-model / modelValue', 'v-model:visible → v-model / modelValue'],
157
+ ]),
71
158
  },
72
159
  {
73
160
  component: 'Drawer',
74
161
  when: ['需要侧边编辑或查看详情', '需要保留底层列表或工作区上下文', '内容比普通确认框更长'],
75
- whenEn: ['Side editing or detail inspection is needed', 'The underlying list or workspace context should remain visible', 'The content is longer than a normal confirmation'],
162
+ whenEn: [
163
+ 'Side editing or detail inspection is needed',
164
+ 'The underlying list or workspace context should remain visible',
165
+ 'The content is longer than a normal confirmation',
166
+ ],
76
167
  avoidWhen: ['只是简单确认', '操作必须阻塞用户直到明确确认'],
77
168
  avoidWhenEn: ['It is only a simple confirmation', 'The action must block the user until an explicit decision'],
169
+ recipe: recipe([
170
+ ['v-model', 'v-model'],
171
+ ['position="right"(常用)', 'position="right" (typical)'],
172
+ ['header 说明侧栏任务', 'header describes the side-panel job'],
173
+ ], [['#footer 操作区', '#footer actions']], [['@close', '@close']]),
174
+ ...anti([['简单 Yes/No 确认用 Drawer → ConfirmDialog / ConfirmPopup', 'Simple Yes/No in Drawer → ConfirmDialog / ConfirmPopup']]),
78
175
  },
79
176
  {
80
177
  component: 'Popover',
81
178
  when: ['轻量上下文操作或补充信息', '不需要阻塞页面', '内容与触发元素强相关'],
82
- whenEn: ['Lightweight contextual actions or supporting information', 'The page should not be blocked', 'The content is tightly related to the trigger'],
179
+ whenEn: [
180
+ 'Lightweight contextual actions or supporting information',
181
+ 'The page should not be blocked',
182
+ 'The content is tightly related to the trigger',
183
+ ],
83
184
  avoidWhen: ['需要复杂表单或危险操作确认'],
84
185
  avoidWhenEn: ['A complex form or destructive confirmation is required'],
186
+ recipe: recipe([
187
+ ['v-model 或触发器控制显示', 'v-model or trigger-controlled visibility'],
188
+ ['placement 相对触发元素', 'placement relative to the trigger'],
189
+ ['内容保持短小', 'Keep content short'],
190
+ ], [['默认插槽:轻量内容', 'Default slot: lightweight content']]),
191
+ ...anti([['复杂表单塞进 Popover → Dialog / Drawer', 'Complex form in Popover → Dialog / Drawer']]),
85
192
  },
86
193
  {
87
194
  component: 'Tooltip',
@@ -89,6 +196,12 @@ export const componentDecisions = [
89
196
  whenEn: ['Only a short explanation is needed', 'Extra help is needed on hover or focus'],
90
197
  avoidWhen: ['信息是必需内容', '需要放置交互控件'],
91
198
  avoidWhenEn: ['The information is essential content', 'Interactive controls need to be placed inside'],
199
+ recipe: recipe([
200
+ ['content 或默认插槽短文案', 'content or default slot short copy'],
201
+ ['placement', 'placement'],
202
+ ['包裹触发元素', 'Wrap the trigger element'],
203
+ ]),
204
+ ...anti([['Tooltip 里放按钮/表单 → Popover / Dialog', 'Buttons/forms inside Tooltip → Popover / Dialog']]),
92
205
  },
93
206
  ],
94
207
  },
@@ -103,30 +216,75 @@ export const componentDecisions = [
103
216
  {
104
217
  component: 'Table',
105
218
  when: ['数据有稳定列结构', '用户需要排序、筛选、批量或行操作', '需要高密度比较多条记录'],
106
- whenEn: ['Data has a stable column structure', 'Users need sorting, filtering, bulk actions, or row actions', 'Many records must be compared at high density'],
219
+ whenEn: [
220
+ 'Data has a stable column structure',
221
+ 'Users need sorting, filtering, bulk actions, or row actions',
222
+ 'Many records must be compared at high density',
223
+ ],
107
224
  avoidWhen: ['每条数据结构差异很大', '移动端无法承载横向列结构'],
108
225
  avoidWhenEn: ['Each record has a very different structure', 'A horizontal column layout cannot work on mobile'],
226
+ recipe: recipe([
227
+ ['columns + rows(没有 data prop)', 'columns + rows (no data prop)'],
228
+ ['row-key 默认 id;不稳定时显式指定', 'row-key defaults to id; set explicitly when needed'],
229
+ ['分页:paginator + rows-per-page 或 v-model:page', 'Paging: paginator + rows-per-page or v-model:page'],
230
+ ['行选择:selectionMode + v-model:selection', 'Row select: selectionMode + v-model:selection'],
231
+ ], [
232
+ ['#cell-{key} 自定义单元格', '#cell-{key} custom cells'],
233
+ ['#empty 放 MEmpty', '#empty with MEmpty'],
234
+ ]),
235
+ ...anti([
236
+ [':data → :rows', ':data → :rows'],
237
+ ['手写 <table> → MTable', 'Hand-rolled <table> → MTable'],
238
+ ]),
109
239
  },
110
240
  {
111
241
  component: 'DataView',
112
242
  when: ['数据适合卡片或自定义列表项', '视觉浏览比列对齐更重要', '同一数据需要切换多种展示布局'],
113
- whenEn: ['Data fits cards or custom list items', 'Visual browsing matters more than column alignment', 'The same data needs multiple presentation layouts'],
243
+ whenEn: [
244
+ 'Data fits cards or custom list items',
245
+ 'Visual browsing matters more than column alignment',
246
+ 'The same data needs multiple presentation layouts',
247
+ ],
114
248
  avoidWhen: ['用户必须精确比较字段', '需要复杂列级排序或固定列'],
115
249
  avoidWhenEn: ['Users must compare fields precisely', 'Complex column sorting or frozen columns are required'],
250
+ recipe: recipe([
251
+ ['value / 数据源按文档绑定', 'Bind value / data source per docs'],
252
+ ['layout 切换列表/网格等', 'layout switches list/grid etc.'],
253
+ ['分页与空态按文档配置', 'Configure paging and empty per docs'],
254
+ ], [['#list / #grid 等项模板', '#list / #grid item templates']]),
255
+ ...anti([['需要列对齐比较仍用 DataView → Table', 'Need column compare but used DataView → Table']]),
116
256
  },
117
257
  {
118
258
  component: 'TreeTable',
119
259
  when: ['数据同时具有表格列和父子层级', '用户需要展开、收起层级节点'],
120
- whenEn: ['Data has both table columns and parent-child hierarchy', 'Users need to expand and collapse hierarchy nodes'],
260
+ whenEn: [
261
+ 'Data has both table columns and parent-child hierarchy',
262
+ 'Users need to expand and collapse hierarchy nodes',
263
+ ],
121
264
  avoidWhen: ['数据没有真实层级关系', '普通 Table 已能表达关系'],
122
265
  avoidWhenEn: ['There is no real hierarchy', 'A regular Table already expresses the relationship'],
266
+ recipe: recipe([
267
+ ['columns + 带 children 的行数据', 'columns + rows with children'],
268
+ ['展开相关 prop 按文档(expandedKeys 等)', 'Expansion props per docs (expandedKeys etc.)'],
269
+ ['row-key 稳定', 'Stable row-key'],
270
+ ], [['#cell-{key}', '#cell-{key}']]),
271
+ ...anti([['无层级硬用 TreeTable → Table', 'No hierarchy but TreeTable → Table']]),
123
272
  },
124
273
  {
125
274
  component: 'Tree',
126
275
  when: ['主要任务是浏览或选择层级节点', '节点信息不需要多列比较'],
127
- whenEn: ['The main task is browsing or selecting hierarchy nodes', 'Nodes do not need multi-column comparison'],
276
+ whenEn: [
277
+ 'The main task is browsing or selecting hierarchy nodes',
278
+ 'Nodes do not need multi-column comparison',
279
+ ],
128
280
  avoidWhen: ['每行需要展示多个可比较字段'],
129
281
  avoidWhenEn: ['Each row needs several comparable fields'],
282
+ recipe: recipe([
283
+ [':value / value 树节点数据', ':value / value tree nodes'],
284
+ ['selectionMode 单选/多选', 'selectionMode single/multiple'],
285
+ ['v-model:selectionKeys 或文档等价绑定', 'v-model:selectionKeys or documented equivalent'],
286
+ ]),
287
+ ...anti([['表单里选组织树 → 优先 TreeSelect', 'Picking org tree in a form → prefer TreeSelect']]),
130
288
  },
131
289
  ],
132
290
  },
@@ -136,56 +294,142 @@ export const componentDecisions = [
136
294
  titleEn: 'Choosing a selection control',
137
295
  question: '选项是平面少量、层级结构、多选标签,还是需要输入搜索?',
138
296
  questionEn: 'Are options a small flat set, a hierarchy, multi-select tags, or searchable input?',
139
- keywords: ['选择', '下拉', '多选', '树选择', '搜索选择', '级联', '列表选择', '按钮组', 'select', 'treeselect', 'autocomplete', 'cascadeselect', 'listbox', 'selectbutton', 'radio', 'dropdown'],
297
+ keywords: [
298
+ '选择',
299
+ '下拉',
300
+ '多选',
301
+ '树选择',
302
+ '搜索选择',
303
+ '级联',
304
+ '列表选择',
305
+ '按钮组',
306
+ 'select',
307
+ 'treeselect',
308
+ 'autocomplete',
309
+ 'cascadeselect',
310
+ 'listbox',
311
+ 'selectbutton',
312
+ 'radio',
313
+ 'dropdown',
314
+ ],
140
315
  options: [
141
316
  {
142
317
  component: 'Select',
143
318
  when: ['平面选项数量中等', '需要单选或多选', '表单字段需要明确选项集合'],
144
- whenEn: ['The options are a medium-sized flat set', 'Single or multiple selection is needed', 'The form needs a defined option set'],
319
+ whenEn: [
320
+ 'The options are a medium-sized flat set',
321
+ 'Single or multiple selection is needed',
322
+ 'The form needs a defined option set',
323
+ ],
145
324
  avoidWhen: ['选项有明显层级', '用户需要输入自由文本并搜索建议'],
146
325
  avoidWhenEn: ['Options have a meaningful hierarchy', 'Users need free text with suggestions'],
326
+ recipe: recipe([
327
+ ['v-model', 'v-model'],
328
+ [':options 选项数组', ':options array'],
329
+ ['表单内 fluid;可 clearable(或 showClear)', 'In forms: fluid; clearable (or showClear)'],
330
+ ['多选按文档 multiple', 'multiple per docs when needed'],
331
+ ], undefined, [['@change / @update:modelValue', '@change / @update:modelValue']]),
332
+ ...anti([
333
+ ['MDropdown 当枚举选择 → MSelect', 'MDropdown as enum picker → MSelect'],
334
+ ['层级数据用 Select → TreeSelect / CascadeSelect', 'Hierarchical data in Select → TreeSelect / CascadeSelect'],
335
+ ]),
147
336
  },
148
337
  {
149
338
  component: 'TreeSelect',
150
339
  when: ['选项有父子层级', '用户需要按组织、分类或资源树选择'],
151
- whenEn: ['Options have parent-child hierarchy', 'Users select from organizations, categories, or resource trees'],
340
+ whenEn: [
341
+ 'Options have parent-child hierarchy',
342
+ 'Users select from organizations, categories, or resource trees',
343
+ ],
152
344
  avoidWhen: ['选项只是简单平面枚举'],
153
345
  avoidWhenEn: ['Options are a simple flat enum'],
346
+ recipe: recipe([
347
+ ['v-model', 'v-model'],
348
+ [':options 树形数据', ':options tree data'],
349
+ ['fluid;selectionMode 按需', 'fluid; selectionMode as needed'],
350
+ ]),
351
+ ...anti([['平面枚举用 TreeSelect → Select', 'Flat enum with TreeSelect → Select']]),
154
352
  },
155
353
  {
156
354
  component: 'AutoComplete',
157
355
  when: ['用户需要输入关键词搜索建议', '候选项很多或来自远程接口', '输入值本身也有意义'],
158
- whenEn: ['Users type keywords to search suggestions', 'There are many or remote candidates', 'The entered value is meaningful itself'],
356
+ whenEn: [
357
+ 'Users type keywords to search suggestions',
358
+ 'There are many or remote candidates',
359
+ 'The entered value is meaningful itself',
360
+ ],
159
361
  avoidWhen: ['用户只能从固定枚举中选择', '不应该允许自由输入'],
160
362
  avoidWhenEn: ['Users must choose from a fixed enum', 'Free input must not be allowed'],
363
+ recipe: recipe([
364
+ ['v-model', 'v-model'],
365
+ ['suggestions / 远程加载按文档', 'suggestions / remote load per docs'],
366
+ ['fluid', 'fluid'],
367
+ ], undefined, [['@complete 拉取建议', '@complete to fetch suggestions']]),
368
+ ...anti([['固定枚举却允许自由输入 → Select', 'Fixed enum allowing free text → Select']]),
161
369
  },
162
370
  {
163
371
  component: 'SelectButton',
164
372
  when: ['选项很少且需要全部露出', '单选或多选都适合按钮组'],
165
- whenEn: ['Only a few options, and all of them should stay visible', 'Single or multiple selection works as a button group'],
373
+ whenEn: [
374
+ 'Only a few options, and all of them should stay visible',
375
+ 'Single or multiple selection works as a button group',
376
+ ],
166
377
  avoidWhen: ['选项超过大约 5 个', '选项很长或来自远程搜索'],
167
378
  avoidWhenEn: ['More than about five options', 'Labels are long or options come from remote search'],
379
+ recipe: recipe([
380
+ ['v-model', 'v-model'],
381
+ [':options', ':options'],
382
+ ['选项文案保持短', 'Keep option labels short'],
383
+ ]),
384
+ ...anti([['>5 个选项仍用 SelectButton → Select', '>5 options still SelectButton → Select']]),
168
385
  },
169
386
  {
170
387
  component: 'Radio',
171
388
  when: ['少量互斥选项需要和表单文案一起阅读', '一次只能选一个'],
172
- whenEn: ['A few mutually exclusive options should be read with the form copy', 'Only one value can be selected'],
389
+ whenEn: [
390
+ 'A few mutually exclusive options should be read with the form copy',
391
+ 'Only one value can be selected',
392
+ ],
173
393
  avoidWhen: ['多选', '选项很多需要收进弹出层'],
174
394
  avoidWhenEn: ['Multiple selection', 'Too many options for an always-visible list'],
395
+ recipe: recipe([
396
+ ['MRadioGroup v-model', 'MRadioGroup v-model'],
397
+ ['各 MRadio value', 'Each MRadio value'],
398
+ ['与 MFormItem 一起用', 'Use with MFormItem'],
399
+ ]),
400
+ ...anti([['多选需求用 Radio → Checkbox / Select multiple', 'Need multi-select but Radio → Checkbox / Select multiple']]),
175
401
  },
176
402
  {
177
403
  component: 'Listbox',
178
404
  when: ['选项需要始终以列表展示', '可筛选,但不希望收成下拉框'],
179
- whenEn: ['Options should stay visible as a list', 'Filtering is useful, but a collapsed dropdown is not'],
405
+ whenEn: [
406
+ 'Options should stay visible as a list',
407
+ 'Filtering is useful, but a collapsed dropdown is not',
408
+ ],
180
409
  avoidWhen: ['页面空间紧、只需要一个闭合的选择框'],
181
410
  avoidWhenEn: ['Space is tight and a closed select is enough'],
411
+ recipe: recipe([
412
+ ['v-model', 'v-model'],
413
+ [':options', ':options'],
414
+ ['filter 按需开启', 'filter when needed'],
415
+ ]),
416
+ ...anti([['空间紧仍用 Listbox → Select', 'Tight space but Listbox → Select']]),
182
417
  },
183
418
  {
184
419
  component: 'CascadeSelect',
185
420
  when: ['值要按多级分栏逐级点选', '层级是路径而不是可勾选的树'],
186
- whenEn: ['The value is chosen column by column through levels', 'The hierarchy is a path, not a checkable tree'],
421
+ whenEn: [
422
+ 'The value is chosen column by column through levels',
423
+ 'The hierarchy is a path, not a checkable tree',
424
+ ],
187
425
  avoidWhen: ['需要勾选树节点或搜索整棵树'],
188
426
  avoidWhenEn: ['Users need to check tree nodes or search the whole tree'],
427
+ recipe: recipe([
428
+ ['v-model', 'v-model'],
429
+ [':options 级联数据', ':options cascade data'],
430
+ ['fluid', 'fluid'],
431
+ ]),
432
+ ...anti([['可勾选树节点用 CascadeSelect → TreeSelect', 'Checkable tree nodes with CascadeSelect → TreeSelect']]),
189
433
  },
190
434
  ],
191
435
  },
@@ -200,23 +444,61 @@ export const componentDecisions = [
200
444
  {
201
445
  component: 'Card',
202
446
  when: ['内容是页面中的独立业务区块', '需要标题、副标题、页脚或 hover 表面', '需要清晰的边界和内边距'],
203
- whenEn: ['The content is an independent business section', 'A title, subtitle, footer, or hover surface is useful', 'A clear boundary and padding are needed'],
204
- avoidWhen: ['页面已有过多嵌套表面', '内容只是简单分组'],
205
- avoidWhenEn: ['The page already has too many nested surfaces', 'The content is only a simple group'],
447
+ whenEn: [
448
+ 'The content is an independent business section',
449
+ 'A title, subtitle, footer, or hover surface is useful',
450
+ 'A clear boundary and padding are needed',
451
+ ],
452
+ avoidWhen: ['页面已有过多嵌套表面', '内容只是简单分组', '列表表格外层'],
453
+ avoidWhenEn: [
454
+ 'The page already has too many nested surfaces',
455
+ 'The content is only a simple group',
456
+ 'Wrapping a list table',
457
+ ],
458
+ recipe: recipe([
459
+ ['title / subtitle 按需', 'title / subtitle as needed'],
460
+ ['shadow="always"|"hover"|"never"', 'shadow="always"|"hover"|"never"'],
461
+ ['仪表盘图表区可用 shadow="always"', 'Dashboard chart areas may use shadow="always"'],
462
+ ], [
463
+ ['#header / #footer / #title 按需', '#header / #footer / #title as needed'],
464
+ ['默认插槽正文', 'Default slot body'],
465
+ ]),
466
+ ...anti([
467
+ ['MPageFilters / MTable 外再套 MCard → 去掉外层 Card', 'MCard around MPageFilters / MTable → remove outer Card'],
468
+ ['表单主体用 Card 叠 PageSection form → 只用 PageSection', 'Card + PageSection form stack → PageSection only'],
469
+ ]),
206
470
  },
207
471
  {
208
472
  component: 'Panel',
209
473
  when: ['需要可折叠或强调一个较长内容区块', '内容具有明确的面板标题'],
210
- whenEn: ['A collapsible or prominent longer section is needed', 'The content has a clear panel heading'],
474
+ whenEn: [
475
+ 'A collapsible or prominent longer section is needed',
476
+ 'The content has a clear panel heading',
477
+ ],
211
478
  avoidWhen: ['只需要普通内容容器', '标题和边界会增加视觉噪音'],
212
479
  avoidWhenEn: ['A regular content container is enough', 'A heading and boundary would add visual noise'],
480
+ recipe: recipe([
481
+ ['header 面板标题', 'header panel title'],
482
+ ['toggleable 可折叠时开启', 'toggleable when collapsible'],
483
+ ['collapsed / v-model:collapsed 按文档', 'collapsed / v-model:collapsed per docs'],
484
+ ]),
485
+ ...anti([['装饰性折叠用不必要的 Panel → Card 或纯布局', 'Decorative Panel → Card or plain layout']]),
213
486
  },
214
487
  {
215
488
  component: 'Fieldset',
216
489
  when: ['需要语义化分组相关表单字段', '分组标题对理解表单很重要'],
217
- whenEn: ['Related form fields need semantic grouping', 'A group title is important for understanding the form'],
490
+ whenEn: [
491
+ 'Related form fields need semantic grouping',
492
+ 'A group title is important for understanding the form',
493
+ ],
218
494
  avoidWhen: ['内容不是表单字段', '只是为了增加装饰边框'],
219
495
  avoidWhenEn: ['The content is not form fields', 'The border would be purely decorative'],
496
+ recipe: recipe([
497
+ ['legend / 标题文案', 'legend / title copy'],
498
+ ['内放 MFormItem 字段', 'Place MFormItem fields inside'],
499
+ ['勿用于非表单装饰框', 'Do not use as a non-form decorative box'],
500
+ ]),
501
+ ...anti([['非表单内容用 Fieldset → Card / PageSection', 'Non-form content in Fieldset → Card / PageSection']]),
220
502
  },
221
503
  ],
222
504
  },
@@ -226,28 +508,67 @@ export const componentDecisions = [
226
508
  titleEn: 'Choosing page spacing/layout components',
227
509
  question: '控件组、页面区块、栅格之间需要哪种间距与对齐方式?',
228
510
  questionEn: 'What kind of spacing and alignment is needed between controls, page sections, or grid areas?',
229
- keywords: ['间距', '布局', 'gap', 'flex', 'space', 'page', 'toolbar', 'filters', 'spacing', 'layout', 'stack'],
511
+ keywords: [
512
+ '间距',
513
+ '布局',
514
+ 'gap',
515
+ 'flex',
516
+ 'space',
517
+ 'page',
518
+ 'toolbar',
519
+ 'filters',
520
+ 'spacing',
521
+ 'layout',
522
+ 'stack',
523
+ ],
230
524
  options: [
231
525
  {
232
526
  component: 'PageContent',
233
527
  when: ['页面主内容区需要统一的 padding 和垂直 gap', '列表/表单/仪表盘区块纵向堆叠'],
234
- whenEn: ['The main content area needs consistent padding and vertical gap', 'List, form, or dashboard sections stack vertically'],
528
+ whenEn: [
529
+ 'The main content area needs consistent padding and vertical gap',
530
+ 'List, form, or dashboard sections stack vertically',
531
+ ],
235
532
  avoidWhen: ['只是两个按钮之间的水平间距'],
236
533
  avoidWhenEn: ['Only horizontal spacing between two buttons is needed'],
534
+ recipe: recipe([
535
+ ['默认包裹列表/表单主列', 'Default wrapper for list/form main column'],
536
+ ['表单页 width="narrow"', 'Form pages: width="narrow"'],
537
+ ['bands 默认即可;勿再套多余 padding 容器', 'Default bands; avoid extra padding wrappers'],
538
+ ]),
539
+ ...anti([['按钮间距用 PageContent → Space / Flex', 'Button spacing via PageContent → Space / Flex']]),
237
540
  },
238
541
  {
239
542
  component: 'Flex',
240
543
  when: ['同一行控件需要 gap 和对齐', '工具栏内操作组、筛选控件组'],
241
- whenEn: ['Controls on one row need gap and alignment', 'Toolbar action groups or filter control rows'],
544
+ whenEn: [
545
+ 'Controls on one row need gap and alignment',
546
+ 'Toolbar action groups or filter control rows',
547
+ ],
242
548
  avoidWhen: ['整个页面区块的 padding 和垂直 rhythm'],
243
549
  avoidWhenEn: ['Padding and vertical rhythm for whole page sections'],
550
+ recipe: recipe([
551
+ ['gap / align / justify 按需(或等价 class/prop)', 'gap / align / justify as needed'],
552
+ ['筛选行、工具栏内组优先 Flex', 'Prefer Flex for filter rows and toolbar groups'],
553
+ ['不要用 Flex 代替 PageContent 的页面 padding', 'Do not replace PageContent page padding with Flex'],
554
+ ]),
555
+ ...anti([['整页垂直节奏用 Flex → PageContent', 'Whole-page vertical rhythm via Flex → PageContent']]),
244
556
  },
245
557
  {
246
558
  component: 'Space',
247
559
  when: ['简单 wrap 控件组且不需要 justify 语义', '表格行内操作按钮组'],
248
- whenEn: ['A simple wrapped control group without justify semantics', 'Inline row action button groups'],
560
+ whenEn: [
561
+ 'A simple wrapped control group without justify semantics',
562
+ 'Inline row action button groups',
563
+ ],
249
564
  avoidWhen: ['页面级标题与主操作两端对齐'],
250
565
  avoidWhenEn: ['Page-level title and primary action need space-between alignment'],
566
+ recipe: recipe([
567
+ ['包裹 MButton 组', 'Wrap MButton groups'],
568
+ ['行内操作常用 size="small" text 按钮', 'Row actions often size="small" text buttons'],
569
+ ['Dialog #footer 内可用 Space + justify-content:flex-end', 'In Dialog #footer: Space + justify-content:flex-end'],
570
+ ]),
571
+ ...anti([['标题与主按钮两端对齐只用 Space → PageToolbar / Flex justify', 'Title/actions space-between via Space alone → PageToolbar / Flex']]),
251
572
  },
252
573
  {
253
574
  component: 'Grid',
@@ -255,6 +576,12 @@ export const componentDecisions = [
255
576
  whenEn: ['KPI cards, dashboard two-column layouts, responsive columns'],
256
577
  avoidWhen: ['单个筛选行或表单字段列'],
257
578
  avoidWhenEn: ['A single filter row or form field column'],
579
+ recipe: recipe([
580
+ ['列数用文档 columns / 响应式配置', 'Column count via docs columns / responsive config'],
581
+ ['KPI:Grid + MPageStat', 'KPIs: Grid + MPageStat'],
582
+ ['gap 走 token,勿写裸 px', 'gap via tokens; no raw px'],
583
+ ]),
584
+ ...anti([['筛选行用 Grid → Flex / PageFilters', 'Filter row via Grid → Flex / PageFilters']]),
258
585
  },
259
586
  ],
260
587
  },
@@ -264,7 +591,19 @@ export const componentDecisions = [
264
591
  titleEn: 'Choosing page section components',
265
592
  question: '这是筛选区、标题区、表单表面、KPI 还是图表占位?',
266
593
  questionEn: 'Is this a filter bar, page header, form surface, KPI metric, or chart placeholder?',
267
- keywords: ['筛选', '工具栏', '标题', '表单', 'kpi', '占位', 'page filters', 'toolbar', 'header', 'stat', 'placeholder'],
594
+ keywords: [
595
+ '筛选',
596
+ '工具栏',
597
+ '标题',
598
+ '表单',
599
+ 'kpi',
600
+ '占位',
601
+ 'page filters',
602
+ 'toolbar',
603
+ 'header',
604
+ 'stat',
605
+ 'placeholder',
606
+ ],
268
607
  options: [
269
608
  {
270
609
  component: 'PageFilters',
@@ -272,6 +611,12 @@ export const componentDecisions = [
272
611
  whenEn: ['Filter or search region on list pages', 'A consistent muted background and border is needed'],
273
612
  avoidWhen: ['普通表单字段分组', 'KPI 指标展示'],
274
613
  avoidWhenEn: ['Regular form field grouping', 'KPI metric display'],
614
+ recipe: recipe([
615
+ ['内放 MInput / MSelect 等筛选控件', 'Place MInput / MSelect filters inside'],
616
+ ['可 collapsible + #advanced', 'Optional collapsible + #advanced'],
617
+ ['不要外包 MCard', 'Do not wrap in MCard'],
618
+ ], [['#advanced 高级筛选', '#advanced for advanced filters']]),
619
+ ...anti([['表单字段组用 PageFilters → PageSection form / Fieldset', 'Form field groups via PageFilters → PageSection form / Fieldset']]),
275
620
  },
276
621
  {
277
622
  component: 'PageToolbar',
@@ -279,6 +624,12 @@ export const componentDecisions = [
279
624
  whenEn: ['Page title plus primary actions on the right', 'Create button area on list pages'],
280
625
  avoidWhen: ['带长描述的表单引导区'],
281
626
  avoidWhenEn: ['Form intro areas with long descriptions'],
627
+ recipe: recipe([
628
+ ['左侧标题、右侧主按钮', 'Title left, primary action right'],
629
+ ['主按钮 severity="primary";全页仅一个主按钮', 'Primary button severity="primary"; one primary per page'],
630
+ ['批量操作条也可放工具栏区', 'Bulk action bars can sit in the toolbar area'],
631
+ ], [['#actions 右侧操作', '#actions for right-side actions']]),
632
+ ...anti([['需要长描述仍用 Toolbar → PageHeader', 'Long description but Toolbar → PageHeader']]),
282
633
  },
283
634
  {
284
635
  component: 'PageHeader',
@@ -286,6 +637,15 @@ export const componentDecisions = [
286
637
  whenEn: ['Page title plus descriptive copy', 'Form or detail page intros'],
287
638
  avoidWhen: ['只有标题和单个主按钮的列表工具栏'],
288
639
  avoidWhenEn: ['List toolbars with only a title and one primary button'],
640
+ recipe: recipe([
641
+ ['title + description', 'title + description'],
642
+ ['详情可用 #breadcrumb / #tags(放 MStatus)', 'Detail: #breadcrumb / #tags (MStatus)'],
643
+ ['列表页仅标题+新建时优先 PageToolbar', 'List title+create only → prefer PageToolbar'],
644
+ ], [
645
+ ['#breadcrumb', '#breadcrumb'],
646
+ ['#tags / #actions', '#tags / #actions'],
647
+ ]),
648
+ ...anti([['纯列表标题+新建仍用 Header → PageToolbar', 'List title+create still Header → PageToolbar']]),
289
649
  },
290
650
  {
291
651
  component: 'PageSection',
@@ -293,6 +653,12 @@ export const componentDecisions = [
293
653
  whenEn: ['Form body surface (variant="form")', 'Footer actions (variant="actions")'],
294
654
  avoidWhen: ['列表页筛选区', 'KPI 卡片'],
295
655
  avoidWhenEn: ['List page filter areas', 'KPI cards'],
656
+ recipe: recipe([
657
+ ['variant="form" 表单主体', 'variant="form" form body'],
658
+ ['variant="actions" 底栏', 'variant="actions" footer bar'],
659
+ ['可选 title 分组', 'Optional title for grouping'],
660
+ ]),
661
+ ...anti([['variant="form" 外再套 MCard → 双边框', 'MCard outside variant="form" → double border']]),
296
662
  },
297
663
  {
298
664
  component: 'PageStat',
@@ -300,6 +666,12 @@ export const componentDecisions = [
300
666
  whenEn: ['Dashboard KPI cards', 'Needs label/value/trend/icon structure'],
301
667
  avoidWhen: ['普通内容分组', '表格或表单'],
302
668
  avoidWhenEn: ['Generic content grouping', 'Tables or forms'],
669
+ recipe: recipe([
670
+ ['label + value', 'label + value'],
671
+ ['trend / trendSeverity / trendDirection 按需', 'trend / trendSeverity / trendDirection as needed'],
672
+ ['loading 骨架;layout card|plain', 'loading skeleton; layout card|plain'],
673
+ ]),
674
+ ...anti([['普通段落用 PageStat → 文案或 Card', 'Paragraph as PageStat → copy or Card']]),
303
675
  },
304
676
  {
305
677
  component: 'PagePlaceholder',
@@ -307,6 +679,12 @@ export const componentDecisions = [
307
679
  whenEn: ['Charts, maps, or media areas are not wired yet', 'A dashed placeholder surface is needed'],
308
680
  avoidWhen: ['真实数据表格或表单'],
309
681
  avoidWhenEn: ['Real data tables or forms'],
682
+ recipe: recipe([
683
+ ['description 说明待接入内容', 'description explains pending content'],
684
+ ['可配合 MEmpty 于 Card 内(仪表盘图表区)', 'Or MEmpty inside MCard for dashboard charts'],
685
+ ['minHeight 避免占位塌缩', 'minHeight avoids collapsed placeholders'],
686
+ ]),
687
+ ...anti([['真实表格用 Placeholder → MTable', 'Real table via Placeholder → MTable']]),
310
688
  },
311
689
  ],
312
690
  },
@@ -340,7 +718,16 @@ export const componentDecisions = [
340
718
  'Main page scroll should follow the layout theme',
341
719
  ],
342
720
  avoidWhen: ['单个卡片内部的小块内容', '需要业务自行控制滚动的 Dialog/Drawer 内容'],
343
- avoidWhenEn: ['Small regions inside a single card', 'Dialog/Drawer bodies where apps control scrolling'],
721
+ avoidWhenEn: [
722
+ 'Small regions inside a single card',
723
+ 'Dialog/Drawer bodies where apps control scrolling',
724
+ ],
725
+ recipe: recipe([
726
+ ['MLayout fill-viewport(或 :fill-viewport="true")', 'MLayout fill-viewport (or :fill-viewport="true")'],
727
+ ['内容放 MLayoutContent', 'Content in MLayoutContent'],
728
+ ['侧栏用 MLayoutSider + MMenu,不是随便一个 Drawer', 'Sider: MLayoutSider + MMenu, not a random Drawer'],
729
+ ]),
730
+ ...anti([['在 Layout 外再包一层 100vh 滚动 → 去掉', 'Extra 100vh scroll wrapper outside Layout → remove']]),
344
731
  },
345
732
  {
346
733
  component: 'MScrollbar',
@@ -364,6 +751,12 @@ export const componentDecisions = [
364
751
  'Native control scrolling such as Textarea',
365
752
  'Areas that already scroll internally (MTable, MLayoutContent, Select popups)',
366
753
  ],
754
+ recipe: recipe([
755
+ ['显式限高后包裹内容', 'Wrap content after an explicit max-height'],
756
+ ['只包业务自有滚动区', 'Only wrap app-owned scroll regions'],
757
+ ['不要叠两层滚动', 'Do not nest two scroll containers'],
758
+ ]),
759
+ ...anti([['MTable 外包 MScrollbar → 去掉外层', 'MScrollbar around MTable → remove outer']]),
367
760
  },
368
761
  {
369
762
  component: 'Built-in (no extra wrapper)',
@@ -377,6 +770,12 @@ export const componentDecisions = [
377
770
  ],
378
771
  avoidWhen: ['在已内置滚动的组件外再包一层滚动容器'],
379
772
  avoidWhenEn: ['Wrapping another scroll container around built-in scroll chrome'],
773
+ recipe: recipe([
774
+ ['信任组件内置滚动,不再包一层', 'Trust built-in scroll; no extra wrapper'],
775
+ ['Table / LayoutContent 直接用', 'Use Table / LayoutContent directly'],
776
+ ['菜单/Select 弹出层也不要外包滚动', 'Do not wrap menu/Select popups either'],
777
+ ]),
778
+ ...anti([['双重滚动条 → 去掉业务包装层', 'Double scrollbars → remove app wrapper']]),
380
779
  },
381
780
  ],
382
781
  },
@@ -394,6 +793,12 @@ export const componentDecisions = [
394
793
  whenEn: ['List page data tables', 'MPageFilters already provides the filter boundary'],
395
794
  avoidWhen: ['需要独立卡片标题的内容模块'],
396
795
  avoidWhenEn: ['Content modules that need an independent card title'],
796
+ recipe: recipe([
797
+ ['MPageContent > MPageFilters + MTable', 'MPageContent > MPageFilters + MTable'],
798
+ ['表格直接放 PageContent,不套 Card', 'Table directly under PageContent, no Card'],
799
+ ['空态用 Table #empty + MEmpty', 'Empty: Table #empty + MEmpty'],
800
+ ]),
801
+ ...anti([['Table 外包 MCard → 去掉 Card', 'MCard around Table → remove Card']]),
397
802
  },
398
803
  {
399
804
  component: 'MCard',
@@ -401,6 +806,12 @@ export const componentDecisions = [
401
806
  whenEn: ['Chart or detail areas on dashboards', 'Content modules that need a card title'],
402
807
  avoidWhen: ['列表页表格外层', '筛选区外层'],
403
808
  avoidWhenEn: ['Wrapping list page tables', 'Wrapping filter regions'],
809
+ recipe: recipe([
810
+ ['title + 正文;图表待接入可用 MEmpty', 'title + body; pending charts may use MEmpty'],
811
+ ['shadow 按仪表盘需要选择', 'Choose shadow for dashboard needs'],
812
+ ['仅用于需要独立标题的模块', 'Only for modules that need their own title'],
813
+ ]),
814
+ ...anti([['列表筛选区套 Card → PageFilters 自带边界', 'Card around filters → PageFilters has its own border']]),
404
815
  },
405
816
  {
406
817
  component: 'PageSection',
@@ -408,6 +819,12 @@ export const componentDecisions = [
408
819
  whenEn: ['Form page bodies or action footers', 'A unified form surface is needed'],
409
820
  avoidWhen: ['再套 MCard variant form 导致双边框'],
410
821
  avoidWhenEn: ['Wrapping again with MCard and causing double borders'],
822
+ recipe: recipe([
823
+ ['variant="form" / variant="actions"', 'variant="form" / variant="actions"'],
824
+ ['不要再外包 MCard', 'Do not wrap another MCard'],
825
+ ['表单字段放 form 段,按钮放 actions 段', 'Fields in form section; buttons in actions section'],
826
+ ]),
827
+ ...anti([['PageSection form + MCard → 只留 PageSection', 'PageSection form + MCard → PageSection only']]),
411
828
  },
412
829
  ],
413
830
  },
@@ -449,20 +866,47 @@ export const componentDecisions = [
449
866
  'Progress can be measured',
450
867
  'Only the button’s own loading state is needed',
451
868
  ],
869
+ recipe: recipe([
870
+ ['包裹内容:MLoading :loading="…" 或 v-loading', 'Wrap content: MLoading :loading or v-loading'],
871
+ ['全屏:fullscreen / loading.service', 'Fullscreen: fullscreen / loading.service'],
872
+ ['按钮等待用 Button loading,不要盖整表', 'Button waits: Button loading, not a full-form mask'],
873
+ ]),
874
+ ...anti([
875
+ ['区域遮罩用 ProgressSpinner → Loading / v-loading', 'Region mask via ProgressSpinner → Loading / v-loading'],
876
+ ['按钮转圈用整页 Loading → Button :loading', 'Full-page Loading for button wait → Button :loading'],
877
+ ]),
452
878
  },
453
879
  {
454
880
  component: 'Skeleton',
455
881
  when: ['内容结构已知,用占位块表示即将出现的卡片、文本或列表', '仪表盘或详情初次进入'],
456
- whenEn: ['The content structure is known and placeholders stand in for cards, text, or lists', 'First paint of a dashboard or detail view'],
882
+ whenEn: [
883
+ 'The content structure is known and placeholders stand in for cards, text, or lists',
884
+ 'First paint of a dashboard or detail view',
885
+ ],
457
886
  avoidWhen: ['需要明确的“正在加载”遮罩', '进度可量化'],
458
887
  avoidWhenEn: ['An explicit loading mask is required', 'Progress can be measured'],
888
+ recipe: recipe([
889
+ ['shape rectangle|circle;text + repeat 模拟多行', 'shape rectangle|circle; text + repeat for lines'],
890
+ ['width / height 贴近最终布局', 'width / height close to final layout'],
891
+ ['PageStat 可用自带 loading', 'PageStat has its own loading'],
892
+ ]),
893
+ ...anti([['未知时长又要文案遮罩 → Loading', 'Unknown wait needing caption mask → Loading']]),
459
894
  },
460
895
  {
461
896
  component: 'ProgressBar',
462
897
  when: ['进度有百分比或可估算完成度', '上传、导入等长任务'],
463
- whenEn: ['Progress has a percentage or an estimated completion', 'Long tasks such as upload or import'],
898
+ whenEn: [
899
+ 'Progress has a percentage or an estimated completion',
900
+ 'Long tasks such as upload or import',
901
+ ],
464
902
  avoidWhen: ['不知道还要等多久', '只是挡住一块区域'],
465
903
  avoidWhenEn: ['Duration is unknown', 'The only goal is to cover a region'],
904
+ recipe: recipe([
905
+ ['value 0–100(或文档等价)', 'value 0–100 (or docs equivalent)'],
906
+ ['显示百分比文案', 'Show percent label'],
907
+ ['上传/导入长任务优先', 'Prefer for upload/import long tasks'],
908
+ ]),
909
+ ...anti([['未知进度用 ProgressBar → Loading', 'Unknown progress via ProgressBar → Loading']]),
466
910
  },
467
911
  {
468
912
  component: 'ProgressSpinner',
@@ -470,6 +914,12 @@ export const componentDecisions = [
470
914
  whenEn: ['A small inline spinner beside a control, without covering content'],
471
915
  avoidWhen: ['区域遮罩、全屏等待或带文案的加载态'],
472
916
  avoidWhenEn: ['A region mask, fullscreen wait, or loading state with a caption'],
917
+ recipe: recipe([
918
+ ['仅作行内指示', 'Inline indicator only'],
919
+ ['尺寸保持小', 'Keep size small'],
920
+ ['不要盖住整块区域', 'Do not cover a whole region'],
921
+ ]),
922
+ ...anti([['当页面遮罩用 Spinner → Loading', 'Spinner as page mask → Loading']]),
473
923
  },
474
924
  {
475
925
  component: 'BlockUI',
@@ -477,6 +927,12 @@ export const componentDecisions = [
477
927
  whenEn: ['Interaction should be blocked without saying that something is loading'],
478
928
  avoidWhen: ['用户需要知道正在等待结果'],
479
929
  avoidWhenEn: ['The user needs to know a result is still pending'],
930
+ recipe: recipe([
931
+ ['blocked 控制遮罩', 'blocked toggles the mask'],
932
+ ['包裹需要禁用的区域', 'Wrap the region to disable'],
933
+ ['无加载文案时用,有文案改 Loading', 'Use when no loading caption; otherwise Loading'],
934
+ ]),
935
+ ...anti([['需要“加载中”文案仍用 BlockUI → Loading', 'Need loading caption but BlockUI → Loading']]),
480
936
  },
481
937
  ],
482
938
  },
@@ -486,21 +942,51 @@ export const componentDecisions = [
486
942
  titleEn: 'Choosing status, tag, chip, or badge',
487
943
  question: '这是行内业务状态、分类标签、可移除实体,还是挂在控件上的数量?',
488
944
  questionEn: 'Is this an inline business status, a category label, a removable entity, or a count on another control?',
489
- keywords: ['行内状态', '状态点', '角标', '徽章', '可关闭标签', '芯片', 'status', 'badge', 'chip', 'tag', 'closable'],
945
+ keywords: [
946
+ '行内状态',
947
+ '状态点',
948
+ '角标',
949
+ '徽章',
950
+ '可关闭标签',
951
+ '芯片',
952
+ 'status',
953
+ 'badge',
954
+ 'chip',
955
+ 'tag',
956
+ 'closable',
957
+ ],
490
958
  options: [
491
959
  {
492
960
  component: 'Status',
493
961
  when: ['表格单元格或标题旁的业务状态', '需要圆点或语义图标加短文案'],
494
- whenEn: ['Business status in a table cell or beside a title', 'A dot or semantic icon plus a short label'],
962
+ whenEn: [
963
+ 'Business status in a table cell or beside a title',
964
+ 'A dot or semantic icon plus a short label',
965
+ ],
495
966
  avoidWhen: ['可关闭的分类标签', '挂在按钮上的数字'],
496
967
  avoidWhenEn: ['A closable category label', 'A count attached to a button'],
968
+ recipe: recipe([
969
+ ['label + severity', 'label + severity'],
970
+ ['variant 默认 dot;可用 tag|text', 'variant default dot; or tag|text'],
971
+ ['表格状态列优先 MStatus,不要一排实心 Tag', 'Prefer MStatus in table status columns over solid Tags'],
972
+ ]),
973
+ ...anti([['可关闭分类用 Status → Tag closable', 'Closable category via Status → Tag closable']]),
497
974
  },
498
975
  {
499
976
  component: 'Tag',
500
977
  when: ['分类、筛选结果或可关闭标签', '需要比 Status 更像芯片的表面'],
501
- whenEn: ['A category, filter result, or closable label', 'A chip-like surface rather than an inline status'],
978
+ whenEn: [
979
+ 'A category, filter result, or closable label',
980
+ 'A chip-like surface rather than an inline status',
981
+ ],
502
982
  avoidWhen: ['只是一行里的轻量状态', '带图片且代表一个可移除实体'],
503
983
  avoidWhenEn: ['A lightweight inline status', 'An entity with an image that can be removed'],
984
+ recipe: recipe([
985
+ ['value 或默认插槽文案', 'value or default-slot copy'],
986
+ ['severity;closable + @close', 'severity; closable + @close'],
987
+ ['rounded / bordered 按需', 'rounded / bordered as needed'],
988
+ ], undefined, [['@close', '@close']]),
989
+ ...anti([['行内启用/停用状态用 Tag → Status', 'Enabled/disabled inline via Tag → Status']]),
504
990
  },
505
991
  {
506
992
  component: 'Chip',
@@ -508,6 +994,12 @@ export const componentDecisions = [
508
994
  whenEn: ['A short entity that may include an icon, image, and remove action'],
509
995
  avoidWhen: ['纯状态色点', '只是分类色块'],
510
996
  avoidWhenEn: ['A status dot only', 'A category color block only'],
997
+ recipe: recipe([
998
+ ['label;icon 或 image', 'label; icon or image'],
999
+ ['removable + @remove', 'removable + @remove'],
1000
+ ['severity 按需', 'severity as needed'],
1001
+ ], undefined, [['@remove', '@remove']]),
1002
+ ...anti([['纯状态点用 Chip → Status', 'Status dot via Chip → Status']]),
511
1003
  },
512
1004
  {
513
1005
  component: 'Badge',
@@ -515,6 +1007,12 @@ export const componentDecisions = [
515
1007
  whenEn: ['A count or dot attached to a button, avatar, or other control'],
516
1008
  avoidWhen: ['独立成行的状态文案'],
517
1009
  avoidWhenEn: ['A standalone status sentence'],
1010
+ recipe: recipe([
1011
+ ['value 数量;省略则圆点', 'value count; omit for dot'],
1012
+ ['severity;max 封顶', 'severity; max cap'],
1013
+ ['默认插槽包裹宿主控件', 'Default slot wraps the host control'],
1014
+ ], [['默认插槽:被角标包裹的控件', 'Default slot: host control']]),
1015
+ ...anti([['独立状态文案用 Badge → Status / Tag', 'Standalone status sentence via Badge → Status / Tag']]),
518
1016
  },
519
1017
  ],
520
1018
  },
@@ -524,21 +1022,67 @@ export const componentDecisions = [
524
1022
  titleEn: 'Empty state vs result page',
525
1023
  question: '这是没有数据,还是流程已经结束的成功、失败或 HTTP 状态?',
526
1024
  questionEn: 'Is there simply no data, or has a flow ended in success, failure, or an HTTP status?',
527
- keywords: ['空状态', '无数据', '首次使用', '筛选无结果', '结果页', '404', '403', '500', 'empty', 'result'],
1025
+ keywords: [
1026
+ '空状态',
1027
+ '无数据',
1028
+ '首次使用',
1029
+ '筛选无结果',
1030
+ '结果页',
1031
+ '404',
1032
+ '403',
1033
+ '500',
1034
+ 'empty',
1035
+ 'result',
1036
+ ],
528
1037
  options: [
529
1038
  {
530
1039
  component: 'Empty',
531
1040
  when: ['列表没有行', '筛选没有命中', '首次使用,需要引导创建'],
532
- whenEn: ['A list has no rows', 'Filters matched nothing', 'First use, and the next step is to create something'],
1041
+ whenEn: [
1042
+ 'A list has no rows',
1043
+ 'Filters matched nothing',
1044
+ 'First use, and the next step is to create something',
1045
+ ],
533
1046
  avoidWhen: ['接口失败', '无权限', '页面不存在', '提交成功回执'],
534
- avoidWhenEn: ['The request failed', 'Permission was denied', 'The page does not exist', 'A submit-success receipt'],
1047
+ avoidWhenEn: [
1048
+ 'The request failed',
1049
+ 'Permission was denied',
1050
+ 'The page does not exist',
1051
+ 'A submit-success receipt',
1052
+ ],
1053
+ recipe: recipe([
1054
+ ['title + description', 'title + description'],
1055
+ ['icon 或 illustration 按需(默认即可)', 'icon or illustration as needed (default OK)'],
1056
+ ['表格内放在 #empty', 'Inside tables: #empty slot'],
1057
+ ], [['#extra 放下一步按钮(创建…)', '#extra for next-step button (Create…)']]),
1058
+ ...anti([
1059
+ ['403/404 用 Empty → Result', '403/404 via Empty → Result'],
1060
+ ['#action → #extra', '#action → #extra'],
1061
+ ]),
535
1062
  },
536
1063
  {
537
1064
  component: 'Result',
538
- when: ['提交成功或失败的终点页', '403 / 404 / 500 等阻断状态', '需要明确的下一步(返回、重试、回首页)'],
539
- whenEn: ['A terminal success or failure page', 'A blocking 403 / 404 / 500 state', 'The next step must be explicit (back, retry, home)'],
1065
+ when: [
1066
+ '提交成功或失败的终点页',
1067
+ '403 / 404 / 500 等阻断状态',
1068
+ '需要明确的下一步(返回、重试、回首页)',
1069
+ ],
1070
+ whenEn: [
1071
+ 'A terminal success or failure page',
1072
+ 'A blocking 403 / 404 / 500 state',
1073
+ 'The next step must be explicit (back, retry, home)',
1074
+ ],
540
1075
  avoidWhen: ['正常的无数据', '表格内部的空行'],
541
1076
  avoidWhenEn: ['A normal empty collection', 'The empty slot inside a table'],
1077
+ recipe: recipe([
1078
+ ['status:success|error|403|404|500 等', 'status: success|error|403|404|500 etc.'],
1079
+ ['title + description', 'title + description'],
1080
+ ['size 按页面比重', 'size for page weight'],
1081
+ ], [['#footer 逃逸/下一步按钮', '#footer escape / next actions']]),
1082
+ ...anti([
1083
+ ['表格无数据用 Result → Empty', 'Table no-rows via Result → Empty'],
1084
+ ['操作插槽写成 #extra → #footer', 'Actions slot #extra → #footer'],
1085
+ ]),
542
1086
  },
543
1087
  ],
544
1088
  },
@@ -548,7 +1092,17 @@ export const componentDecisions = [
548
1092
  titleEn: 'Choosing a menu',
549
1093
  question: '这是按钮上的操作项、侧栏导航、右键菜单,还是全局命令搜索?',
550
1094
  questionEn: 'Is this an action menu on a trigger, sider navigation, a context menu, or a global command search?',
551
- keywords: ['动作菜单', '右键', '右键菜单', '命令面板', '导航菜单', 'context menu', 'command menu', 'menubar', '上下文菜单'],
1095
+ keywords: [
1096
+ '动作菜单',
1097
+ '右键',
1098
+ '右键菜单',
1099
+ '命令面板',
1100
+ '导航菜单',
1101
+ 'context menu',
1102
+ 'command menu',
1103
+ 'menubar',
1104
+ '上下文菜单',
1105
+ ],
552
1106
  options: [
553
1107
  {
554
1108
  component: 'Dropdown',
@@ -556,13 +1110,28 @@ export const componentDecisions = [
556
1110
  whenEn: ['Actions open from a button or icon', 'Edit, delete, or more'],
557
1111
  avoidWhen: ['表单里选一个枚举值', '常驻侧栏导航'],
558
1112
  avoidWhenEn: ['Choosing an enum in a form', 'Persistent sider navigation'],
1113
+ recipe: recipe([
1114
+ [':model / items 操作项', ':model / items for actions'],
1115
+ ['触发器放默认插槽(按钮/图标)', 'Trigger in default slot (button/icon)'],
1116
+ ['危险项用文档 severity / 确认流', 'Danger items: docs severity / confirm flow'],
1117
+ ]),
1118
+ ...anti([['表单枚举用 Dropdown → Select', 'Form enum via Dropdown → Select']]),
559
1119
  },
560
1120
  {
561
1121
  component: 'Menu',
562
1122
  when: ['后台侧栏或页面内的常驻导航', '需要选中项与路由同步'],
563
- whenEn: ['Persistent navigation in an admin sider or page', 'Selection should stay in sync with the route'],
1123
+ whenEn: [
1124
+ 'Persistent navigation in an admin sider or page',
1125
+ 'Selection should stay in sync with the route',
1126
+ ],
564
1127
  avoidWhen: ['一次性操作菜单', '右键弹出'],
565
1128
  avoidWhenEn: ['A one-shot action menu', 'A right-click popup'],
1129
+ recipe: recipe([
1130
+ [':model 导航项(含 icon)', ':model nav items (with icon)'],
1131
+ ['放在 MLayoutSider 内', 'Place inside MLayoutSider'],
1132
+ ['与路由选中态同步', 'Sync selection with the route'],
1133
+ ]),
1134
+ ...anti([['行内「更多」用 Menu → Dropdown', 'Row More via Menu → Dropdown']]),
566
1135
  },
567
1136
  {
568
1137
  component: 'ContextMenu',
@@ -570,6 +1139,12 @@ export const componentDecisions = [
570
1139
  whenEn: ['The menu opens at the pointer, usually from a right-click'],
571
1140
  avoidWhen: ['可见的导航', '表单选择'],
572
1141
  avoidWhenEn: ['Visible navigation', 'Form selection'],
1142
+ recipe: recipe([
1143
+ [':model 菜单项', ':model menu items'],
1144
+ ['绑定上下文目标事件(右键)', 'Bind to context target (right-click)'],
1145
+ ['在指针位置打开,不是侧栏常驻', 'Opens at pointer; not persistent sider nav'],
1146
+ ]),
1147
+ ...anti([['常驻导航用 ContextMenu → Menu', 'Persistent nav via ContextMenu → Menu']]),
573
1148
  },
574
1149
  {
575
1150
  component: 'CommandMenu',
@@ -577,6 +1152,12 @@ export const componentDecisions = [
577
1152
  whenEn: ['Global command search or jump', 'Keyboard-first, such as Cmd/Ctrl+K'],
578
1153
  avoidWhen: ['少量固定操作放在按钮旁即可'],
579
1154
  avoidWhenEn: ['A few fixed actions fit next to a button'],
1155
+ recipe: recipe([
1156
+ ['v-model 开关', 'v-model open state'],
1157
+ ['命令列表 / 搜索按文档', 'Commands / search per docs'],
1158
+ ['全局快捷键打开', 'Open via global shortcut'],
1159
+ ]),
1160
+ ...anti([['两三个固定操作也用 CommandMenu → Dropdown / 按钮', 'Two fixed actions via CommandMenu → Dropdown / buttons']]),
580
1161
  },
581
1162
  {
582
1163
  component: 'Menubar',
@@ -584,6 +1165,145 @@ export const componentDecisions = [
584
1165
  whenEn: ['A top application menu with many grouped items'],
585
1166
  avoidWhen: ['侧栏只有一层链接'],
586
1167
  avoidWhenEn: ['The sider is a single level of links'],
1168
+ recipe: recipe([
1169
+ [':model 分组菜单', ':model grouped menus'],
1170
+ ['放在顶栏而非侧栏', 'Place in top bar, not sider'],
1171
+ ['项多且需要分组时再用', 'Use when many items need grouping'],
1172
+ ]),
1173
+ ...anti([['侧栏单层链接用 Menubar → Menu', 'Single-level sider via Menubar → Menu']]),
1174
+ },
1175
+ ],
1176
+ },
1177
+ {
1178
+ id: 'feedback-choice',
1179
+ title: '如何选择操作反馈',
1180
+ titleEn: 'Choosing operation feedback',
1181
+ question: '这是一句话结果、带详情的通知,还是要留在表单里的错误?',
1182
+ questionEn: 'Is this a one-line result, a notice with detail, or an error that must stay in the form?',
1183
+ keywords: [
1184
+ '反馈',
1185
+ '提示',
1186
+ '消息',
1187
+ 'toast',
1188
+ 'message',
1189
+ '已保存',
1190
+ '错误',
1191
+ 'alert',
1192
+ 'errorMessage',
1193
+ '通知',
1194
+ ],
1195
+ options: [
1196
+ {
1197
+ component: 'message',
1198
+ when: ['CRUD / 保存 / 删除后的单行结果', '不需要单独标题和详情'],
1199
+ whenEn: ['One-line result after CRUD / save / delete', 'No separate title and detail needed'],
1200
+ avoidWhen: ['需要 summary + detail', '错误必须留在表单直到修正'],
1201
+ avoidWhenEn: ['Need summary + detail', 'Error must stay in the form until fixed'],
1202
+ recipe: recipe([
1203
+ ["import { message } from 'morya-ui'", "import { message } from 'morya-ui'"],
1204
+ ["message.success('已保存') / info / warn / error", "message.success('Saved') / info / warn / error"],
1205
+ ['大多数操作反馈的默认选择', 'Default for most operation feedback'],
1206
+ ]),
1207
+ ...anti([
1208
+ ["toast.add({ summary: '已保存' }) → message.success('已保存')", "toast.add({ summary: 'Saved' }) → message.success('Saved')"],
1209
+ ['<MMessage severity> 当内嵌 Alert → errorMessage / role="alert"', '<MMessage severity> as inline Alert → errorMessage / role="alert"'],
1210
+ ]),
1211
+ },
1212
+ {
1213
+ component: 'toast',
1214
+ when: ['需要 summary + detail', '异步/后台任务完成感'],
1215
+ whenEn: ['Need summary + detail', 'Async / background job completion feel'],
1216
+ avoidWhen: ['只有一句话的 CRUD 回执'],
1217
+ avoidWhenEn: ['One-sentence CRUD receipts'],
1218
+ recipe: recipe([
1219
+ ["import { toast } from 'morya-ui'", "import { toast } from 'morya-ui'"],
1220
+ ['toast.success({ summary, detail })', 'toast.success({ summary, detail })'],
1221
+ ['不要用 toast 代替默认 message', 'Do not replace default message with toast'],
1222
+ ]),
1223
+ ...anti([['单行「已保存」用 toast → message', 'One-line Saved via toast → message']]),
1224
+ },
1225
+ {
1226
+ component: 'field errorMessage / role="alert"',
1227
+ when: ['字段校验错误需常驻', '登录/鉴权失败需留在表单区'],
1228
+ whenEn: [
1229
+ 'Field validation errors must persist',
1230
+ 'Login/auth failures should stay in the form region',
1231
+ ],
1232
+ avoidWhen: ['一次性操作成功提示'],
1233
+ avoidWhenEn: ['One-shot success toasts/messages'],
1234
+ recipe: recipe([
1235
+ ['字段:invalid + errorMessage', 'Fields: invalid + errorMessage'],
1236
+ ['表单级:token 样式的 role="alert" 条(见 login-page)', 'Form-level: token-styled role="alert" bar (see login-page)'],
1237
+ ['<MMessage> 只是 message 宿主,不是内嵌 Alert', '<MMessage> is the message host, not an inline Alert'],
1238
+ ]),
1239
+ ...anti([
1240
+ ['登录失败只闪 Toast → 表单区 alert / errorMessage', 'Login failure only via Toast → form alert / errorMessage'],
1241
+ ['编造 MMessage severity 插槽 Alert API', 'Invented MMessage severity slot Alert API'],
1242
+ ]),
1243
+ },
1244
+ ],
1245
+ },
1246
+ {
1247
+ id: 'confirm-choice',
1248
+ title: '如何选择确认框',
1249
+ titleEn: 'Choosing a confirmation UI',
1250
+ question: '危险操作需要居中确认,还是贴着触发点的轻量确认?',
1251
+ questionEn: 'Does the destructive action need a centered confirm, or a lightweight confirm near the trigger?',
1252
+ keywords: [
1253
+ '确认',
1254
+ '删除',
1255
+ '危险',
1256
+ '二次确认',
1257
+ 'confirm',
1258
+ 'delete',
1259
+ 'destructive',
1260
+ 'ConfirmDialog',
1261
+ 'ConfirmPopup',
1262
+ ],
1263
+ options: [
1264
+ {
1265
+ component: 'ConfirmDialog',
1266
+ when: ['删除/不可逆操作需要明确确认', '需要标题、说明与阻塞式决定', '列表行删除的默认选择'],
1267
+ whenEn: [
1268
+ 'Delete / irreversible actions need explicit confirm',
1269
+ 'Need header, message, and a blocking decision',
1270
+ 'Default for list-row delete',
1271
+ ],
1272
+ avoidWhen: ['只需贴着按钮的轻量确认', '复杂编辑表单(那是 Dialog)'],
1273
+ avoidWhenEn: ['Only a lightweight confirm near the button', 'Complex edit forms (that is Dialog)'],
1274
+ recipe: recipe([
1275
+ ['v-model / modelValue', 'v-model / modelValue'],
1276
+ ['header + message', 'header + message'],
1277
+ ['acceptLabel / rejectLabel', 'acceptLabel / rejectLabel'],
1278
+ ['acceptSeverity="danger" 用于删除', 'acceptSeverity="danger" for delete'],
1279
+ ], undefined, [
1280
+ ['@accept 执行删除', '@accept to perform delete'],
1281
+ ['@reject 关闭', '@reject to dismiss'],
1282
+ ]),
1283
+ ...anti([
1284
+ ['普通 Dialog 手写「确定/取消」删除 → ConfirmDialog', 'Hand-rolled Dialog Yes/No delete → ConfirmDialog'],
1285
+ ['编辑表单误用 ConfirmDialog → Dialog + Form', 'Edit form via ConfirmDialog → Dialog + Form'],
1286
+ ]),
1287
+ },
1288
+ {
1289
+ component: 'ConfirmPopup',
1290
+ when: ['确认应贴着触发按钮/行内操作', '文案短、不需要大对话框'],
1291
+ whenEn: [
1292
+ 'Confirm should sit next to the trigger button / row action',
1293
+ 'Short copy; no large dialog needed',
1294
+ ],
1295
+ avoidWhen: ['需要醒目标题与强阻塞感的删除', '无定位目标时的全局确认'],
1296
+ avoidWhenEn: [
1297
+ 'High-stakes delete needing a strong blocking title',
1298
+ 'Global confirm without an anchor target',
1299
+ ],
1300
+ recipe: recipe([
1301
+ ['v-model / modelValue', 'v-model / modelValue'],
1302
+ ['message;acceptSeverity="danger" 按需', 'message; acceptSeverity="danger" as needed'],
1303
+ ['target 锚定触发元素(或 position)', 'target anchors the trigger (or position)'],
1304
+ ['placement top|bottom|left|right', 'placement top|bottom|left|right'],
1305
+ ], undefined, [['@accept / @reject', '@accept / @reject']]),
1306
+ ...anti([['无 target 的全局危险确认 → ConfirmDialog', 'Global dangerous confirm without target → ConfirmDialog']]),
587
1307
  },
588
1308
  ],
589
1309
  },
@@ -603,3 +1323,19 @@ export function scoreDecision(decision, query) {
603
1323
  return score;
604
1324
  }, 0);
605
1325
  }
1326
+ /** Ensures every option carries a non-empty L2 recipe (used by tests / generate script). */
1327
+ export function assertDecisionRecipes(decisions = componentDecisions) {
1328
+ for (const decision of decisions) {
1329
+ for (const option of decision.options) {
1330
+ if (!option.recipe?.props?.length) {
1331
+ throw new Error(`Missing recipe.props: ${decision.id} / ${option.component}`);
1332
+ }
1333
+ if (option.recipe.props.length < 3 || option.recipe.props.length > 8) {
1334
+ throw new Error(`recipe.props must be 3–8 items: ${decision.id} / ${option.component} (${option.recipe.props.length})`);
1335
+ }
1336
+ if (!option.antiPatterns?.length) {
1337
+ throw new Error(`Missing antiPatterns: ${decision.id} / ${option.component}`);
1338
+ }
1339
+ }
1340
+ }
1341
+ }