@morya-ui/mcp 0.3.4 → 0.3.6

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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -151
  3. package/bin/morya-ui-mcp.js +2 -2
  4. package/data/catalog.json +40 -40
  5. package/data/example-coverage.json +2 -2
  6. package/data/golden-pages/dashboard-page.vue +138 -138
  7. package/data/golden-pages/detail-page.vue +178 -178
  8. package/data/golden-pages/empty-state.vue +56 -56
  9. package/data/golden-pages/form-in-dialog.vue +166 -166
  10. package/data/golden-pages/form-page.vue +107 -107
  11. package/data/golden-pages/landing-page.vue +358 -358
  12. package/data/golden-pages/list-page-dense.vue +168 -166
  13. package/data/golden-pages/list-page-rail.vue +272 -270
  14. package/data/golden-pages/list-page.vue +222 -220
  15. package/data/golden-pages/login-page.vue +231 -231
  16. package/data/golden-pages/result-page.vue +84 -84
  17. package/data/golden-pages/settings-page.vue +163 -163
  18. package/data/golden-pages/wizard-form.vue +182 -182
  19. package/dist/__tests__/fidelity.test.d.ts +1 -0
  20. package/dist/__tests__/fidelity.test.js +88 -0
  21. package/dist/__tests__/page-snippets.test.js +42 -5
  22. package/dist/__tests__/tools.test.js +49 -29
  23. package/dist/decisions.d.ts +2 -0
  24. package/dist/decisions.js +34 -6
  25. package/dist/golden-pages.d.ts +6 -1
  26. package/dist/golden-pages.js +34 -33
  27. package/dist/index.js +52 -9
  28. package/dist/page-snippets.js +1041 -280
  29. package/dist/patterns.d.ts +4 -2
  30. package/dist/patterns.js +35 -6
  31. package/dist/reference-brief.d.ts +55 -0
  32. package/dist/reference-brief.js +468 -0
  33. package/dist/style-presets.d.ts +13 -32
  34. package/dist/style-presets.js +100 -291
  35. package/dist/style-shells.d.ts +20 -0
  36. package/dist/style-shells.js +249 -0
  37. package/dist/tools.d.ts +21 -3
  38. package/dist/tools.js +496 -424
  39. package/package.json +11 -13
@@ -1,103 +1,417 @@
1
1
  export const pageSnippets = [
2
2
  {
3
3
  id: 'list-filters',
4
- title: '列表页筛选区',
5
- titleEn: 'List page filter bar',
6
- description: '在 MPageContent 内追加或替换筛选区,使用 MPageFilters + MSpace 包裹控件。',
7
- descriptionEn: 'Add or replace a filter region inside MPageContent using MPageFilters and MSpace.',
4
+ title: '列表页基础筛选栏',
5
+ titleEn: 'Basic list filter bar',
6
+ description: '关键词 + 1–2 个枚举 + 查询/重置。条件少、无需已选摘要时用本片段;有次要条件用 list-filters-collapsible;默认列表推荐 list-filters-stack。',
7
+ descriptionEn: 'Keyword + 1–2 enums + query/reset. Use for few filters without chips; secondary fields → list-filters-collapsible; default list → list-filters-stack.',
8
8
  pageTypes: ['list'],
9
- keywords: ['筛选', '搜索', '查询', 'filters', 'filter', 'search', 'keyword', 'reset'],
9
+ keywords: ['筛选', '搜索', '查询', 'filters', 'filter', 'search', 'keyword', 'reset', '基础筛选'],
10
10
  imports: ['MPageFilters', 'MSpace', 'MInput', 'MSelect', 'MButton'],
11
- scriptSetup: `const keyword = ref('')
12
- const status = ref<string | undefined>()
13
- const statusOptions = [
14
- { label: '全部', value: '' },
15
- { label: '启用', value: 'active' },
16
- { label: '停用', value: 'inactive' },
17
- ]`,
18
- template: `<MPageFilters aria-label="筛选" variant="filled">
19
- <MSpace wrap>
20
- <MInput v-model="keyword" placeholder="搜索名称" clearable style="width: 14rem" />
21
- <MSelect
22
- v-model="status"
23
- :options="statusOptions"
24
- placeholder="状态"
25
- clearable
26
- style="width: 10rem"
27
- />
28
- <MButton severity="secondary">查询</MButton>
29
- <MButton severity="secondary" text>重置</MButton>
30
- </MSpace>
11
+ scriptSetup: `const keyword = ref('')
12
+ const status = ref<string | undefined>()
13
+ const statusOptions = [
14
+ { label: '全部', value: '' },
15
+ { label: '启用', value: 'active' },
16
+ { label: '停用', value: 'inactive' },
17
+ ]
18
+
19
+ function resetFilters() {
20
+ keyword.value = ''
21
+ status.value = undefined
22
+ }`,
23
+ template: `<MPageFilters aria-label="筛选" variant="filled">
24
+ <MSpace wrap>
25
+ <MInput v-model="keyword" placeholder="搜索名称" clearable style="width: 14rem" />
26
+ <MSelect
27
+ v-model="status"
28
+ :options="statusOptions"
29
+ placeholder="状态"
30
+ clearable
31
+ style="width: 10rem"
32
+ />
33
+ </MSpace>
34
+ <template #actions>
35
+ <MButton severity="secondary">查询</MButton>
36
+ <MButton severity="secondary" text @click="resetFilters">重置</MButton>
37
+ </template>
31
38
  </MPageFilters>`,
32
- rules: ['放在 MPageHeader 之后、表格之前', 'viewport 内仅一个 primary(通常在 Header)', '不要外包 MCard'],
33
- rulesEn: ['Place after MPageHeader and before the table', 'Keep one primary in the viewport (usually in Header)', 'Do not wrap with MCard'],
34
- avoid: ['不要手写 .page-filters 类', '不要用 MCard 包筛选区'],
35
- avoidEn: ['Do not hand-write .page-filters classes', 'Do not wrap filters in MCard'],
39
+ rules: [
40
+ '放在 MPageHeader 之后、表格(或 FilterChips)之前',
41
+ '查询/重置放 #actions(尾部簇);viewport 内 primary 通常在 Header',
42
+ 'variant 默认 filled;dense craft 可用 plain + size="small"',
43
+ '不要外包 MCard',
44
+ ],
45
+ rulesEn: [
46
+ 'Place after MPageHeader and before the table (or FilterChips)',
47
+ 'Query/reset in #actions (trailing cluster); viewport primary usually in Header',
48
+ 'Default variant filled; dense craft may use plain + size="small"',
49
+ 'Do not wrap with MCard',
50
+ ],
51
+ avoid: [
52
+ '不要手写 .page-filters 类',
53
+ '不要用 MCard 包筛选区',
54
+ '条件 ≥3 且含次要字段时不要硬用本片段 → list-filters-collapsible / list-filters-stack',
55
+ ],
56
+ avoidEn: [
57
+ 'Do not hand-write .page-filters classes',
58
+ 'Do not wrap filters in MCard',
59
+ 'When ≥3 filters with secondary fields → list-filters-collapsible / list-filters-stack',
60
+ ],
36
61
  },
37
62
  {
38
63
  id: 'list-filters-collapsible',
39
- title: '列表页折叠筛选',
40
- titleEn: 'Collapsible list filters',
41
- description: 'MPageFilters collapsible + #advanced 隐藏次要字段。',
42
- descriptionEn: 'Hide secondary fields with collapsible MPageFilters and an #advanced slot.',
64
+ title: '列表页折叠高级筛选',
65
+ titleEn: 'Collapsible advanced list filters',
66
+ description: '常用条件在 default,次要条件在 #advanced;对齐 list-page 黄金结构。通常再接 list-filter-chips,或直接用 list-filters-stack。',
67
+ descriptionEn: 'Primary filters in default, secondary in #advanced (list-page structure). Usually pair with list-filter-chips, or use list-filters-stack.',
43
68
  pageTypes: ['list'],
44
- keywords: ['折叠', '高级筛选', 'collapse', 'advanced', 'expand', 'filter'],
69
+ keywords: ['折叠', '高级筛选', 'collapse', 'advanced', 'expand', 'filter', '次要条件'],
45
70
  imports: ['MPageFilters', 'MSpace', 'MInput', 'MSelect', 'MButton'],
46
- scriptSetup: `const filtersExpanded = ref(false)
47
- const keyword = ref('')
48
- const department = ref<string | null>(null)`,
49
- template: `<MPageFilters
50
- v-model:expanded="filtersExpanded"
51
- aria-label="筛选"
52
- variant="filled"
53
- collapsible
54
- >
55
- <MSpace wrap>
56
- <MInput v-model="keyword" placeholder="搜索名称" clearable style="width: 14rem" />
57
- <MButton severity="secondary">查询</MButton>
58
- <MButton severity="secondary" text>重置</MButton>
59
- </MSpace>
60
- <template #advanced>
61
- <MSpace wrap>
62
- <MSelect
63
- v-model="department"
64
- :options="[{ label: '研发', value: '研发' }, { label: '运营', value: '运营' }]"
65
- placeholder="部门"
66
- clearable
67
- style="width: 10rem"
68
- />
69
- </MSpace>
70
- </template>
71
+ scriptSetup: `const filtersExpanded = ref(false)
72
+ const keyword = ref('')
73
+ const status = ref<string | undefined>()
74
+ const department = ref<string | null>(null)
75
+ const statusOptions = [
76
+ { label: '全部', value: '' },
77
+ { label: '启用', value: 'active' },
78
+ { label: '停用', value: 'inactive' },
79
+ ]
80
+ const departmentOptions = [
81
+ { label: '研发', value: '研发' },
82
+ { label: '运营', value: '运营' },
83
+ { label: '市场', value: '市场' },
84
+ ]
85
+
86
+ function resetFilters() {
87
+ keyword.value = ''
88
+ status.value = undefined
89
+ department.value = null
90
+ }`,
91
+ template: `<MPageFilters
92
+ v-model:expanded="filtersExpanded"
93
+ aria-label="筛选"
94
+ variant="filled"
95
+ collapsible
96
+ >
97
+ <MSpace wrap>
98
+ <MInput v-model="keyword" placeholder="搜索名称" clearable style="width: 14rem" />
99
+ <MSelect
100
+ v-model="status"
101
+ :options="statusOptions"
102
+ placeholder="状态"
103
+ clearable
104
+ style="width: 10rem"
105
+ />
106
+ </MSpace>
107
+ <template #actions>
108
+ <MButton severity="secondary">查询</MButton>
109
+ <MButton severity="secondary" text @click="resetFilters">重置</MButton>
110
+ </template>
111
+ <template #advanced>
112
+ <MSpace wrap>
113
+ <MSelect
114
+ v-model="department"
115
+ :options="departmentOptions"
116
+ placeholder="部门"
117
+ clearable
118
+ style="width: 10rem"
119
+ />
120
+ </MSpace>
121
+ </template>
71
122
  </MPageFilters>`,
72
- rules: ['常用条件放 default,次要条件放 #advanced', '查询/重置保持 secondary'],
73
- rulesEn: ['Keep primary filters in default; secondary filters in #advanced', 'Query/reset stay secondary'],
74
- avoid: ['不要用 MCard 包整个筛选区'],
75
- avoidEn: ['Do not wrap the whole filter bar in MCard'],
123
+ rules: [
124
+ '常用条件放 default,次要条件放 #advanced',
125
+ '查询/重置放 #actions;toggle 自带「高级筛选/收起」+ chevron',
126
+ '必须 collapsible + v-model:expanded + #advanced',
127
+ '重置应清掉 advanced 字段',
128
+ ],
129
+ rulesEn: [
130
+ 'Primary filters in default; secondary in #advanced',
131
+ 'Query/reset in #actions; toggle ships Advanced/Collapse + chevron',
132
+ 'Require collapsible + v-model:expanded + #advanced',
133
+ 'Reset clears advanced fields too',
134
+ ],
135
+ avoid: [
136
+ '不要用 MCard 包整个筛选区',
137
+ '不要只有 #advanced 却不设 collapsible',
138
+ '不要把查询按钮放进 #advanced',
139
+ ],
140
+ avoidEn: [
141
+ 'Do not wrap the whole filter bar in MCard',
142
+ 'Do not provide #advanced without collapsible',
143
+ 'Do not put Query inside #advanced',
144
+ ],
76
145
  },
77
146
  {
78
147
  id: 'list-filter-chips',
79
- title: '列表页已选筛选',
148
+ title: '列表页已选筛选 Chips',
80
149
  titleEn: 'Active filter chips',
81
- description: 'MPageFilterChips + MTag closable 展示并清除已选条件。',
82
- descriptionEn: 'Show and clear active filters with MPageFilterChips and closable MTag.',
150
+ description: 'MPageFilterChips + closable MTag;放在 MPageFilters 与表格之间。需配合筛选 ref + activeFilters computed;完整栈见 list-filters-stack。',
151
+ descriptionEn: 'MPageFilterChips + closable MTag between filters and table. Needs filter refs + activeFilters computed; full stack → list-filters-stack.',
83
152
  pageTypes: ['list'],
84
- keywords: ['已选', 'chip', 'tag', 'active', 'clear', '筛选'],
153
+ keywords: ['已选', 'chip', 'tag', 'active', 'clear', '筛选', 'FilterChips', '已选筛选'],
85
154
  imports: ['MPageFilterChips', 'MTag'],
86
- template: `<MPageFilterChips label="已选" aria-label="已选筛选">
87
- <MTag
88
- v-for="item in activeFilters"
89
- :key="item.key"
90
- :value="item.label"
91
- size="small"
92
- bordered
93
- closable
94
- @close="clearFilter(item.key)"
95
- />
155
+ scriptSetup: `const keyword = ref('')
156
+ const status = ref<string | undefined>()
157
+ const department = ref<string | null>(null)
158
+ const statusOptions = [
159
+ { label: '全部', value: '' },
160
+ { label: '启用', value: 'active' },
161
+ { label: '停用', value: 'inactive' },
162
+ ]
163
+
164
+ const activeFilters = computed(() => {
165
+ const items: Array<{ key: string; label: string }> = []
166
+ if (keyword.value.trim()) {
167
+ items.push({ key: 'keyword', label: \`关键词:\${keyword.value.trim()}\` })
168
+ }
169
+ if (status.value) {
170
+ const label = statusOptions.find((o) => o.value === status.value)?.label ?? status.value
171
+ items.push({ key: 'status', label: \`状态:\${label}\` })
172
+ }
173
+ if (department.value) {
174
+ items.push({ key: 'department', label: \`部门:\${department.value}\` })
175
+ }
176
+ return items
177
+ })
178
+
179
+ function clearFilter(key: string) {
180
+ if (key === 'keyword') keyword.value = ''
181
+ if (key === 'status') status.value = undefined
182
+ if (key === 'department') department.value = null
183
+ }`,
184
+ template: `<MPageFilterChips v-if="activeFilters.length" label="已选" aria-label="已选筛选">
185
+ <MTag
186
+ v-for="item in activeFilters"
187
+ :key="item.key"
188
+ :value="item.label"
189
+ size="small"
190
+ bordered
191
+ closable
192
+ @close="clearFilter(item.key)"
193
+ />
194
+ </MPageFilterChips>`,
195
+ rules: [
196
+ '放在 MPageFilters 之后、表格之前',
197
+ '用 computed 从筛选状态生成 activeFilters;关闭 Tag 时写回对应 ref',
198
+ '无已选条件时用 v-if 隐藏整块',
199
+ ],
200
+ rulesEn: [
201
+ 'Place after MPageFilters and before the table',
202
+ 'Derive activeFilters from filter state; closing a Tag writes back the ref',
203
+ 'Hide the whole block with v-if when empty',
204
+ ],
205
+ avoid: [
206
+ '不要手写 pill 样式或 dashed 壳',
207
+ '不要用 MTag 代替筛选控件本身',
208
+ '不要把 Chips 塞进 MPageFilters 默认槽(可用 #active,但更常见是下方独立 Chips)',
209
+ ],
210
+ avoidEn: [
211
+ 'Do not hand-write pill or dashed shells',
212
+ 'Do not replace filter controls with MTag',
213
+ 'Do not put chips in the PageFilters default slot (optional #active; prefer sibling Chips)',
214
+ ],
215
+ },
216
+ {
217
+ id: 'list-filters-stack',
218
+ title: '列表筛选完整栈(折叠 + 已选)',
219
+ titleEn: 'Full list filter stack (collapsible + chips)',
220
+ description: '默认推荐的列表筛选组合:MPageFilters collapsible + #advanced + MPageFilterChips。对齐 list-page / list-page-rail 黄金结构;dense 无高级条件时可改用 list-filters(plain/small)。',
221
+ descriptionEn: 'Default recommended list filter composition: collapsible MPageFilters + #advanced + MPageFilterChips. Mirrors list-page / list-page-rail; dense with few fields may use list-filters (plain/small).',
222
+ pageTypes: ['list'],
223
+ keywords: [
224
+ '筛选组合',
225
+ '筛选栈',
226
+ '高级筛选',
227
+ '已选筛选',
228
+ 'filter stack',
229
+ 'filters stack',
230
+ 'collapsible',
231
+ 'chips',
232
+ 'list-page',
233
+ '完整筛选',
234
+ ],
235
+ imports: [
236
+ 'MPageFilters',
237
+ 'MPageFilterChips',
238
+ 'MSpace',
239
+ 'MInput',
240
+ 'MSelect',
241
+ 'MButton',
242
+ 'MTag',
243
+ ],
244
+ scriptSetup: `const filtersExpanded = ref(false)
245
+ const keyword = ref('')
246
+ const status = ref<string | undefined>()
247
+ const department = ref<string | null>(null)
248
+
249
+ const statusOptions = [
250
+ { label: '全部', value: '' },
251
+ { label: '启用', value: 'active' },
252
+ { label: '停用', value: 'inactive' },
253
+ ]
254
+ const departmentOptions = [
255
+ { label: '研发', value: '研发' },
256
+ { label: '运营', value: '运营' },
257
+ { label: '市场', value: '市场' },
258
+ ]
259
+
260
+ const activeFilters = computed(() => {
261
+ const items: Array<{ key: string; label: string }> = []
262
+ if (keyword.value.trim()) {
263
+ items.push({ key: 'keyword', label: \`关键词:\${keyword.value.trim()}\` })
264
+ }
265
+ if (status.value) {
266
+ const label = statusOptions.find((o) => o.value === status.value)?.label ?? status.value
267
+ items.push({ key: 'status', label: \`状态:\${label}\` })
268
+ }
269
+ if (department.value) {
270
+ items.push({ key: 'department', label: \`部门:\${department.value}\` })
271
+ }
272
+ return items
273
+ })
274
+
275
+ function clearFilter(key: string) {
276
+ if (key === 'keyword') keyword.value = ''
277
+ if (key === 'status') status.value = undefined
278
+ if (key === 'department') department.value = null
279
+ }
280
+
281
+ function resetFilters() {
282
+ keyword.value = ''
283
+ status.value = undefined
284
+ department.value = null
285
+ }`,
286
+ template: `<MPageFilters
287
+ v-model:expanded="filtersExpanded"
288
+ aria-label="筛选"
289
+ variant="filled"
290
+ collapsible
291
+ >
292
+ <MSpace wrap>
293
+ <MInput v-model="keyword" placeholder="搜索名称" clearable style="width: 14rem" />
294
+ <MSelect
295
+ v-model="status"
296
+ :options="statusOptions"
297
+ placeholder="状态"
298
+ clearable
299
+ style="width: 10rem"
300
+ />
301
+ </MSpace>
302
+ <template #actions>
303
+ <MButton severity="secondary">查询</MButton>
304
+ <MButton severity="secondary" text @click="resetFilters">重置</MButton>
305
+ </template>
306
+ <template #advanced>
307
+ <MSpace wrap>
308
+ <MSelect
309
+ v-model="department"
310
+ :options="departmentOptions"
311
+ placeholder="部门"
312
+ clearable
313
+ style="width: 10rem"
314
+ />
315
+ </MSpace>
316
+ </template>
317
+ </MPageFilters>
318
+
319
+ <MPageFilterChips v-if="activeFilters.length" label="已选" aria-label="已选筛选">
320
+ <MTag
321
+ v-for="item in activeFilters"
322
+ :key="item.key"
323
+ :value="item.label"
324
+ size="small"
325
+ bordered
326
+ closable
327
+ @close="clearFilter(item.key)"
328
+ />
96
329
  </MPageFilterChips>`,
97
- rules: ['放在 MPageFilters 之后、表格之前', '用业务 computed 生成 activeFilters'],
98
- rulesEn: ['Place after MPageFilters and before the table', 'Derive activeFilters from filter state'],
99
- avoid: ['不要手写 pill 样式或 dashed 壳'],
100
- avoidEn: ['Do not hand-write pill or dashed shells'],
330
+ rules: [
331
+ '顺序:Header → Filters → FilterChips →(可选 Toolbar)→ Table',
332
+ '查询/重置放 #actions;toggle 为「高级筛选/收起」+ chevron',
333
+ '重置与 Chip @close 必须写回同一套筛选 ref',
334
+ 'dense/compact 且无高级条件:可降级为 list-filters-dense',
335
+ ],
336
+ rulesEn: [
337
+ 'Order: Header → Filters → FilterChips → (optional Toolbar) → Table',
338
+ 'Query/reset in #actions; toggle is Advanced/Collapse + chevron',
339
+ 'Reset and Chip @close must write the same filter refs',
340
+ 'Dense/compact without advanced fields: fall back to list-filters-dense',
341
+ ],
342
+ avoid: [
343
+ '不要拆成互不共享状态的 Filters 与 Chips',
344
+ '不要用 MCard 包筛选栈',
345
+ '不要用手写 div 模拟已选条',
346
+ ],
347
+ avoidEn: [
348
+ 'Do not split Filters and Chips with disconnected state',
349
+ 'Do not wrap the filter stack in MCard',
350
+ 'Do not hand-roll an active-filter bar',
351
+ ],
352
+ },
353
+ {
354
+ id: 'list-filters-dense',
355
+ title: '列表页紧凑筛选栏',
356
+ titleEn: 'Dense list filter bar',
357
+ description: 'list-page-dense craft:variant="plain" + size="small",字段少、无折叠/Chips。监控/告警等高密度列表用。',
358
+ descriptionEn: 'list-page-dense craft: variant="plain" + size="small", few fields, no collapse/chips. For dense monitoring/alert lists.',
359
+ pageTypes: ['list'],
360
+ keywords: ['dense', 'compact', '紧凑', 'plain', '监控', '告警', '小尺寸筛选'],
361
+ imports: ['MPageFilters', 'MSpace', 'MInput', 'MSelect', 'MButton'],
362
+ scriptSetup: `const keyword = ref('')
363
+ const status = ref<string | undefined>()
364
+ const statusOptions = [
365
+ { label: '全部', value: '' },
366
+ { label: '启用', value: 'active' },
367
+ { label: '停用', value: 'inactive' },
368
+ ]
369
+
370
+ function resetFilters() {
371
+ keyword.value = ''
372
+ status.value = undefined
373
+ }`,
374
+ template: `<MPageFilters aria-label="筛选" variant="plain">
375
+ <MSpace wrap>
376
+ <MInput
377
+ v-model="keyword"
378
+ placeholder="搜索"
379
+ clearable
380
+ size="small"
381
+ style="width: 11rem"
382
+ />
383
+ <MSelect
384
+ v-model="status"
385
+ :options="statusOptions"
386
+ placeholder="状态"
387
+ clearable
388
+ size="small"
389
+ style="width: 8rem"
390
+ />
391
+ </MSpace>
392
+ <template #actions>
393
+ <MButton severity="secondary" size="small">查询</MButton>
394
+ <MButton severity="secondary" size="small" text @click="resetFilters">重置</MButton>
395
+ </template>
396
+ </MPageFilters>`,
397
+ rules: [
398
+ '仅在 dense/compact craft 使用;默认列表用 list-filters-stack',
399
+ '控件统一 size="small";Filters 用 variant="plain"',
400
+ '查询/重置放 #actions,仍为 secondary',
401
+ ],
402
+ rulesEn: [
403
+ 'Only for dense/compact craft; default lists use list-filters-stack',
404
+ 'Controls size="small"; Filters variant="plain"',
405
+ 'Query/reset in #actions; stay secondary',
406
+ ],
407
+ avoid: [
408
+ '不要在普通 soft 列表默认套 dense 筛选',
409
+ '不要在 dense 栏再叠一排大号 filled Filters',
410
+ ],
411
+ avoidEn: [
412
+ 'Do not default soft lists to dense filters',
413
+ 'Do not stack a large filled Filters bar on dense craft',
414
+ ],
101
415
  },
102
416
  {
103
417
  id: 'list-toolbar',
@@ -108,10 +422,10 @@ const department = ref<string | null>(null)`,
108
422
  pageTypes: ['list'],
109
423
  keywords: ['页头', '标题', '新建', 'header', 'title', 'create', 'actions'],
110
424
  imports: ['MPageHeader', 'MButton'],
111
- template: `<MPageHeader title="用户管理" description="维护账号、角色与权限。">
112
- <template #actions>
113
- <MButton severity="primary">新建用户</MButton>
114
- </template>
425
+ template: `<MPageHeader title="用户管理" description="维护账号、角色与权限。">
426
+ <template #actions>
427
+ <MButton severity="primary">新建用户</MButton>
428
+ </template>
115
429
  </MPageHeader>`,
116
430
  rules: ['页级标题用 MPageHeader,不要用 MPageToolbar 当第二页头', '主操作放 #actions'],
117
431
  rulesEn: ['Use MPageHeader for page identity; do not use MPageToolbar as a second page title', 'Put primary actions in #actions'],
@@ -122,20 +436,20 @@ const department = ref<string | null>(null)`,
122
436
  id: 'list-batch-toolbar',
123
437
  title: '列表页批量操作条',
124
438
  titleEn: 'List batch action row',
125
- description: '表格多选时的批量操作,放在筛选区之后(可选)。',
126
- descriptionEn: 'Optional batch actions after filters when rows are selectable.',
439
+ description: '表格多选时的批量操作,放在筛选区(及 FilterChips)之后(可选)。',
440
+ descriptionEn: 'Optional batch actions after filters (and FilterChips) when rows are selectable.',
127
441
  pageTypes: ['list'],
128
442
  keywords: ['批量', '已选', 'toolbar', 'batch', 'export', 'delete'],
129
443
  imports: ['MPageToolbar', 'MButton'],
130
- template: `<MPageToolbar>
131
- <span style="color:var(--m-color-text-muted);font-size:var(--m-font-size-sm)">已选 0 项</span>
132
- <template #actions>
133
- <MButton severity="secondary" text>导出</MButton>
134
- <MButton severity="danger" text>删除</MButton>
135
- </template>
444
+ template: `<MPageToolbar>
445
+ <span style="color:var(--m-color-text-muted);font-size:var(--m-font-size-sm)">已选 0 项</span>
446
+ <template #actions>
447
+ <MButton severity="secondary" text>导出</MButton>
448
+ <MButton severity="danger" text>删除</MButton>
449
+ </template>
136
450
  </MPageToolbar>`,
137
- rules: ['左侧用 default 插槽展示已选数量', '批量操作用 text/outlined,danger 仅 destructive'],
138
- rulesEn: ['Use the default slot for selection count', 'Batch actions use text/outlined; danger only for destructive ops'],
451
+ rules: ['左侧用 default 插槽展示已选数量', '批量操作用 text/outlined,danger 仅 destructive', '放在 FilterChips 之后、表格之前'],
452
+ rulesEn: ['Use the default slot for selection count', 'Batch actions use text/outlined; danger only for destructive ops', 'Place after FilterChips and before the table'],
139
453
  avoid: ['不要用 Toolbar title 充当页面标题'],
140
454
  avoidEn: ['Do not use Toolbar title as the page heading'],
141
455
  },
@@ -143,8 +457,8 @@ const department = ref<string | null>(null)`,
143
457
  id: 'list-table',
144
458
  title: '列表页表格',
145
459
  titleEn: 'List page table',
146
- description: '标准 MTable,直接放在 MPageContent 内。全视口主列表可再加 fill(表体滚、分页贴底);嵌入/短页不要 fill。',
147
- descriptionEn: 'Standard MTable in MPageContent. Add fill only for full-viewport main lists; skip fill for embedded/short pages.',
460
+ description: '标准 MTable:状态列 MStatus、数字列 align end、行操作 small text、空态带下一步。全视口主列表可加 fill;嵌入/短页不要 fill。',
461
+ descriptionEn: 'MTable with MStatus status, end-aligned numbers, small text row actions, next-step empty. Add fill only for full-viewport main lists.',
148
462
  pageTypes: ['list'],
149
463
  keywords: [
150
464
  '表格',
@@ -157,60 +471,84 @@ const department = ref<string | null>(null)`,
157
471
  'fill',
158
472
  '撑满',
159
473
  '分页',
474
+ 'tabular',
475
+ '对齐',
160
476
  ],
161
- imports: ['MTable', 'MEmpty', 'MButton'],
162
- scriptSetup: `const columns = [
163
- { key: 'name', label: '名称' },
164
- { key: 'status', label: '状态' },
165
- { key: 'updatedAt', label: '更新时间' },
166
- { key: 'actions', label: '操作', width: 128 },
167
- ]
168
- const rows = ref<Record<string, unknown>[]>([])
477
+ imports: ['MTable', 'MEmpty', 'MButton', 'MStatus', 'MSpace'],
478
+ scriptSetup: `const columns = [
479
+ { key: 'name', label: '名称' },
480
+ { key: 'status', label: '状态', width: 110 },
481
+ { key: 'count', label: '数量', width: 96, align: 'end' as const },
482
+ { key: 'updatedAt', label: '更新时间', width: 160 },
483
+ { key: 'actions', label: '操作', width: 128 },
484
+ ]
485
+ const rows = ref<Record<string, unknown>[]>([])
169
486
  const loading = ref(false)`,
170
- template: `<!-- 全视口主列表再加 fill;嵌入/短页去掉 fill -->
171
- <MTable
172
- :columns="columns"
173
- :rows="rows"
174
- :loading="loading"
175
- fill
176
- paginator
177
- :rows-per-page="10"
178
- striped
179
- bordered
180
- row-key="id"
181
- aria-label="数据列表"
182
- >
183
- <template #empty>
184
- <MEmpty
185
- title="还没有数据"
186
- description="创建第一条记录后,这里会列出结果。"
187
- icon="inbox"
188
- >
189
- <template #extra>
190
- <MButton severity="primary">新建</MButton>
191
- </template>
192
- </MEmpty>
193
- </template>
487
+ template: `<!-- 全视口主列表再加 fill;嵌入/短页去掉 fill -->
488
+ <MTable
489
+ :columns="columns"
490
+ :rows="rows"
491
+ :loading="loading"
492
+ fill
493
+ paginator
494
+ :rows-per-page="10"
495
+ striped
496
+ bordered
497
+ row-key="id"
498
+ aria-label="数据列表"
499
+ >
500
+ <template #cell-status="{ value }">
501
+ <MStatus
502
+ :label="value === 'active' || value === 'online' ? '启用' : '停用'"
503
+ :severity="value === 'active' || value === 'online' ? 'success' : 'secondary'"
504
+ />
505
+ </template>
506
+ <template #cell-count="{ value }">
507
+ <span style="font-variant-numeric: tabular-nums">{{ value ?? '—' }}</span>
508
+ </template>
509
+ <template #cell-actions>
510
+ <MSpace>
511
+ <MButton severity="secondary" size="small" text>编辑</MButton>
512
+ <MButton severity="danger" size="small" text>删除</MButton>
513
+ </MSpace>
514
+ </template>
515
+ <template #empty>
516
+ <MEmpty
517
+ title="还没有成员"
518
+ description="新建第一条后,这里会列出姓名、状态与更新时间。"
519
+ icon="inbox"
520
+ >
521
+ <template #extra>
522
+ <MButton severity="primary">新建成员</MButton>
523
+ </template>
524
+ </MEmpty>
525
+ </template>
194
526
  </MTable>`,
195
527
  rules: [
196
528
  '先判断是否适合 fill:全视口后台主列表 + 表格是主任务 → MPageContent fill + MTable fill',
197
529
  '嵌入表、短页、整页文档滚动 → 不要 fill,内容高度即可',
198
- '表格直接放在 MPageContent 内;空态用 MEmpty',
530
+ '业务状态列用 MStatus;分类标签才用 MTag',
531
+ '数量/金额列 columns.align = end,并用 tabular-nums',
532
+ '行操作用 size="small" + text;空态文案写领域下一步,禁止「示例 / Name / No data」',
199
533
  ],
200
534
  rulesEn: [
201
535
  'Decide fill first: full-viewport admin main list → MPageContent fill + MTable fill',
202
536
  'Embedded / short / document-scroll pages → skip fill',
203
- 'Place the table directly in MPageContent; use MEmpty for zero-data states',
537
+ 'Business status → MStatus; categories → MTag',
538
+ 'Numeric columns: align end + tabular-nums',
539
+ 'Row actions: size="small" + text; empty copy is domain-real — no “示例 / Name / No data”',
204
540
  ],
205
541
  avoid: [
206
542
  '不要用 MCard 包裹 bordered MTable',
207
543
  '不要对嵌入/短页硬套 fill',
208
544
  '适合 fill 时不要手写 min-height / calc',
545
+ '不要在 #cell-status 默认用 MTag',
209
546
  ],
210
547
  avoidEn: [
211
548
  'Do not wrap a bordered MTable with MCard',
212
549
  'Do not force fill on embedded or short pages',
213
550
  'Do not hand-write min-height / calc when fill is appropriate',
551
+ 'Do not default #cell-status to MTag',
214
552
  ],
215
553
  },
216
554
  {
@@ -222,11 +560,11 @@ const loading = ref(false)`,
222
560
  pageTypes: ['list'],
223
561
  keywords: ['行操作', '编辑', '删除', 'actions', 'row', 'cell'],
224
562
  imports: ['MSpace', 'MButton'],
225
- template: `<template #cell-actions>
226
- <MSpace>
227
- <MButton severity="secondary" size="small" text>编辑</MButton>
228
- <MButton severity="danger" size="small" text>删除</MButton>
229
- </MSpace>
563
+ template: `<template #cell-actions>
564
+ <MSpace>
565
+ <MButton severity="secondary" size="small" text>编辑</MButton>
566
+ <MButton severity="danger" size="small" text>删除</MButton>
567
+ </MSpace>
230
568
  </template>`,
231
569
  rules: ['行内操作用 size="small" + text/outlined,避免一排实心按钮', '危险操作使用 severity="danger" 并配合确认弹窗'],
232
570
  rulesEn: ['Use size="small" with text/outlined row actions', 'Use severity="danger" with confirmation for destructive actions'],
@@ -242,11 +580,11 @@ const loading = ref(false)`,
242
580
  pageTypes: ['list'],
243
581
  keywords: ['分类', '标签', '可关闭', 'chip', 'category', 'tag-only'],
244
582
  imports: ['MTag'],
245
- template: `<template #cell-category="{ value }">
246
- <MTag
247
- :value="String(value ?? '')"
248
- severity="info"
249
- />
583
+ template: `<template #cell-category="{ value }">
584
+ <MTag
585
+ :value="String(value ?? '')"
586
+ severity="info"
587
+ />
250
588
  </template>`,
251
589
  rules: ['分类/强调态用 MTag;行内业务状态用 MStatus(list-status-dot)', '不要用 Button 颜色表达状态'],
252
590
  rulesEn: ['Use MTag for categories; use MStatus for business status (list-status-dot)', 'Do not use Button colors for status'],
@@ -262,11 +600,11 @@ const loading = ref(false)`,
262
600
  pageTypes: ['list'],
263
601
  keywords: ['状态', 'status', 'dot', 'cell-status', '在线', '启用', '停用', '业务状态'],
264
602
  imports: ['MStatus'],
265
- template: `<template #cell-status="{ value }">
266
- <MStatus
267
- :label="value === 'active' || value === 'online' ? '启用' : '停用'"
268
- :severity="value === 'active' || value === 'online' ? 'success' : 'secondary'"
269
- />
603
+ template: `<template #cell-status="{ value }">
604
+ <MStatus
605
+ :label="value === 'active' || value === 'online' ? '启用' : '停用'"
606
+ :severity="value === 'active' || value === 'online' ? 'success' : 'secondary'"
607
+ />
270
608
  </template>`,
271
609
  rules: ['行内轻量业务状态优先 MStatus', '不要用 Button 颜色表达状态'],
272
610
  rulesEn: ['Prefer MStatus for lightweight inline business status', 'Do not use Button colors for status'],
@@ -277,25 +615,33 @@ const loading = ref(false)`,
277
615
  id: 'empty-block',
278
616
  title: '空状态 MEmpty',
279
617
  titleEn: 'Empty state with MEmpty',
280
- description: '列表或内容区无数据时使用 MEmpty,操作放在 #extra。',
281
- descriptionEn: 'Use MEmpty for no-data regions; put actions in #extra.',
618
+ description: '列表或内容区无数据时使用 MEmpty;标题/说明写领域下一步,#extra 放唯一 primary。',
619
+ descriptionEn: 'MEmpty for no-data; domain next-step copy; one primary in #extra.',
282
620
  pageTypes: ['list', 'common'],
283
621
  keywords: ['空状态', '无数据', 'empty', 'zero state', 'no data'],
284
622
  imports: ['MEmpty', 'MButton'],
285
- template: `<MEmpty
286
- title="还没有数据"
287
- description="创建第一条记录后即可在此查看。"
288
- icon="database"
289
- >
290
- <template #extra>
291
- <MButton label="新建" />
292
- <MButton label="导入" severity="secondary" text />
293
- </template>
623
+ template: `<MEmpty
624
+ title="还没有成员"
625
+ description="邀请或新建第一位成员后,这里会显示状态与最近活动。"
626
+ icon="users"
627
+ >
628
+ <template #extra>
629
+ <MButton severity="primary">新建成员</MButton>
630
+ <MButton severity="secondary" text>导入名单</MButton>
631
+ </template>
294
632
  </MEmpty>`,
295
- rules: ['正常无数据用 MEmpty,不要用错误色', '主 CTA 用 primary,次动作用 text/secondary'],
296
- rulesEn: ['Use MEmpty for normal emptiness, not error colors', 'Primary CTA + secondary/text for lesser actions'],
297
- avoid: ['不要用手写 div 拼空态', '不要用 MResult 表达无数据'],
298
- avoidEn: ['Do not hand-roll empty markup', 'Do not use MResult for no-data'],
633
+ rules: [
634
+ '正常无数据用 MEmpty,不要用错误色',
635
+ '主 CTA 用 severity="primary",次动作用 text/secondary',
636
+ '文案写清「现在可以做什么」,禁止「暂无数据 / No data / 示例」',
637
+ ],
638
+ rulesEn: [
639
+ 'Use MEmpty for normal emptiness, not error colors',
640
+ 'Primary CTA with severity="primary"; lesser actions text/secondary',
641
+ 'Copy says what to do next — no “暂无数据 / No data / 示例”',
642
+ ],
643
+ avoid: ['不要用手写 div 拼空态', '不要用 MResult 表达无数据', '不要放两个 filled primary'],
644
+ avoidEn: ['Do not hand-roll empty markup', 'Do not use MResult for no-data', 'Do not place two filled primaries'],
299
645
  },
300
646
  {
301
647
  id: 'result-block',
@@ -306,11 +652,11 @@ const loading = ref(false)`,
306
652
  pageTypes: ['common'],
307
653
  keywords: ['结果', '成功', '失败', '404', '403', 'result', 'success', 'error'],
308
654
  imports: ['MResult', 'MButton'],
309
- template: `<MResult status="success" description="订单已创建,可在列表中查看详情。">
310
- <template #extra>
311
- <MButton label="查看订单" />
312
- <MButton label="返回列表" severity="secondary" text />
313
- </template>
655
+ template: `<MResult status="success" description="订单已创建,可在列表中查看详情。">
656
+ <template #extra>
657
+ <MButton label="查看订单" />
658
+ <MButton label="返回列表" severity="secondary" text />
659
+ </template>
314
660
  </MResult>`,
315
661
  rules: ['流程终点用 MResult', '提供明确下一步操作'],
316
662
  rulesEn: ['Use MResult for terminal outcomes', 'Provide clear next-step actions'],
@@ -336,25 +682,52 @@ const loading = ref(false)`,
336
682
  id: 'form-body',
337
683
  title: '表单主体',
338
684
  titleEn: 'Form body surface',
339
- description: 'MPageSection variant="form" 包裹 MForm 字段。',
340
- descriptionEn: 'Wrap MForm fields with MPageSection variant="form".',
685
+ description: 'MPageSection variant="form" + MForm;左标签时设 label-position/label-width;常驻错误用字段 errorMessage。',
686
+ descriptionEn: 'MPageSection form + MForm; set label-position/label-width for left labels; persistent errors via errorMessage.',
341
687
  pageTypes: ['form'],
342
- keywords: ['表单', 'form', 'fields', 'formitem'],
343
- imports: ['MPageSection', 'MForm', 'MFormItem', 'MInput'],
344
- scriptSetup: `const model = reactive({ name: '' })
345
- const submitting = ref(false)`,
346
- template: `<MPageSection variant="form">
347
- <MForm @submit.prevent="onSubmit">
348
- <MFormItem label="姓名" name="name" required>
349
- <MInput v-model="model.name" placeholder="请输入姓名" fluid />
350
- </MFormItem>
351
- <!-- 更多字段 -->
352
- </MForm>
688
+ keywords: ['表单', 'form', 'fields', 'formitem', 'label-width', 'errorMessage'],
689
+ imports: ['MPageSection', 'MForm', 'MFormItem', 'MInput', 'MSelect'],
690
+ scriptSetup: `const model = reactive({ name: '', role: undefined as string | undefined })
691
+ const submitting = ref(false)
692
+ const roleOptions = [
693
+ { label: '管理员', value: 'admin' },
694
+ { label: '成员', value: 'member' },
695
+ ]`,
696
+ template: `<MPageSection variant="form">
697
+ <MForm
698
+ label-position="left"
699
+ label-width="6rem"
700
+ @submit.prevent="onSubmit"
701
+ >
702
+ <MFormItem label="姓名" name="name" required>
703
+ <MInput v-model="model.name" placeholder="与工牌一致的姓名" fluid />
704
+ </MFormItem>
705
+ <MFormItem label="角色" name="role" required>
706
+ <MSelect
707
+ v-model="model.role"
708
+ :options="roleOptions"
709
+ placeholder="选择角色"
710
+ clearable
711
+ fluid
712
+ />
713
+ </MFormItem>
714
+ <!-- 更多字段;常驻错误:error-message="请填写必填项" -->
715
+ </MForm>
353
716
  </MPageSection>`,
354
- rules: ['字段用 MFormItem + fluid 控件', '表单表面由 MPageSection 提供,不要重复 MCard'],
355
- rulesEn: ['Use MFormItem with fluid controls', 'Let MPageSection provide the surface; do not duplicate MCard'],
356
- avoid: ['不要绕过 MForm 手写校验状态'],
357
- avoidEn: ['Do not bypass MForm for validation state'],
717
+ rules: [
718
+ '字段用 MFormItem + fluid 控件',
719
+ '左标签布局设 label-position="left" + label-width(约 5–7rem)',
720
+ '表单表面由 MPageSection 提供,不要重复 MCard',
721
+ '常驻错误用字段 errorMessage 或 token 样式 role="alert",不要用 <MMessage> 当内嵌 Alert',
722
+ ],
723
+ rulesEn: [
724
+ 'Use MFormItem with fluid controls',
725
+ 'Left labels: label-position="left" + label-width (~5–7rem)',
726
+ 'Let MPageSection provide the surface; do not duplicate MCard',
727
+ 'Persistent errors: field errorMessage or token role="alert" — not <MMessage> as inline alert',
728
+ ],
729
+ avoid: ['不要绕过 MForm 手写校验状态', '不要把错误只画成红色边框而无文案'],
730
+ avoidEn: ['Do not bypass MForm for validation state', 'Do not show errors as red borders without copy'],
358
731
  },
359
732
  {
360
733
  id: 'form-actions',
@@ -365,11 +738,11 @@ const submitting = ref(false)`,
365
738
  pageTypes: ['form'],
366
739
  keywords: ['保存', '取消', 'submit', 'actions', 'footer'],
367
740
  imports: ['MPageSection', 'MSpace', 'MButton'],
368
- template: `<MPageSection variant="actions">
369
- <MSpace>
370
- <MButton native-type="submit" severity="primary" :loading="submitting">保存</MButton>
371
- <MButton severity="secondary">取消</MButton>
372
- </MSpace>
741
+ template: `<MPageSection variant="actions">
742
+ <MSpace>
743
+ <MButton native-type="submit" severity="primary" :loading="submitting">保存</MButton>
744
+ <MButton severity="secondary">取消</MButton>
745
+ </MSpace>
373
746
  </MPageSection>`,
374
747
  rules: ['提交按钮在前,取消 secondary', '保存中传 loading 防重复提交'],
375
748
  rulesEn: ['Submit first, cancel secondary', 'Pass loading while saving to prevent duplicate submits'],
@@ -380,31 +753,59 @@ const submitting = ref(false)`,
380
753
  id: 'dashboard-kpi-grid',
381
754
  title: '仪表盘 KPI 栅格',
382
755
  titleEn: 'Dashboard KPI grid',
383
- description: 'MGrid + MPageStat 展示关键指标。',
384
- descriptionEn: 'Key metrics with MGrid and MPageStat.',
756
+ description: 'MGrid + MPageStat:带 trend / trendDirection / trendSeverity;高密条可用 density="compact"。',
757
+ descriptionEn: 'MGrid + MPageStat with trend / trendDirection / trendSeverity; use density="compact" for dense strips.',
385
758
  pageTypes: ['dashboard'],
386
- keywords: ['kpi', '指标', 'stat', 'grid', 'metrics'],
759
+ keywords: ['kpi', '指标', 'stat', 'grid', 'metrics', 'trend', 'compact'],
387
760
  imports: ['MGrid', 'MGridItem', 'MPageStat'],
388
- scriptSetup: `const metrics = ref([
389
- { label: '总用户', value: '12,480', trend: '+8.2%', trendDirection: 'up' as const, trendLabel: '较上月', icon: 'users' },
390
- { label: '今日活跃', value: '1,926', trend: '+3.1%', trendDirection: 'up' as const, trendLabel: '较昨日', icon: 'activity' },
761
+ scriptSetup: `const metrics = ref([
762
+ {
763
+ label: '在册成员',
764
+ value: '12,480',
765
+ trend: '+8.2%',
766
+ trendDirection: 'up' as const,
767
+ trendSeverity: 'success' as const,
768
+ trendLabel: '较上月',
769
+ icon: 'users',
770
+ },
771
+ {
772
+ label: '今日活跃',
773
+ value: '1,926',
774
+ trend: '-3.1%',
775
+ trendDirection: 'down' as const,
776
+ trendSeverity: 'warn' as const,
777
+ trendLabel: '较昨日',
778
+ icon: 'activity',
779
+ },
391
780
  ])`,
392
- template: `<MGrid :cols="4" :x-gap="16" :y-gap="16" responsive="screen">
393
- <MGridItem v-for="metric in metrics" :key="metric.label" :span="1">
394
- <MPageStat
395
- :label="metric.label"
396
- :value="metric.value"
397
- :trend="metric.trend"
398
- :trend-direction="metric.trendDirection"
399
- :trend-label="metric.trendLabel"
400
- :icon="metric.icon"
401
- />
402
- </MGridItem>
403
- </MGrid>`,
404
- rules: ['KPI 用 MPageStat,不要每页自定义 stat CSS', 'responsive="screen" 便于窄屏降列'],
405
- rulesEn: ['Use MPageStat for KPIs', 'Use responsive="screen" for narrow layouts'],
406
- avoid: ['不要每个指标手写 font-size/颜色'],
407
- avoidEn: ['Do not hand-write font sizes/colors per metric'],
781
+ template: `<MGrid :cols="4" :x-gap="16" :y-gap="16" responsive="screen">
782
+ <MGridItem v-for="metric in metrics" :key="metric.label" :span="1">
783
+ <MPageStat
784
+ :label="metric.label"
785
+ :value="metric.value"
786
+ :trend="metric.trend"
787
+ :trend-direction="metric.trendDirection"
788
+ :trend-severity="metric.trendSeverity"
789
+ :trend-label="metric.trendLabel"
790
+ :icon="metric.icon"
791
+ />
792
+ </MGridItem>
793
+ </MGrid>
794
+ <!-- 高密条带示例:density="compact" layout="plain" orientation="inline" -->`,
795
+ rules: [
796
+ 'KPI 用 MPageStat,不要每页自定义 stat CSS',
797
+ '趋势务必带 trendDirection + trendSeverity(升 success / 降 warn|danger)',
798
+ 'responsive="screen" 便于窄屏降列',
799
+ '密集工作台可用 density="compact";加载中用 loading',
800
+ ],
801
+ rulesEn: [
802
+ 'Use MPageStat for KPIs — no per-page stat CSS',
803
+ 'Always set trendDirection + trendSeverity (up→success, down→warn|danger)',
804
+ 'Use responsive="screen" for narrow layouts',
805
+ 'Dense dashboards may use density="compact"; use loading while fetching',
806
+ ],
807
+ avoid: ['不要每个指标手写 font-size/颜色', '不要只有数字没有趋势语义(用户要求趋势时)'],
808
+ avoidEn: ['Do not hand-write font sizes/colors per metric', 'Do not omit trend semantics when the brief asks for trends'],
408
809
  },
409
810
  {
410
811
  id: 'dashboard-chart-card',
@@ -415,14 +816,14 @@ const submitting = ref(false)`,
415
816
  pageTypes: ['dashboard'],
416
817
  keywords: ['chart', '图表', 'placeholder', 'card', 'trend'],
417
818
  imports: ['MCard', 'MEmpty', 'MSkeleton'],
418
- template: `<MCard title="趋势概览" shadow="always">
419
- <MSkeleton v-if="loading" height="8rem" />
420
- <MEmpty
421
- v-else
422
- aria-label="图表占位"
423
- title="暂无图表数据"
424
- description="图表区域(接入 ECharts / 业务组件)"
425
- />
819
+ template: `<MCard title="趋势概览" shadow="always">
820
+ <MSkeleton v-if="loading" height="8rem" />
821
+ <MEmpty
822
+ v-else
823
+ aria-label="图表占位"
824
+ title="暂无图表数据"
825
+ description="图表区域(接入 ECharts / 业务组件)"
826
+ />
426
827
  </MCard>`,
427
828
  rules: ['图表区适合 MCard,因为需要 card 标题', '加载中用 MSkeleton'],
428
829
  rulesEn: ['Charts belong in MCard when a card title is needed', 'Use MSkeleton while loading'],
@@ -438,18 +839,18 @@ const submitting = ref(false)`,
438
839
  pageTypes: ['dashboard'],
439
840
  keywords: ['recent', '最近', 'table', 'card', 'list'],
440
841
  imports: ['MCard', 'MTable', 'MStatus'],
441
- scriptSetup: `const recentColumns = [
442
- { key: 'id', label: '工单号', width: 96 },
443
- { key: 'title', label: '标题' },
444
- { key: 'status', label: '状态', width: 110 },
445
- ]
842
+ scriptSetup: `const recentColumns = [
843
+ { key: 'id', label: '工单号', width: 96 },
844
+ { key: 'title', label: '标题' },
845
+ { key: 'status', label: '状态', width: 110 },
846
+ ]
446
847
  const recentRows = ref<Record<string, unknown>[]>([])`,
447
- template: `<MCard title="最近工单">
448
- <MTable :columns="recentColumns" :rows="recentRows" size="small" :paginator="false" bordered row-key="id">
449
- <template #cell-status="{ value }">
450
- <MStatus :label="String(value ?? '')" severity="secondary" />
451
- </template>
452
- </MTable>
848
+ template: `<MCard title="最近工单">
849
+ <MTable :columns="recentColumns" :rows="recentRows" size="small" :paginator="false" bordered row-key="id">
850
+ <template #cell-status="{ value }">
851
+ <MStatus :label="String(value ?? '')" severity="secondary" />
852
+ </template>
853
+ </MTable>
453
854
  </MCard>`,
454
855
  rules: ['明细列表放 MCard 内合理', 'size="small" 适合卡片内表格', '行内业务状态用 MStatus'],
455
856
  rulesEn: ['Detail lists inside MCard are appropriate', 'Use size="small" for in-card tables', 'Use MStatus for row business status'],
@@ -465,10 +866,10 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
465
866
  pageTypes: ['list', 'common'],
466
867
  keywords: ['page content', 'stack', 'layout content', '容器'],
467
868
  imports: ['MPageContent'],
468
- template: `<MLayoutContent>
469
- <MPageContent>
470
- <!-- MPageFilters → MPageToolbar → MTable -->
471
- </MPageContent>
869
+ template: `<MLayoutContent>
870
+ <MPageContent>
871
+ <!-- MPageFilters → MPageToolbar → MTable -->
872
+ </MPageContent>
472
873
  </MLayoutContent>`,
473
874
  rules: ['所有列表区块放在 MPageContent 内', '区块顺序:筛选 → 工具栏 → 表格'],
474
875
  rulesEn: ['Keep all list sections inside MPageContent', 'Order: filters → toolbar → table'],
@@ -484,10 +885,10 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
484
885
  pageTypes: ['form', 'common'],
485
886
  keywords: ['narrow', 'form content', 'max-width'],
486
887
  imports: ['MPageContent'],
487
- template: `<MLayoutContent>
488
- <MPageContent width="narrow">
489
- <!-- MPageHeader → MPageSection form → actions -->
490
- </MPageContent>
888
+ template: `<MLayoutContent>
889
+ <MPageContent width="narrow">
890
+ <!-- MPageHeader → MPageSection form → actions -->
891
+ </MPageContent>
491
892
  </MLayoutContent>`,
492
893
  rules: ['表单页使用 width="narrow"', '不要手写 max-width CSS'],
493
894
  rulesEn: ['Use width="narrow" on form pages', 'Do not hand-write max-width CSS'],
@@ -503,20 +904,380 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
503
904
  pageTypes: ['detail'],
504
905
  keywords: ['detail', '详情', 'edit', 'header'],
505
906
  imports: ['MPageHeader', 'MButton', 'MStatus', 'MSpace'],
506
- template: `<MPageHeader title="示例资源" description="查看摘要与属性。">
507
- <template #actions>
508
- <MSpace>
509
- <MStatus label="正常" severity="success" />
510
- <MButton severity="primary">编辑</MButton>
511
- <MButton severity="danger" text>删除</MButton>
512
- </MSpace>
513
- </template>
907
+ template: `<MPageHeader title="示例资源" description="查看摘要与属性。">
908
+ <template #actions>
909
+ <MSpace>
910
+ <MStatus label="正常" severity="success" />
911
+ <MButton severity="primary">编辑</MButton>
912
+ <MButton severity="danger" text>删除</MButton>
913
+ </MSpace>
914
+ </template>
514
915
  </MPageHeader>`,
515
916
  rules: ['状态用 MStatus 放在 actions 区', '编辑为 primary,删除用 danger text'],
516
917
  rulesEn: ['Use MStatus in the actions area', 'Edit is primary; delete uses danger text'],
517
918
  avoid: ['不要把操作散落到多个无关区域'],
518
919
  avoidEn: ['Do not scatter actions across unrelated areas'],
519
920
  },
921
+ {
922
+ id: 'page-header-actions',
923
+ title: '页头单主操作',
924
+ titleEn: 'Page header with one primary action',
925
+ description: 'MPageHeader + #actions:视口内仅一个 filled primary,次要用 secondary/text。',
926
+ descriptionEn: 'MPageHeader + #actions: one filled primary in the viewport; secondary/text for lesser actions.',
927
+ pageTypes: ['list', 'form', 'dashboard', 'detail', 'common'],
928
+ keywords: ['页头', '主按钮', '新建', 'header', 'primary', 'actions', 'page-header'],
929
+ imports: ['MPageHeader', 'MButton', 'MSpace'],
930
+ template: `<MPageHeader title="用户管理" description="管理成员与角色。">
931
+ <template #actions>
932
+ <MSpace>
933
+ <MButton severity="secondary" text>导入</MButton>
934
+ <MButton severity="primary">新建用户</MButton>
935
+ </MSpace>
936
+ </template>
937
+ </MPageHeader>`,
938
+ rules: ['视口内仅一个 filled primary', '次要动作用 secondary 或 text', '批量操作用 MPageToolbar,不要塞进页头'],
939
+ rulesEn: ['Only one filled primary in the viewport', 'Lesser actions: secondary or text', 'Batch actions belong in MPageToolbar, not the header'],
940
+ avoid: ['不要在页头和筛选区各放一个实心 primary', '不要把危险删除做成页头唯一实心按钮'],
941
+ avoidEn: ['Do not place a solid primary in both header and filters', 'Do not make destructive delete the only solid header button'],
942
+ },
943
+ {
944
+ id: 'form-in-dialog',
945
+ title: '列表内弹窗短表单',
946
+ titleEn: 'Short form in dialog from a list',
947
+ description: '同页 MDialog + MForm + #footer;短 CRUD 默认路径(约 ≤8 字段)。',
948
+ descriptionEn: 'Same-page MDialog + MForm + #footer; default short CRUD path (~≤8 fields).',
949
+ pageTypes: ['list', 'common'],
950
+ keywords: [
951
+ '弹窗表单',
952
+ '新建弹窗',
953
+ '编辑弹窗',
954
+ 'dialog form',
955
+ 'form-in-dialog',
956
+ 'modal form',
957
+ 'inline create',
958
+ ],
959
+ imports: ['MDialog', 'MForm', 'MFormItem', 'MInput', 'MSelect', 'MButton', 'MSpace', 'message'],
960
+ scriptSetup: `const dialogOpen = ref(false)
961
+ const submitting = ref(false)
962
+ const model = reactive({
963
+ name: '',
964
+ email: '',
965
+ role: undefined as string | undefined,
966
+ })
967
+ const roleOptions = [
968
+ { label: '管理员', value: 'admin' },
969
+ { label: '成员', value: 'member' },
970
+ ]
971
+
972
+ function resetModel() {
973
+ model.name = ''
974
+ model.email = ''
975
+ model.role = undefined
976
+ }
977
+
978
+ async function onSave() {
979
+ submitting.value = true
980
+ try {
981
+ // await api.save(model)
982
+ message.success('已保存')
983
+ dialogOpen.value = false
984
+ resetModel()
985
+ } finally {
986
+ submitting.value = false
987
+ }
988
+ }`,
989
+ template: `<MDialog
990
+ v-model="dialogOpen"
991
+ header="新建用户"
992
+ width="32rem"
993
+ @close="resetModel"
994
+ >
995
+ <MForm @submit.prevent="onSave">
996
+ <MFormItem label="姓名" name="name" required>
997
+ <MInput v-model="model.name" placeholder="请输入姓名" fluid />
998
+ </MFormItem>
999
+ <MFormItem label="邮箱" name="email" required>
1000
+ <MInput v-model="model.email" type="email" placeholder="name@example.com" fluid />
1001
+ </MFormItem>
1002
+ <MFormItem label="角色" name="role" required>
1003
+ <MSelect v-model="model.role" :options="roleOptions" placeholder="请选择角色" fluid />
1004
+ </MFormItem>
1005
+ </MForm>
1006
+ <template #footer>
1007
+ <MSpace style="justify-content: flex-end; width: 100%">
1008
+ <MButton severity="secondary" text :disabled="submitting" @click="dialogOpen = false">
1009
+ 取消
1010
+ </MButton>
1011
+ <MButton severity="primary" :loading="submitting" @click="onSave">
1012
+ 保存
1013
+ </MButton>
1014
+ </MSpace>
1015
+ </template>
1016
+ </MDialog>`,
1017
+ rules: [
1018
+ '留在列表页,不另开路由',
1019
+ '宽度约 28–36rem;字段用 fluid',
1020
+ '取消/保存放 #footer;成功用 message.success',
1021
+ ],
1022
+ rulesEn: [
1023
+ 'Stay on the list page; do not open a new route',
1024
+ 'Width ~28–36rem; fields use fluid',
1025
+ 'Cancel/save in #footer; success via message.success',
1026
+ ],
1027
+ avoid: [
1028
+ '不要用 visible 代替 v-model',
1029
+ '不要把长多分组表单塞进 Dialog → form-page / form-in-drawer',
1030
+ '不要用 toast 代替单行 message',
1031
+ ],
1032
+ avoidEn: [
1033
+ 'Do not use visible instead of v-model',
1034
+ 'Do not stuff long multi-section forms into Dialog → form-page / form-in-drawer',
1035
+ 'Do not use toast for a one-line message',
1036
+ ],
1037
+ },
1038
+ {
1039
+ id: 'form-in-drawer',
1040
+ title: '列表内侧滑表单',
1041
+ titleEn: 'Form in drawer from a list',
1042
+ description: '同页 MDrawer + MForm + #footer;比 Dialog 更长但仍需对照列表。',
1043
+ descriptionEn: 'Same-page MDrawer + MForm + #footer; longer than a dialog but list context still matters.',
1044
+ pageTypes: ['list', 'detail', 'common'],
1045
+ keywords: ['抽屉表单', '侧滑编辑', 'drawer form', 'form-in-drawer', 'side panel edit'],
1046
+ imports: ['MDrawer', 'MForm', 'MFormItem', 'MInput', 'MButton', 'MSpace', 'message'],
1047
+ scriptSetup: `const drawerOpen = ref(false)
1048
+ const submitting = ref(false)
1049
+ const model = reactive({ name: '', note: '' })
1050
+
1051
+ async function onSave() {
1052
+ submitting.value = true
1053
+ try {
1054
+ message.success('已保存')
1055
+ drawerOpen.value = false
1056
+ } finally {
1057
+ submitting.value = false
1058
+ }
1059
+ }`,
1060
+ template: `<MDrawer
1061
+ v-model="drawerOpen"
1062
+ header="编辑详情"
1063
+ position="right"
1064
+ size="28rem"
1065
+ @close="/* reset draft */"
1066
+ >
1067
+ <MForm @submit.prevent="onSave">
1068
+ <MFormItem label="名称" name="name" required>
1069
+ <MInput v-model="model.name" fluid />
1070
+ </MFormItem>
1071
+ <MFormItem label="备注" name="note">
1072
+ <MInput v-model="model.note" fluid />
1073
+ </MFormItem>
1074
+ </MForm>
1075
+ <template #footer>
1076
+ <MSpace style="justify-content: flex-end; width: 100%">
1077
+ <MButton severity="secondary" text :disabled="submitting" @click="drawerOpen = false">
1078
+ 取消
1079
+ </MButton>
1080
+ <MButton severity="primary" :loading="submitting" @click="onSave">
1081
+ 保存
1082
+ </MButton>
1083
+ </MSpace>
1084
+ </template>
1085
+ </MDrawer>`,
1086
+ rules: ['position 常用 right;较长表单可加大 size', '字段 fluid;底栏放 #footer', '成功用 message.success'],
1087
+ rulesEn: ['position usually right; raise size for longer forms', 'Fields fluid; actions in #footer', 'Success via message.success'],
1088
+ avoid: ['极短 2–3 字段也用 Drawer → form-in-dialog', '危险删除只用 Drawer 无确认 → confirm-delete'],
1089
+ avoidEn: ['Tiny 2–3 field forms → form-in-dialog', 'Destructive delete without confirm → confirm-delete'],
1090
+ },
1091
+ {
1092
+ id: 'confirm-delete',
1093
+ title: '危险删除确认',
1094
+ titleEn: 'Destructive delete confirm',
1095
+ description: 'MConfirmDialog + message.success;列表行删除默认路径。',
1096
+ descriptionEn: 'MConfirmDialog + message.success; default for list-row delete.',
1097
+ pageTypes: ['list', 'detail', 'common'],
1098
+ keywords: ['删除', '确认', '二次确认', 'confirm', 'delete', 'ConfirmDialog', '危险'],
1099
+ imports: ['MConfirmDialog', 'message'],
1100
+ scriptSetup: `const confirmOpen = ref(false)
1101
+ const pendingId = ref<string | null>(null)
1102
+
1103
+ function askDelete(id: string) {
1104
+ pendingId.value = id
1105
+ confirmOpen.value = true
1106
+ }
1107
+
1108
+ function onAcceptDelete() {
1109
+ // await api.remove(pendingId.value)
1110
+ message.success('已删除')
1111
+ pendingId.value = null
1112
+ confirmOpen.value = false
1113
+ }`,
1114
+ template: `<MConfirmDialog
1115
+ v-model="confirmOpen"
1116
+ header="确认删除"
1117
+ message="删除后不可恢复,确定继续?"
1118
+ accept-label="删除"
1119
+ reject-label="取消"
1120
+ accept-severity="danger"
1121
+ @accept="onAcceptDelete"
1122
+ />`,
1123
+ rules: ['用 MConfirmDialog,不要手写 Dialog 确定/取消', 'acceptSeverity="danger"', '成功用 message.success,不要 toast'],
1124
+ rulesEn: ['Use MConfirmDialog; do not hand-roll Dialog Yes/No', 'acceptSeverity="danger"', 'Success via message.success, not toast'],
1125
+ avoid: ['普通 Dialog 冒充删除确认', '编辑表单误用 ConfirmDialog'],
1126
+ avoidEn: ['Faking delete confirm with Dialog', 'Using ConfirmDialog for edit forms'],
1127
+ },
1128
+ {
1129
+ id: 'row-actions-menu',
1130
+ title: '表格行更多操作菜单',
1131
+ titleEn: 'Table row overflow actions menu',
1132
+ description: '操作较多时用 MDropdown 收纳行内动作;少量明确操作仍用 list-row-actions。',
1133
+ descriptionEn: 'Use MDropdown when there are many row actions; prefer list-row-actions for a few explicit buttons.',
1134
+ pageTypes: ['list', 'common'],
1135
+ keywords: ['更多', '行菜单', 'dropdown', 'row menu', 'overflow', 'actions menu'],
1136
+ imports: ['MDropdown', 'MButton'],
1137
+ scriptSetup: `const rowMenu = [
1138
+ { label: '编辑', command: () => {/* open edit */} },
1139
+ { label: '复制', command: () => {/* duplicate */} },
1140
+ { label: '删除', severity: 'danger', command: () => {/* askDelete */} },
1141
+ ]`,
1142
+ template: `<template #cell-actions>
1143
+ <MDropdown :model="rowMenu">
1144
+ <MButton severity="secondary" size="small" text icon="more-horizontal" aria-label="更多操作" />
1145
+ </MDropdown>
1146
+ </template>`,
1147
+ rules: ['操作很多时再用 Dropdown;2–3 个明确操作用 text 按钮', '危险项走 confirm-delete', '图标按钮必须 aria-label'],
1148
+ rulesEn: ['Use Dropdown when many actions; 2–3 explicit actions stay as text buttons', 'Danger → confirm-delete', 'Icon-only buttons need aria-label'],
1149
+ avoid: ['表单枚举用 Dropdown → Select', '只有编辑/删除也强行收进菜单'],
1150
+ avoidEn: ['Form enums via Dropdown → Select', 'Forcing edit/delete into a menu when two text buttons suffice'],
1151
+ },
1152
+ {
1153
+ id: 'wizard-steps',
1154
+ title: '分步向导 Stepper + Form',
1155
+ titleEn: 'Wizard stepper with per-step form',
1156
+ description: 'MStepper :steps + 每步一个 MForm;完成可用 result-block。',
1157
+ descriptionEn: 'MStepper :steps + one MForm per step; finish with result-block when useful.',
1158
+ pageTypes: ['form', 'common'],
1159
+ keywords: ['向导', '分步', '步骤', 'wizard', 'stepper', 'multi-step'],
1160
+ imports: ['MStepper', 'MForm', 'MFormItem', 'MInput', 'MButton', 'MSpace', 'MPageSection', 'message'],
1161
+ scriptSetup: `const activeStep = ref(0)
1162
+ const submitting = ref(false)
1163
+ const steps = [
1164
+ { label: '基本信息', description: '名称与说明' },
1165
+ { label: '确认', description: '核对后创建' },
1166
+ ]
1167
+ const model = reactive({ name: '' })
1168
+
1169
+ function back() {
1170
+ if (activeStep.value > 0) activeStep.value -= 1
1171
+ }
1172
+ function next() {
1173
+ if (activeStep.value < steps.length - 1) activeStep.value += 1
1174
+ }
1175
+ async function finish() {
1176
+ submitting.value = true
1177
+ try {
1178
+ message.success('已创建')
1179
+ } finally {
1180
+ submitting.value = false
1181
+ }
1182
+ }`,
1183
+ template: `<MStepper v-model="activeStep" :steps="steps" linear />
1184
+ <MPageSection variant="form">
1185
+ <MForm v-if="activeStep === 0" @submit.prevent="next">
1186
+ <MFormItem label="名称" name="name" required>
1187
+ <MInput v-model="model.name" fluid />
1188
+ </MFormItem>
1189
+ </MForm>
1190
+ <div v-else>
1191
+ <p>确认创建「{{ model.name }}」?</p>
1192
+ </div>
1193
+ </MPageSection>
1194
+ <MPageSection variant="actions">
1195
+ <MSpace>
1196
+ <MButton severity="secondary" text :disabled="activeStep === 0" @click="back">上一步</MButton>
1197
+ <MButton v-if="activeStep < steps.length - 1" severity="primary" @click="next">下一步</MButton>
1198
+ <MButton v-else severity="primary" :loading="submitting" @click="finish">创建</MButton>
1199
+ </MSpace>
1200
+ </MPageSection>`,
1201
+ rules: ['用 :steps(不是 :items)', '每步一个主任务;进入下一步前校验当前步', '完成文案与「下一步」区分'],
1202
+ rulesEn: ['Use :steps (not :items)', 'One job per step; validate before advancing', 'Finish label differs from Next'],
1203
+ avoid: ['所有字段藏在单页超长表单', '允许跳过有前置依赖的步骤'],
1204
+ avoidEn: ['Hiding all fields on one long page', 'Skipping steps with prerequisites'],
1205
+ },
1206
+ {
1207
+ id: 'auth-split-shell',
1208
+ title: '登录左右分栏壳',
1209
+ titleEn: 'Auth split brand + form shell',
1210
+ description: '左品牌 / 右表单;控件 M*;失败用 role="alert" 或 errorMessage,不用 Toast。',
1211
+ descriptionEn: 'Brand left / form right; M* controls; failures via role="alert" or errorMessage, not Toast.',
1212
+ pageTypes: ['common'],
1213
+ keywords: ['登录', '认证', '分栏', 'login', 'auth', 'split', 'brand'],
1214
+ imports: ['MForm', 'MFormItem', 'MInput', 'MInputPassword', 'MButton', 'MSpace', 'MConfigProvider', 'zhCN'],
1215
+ scriptSetup: `const submitting = ref(false)
1216
+ const formError = ref('')
1217
+ const model = reactive({ email: '', password: '' })
1218
+
1219
+ async function onSubmit() {
1220
+ formError.value = ''
1221
+ submitting.value = true
1222
+ try {
1223
+ if (!model.email || !model.password) {
1224
+ formError.value = '请输入邮箱和密码后再试。'
1225
+ }
1226
+ } finally {
1227
+ submitting.value = false
1228
+ }
1229
+ }`,
1230
+ template: `<MConfigProvider :locale="zhCN">
1231
+ <div class="login-shell">
1232
+ <aside class="login-brand" aria-label="品牌">
1233
+ <p class="login-brand__mark">产品名</p>
1234
+ <h1 class="login-brand__title">一句品牌主张</h1>
1235
+ <p class="login-brand__lead">一句支持说明。</p>
1236
+ </aside>
1237
+ <main class="login-main">
1238
+ <div class="login-panel">
1239
+ <header class="login-panel__header">
1240
+ <h2>登录</h2>
1241
+ <p>使用工作邮箱进入。</p>
1242
+ </header>
1243
+ <p v-if="formError" class="login-alert" role="alert">{{ formError }}</p>
1244
+ <MForm @submit="onSubmit">
1245
+ <MFormItem label="邮箱" name="email" required>
1246
+ <MInput v-model="model.email" type="email" autocomplete="username" fluid />
1247
+ </MFormItem>
1248
+ <MFormItem label="密码" name="password" required>
1249
+ <MInputPassword v-model="model.password" autocomplete="current-password" fluid />
1250
+ </MFormItem>
1251
+ <MSpace style="margin-top: var(--m-space-2)" alignment="center">
1252
+ <MButton type="submit" label="登录" severity="primary" :loading="submitting" />
1253
+ <MButton type="button" label="忘记密码" severity="secondary" text />
1254
+ </MSpace>
1255
+ </MForm>
1256
+ </div>
1257
+ </main>
1258
+ </div>
1259
+ </MConfigProvider>`,
1260
+ rules: [
1261
+ '品牌区用 --m-* / color-mix,默认扁平勿叠 aurora',
1262
+ '失败留在表单区(role="alert" 或 errorMessage),不要 Toast',
1263
+ '窄屏改为单栏:表单在上或品牌收短',
1264
+ ],
1265
+ rulesEn: [
1266
+ 'Brand panel: --m-* / color-mix; flat by default — no stacked aurora',
1267
+ 'Keep failures in the form (role="alert" or errorMessage), not Toast',
1268
+ 'Narrow screens: single column',
1269
+ ],
1270
+ avoid: [
1271
+ '不要把 MMessage 当内嵌 Alert',
1272
+ '不要引入第二套 UI 库做登录壳',
1273
+ '不要未询问就加玻璃/霓虹/全页渐变',
1274
+ ],
1275
+ avoidEn: [
1276
+ 'Do not use MMessage as an inline Alert',
1277
+ 'Do not bring a second UI kit for the auth shell',
1278
+ 'Do not add glass/neon/full-page gradients unless asked',
1279
+ ],
1280
+ },
520
1281
  {
521
1282
  id: 'layout-app-shell',
522
1283
  title: '后台应用骨架',
@@ -536,29 +1297,29 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
536
1297
  'MConfigProvider',
537
1298
  'zhCN',
538
1299
  ],
539
- template: `<MConfigProvider :locale="zhCN">
540
- <MLayout fill-viewport has-sider>
541
- <MLayoutSider v-model:collapsed="siderCollapsed" bordered :collapsed-width="72">
542
- <MMenu
543
- :model="[
544
- { key: 'users', label: '用户管理', icon: 'user', to: '/users' },
545
- { key: 'roles', label: '角色管理', icon: 'shield', to: '/roles' },
546
- ]"
547
- :collapsed="siderCollapsed"
548
- :collapsed-width="72"
549
- />
550
- </MLayoutSider>
551
- <MLayout>
552
- <MLayoutHeader :padding="'var(--m-space-4) var(--m-space-6)'">
553
- <MBreadcrumb :model="[{ label: '首页', to: '/' }, { label: '当前页' }]" />
554
- </MLayoutHeader>
555
- <MLayoutContent>
556
- <MPageContent>
557
- <!-- page sections -->
558
- </MPageContent>
559
- </MLayoutContent>
560
- </MLayout>
561
- </MLayout>
1300
+ template: `<MConfigProvider :locale="zhCN">
1301
+ <MLayout fill-viewport has-sider>
1302
+ <MLayoutSider v-model:collapsed="siderCollapsed" bordered :collapsed-width="72">
1303
+ <MMenu
1304
+ :model="[
1305
+ { key: 'users', label: '用户管理', icon: 'user', to: '/users' },
1306
+ { key: 'roles', label: '角色管理', icon: 'shield', to: '/roles' },
1307
+ ]"
1308
+ :collapsed="siderCollapsed"
1309
+ :collapsed-width="72"
1310
+ />
1311
+ </MLayoutSider>
1312
+ <MLayout>
1313
+ <MLayoutHeader :padding="'var(--m-space-4) var(--m-space-6)'">
1314
+ <MBreadcrumb :model="[{ label: '首页', to: '/' }, { label: '当前页' }]" />
1315
+ </MLayoutHeader>
1316
+ <MLayoutContent>
1317
+ <MPageContent>
1318
+ <!-- page sections -->
1319
+ </MPageContent>
1320
+ </MLayoutContent>
1321
+ </MLayout>
1322
+ </MLayout>
562
1323
  </MConfigProvider>`,
563
1324
  rules: [
564
1325
  '根布局用 fillViewport',
@@ -591,19 +1352,19 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
591
1352
  'log',
592
1353
  ],
593
1354
  imports: ['MScrollbar', 'MCard'],
594
- template: `<MCard title="活动日志">
595
- <MScrollbar max-height="16rem">
596
- <ul style="margin:0;padding:0;list-style:none">
597
- <li v-for="item in logLines" :key="item.id" style="padding:var(--m-space-2) 0;border-bottom:1px solid var(--m-color-border)">
598
- {{ item.text }}
599
- </li>
600
- </ul>
601
- </MScrollbar>
1355
+ template: `<MCard title="活动日志">
1356
+ <MScrollbar max-height="16rem">
1357
+ <ul style="margin:0;padding:0;list-style:none">
1358
+ <li v-for="item in logLines" :key="item.id" style="padding:var(--m-space-2) 0;border-bottom:1px solid var(--m-color-border)">
1359
+ {{ item.text }}
1360
+ </li>
1361
+ </ul>
1362
+ </MScrollbar>
602
1363
  </MCard>`,
603
- scriptSetup: `const logLines = [
604
- { id: '1', text: '用户 admin 登录成功' },
605
- { id: '2', text: '导出任务已完成' },
606
- { id: '3', text: '配置已保存' },
1364
+ scriptSetup: `const logLines = [
1365
+ { id: '1', text: '用户 admin 登录成功' },
1366
+ { id: '2', text: '导出任务已完成' },
1367
+ { id: '3', text: '配置已保存' },
607
1368
  ]`,
608
1369
  rules: [
609
1370
  '整页主滚动推荐 MLayout fillViewport',