@morya-ui/mcp 0.3.3 → 0.3.5

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.
@@ -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
+ />
96
194
  </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'],
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
+ />
329
+ </MPageFilterChips>`,
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,46 +457,75 @@ const department = ref<string | null>(null)`,
143
457
  id: 'list-table',
144
458
  title: '列表页表格',
145
459
  titleEn: 'List page table',
146
- description: '标准 MTable,直接放在 MPageContent 内,不额外包 Card。',
147
- descriptionEn: 'Standard MTable placed directly in MPageContent without an extra Card wrapper.',
460
+ description: '标准 MTable,直接放在 MPageContent 内。全视口主列表可再加 fill(表体滚、分页贴底);嵌入/短页不要 fill。',
461
+ descriptionEn: 'Standard MTable in MPageContent. Add fill only for full-viewport main lists; skip fill for embedded/short pages.',
148
462
  pageTypes: ['list'],
149
- keywords: ['表格', 'table', 'pagination', 'paginator', 'empty', 'columns', 'rows'],
463
+ keywords: [
464
+ '表格',
465
+ 'table',
466
+ 'pagination',
467
+ 'paginator',
468
+ 'empty',
469
+ 'columns',
470
+ 'rows',
471
+ 'fill',
472
+ '撑满',
473
+ '分页',
474
+ ],
150
475
  imports: ['MTable', 'MEmpty', 'MButton'],
151
- scriptSetup: `const columns = [
152
- { key: 'name', label: '名称' },
153
- { key: 'status', label: '状态' },
154
- { key: 'updatedAt', label: '更新时间' },
155
- { key: 'actions', label: '操作', width: 128 },
156
- ]
157
- const rows = ref<Record<string, unknown>[]>([])
476
+ scriptSetup: `const columns = [
477
+ { key: 'name', label: '名称' },
478
+ { key: 'status', label: '状态' },
479
+ { key: 'updatedAt', label: '更新时间' },
480
+ { key: 'actions', label: '操作', width: 128 },
481
+ ]
482
+ const rows = ref<Record<string, unknown>[]>([])
158
483
  const loading = ref(false)`,
159
- template: `<MTable
160
- :columns="columns"
161
- :rows="rows"
162
- :loading="loading"
163
- paginator
164
- :rows-per-page="10"
165
- striped
166
- bordered
167
- row-key="id"
168
- aria-label="数据列表"
169
- >
170
- <template #empty>
171
- <MEmpty
172
- title="还没有数据"
173
- description="创建第一条记录后,这里会列出结果。"
174
- icon="inbox"
175
- >
176
- <template #extra>
177
- <MButton severity="primary">新建</MButton>
178
- </template>
179
- </MEmpty>
180
- </template>
484
+ template: `<!-- 全视口主列表再加 fill;嵌入/短页去掉 fill -->
485
+ <MTable
486
+ :columns="columns"
487
+ :rows="rows"
488
+ :loading="loading"
489
+ fill
490
+ paginator
491
+ :rows-per-page="10"
492
+ striped
493
+ bordered
494
+ row-key="id"
495
+ aria-label="数据列表"
496
+ >
497
+ <template #empty>
498
+ <MEmpty
499
+ title="还没有数据"
500
+ description="创建第一条记录后,这里会列出结果。"
501
+ icon="inbox"
502
+ >
503
+ <template #extra>
504
+ <MButton severity="primary">新建</MButton>
505
+ </template>
506
+ </MEmpty>
507
+ </template>
181
508
  </MTable>`,
182
- rules: ['表格直接放在 MPageContent 内', '空态用 MEmpty,不要留空白或单行灰字'],
183
- rulesEn: ['Place the table directly in MPageContent', 'Use MEmpty for zero-data states, not a blank or muted sentence'],
184
- avoid: ['不要用 MCard 包裹 bordered MTable'],
185
- avoidEn: ['Do not wrap a bordered MTable with MCard'],
509
+ rules: [
510
+ '先判断是否适合 fill:全视口后台主列表 + 表格是主任务 → MPageContent fill + MTable fill',
511
+ '嵌入表、短页、整页文档滚动 → 不要 fill,内容高度即可',
512
+ '表格直接放在 MPageContent 内;空态用 MEmpty',
513
+ ],
514
+ rulesEn: [
515
+ 'Decide fill first: full-viewport admin main list → MPageContent fill + MTable fill',
516
+ 'Embedded / short / document-scroll pages → skip fill',
517
+ 'Place the table directly in MPageContent; use MEmpty for zero-data states',
518
+ ],
519
+ avoid: [
520
+ '不要用 MCard 包裹 bordered MTable',
521
+ '不要对嵌入/短页硬套 fill',
522
+ '适合 fill 时不要手写 min-height / calc',
523
+ ],
524
+ avoidEn: [
525
+ 'Do not wrap a bordered MTable with MCard',
526
+ 'Do not force fill on embedded or short pages',
527
+ 'Do not hand-write min-height / calc when fill is appropriate',
528
+ ],
186
529
  },
187
530
  {
188
531
  id: 'list-row-actions',
@@ -193,11 +536,11 @@ const loading = ref(false)`,
193
536
  pageTypes: ['list'],
194
537
  keywords: ['行操作', '编辑', '删除', 'actions', 'row', 'cell'],
195
538
  imports: ['MSpace', 'MButton'],
196
- template: `<template #cell-actions>
197
- <MSpace>
198
- <MButton severity="secondary" size="small" text>编辑</MButton>
199
- <MButton severity="danger" size="small" text>删除</MButton>
200
- </MSpace>
539
+ template: `<template #cell-actions>
540
+ <MSpace>
541
+ <MButton severity="secondary" size="small" text>编辑</MButton>
542
+ <MButton severity="danger" size="small" text>删除</MButton>
543
+ </MSpace>
201
544
  </template>`,
202
545
  rules: ['行内操作用 size="small" + text/outlined,避免一排实心按钮', '危险操作使用 severity="danger" 并配合确认弹窗'],
203
546
  rulesEn: ['Use size="small" with text/outlined row actions', 'Use severity="danger" with confirmation for destructive actions'],
@@ -213,11 +556,11 @@ const loading = ref(false)`,
213
556
  pageTypes: ['list'],
214
557
  keywords: ['分类', '标签', '可关闭', 'chip', 'category', 'tag-only'],
215
558
  imports: ['MTag'],
216
- template: `<template #cell-category="{ value }">
217
- <MTag
218
- :value="String(value ?? '')"
219
- severity="info"
220
- />
559
+ template: `<template #cell-category="{ value }">
560
+ <MTag
561
+ :value="String(value ?? '')"
562
+ severity="info"
563
+ />
221
564
  </template>`,
222
565
  rules: ['分类/强调态用 MTag;行内业务状态用 MStatus(list-status-dot)', '不要用 Button 颜色表达状态'],
223
566
  rulesEn: ['Use MTag for categories; use MStatus for business status (list-status-dot)', 'Do not use Button colors for status'],
@@ -233,11 +576,11 @@ const loading = ref(false)`,
233
576
  pageTypes: ['list'],
234
577
  keywords: ['状态', 'status', 'dot', 'cell-status', '在线', '启用', '停用', '业务状态'],
235
578
  imports: ['MStatus'],
236
- template: `<template #cell-status="{ value }">
237
- <MStatus
238
- :label="value === 'active' || value === 'online' ? '启用' : '停用'"
239
- :severity="value === 'active' || value === 'online' ? 'success' : 'secondary'"
240
- />
579
+ template: `<template #cell-status="{ value }">
580
+ <MStatus
581
+ :label="value === 'active' || value === 'online' ? '启用' : '停用'"
582
+ :severity="value === 'active' || value === 'online' ? 'success' : 'secondary'"
583
+ />
241
584
  </template>`,
242
585
  rules: ['行内轻量业务状态优先 MStatus', '不要用 Button 颜色表达状态'],
243
586
  rulesEn: ['Prefer MStatus for lightweight inline business status', 'Do not use Button colors for status'],
@@ -253,15 +596,15 @@ const loading = ref(false)`,
253
596
  pageTypes: ['list', 'common'],
254
597
  keywords: ['空状态', '无数据', 'empty', 'zero state', 'no data'],
255
598
  imports: ['MEmpty', 'MButton'],
256
- template: `<MEmpty
257
- title="还没有数据"
258
- description="创建第一条记录后即可在此查看。"
259
- icon="database"
260
- >
261
- <template #extra>
262
- <MButton label="新建" />
263
- <MButton label="导入" severity="secondary" text />
264
- </template>
599
+ template: `<MEmpty
600
+ title="还没有数据"
601
+ description="创建第一条记录后即可在此查看。"
602
+ icon="database"
603
+ >
604
+ <template #extra>
605
+ <MButton label="新建" />
606
+ <MButton label="导入" severity="secondary" text />
607
+ </template>
265
608
  </MEmpty>`,
266
609
  rules: ['正常无数据用 MEmpty,不要用错误色', '主 CTA 用 primary,次动作用 text/secondary'],
267
610
  rulesEn: ['Use MEmpty for normal emptiness, not error colors', 'Primary CTA + secondary/text for lesser actions'],
@@ -277,11 +620,11 @@ const loading = ref(false)`,
277
620
  pageTypes: ['common'],
278
621
  keywords: ['结果', '成功', '失败', '404', '403', 'result', 'success', 'error'],
279
622
  imports: ['MResult', 'MButton'],
280
- template: `<MResult status="success" description="订单已创建,可在列表中查看详情。">
281
- <template #extra>
282
- <MButton label="查看订单" />
283
- <MButton label="返回列表" severity="secondary" text />
284
- </template>
623
+ template: `<MResult status="success" description="订单已创建,可在列表中查看详情。">
624
+ <template #extra>
625
+ <MButton label="查看订单" />
626
+ <MButton label="返回列表" severity="secondary" text />
627
+ </template>
285
628
  </MResult>`,
286
629
  rules: ['流程终点用 MResult', '提供明确下一步操作'],
287
630
  rulesEn: ['Use MResult for terminal outcomes', 'Provide clear next-step actions'],
@@ -312,15 +655,15 @@ const loading = ref(false)`,
312
655
  pageTypes: ['form'],
313
656
  keywords: ['表单', 'form', 'fields', 'formitem'],
314
657
  imports: ['MPageSection', 'MForm', 'MFormItem', 'MInput'],
315
- scriptSetup: `const model = reactive({ name: '' })
658
+ scriptSetup: `const model = reactive({ name: '' })
316
659
  const submitting = ref(false)`,
317
- template: `<MPageSection variant="form">
318
- <MForm @submit.prevent="onSubmit">
319
- <MFormItem label="姓名" name="name" required>
320
- <MInput v-model="model.name" placeholder="请输入姓名" fluid />
321
- </MFormItem>
322
- <!-- 更多字段 -->
323
- </MForm>
660
+ template: `<MPageSection variant="form">
661
+ <MForm @submit.prevent="onSubmit">
662
+ <MFormItem label="姓名" name="name" required>
663
+ <MInput v-model="model.name" placeholder="请输入姓名" fluid />
664
+ </MFormItem>
665
+ <!-- 更多字段 -->
666
+ </MForm>
324
667
  </MPageSection>`,
325
668
  rules: ['字段用 MFormItem + fluid 控件', '表单表面由 MPageSection 提供,不要重复 MCard'],
326
669
  rulesEn: ['Use MFormItem with fluid controls', 'Let MPageSection provide the surface; do not duplicate MCard'],
@@ -336,11 +679,11 @@ const submitting = ref(false)`,
336
679
  pageTypes: ['form'],
337
680
  keywords: ['保存', '取消', 'submit', 'actions', 'footer'],
338
681
  imports: ['MPageSection', 'MSpace', 'MButton'],
339
- template: `<MPageSection variant="actions">
340
- <MSpace>
341
- <MButton native-type="submit" severity="primary" :loading="submitting">保存</MButton>
342
- <MButton severity="secondary">取消</MButton>
343
- </MSpace>
682
+ template: `<MPageSection variant="actions">
683
+ <MSpace>
684
+ <MButton native-type="submit" severity="primary" :loading="submitting">保存</MButton>
685
+ <MButton severity="secondary">取消</MButton>
686
+ </MSpace>
344
687
  </MPageSection>`,
345
688
  rules: ['提交按钮在前,取消 secondary', '保存中传 loading 防重复提交'],
346
689
  rulesEn: ['Submit first, cancel secondary', 'Pass loading while saving to prevent duplicate submits'],
@@ -356,21 +699,21 @@ const submitting = ref(false)`,
356
699
  pageTypes: ['dashboard'],
357
700
  keywords: ['kpi', '指标', 'stat', 'grid', 'metrics'],
358
701
  imports: ['MGrid', 'MGridItem', 'MPageStat'],
359
- scriptSetup: `const metrics = ref([
360
- { label: '总用户', value: '12,480', trend: '+8.2%', trendDirection: 'up' as const, trendLabel: '较上月', icon: 'users' },
361
- { label: '今日活跃', value: '1,926', trend: '+3.1%', trendDirection: 'up' as const, trendLabel: '较昨日', icon: 'activity' },
702
+ scriptSetup: `const metrics = ref([
703
+ { label: '总用户', value: '12,480', trend: '+8.2%', trendDirection: 'up' as const, trendLabel: '较上月', icon: 'users' },
704
+ { label: '今日活跃', value: '1,926', trend: '+3.1%', trendDirection: 'up' as const, trendLabel: '较昨日', icon: 'activity' },
362
705
  ])`,
363
- template: `<MGrid :cols="4" :x-gap="16" :y-gap="16" responsive="screen">
364
- <MGridItem v-for="metric in metrics" :key="metric.label" :span="1">
365
- <MPageStat
366
- :label="metric.label"
367
- :value="metric.value"
368
- :trend="metric.trend"
369
- :trend-direction="metric.trendDirection"
370
- :trend-label="metric.trendLabel"
371
- :icon="metric.icon"
372
- />
373
- </MGridItem>
706
+ template: `<MGrid :cols="4" :x-gap="16" :y-gap="16" responsive="screen">
707
+ <MGridItem v-for="metric in metrics" :key="metric.label" :span="1">
708
+ <MPageStat
709
+ :label="metric.label"
710
+ :value="metric.value"
711
+ :trend="metric.trend"
712
+ :trend-direction="metric.trendDirection"
713
+ :trend-label="metric.trendLabel"
714
+ :icon="metric.icon"
715
+ />
716
+ </MGridItem>
374
717
  </MGrid>`,
375
718
  rules: ['KPI 用 MPageStat,不要每页自定义 stat CSS', 'responsive="screen" 便于窄屏降列'],
376
719
  rulesEn: ['Use MPageStat for KPIs', 'Use responsive="screen" for narrow layouts'],
@@ -386,14 +729,14 @@ const submitting = ref(false)`,
386
729
  pageTypes: ['dashboard'],
387
730
  keywords: ['chart', '图表', 'placeholder', 'card', 'trend'],
388
731
  imports: ['MCard', 'MEmpty', 'MSkeleton'],
389
- template: `<MCard title="趋势概览" shadow="always">
390
- <MSkeleton v-if="loading" height="8rem" />
391
- <MEmpty
392
- v-else
393
- aria-label="图表占位"
394
- title="暂无图表数据"
395
- description="图表区域(接入 ECharts / 业务组件)"
396
- />
732
+ template: `<MCard title="趋势概览" shadow="always">
733
+ <MSkeleton v-if="loading" height="8rem" />
734
+ <MEmpty
735
+ v-else
736
+ aria-label="图表占位"
737
+ title="暂无图表数据"
738
+ description="图表区域(接入 ECharts / 业务组件)"
739
+ />
397
740
  </MCard>`,
398
741
  rules: ['图表区适合 MCard,因为需要 card 标题', '加载中用 MSkeleton'],
399
742
  rulesEn: ['Charts belong in MCard when a card title is needed', 'Use MSkeleton while loading'],
@@ -409,18 +752,18 @@ const submitting = ref(false)`,
409
752
  pageTypes: ['dashboard'],
410
753
  keywords: ['recent', '最近', 'table', 'card', 'list'],
411
754
  imports: ['MCard', 'MTable', 'MStatus'],
412
- scriptSetup: `const recentColumns = [
413
- { key: 'id', label: '工单号', width: 96 },
414
- { key: 'title', label: '标题' },
415
- { key: 'status', label: '状态', width: 110 },
416
- ]
755
+ scriptSetup: `const recentColumns = [
756
+ { key: 'id', label: '工单号', width: 96 },
757
+ { key: 'title', label: '标题' },
758
+ { key: 'status', label: '状态', width: 110 },
759
+ ]
417
760
  const recentRows = ref<Record<string, unknown>[]>([])`,
418
- template: `<MCard title="最近工单">
419
- <MTable :columns="recentColumns" :rows="recentRows" size="small" :paginator="false" bordered row-key="id">
420
- <template #cell-status="{ value }">
421
- <MStatus :label="String(value ?? '')" severity="secondary" />
422
- </template>
423
- </MTable>
761
+ template: `<MCard title="最近工单">
762
+ <MTable :columns="recentColumns" :rows="recentRows" size="small" :paginator="false" bordered row-key="id">
763
+ <template #cell-status="{ value }">
764
+ <MStatus :label="String(value ?? '')" severity="secondary" />
765
+ </template>
766
+ </MTable>
424
767
  </MCard>`,
425
768
  rules: ['明细列表放 MCard 内合理', 'size="small" 适合卡片内表格', '行内业务状态用 MStatus'],
426
769
  rulesEn: ['Detail lists inside MCard are appropriate', 'Use size="small" for in-card tables', 'Use MStatus for row business status'],
@@ -436,10 +779,10 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
436
779
  pageTypes: ['list', 'common'],
437
780
  keywords: ['page content', 'stack', 'layout content', '容器'],
438
781
  imports: ['MPageContent'],
439
- template: `<MLayoutContent>
440
- <MPageContent>
441
- <!-- MPageFilters → MPageToolbar → MTable -->
442
- </MPageContent>
782
+ template: `<MLayoutContent>
783
+ <MPageContent>
784
+ <!-- MPageFilters → MPageToolbar → MTable -->
785
+ </MPageContent>
443
786
  </MLayoutContent>`,
444
787
  rules: ['所有列表区块放在 MPageContent 内', '区块顺序:筛选 → 工具栏 → 表格'],
445
788
  rulesEn: ['Keep all list sections inside MPageContent', 'Order: filters → toolbar → table'],
@@ -455,10 +798,10 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
455
798
  pageTypes: ['form', 'common'],
456
799
  keywords: ['narrow', 'form content', 'max-width'],
457
800
  imports: ['MPageContent'],
458
- template: `<MLayoutContent>
459
- <MPageContent width="narrow">
460
- <!-- MPageHeader → MPageSection form → actions -->
461
- </MPageContent>
801
+ template: `<MLayoutContent>
802
+ <MPageContent width="narrow">
803
+ <!-- MPageHeader → MPageSection form → actions -->
804
+ </MPageContent>
462
805
  </MLayoutContent>`,
463
806
  rules: ['表单页使用 width="narrow"', '不要手写 max-width CSS'],
464
807
  rulesEn: ['Use width="narrow" on form pages', 'Do not hand-write max-width CSS'],
@@ -474,20 +817,380 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
474
817
  pageTypes: ['detail'],
475
818
  keywords: ['detail', '详情', 'edit', 'header'],
476
819
  imports: ['MPageHeader', 'MButton', 'MStatus', 'MSpace'],
477
- template: `<MPageHeader title="示例资源" description="查看摘要与属性。">
478
- <template #actions>
479
- <MSpace>
480
- <MStatus label="正常" severity="success" />
481
- <MButton severity="primary">编辑</MButton>
482
- <MButton severity="danger" text>删除</MButton>
483
- </MSpace>
484
- </template>
820
+ template: `<MPageHeader title="示例资源" description="查看摘要与属性。">
821
+ <template #actions>
822
+ <MSpace>
823
+ <MStatus label="正常" severity="success" />
824
+ <MButton severity="primary">编辑</MButton>
825
+ <MButton severity="danger" text>删除</MButton>
826
+ </MSpace>
827
+ </template>
485
828
  </MPageHeader>`,
486
829
  rules: ['状态用 MStatus 放在 actions 区', '编辑为 primary,删除用 danger text'],
487
830
  rulesEn: ['Use MStatus in the actions area', 'Edit is primary; delete uses danger text'],
488
831
  avoid: ['不要把操作散落到多个无关区域'],
489
832
  avoidEn: ['Do not scatter actions across unrelated areas'],
490
833
  },
834
+ {
835
+ id: 'page-header-actions',
836
+ title: '页头单主操作',
837
+ titleEn: 'Page header with one primary action',
838
+ description: 'MPageHeader + #actions:视口内仅一个 filled primary,次要用 secondary/text。',
839
+ descriptionEn: 'MPageHeader + #actions: one filled primary in the viewport; secondary/text for lesser actions.',
840
+ pageTypes: ['list', 'form', 'dashboard', 'detail', 'common'],
841
+ keywords: ['页头', '主按钮', '新建', 'header', 'primary', 'actions', 'page-header'],
842
+ imports: ['MPageHeader', 'MButton', 'MSpace'],
843
+ template: `<MPageHeader title="用户管理" description="管理成员与角色。">
844
+ <template #actions>
845
+ <MSpace>
846
+ <MButton severity="secondary" text>导入</MButton>
847
+ <MButton severity="primary">新建用户</MButton>
848
+ </MSpace>
849
+ </template>
850
+ </MPageHeader>`,
851
+ rules: ['视口内仅一个 filled primary', '次要动作用 secondary 或 text', '批量操作用 MPageToolbar,不要塞进页头'],
852
+ rulesEn: ['Only one filled primary in the viewport', 'Lesser actions: secondary or text', 'Batch actions belong in MPageToolbar, not the header'],
853
+ avoid: ['不要在页头和筛选区各放一个实心 primary', '不要把危险删除做成页头唯一实心按钮'],
854
+ avoidEn: ['Do not place a solid primary in both header and filters', 'Do not make destructive delete the only solid header button'],
855
+ },
856
+ {
857
+ id: 'form-in-dialog',
858
+ title: '列表内弹窗短表单',
859
+ titleEn: 'Short form in dialog from a list',
860
+ description: '同页 MDialog + MForm + #footer;短 CRUD 默认路径(约 ≤8 字段)。',
861
+ descriptionEn: 'Same-page MDialog + MForm + #footer; default short CRUD path (~≤8 fields).',
862
+ pageTypes: ['list', 'common'],
863
+ keywords: [
864
+ '弹窗表单',
865
+ '新建弹窗',
866
+ '编辑弹窗',
867
+ 'dialog form',
868
+ 'form-in-dialog',
869
+ 'modal form',
870
+ 'inline create',
871
+ ],
872
+ imports: ['MDialog', 'MForm', 'MFormItem', 'MInput', 'MSelect', 'MButton', 'MSpace', 'message'],
873
+ scriptSetup: `const dialogOpen = ref(false)
874
+ const submitting = ref(false)
875
+ const model = reactive({
876
+ name: '',
877
+ email: '',
878
+ role: undefined as string | undefined,
879
+ })
880
+ const roleOptions = [
881
+ { label: '管理员', value: 'admin' },
882
+ { label: '成员', value: 'member' },
883
+ ]
884
+
885
+ function resetModel() {
886
+ model.name = ''
887
+ model.email = ''
888
+ model.role = undefined
889
+ }
890
+
891
+ async function onSave() {
892
+ submitting.value = true
893
+ try {
894
+ // await api.save(model)
895
+ message.success('已保存')
896
+ dialogOpen.value = false
897
+ resetModel()
898
+ } finally {
899
+ submitting.value = false
900
+ }
901
+ }`,
902
+ template: `<MDialog
903
+ v-model="dialogOpen"
904
+ header="新建用户"
905
+ width="32rem"
906
+ @close="resetModel"
907
+ >
908
+ <MForm @submit.prevent="onSave">
909
+ <MFormItem label="姓名" name="name" required>
910
+ <MInput v-model="model.name" placeholder="请输入姓名" fluid />
911
+ </MFormItem>
912
+ <MFormItem label="邮箱" name="email" required>
913
+ <MInput v-model="model.email" type="email" placeholder="name@example.com" fluid />
914
+ </MFormItem>
915
+ <MFormItem label="角色" name="role" required>
916
+ <MSelect v-model="model.role" :options="roleOptions" placeholder="请选择角色" fluid />
917
+ </MFormItem>
918
+ </MForm>
919
+ <template #footer>
920
+ <MSpace style="justify-content: flex-end; width: 100%">
921
+ <MButton severity="secondary" text :disabled="submitting" @click="dialogOpen = false">
922
+ 取消
923
+ </MButton>
924
+ <MButton severity="primary" :loading="submitting" @click="onSave">
925
+ 保存
926
+ </MButton>
927
+ </MSpace>
928
+ </template>
929
+ </MDialog>`,
930
+ rules: [
931
+ '留在列表页,不另开路由',
932
+ '宽度约 28–36rem;字段用 fluid',
933
+ '取消/保存放 #footer;成功用 message.success',
934
+ ],
935
+ rulesEn: [
936
+ 'Stay on the list page; do not open a new route',
937
+ 'Width ~28–36rem; fields use fluid',
938
+ 'Cancel/save in #footer; success via message.success',
939
+ ],
940
+ avoid: [
941
+ '不要用 visible 代替 v-model',
942
+ '不要把长多分组表单塞进 Dialog → form-page / form-in-drawer',
943
+ '不要用 toast 代替单行 message',
944
+ ],
945
+ avoidEn: [
946
+ 'Do not use visible instead of v-model',
947
+ 'Do not stuff long multi-section forms into Dialog → form-page / form-in-drawer',
948
+ 'Do not use toast for a one-line message',
949
+ ],
950
+ },
951
+ {
952
+ id: 'form-in-drawer',
953
+ title: '列表内侧滑表单',
954
+ titleEn: 'Form in drawer from a list',
955
+ description: '同页 MDrawer + MForm + #footer;比 Dialog 更长但仍需对照列表。',
956
+ descriptionEn: 'Same-page MDrawer + MForm + #footer; longer than a dialog but list context still matters.',
957
+ pageTypes: ['list', 'detail', 'common'],
958
+ keywords: ['抽屉表单', '侧滑编辑', 'drawer form', 'form-in-drawer', 'side panel edit'],
959
+ imports: ['MDrawer', 'MForm', 'MFormItem', 'MInput', 'MButton', 'MSpace', 'message'],
960
+ scriptSetup: `const drawerOpen = ref(false)
961
+ const submitting = ref(false)
962
+ const model = reactive({ name: '', note: '' })
963
+
964
+ async function onSave() {
965
+ submitting.value = true
966
+ try {
967
+ message.success('已保存')
968
+ drawerOpen.value = false
969
+ } finally {
970
+ submitting.value = false
971
+ }
972
+ }`,
973
+ template: `<MDrawer
974
+ v-model="drawerOpen"
975
+ header="编辑详情"
976
+ position="right"
977
+ size="28rem"
978
+ @close="/* reset draft */"
979
+ >
980
+ <MForm @submit.prevent="onSave">
981
+ <MFormItem label="名称" name="name" required>
982
+ <MInput v-model="model.name" fluid />
983
+ </MFormItem>
984
+ <MFormItem label="备注" name="note">
985
+ <MInput v-model="model.note" fluid />
986
+ </MFormItem>
987
+ </MForm>
988
+ <template #footer>
989
+ <MSpace style="justify-content: flex-end; width: 100%">
990
+ <MButton severity="secondary" text :disabled="submitting" @click="drawerOpen = false">
991
+ 取消
992
+ </MButton>
993
+ <MButton severity="primary" :loading="submitting" @click="onSave">
994
+ 保存
995
+ </MButton>
996
+ </MSpace>
997
+ </template>
998
+ </MDrawer>`,
999
+ rules: ['position 常用 right;较长表单可加大 size', '字段 fluid;底栏放 #footer', '成功用 message.success'],
1000
+ rulesEn: ['position usually right; raise size for longer forms', 'Fields fluid; actions in #footer', 'Success via message.success'],
1001
+ avoid: ['极短 2–3 字段也用 Drawer → form-in-dialog', '危险删除只用 Drawer 无确认 → confirm-delete'],
1002
+ avoidEn: ['Tiny 2–3 field forms → form-in-dialog', 'Destructive delete without confirm → confirm-delete'],
1003
+ },
1004
+ {
1005
+ id: 'confirm-delete',
1006
+ title: '危险删除确认',
1007
+ titleEn: 'Destructive delete confirm',
1008
+ description: 'MConfirmDialog + message.success;列表行删除默认路径。',
1009
+ descriptionEn: 'MConfirmDialog + message.success; default for list-row delete.',
1010
+ pageTypes: ['list', 'detail', 'common'],
1011
+ keywords: ['删除', '确认', '二次确认', 'confirm', 'delete', 'ConfirmDialog', '危险'],
1012
+ imports: ['MConfirmDialog', 'message'],
1013
+ scriptSetup: `const confirmOpen = ref(false)
1014
+ const pendingId = ref<string | null>(null)
1015
+
1016
+ function askDelete(id: string) {
1017
+ pendingId.value = id
1018
+ confirmOpen.value = true
1019
+ }
1020
+
1021
+ function onAcceptDelete() {
1022
+ // await api.remove(pendingId.value)
1023
+ message.success('已删除')
1024
+ pendingId.value = null
1025
+ confirmOpen.value = false
1026
+ }`,
1027
+ template: `<MConfirmDialog
1028
+ v-model="confirmOpen"
1029
+ header="确认删除"
1030
+ message="删除后不可恢复,确定继续?"
1031
+ accept-label="删除"
1032
+ reject-label="取消"
1033
+ accept-severity="danger"
1034
+ @accept="onAcceptDelete"
1035
+ />`,
1036
+ rules: ['用 MConfirmDialog,不要手写 Dialog 确定/取消', 'acceptSeverity="danger"', '成功用 message.success,不要 toast'],
1037
+ rulesEn: ['Use MConfirmDialog; do not hand-roll Dialog Yes/No', 'acceptSeverity="danger"', 'Success via message.success, not toast'],
1038
+ avoid: ['普通 Dialog 冒充删除确认', '编辑表单误用 ConfirmDialog'],
1039
+ avoidEn: ['Faking delete confirm with Dialog', 'Using ConfirmDialog for edit forms'],
1040
+ },
1041
+ {
1042
+ id: 'row-actions-menu',
1043
+ title: '表格行更多操作菜单',
1044
+ titleEn: 'Table row overflow actions menu',
1045
+ description: '操作较多时用 MDropdown 收纳行内动作;少量明确操作仍用 list-row-actions。',
1046
+ descriptionEn: 'Use MDropdown when there are many row actions; prefer list-row-actions for a few explicit buttons.',
1047
+ pageTypes: ['list', 'common'],
1048
+ keywords: ['更多', '行菜单', 'dropdown', 'row menu', 'overflow', 'actions menu'],
1049
+ imports: ['MDropdown', 'MButton'],
1050
+ scriptSetup: `const rowMenu = [
1051
+ { label: '编辑', command: () => {/* open edit */} },
1052
+ { label: '复制', command: () => {/* duplicate */} },
1053
+ { label: '删除', severity: 'danger', command: () => {/* askDelete */} },
1054
+ ]`,
1055
+ template: `<template #cell-actions>
1056
+ <MDropdown :model="rowMenu">
1057
+ <MButton severity="secondary" size="small" text icon="more-horizontal" aria-label="更多操作" />
1058
+ </MDropdown>
1059
+ </template>`,
1060
+ rules: ['操作很多时再用 Dropdown;2–3 个明确操作用 text 按钮', '危险项走 confirm-delete', '图标按钮必须 aria-label'],
1061
+ rulesEn: ['Use Dropdown when many actions; 2–3 explicit actions stay as text buttons', 'Danger → confirm-delete', 'Icon-only buttons need aria-label'],
1062
+ avoid: ['表单枚举用 Dropdown → Select', '只有编辑/删除也强行收进菜单'],
1063
+ avoidEn: ['Form enums via Dropdown → Select', 'Forcing edit/delete into a menu when two text buttons suffice'],
1064
+ },
1065
+ {
1066
+ id: 'wizard-steps',
1067
+ title: '分步向导 Stepper + Form',
1068
+ titleEn: 'Wizard stepper with per-step form',
1069
+ description: 'MStepper :steps + 每步一个 MForm;完成可用 result-block。',
1070
+ descriptionEn: 'MStepper :steps + one MForm per step; finish with result-block when useful.',
1071
+ pageTypes: ['form', 'common'],
1072
+ keywords: ['向导', '分步', '步骤', 'wizard', 'stepper', 'multi-step'],
1073
+ imports: ['MStepper', 'MForm', 'MFormItem', 'MInput', 'MButton', 'MSpace', 'MPageSection', 'message'],
1074
+ scriptSetup: `const activeStep = ref(0)
1075
+ const submitting = ref(false)
1076
+ const steps = [
1077
+ { label: '基本信息', description: '名称与说明' },
1078
+ { label: '确认', description: '核对后创建' },
1079
+ ]
1080
+ const model = reactive({ name: '' })
1081
+
1082
+ function back() {
1083
+ if (activeStep.value > 0) activeStep.value -= 1
1084
+ }
1085
+ function next() {
1086
+ if (activeStep.value < steps.length - 1) activeStep.value += 1
1087
+ }
1088
+ async function finish() {
1089
+ submitting.value = true
1090
+ try {
1091
+ message.success('已创建')
1092
+ } finally {
1093
+ submitting.value = false
1094
+ }
1095
+ }`,
1096
+ template: `<MStepper v-model="activeStep" :steps="steps" linear />
1097
+ <MPageSection variant="form">
1098
+ <MForm v-if="activeStep === 0" @submit.prevent="next">
1099
+ <MFormItem label="名称" name="name" required>
1100
+ <MInput v-model="model.name" fluid />
1101
+ </MFormItem>
1102
+ </MForm>
1103
+ <div v-else>
1104
+ <p>确认创建「{{ model.name }}」?</p>
1105
+ </div>
1106
+ </MPageSection>
1107
+ <MPageSection variant="actions">
1108
+ <MSpace>
1109
+ <MButton severity="secondary" text :disabled="activeStep === 0" @click="back">上一步</MButton>
1110
+ <MButton v-if="activeStep < steps.length - 1" severity="primary" @click="next">下一步</MButton>
1111
+ <MButton v-else severity="primary" :loading="submitting" @click="finish">创建</MButton>
1112
+ </MSpace>
1113
+ </MPageSection>`,
1114
+ rules: ['用 :steps(不是 :items)', '每步一个主任务;进入下一步前校验当前步', '完成文案与「下一步」区分'],
1115
+ rulesEn: ['Use :steps (not :items)', 'One job per step; validate before advancing', 'Finish label differs from Next'],
1116
+ avoid: ['所有字段藏在单页超长表单', '允许跳过有前置依赖的步骤'],
1117
+ avoidEn: ['Hiding all fields on one long page', 'Skipping steps with prerequisites'],
1118
+ },
1119
+ {
1120
+ id: 'auth-split-shell',
1121
+ title: '登录左右分栏壳',
1122
+ titleEn: 'Auth split brand + form shell',
1123
+ description: '左品牌 / 右表单;控件 M*;失败用 role="alert" 或 errorMessage,不用 Toast。',
1124
+ descriptionEn: 'Brand left / form right; M* controls; failures via role="alert" or errorMessage, not Toast.',
1125
+ pageTypes: ['common'],
1126
+ keywords: ['登录', '认证', '分栏', 'login', 'auth', 'split', 'brand'],
1127
+ imports: ['MForm', 'MFormItem', 'MInput', 'MInputPassword', 'MButton', 'MSpace', 'MConfigProvider', 'zhCN'],
1128
+ scriptSetup: `const submitting = ref(false)
1129
+ const formError = ref('')
1130
+ const model = reactive({ email: '', password: '' })
1131
+
1132
+ async function onSubmit() {
1133
+ formError.value = ''
1134
+ submitting.value = true
1135
+ try {
1136
+ if (!model.email || !model.password) {
1137
+ formError.value = '请输入邮箱和密码后再试。'
1138
+ }
1139
+ } finally {
1140
+ submitting.value = false
1141
+ }
1142
+ }`,
1143
+ template: `<MConfigProvider :locale="zhCN">
1144
+ <div class="login-shell">
1145
+ <aside class="login-brand" aria-label="品牌">
1146
+ <p class="login-brand__mark">产品名</p>
1147
+ <h1 class="login-brand__title">一句品牌主张</h1>
1148
+ <p class="login-brand__lead">一句支持说明。</p>
1149
+ </aside>
1150
+ <main class="login-main">
1151
+ <div class="login-panel">
1152
+ <header class="login-panel__header">
1153
+ <h2>登录</h2>
1154
+ <p>使用工作邮箱进入。</p>
1155
+ </header>
1156
+ <p v-if="formError" class="login-alert" role="alert">{{ formError }}</p>
1157
+ <MForm @submit="onSubmit">
1158
+ <MFormItem label="邮箱" name="email" required>
1159
+ <MInput v-model="model.email" type="email" autocomplete="username" fluid />
1160
+ </MFormItem>
1161
+ <MFormItem label="密码" name="password" required>
1162
+ <MInputPassword v-model="model.password" autocomplete="current-password" fluid />
1163
+ </MFormItem>
1164
+ <MSpace style="margin-top: var(--m-space-2)" alignment="center">
1165
+ <MButton type="submit" label="登录" severity="primary" :loading="submitting" />
1166
+ <MButton type="button" label="忘记密码" severity="secondary" text />
1167
+ </MSpace>
1168
+ </MForm>
1169
+ </div>
1170
+ </main>
1171
+ </div>
1172
+ </MConfigProvider>`,
1173
+ rules: [
1174
+ '品牌区用 --m-* / color-mix,默认扁平勿叠 aurora',
1175
+ '失败留在表单区(role="alert" 或 errorMessage),不要 Toast',
1176
+ '窄屏改为单栏:表单在上或品牌收短',
1177
+ ],
1178
+ rulesEn: [
1179
+ 'Brand panel: --m-* / color-mix; flat by default — no stacked aurora',
1180
+ 'Keep failures in the form (role="alert" or errorMessage), not Toast',
1181
+ 'Narrow screens: single column',
1182
+ ],
1183
+ avoid: [
1184
+ '不要把 MMessage 当内嵌 Alert',
1185
+ '不要引入第二套 UI 库做登录壳',
1186
+ '不要未询问就加玻璃/霓虹/全页渐变',
1187
+ ],
1188
+ avoidEn: [
1189
+ 'Do not use MMessage as an inline Alert',
1190
+ 'Do not bring a second UI kit for the auth shell',
1191
+ 'Do not add glass/neon/full-page gradients unless asked',
1192
+ ],
1193
+ },
491
1194
  {
492
1195
  id: 'layout-app-shell',
493
1196
  title: '后台应用骨架',
@@ -507,29 +1210,29 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
507
1210
  'MConfigProvider',
508
1211
  'zhCN',
509
1212
  ],
510
- template: `<MConfigProvider :locale="zhCN">
511
- <MLayout fill-viewport has-sider>
512
- <MLayoutSider v-model:collapsed="siderCollapsed" bordered :collapsed-width="72">
513
- <MMenu
514
- :model="[
515
- { key: 'users', label: '用户管理', icon: 'user', to: '/users' },
516
- { key: 'roles', label: '角色管理', icon: 'shield', to: '/roles' },
517
- ]"
518
- :collapsed="siderCollapsed"
519
- :collapsed-width="72"
520
- />
521
- </MLayoutSider>
522
- <MLayout>
523
- <MLayoutHeader :padding="'var(--m-space-4) var(--m-space-6)'">
524
- <MBreadcrumb :model="[{ label: '首页', to: '/' }, { label: '当前页' }]" />
525
- </MLayoutHeader>
526
- <MLayoutContent>
527
- <MPageContent>
528
- <!-- page sections -->
529
- </MPageContent>
530
- </MLayoutContent>
531
- </MLayout>
532
- </MLayout>
1213
+ template: `<MConfigProvider :locale="zhCN">
1214
+ <MLayout fill-viewport has-sider>
1215
+ <MLayoutSider v-model:collapsed="siderCollapsed" bordered :collapsed-width="72">
1216
+ <MMenu
1217
+ :model="[
1218
+ { key: 'users', label: '用户管理', icon: 'user', to: '/users' },
1219
+ { key: 'roles', label: '角色管理', icon: 'shield', to: '/roles' },
1220
+ ]"
1221
+ :collapsed="siderCollapsed"
1222
+ :collapsed-width="72"
1223
+ />
1224
+ </MLayoutSider>
1225
+ <MLayout>
1226
+ <MLayoutHeader :padding="'var(--m-space-4) var(--m-space-6)'">
1227
+ <MBreadcrumb :model="[{ label: '首页', to: '/' }, { label: '当前页' }]" />
1228
+ </MLayoutHeader>
1229
+ <MLayoutContent>
1230
+ <MPageContent>
1231
+ <!-- page sections -->
1232
+ </MPageContent>
1233
+ </MLayoutContent>
1234
+ </MLayout>
1235
+ </MLayout>
533
1236
  </MConfigProvider>`,
534
1237
  rules: [
535
1238
  '根布局用 fillViewport',
@@ -562,19 +1265,19 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
562
1265
  'log',
563
1266
  ],
564
1267
  imports: ['MScrollbar', 'MCard'],
565
- template: `<MCard title="活动日志">
566
- <MScrollbar max-height="16rem">
567
- <ul style="margin:0;padding:0;list-style:none">
568
- <li v-for="item in logLines" :key="item.id" style="padding:var(--m-space-2) 0;border-bottom:1px solid var(--m-color-border)">
569
- {{ item.text }}
570
- </li>
571
- </ul>
572
- </MScrollbar>
1268
+ template: `<MCard title="活动日志">
1269
+ <MScrollbar max-height="16rem">
1270
+ <ul style="margin:0;padding:0;list-style:none">
1271
+ <li v-for="item in logLines" :key="item.id" style="padding:var(--m-space-2) 0;border-bottom:1px solid var(--m-color-border)">
1272
+ {{ item.text }}
1273
+ </li>
1274
+ </ul>
1275
+ </MScrollbar>
573
1276
  </MCard>`,
574
- scriptSetup: `const logLines = [
575
- { id: '1', text: '用户 admin 登录成功' },
576
- { id: '2', text: '导出任务已完成' },
577
- { id: '3', text: '配置已保存' },
1277
+ scriptSetup: `const logLines = [
1278
+ { id: '1', text: '用户 admin 登录成功' },
1279
+ { id: '2', text: '导出任务已完成' },
1280
+ { id: '3', text: '配置已保存' },
578
1281
  ]`,
579
1282
  rules: [
580
1283
  '整页主滚动推荐 MLayout fillViewport',