@morya-ui/mcp 0.3.5 → 0.3.7

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.
@@ -8,33 +8,33 @@ export const pageSnippets = [
8
8
  pageTypes: ['list'],
9
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
-
19
- function resetFilters() {
20
- keyword.value = ''
21
- status.value = undefined
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
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>
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>
38
38
  </MPageFilters>`,
39
39
  rules: [
40
40
  '放在 MPageHeader 之后、表格(或 FilterChips)之前',
@@ -68,57 +68,57 @@ function resetFilters() {
68
68
  pageTypes: ['list'],
69
69
  keywords: ['折叠', '高级筛选', 'collapse', 'advanced', 'expand', 'filter', '次要条件'],
70
70
  imports: ['MPageFilters', 'MSpace', 'MInput', 'MSelect', 'MButton'],
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
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
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>
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>
122
122
  </MPageFilters>`,
123
123
  rules: [
124
124
  '常用条件放 default,次要条件放 #advanced',
@@ -152,45 +152,45 @@ function resetFilters() {
152
152
  pageTypes: ['list'],
153
153
  keywords: ['已选', 'chip', 'tag', 'active', 'clear', '筛选', 'FilterChips', '已选筛选'],
154
154
  imports: ['MPageFilterChips', 'MTag'],
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
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
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
- />
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
194
  </MPageFilterChips>`,
195
195
  rules: [
196
196
  '放在 MPageFilters 之后、表格之前',
@@ -241,91 +241,91 @@ function clearFilter(key: string) {
241
241
  'MButton',
242
242
  'MTag',
243
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
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
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
- />
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
329
  </MPageFilterChips>`,
330
330
  rules: [
331
331
  '顺序:Header → Filters → FilterChips →(可选 Toolbar)→ Table',
@@ -359,40 +359,40 @@ function resetFilters() {
359
359
  pageTypes: ['list'],
360
360
  keywords: ['dense', 'compact', '紧凑', 'plain', '监控', '告警', '小尺寸筛选'],
361
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
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
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>
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
396
  </MPageFilters>`,
397
397
  rules: [
398
398
  '仅在 dense/compact craft 使用;默认列表用 list-filters-stack',
@@ -422,10 +422,10 @@ function resetFilters() {
422
422
  pageTypes: ['list'],
423
423
  keywords: ['页头', '标题', '新建', 'header', 'title', 'create', 'actions'],
424
424
  imports: ['MPageHeader', 'MButton'],
425
- template: `<MPageHeader title="用户管理" description="维护账号、角色与权限。">
426
- <template #actions>
427
- <MButton severity="primary">新建用户</MButton>
428
- </template>
425
+ template: `<MPageHeader title="用户管理" description="维护账号、角色与权限。">
426
+ <template #actions>
427
+ <MButton severity="primary">新建用户</MButton>
428
+ </template>
429
429
  </MPageHeader>`,
430
430
  rules: ['页级标题用 MPageHeader,不要用 MPageToolbar 当第二页头', '主操作放 #actions'],
431
431
  rulesEn: ['Use MPageHeader for page identity; do not use MPageToolbar as a second page title', 'Put primary actions in #actions'],
@@ -441,12 +441,12 @@ function resetFilters() {
441
441
  pageTypes: ['list'],
442
442
  keywords: ['批量', '已选', 'toolbar', 'batch', 'export', 'delete'],
443
443
  imports: ['MPageToolbar', 'MButton'],
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>
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>
450
450
  </MPageToolbar>`,
451
451
  rules: ['左侧用 default 插槽展示已选数量', '批量操作用 text/outlined,danger 仅 destructive', '放在 FilterChips 之后、表格之前'],
452
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'],
@@ -457,8 +457,8 @@ function resetFilters() {
457
457
  id: 'list-table',
458
458
  title: '列表页表格',
459
459
  titleEn: 'List page table',
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.',
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.',
462
462
  pageTypes: ['list'],
463
463
  keywords: [
464
464
  '表格',
@@ -471,60 +471,84 @@ function resetFilters() {
471
471
  'fill',
472
472
  '撑满',
473
473
  '分页',
474
+ 'tabular',
475
+ '对齐',
474
476
  ],
475
- imports: ['MTable', 'MEmpty', 'MButton'],
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>[]>([])
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>[]>([])
483
486
  const loading = ref(false)`,
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>
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>
508
526
  </MTable>`,
509
527
  rules: [
510
528
  '先判断是否适合 fill:全视口后台主列表 + 表格是主任务 → MPageContent fill + MTable fill',
511
529
  '嵌入表、短页、整页文档滚动 → 不要 fill,内容高度即可',
512
- '表格直接放在 MPageContent 内;空态用 MEmpty',
530
+ '业务状态列用 MStatus;分类标签才用 MTag',
531
+ '数量/金额列 columns.align = end,并用 tabular-nums',
532
+ '行操作用 size="small" + text;空态文案写领域下一步,禁止「示例 / Name / No data」',
513
533
  ],
514
534
  rulesEn: [
515
535
  'Decide fill first: full-viewport admin main list → MPageContent fill + MTable fill',
516
536
  'Embedded / short / document-scroll pages → skip fill',
517
- '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”',
518
540
  ],
519
541
  avoid: [
520
542
  '不要用 MCard 包裹 bordered MTable',
521
543
  '不要对嵌入/短页硬套 fill',
522
544
  '适合 fill 时不要手写 min-height / calc',
545
+ '不要在 #cell-status 默认用 MTag',
523
546
  ],
524
547
  avoidEn: [
525
548
  'Do not wrap a bordered MTable with MCard',
526
549
  'Do not force fill on embedded or short pages',
527
550
  'Do not hand-write min-height / calc when fill is appropriate',
551
+ 'Do not default #cell-status to MTag',
528
552
  ],
529
553
  },
530
554
  {
@@ -536,11 +560,11 @@ const loading = ref(false)`,
536
560
  pageTypes: ['list'],
537
561
  keywords: ['行操作', '编辑', '删除', 'actions', 'row', 'cell'],
538
562
  imports: ['MSpace', 'MButton'],
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>
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>
544
568
  </template>`,
545
569
  rules: ['行内操作用 size="small" + text/outlined,避免一排实心按钮', '危险操作使用 severity="danger" 并配合确认弹窗'],
546
570
  rulesEn: ['Use size="small" with text/outlined row actions', 'Use severity="danger" with confirmation for destructive actions'],
@@ -556,11 +580,11 @@ const loading = ref(false)`,
556
580
  pageTypes: ['list'],
557
581
  keywords: ['分类', '标签', '可关闭', 'chip', 'category', 'tag-only'],
558
582
  imports: ['MTag'],
559
- template: `<template #cell-category="{ value }">
560
- <MTag
561
- :value="String(value ?? '')"
562
- severity="info"
563
- />
583
+ template: `<template #cell-category="{ value }">
584
+ <MTag
585
+ :value="String(value ?? '')"
586
+ severity="info"
587
+ />
564
588
  </template>`,
565
589
  rules: ['分类/强调态用 MTag;行内业务状态用 MStatus(list-status-dot)', '不要用 Button 颜色表达状态'],
566
590
  rulesEn: ['Use MTag for categories; use MStatus for business status (list-status-dot)', 'Do not use Button colors for status'],
@@ -576,11 +600,11 @@ const loading = ref(false)`,
576
600
  pageTypes: ['list'],
577
601
  keywords: ['状态', 'status', 'dot', 'cell-status', '在线', '启用', '停用', '业务状态'],
578
602
  imports: ['MStatus'],
579
- template: `<template #cell-status="{ value }">
580
- <MStatus
581
- :label="value === 'active' || value === 'online' ? '启用' : '停用'"
582
- :severity="value === 'active' || value === 'online' ? 'success' : 'secondary'"
583
- />
603
+ template: `<template #cell-status="{ value }">
604
+ <MStatus
605
+ :label="value === 'active' || value === 'online' ? '启用' : '停用'"
606
+ :severity="value === 'active' || value === 'online' ? 'success' : 'secondary'"
607
+ />
584
608
  </template>`,
585
609
  rules: ['行内轻量业务状态优先 MStatus', '不要用 Button 颜色表达状态'],
586
610
  rulesEn: ['Prefer MStatus for lightweight inline business status', 'Do not use Button colors for status'],
@@ -591,25 +615,33 @@ const loading = ref(false)`,
591
615
  id: 'empty-block',
592
616
  title: '空状态 MEmpty',
593
617
  titleEn: 'Empty state with MEmpty',
594
- description: '列表或内容区无数据时使用 MEmpty,操作放在 #extra。',
595
- 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.',
596
620
  pageTypes: ['list', 'common'],
597
621
  keywords: ['空状态', '无数据', 'empty', 'zero state', 'no data'],
598
622
  imports: ['MEmpty', 'MButton'],
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>
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>
608
632
  </MEmpty>`,
609
- rules: ['正常无数据用 MEmpty,不要用错误色', '主 CTA 用 primary,次动作用 text/secondary'],
610
- rulesEn: ['Use MEmpty for normal emptiness, not error colors', 'Primary CTA + secondary/text for lesser actions'],
611
- avoid: ['不要用手写 div 拼空态', '不要用 MResult 表达无数据'],
612
- 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'],
613
645
  },
614
646
  {
615
647
  id: 'result-block',
@@ -620,11 +652,11 @@ const loading = ref(false)`,
620
652
  pageTypes: ['common'],
621
653
  keywords: ['结果', '成功', '失败', '404', '403', 'result', 'success', 'error'],
622
654
  imports: ['MResult', 'MButton'],
623
- template: `<MResult status="success" description="订单已创建,可在列表中查看详情。">
624
- <template #extra>
625
- <MButton label="查看订单" />
626
- <MButton label="返回列表" severity="secondary" text />
627
- </template>
655
+ template: `<MResult status="success" description="订单已创建,可在列表中查看详情。">
656
+ <template #extra>
657
+ <MButton label="查看订单" />
658
+ <MButton label="返回列表" severity="secondary" text />
659
+ </template>
628
660
  </MResult>`,
629
661
  rules: ['流程终点用 MResult', '提供明确下一步操作'],
630
662
  rulesEn: ['Use MResult for terminal outcomes', 'Provide clear next-step actions'],
@@ -650,25 +682,52 @@ const loading = ref(false)`,
650
682
  id: 'form-body',
651
683
  title: '表单主体',
652
684
  titleEn: 'Form body surface',
653
- description: 'MPageSection variant="form" 包裹 MForm 字段。',
654
- 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.',
655
687
  pageTypes: ['form'],
656
- keywords: ['表单', 'form', 'fields', 'formitem'],
657
- imports: ['MPageSection', 'MForm', 'MFormItem', 'MInput'],
658
- scriptSetup: `const model = reactive({ name: '' })
659
- const submitting = ref(false)`,
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>
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>
667
716
  </MPageSection>`,
668
- rules: ['字段用 MFormItem + fluid 控件', '表单表面由 MPageSection 提供,不要重复 MCard'],
669
- rulesEn: ['Use MFormItem with fluid controls', 'Let MPageSection provide the surface; do not duplicate MCard'],
670
- avoid: ['不要绕过 MForm 手写校验状态'],
671
- 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'],
672
731
  },
673
732
  {
674
733
  id: 'form-actions',
@@ -679,11 +738,11 @@ const submitting = ref(false)`,
679
738
  pageTypes: ['form'],
680
739
  keywords: ['保存', '取消', 'submit', 'actions', 'footer'],
681
740
  imports: ['MPageSection', 'MSpace', 'MButton'],
682
- template: `<MPageSection variant="actions">
683
- <MSpace>
684
- <MButton native-type="submit" severity="primary" :loading="submitting">保存</MButton>
685
- <MButton severity="secondary">取消</MButton>
686
- </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>
687
746
  </MPageSection>`,
688
747
  rules: ['提交按钮在前,取消 secondary', '保存中传 loading 防重复提交'],
689
748
  rulesEn: ['Submit first, cancel secondary', 'Pass loading while saving to prevent duplicate submits'],
@@ -694,31 +753,59 @@ const submitting = ref(false)`,
694
753
  id: 'dashboard-kpi-grid',
695
754
  title: '仪表盘 KPI 栅格',
696
755
  titleEn: 'Dashboard KPI grid',
697
- description: 'MGrid + MPageStat 展示关键指标。',
698
- 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.',
699
758
  pageTypes: ['dashboard'],
700
- keywords: ['kpi', '指标', 'stat', 'grid', 'metrics'],
759
+ keywords: ['kpi', '指标', 'stat', 'grid', 'metrics', 'trend', 'compact'],
701
760
  imports: ['MGrid', 'MGridItem', 'MPageStat'],
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' },
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
+ },
705
780
  ])`,
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>
717
- </MGrid>`,
718
- rules: ['KPI 用 MPageStat,不要每页自定义 stat CSS', 'responsive="screen" 便于窄屏降列'],
719
- rulesEn: ['Use MPageStat for KPIs', 'Use responsive="screen" for narrow layouts'],
720
- avoid: ['不要每个指标手写 font-size/颜色'],
721
- 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'],
722
809
  },
723
810
  {
724
811
  id: 'dashboard-chart-card',
@@ -729,14 +816,14 @@ const submitting = ref(false)`,
729
816
  pageTypes: ['dashboard'],
730
817
  keywords: ['chart', '图表', 'placeholder', 'card', 'trend'],
731
818
  imports: ['MCard', 'MEmpty', 'MSkeleton'],
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
- />
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
+ />
740
827
  </MCard>`,
741
828
  rules: ['图表区适合 MCard,因为需要 card 标题', '加载中用 MSkeleton'],
742
829
  rulesEn: ['Charts belong in MCard when a card title is needed', 'Use MSkeleton while loading'],
@@ -752,18 +839,18 @@ const submitting = ref(false)`,
752
839
  pageTypes: ['dashboard'],
753
840
  keywords: ['recent', '最近', 'table', 'card', 'list'],
754
841
  imports: ['MCard', 'MTable', 'MStatus'],
755
- scriptSetup: `const recentColumns = [
756
- { key: 'id', label: '工单号', width: 96 },
757
- { key: 'title', label: '标题' },
758
- { key: 'status', label: '状态', width: 110 },
759
- ]
842
+ scriptSetup: `const recentColumns = [
843
+ { key: 'id', label: '工单号', width: 96 },
844
+ { key: 'title', label: '标题' },
845
+ { key: 'status', label: '状态', width: 110 },
846
+ ]
760
847
  const recentRows = ref<Record<string, unknown>[]>([])`,
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>
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>
767
854
  </MCard>`,
768
855
  rules: ['明细列表放 MCard 内合理', 'size="small" 适合卡片内表格', '行内业务状态用 MStatus'],
769
856
  rulesEn: ['Detail lists inside MCard are appropriate', 'Use size="small" for in-card tables', 'Use MStatus for row business status'],
@@ -779,10 +866,10 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
779
866
  pageTypes: ['list', 'common'],
780
867
  keywords: ['page content', 'stack', 'layout content', '容器'],
781
868
  imports: ['MPageContent'],
782
- template: `<MLayoutContent>
783
- <MPageContent>
784
- <!-- MPageFilters → MPageToolbar → MTable -->
785
- </MPageContent>
869
+ template: `<MLayoutContent>
870
+ <MPageContent>
871
+ <!-- MPageFilters → MPageToolbar → MTable -->
872
+ </MPageContent>
786
873
  </MLayoutContent>`,
787
874
  rules: ['所有列表区块放在 MPageContent 内', '区块顺序:筛选 → 工具栏 → 表格'],
788
875
  rulesEn: ['Keep all list sections inside MPageContent', 'Order: filters → toolbar → table'],
@@ -798,10 +885,10 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
798
885
  pageTypes: ['form', 'common'],
799
886
  keywords: ['narrow', 'form content', 'max-width'],
800
887
  imports: ['MPageContent'],
801
- template: `<MLayoutContent>
802
- <MPageContent width="narrow">
803
- <!-- MPageHeader → MPageSection form → actions -->
804
- </MPageContent>
888
+ template: `<MLayoutContent>
889
+ <MPageContent width="narrow">
890
+ <!-- MPageHeader → MPageSection form → actions -->
891
+ </MPageContent>
805
892
  </MLayoutContent>`,
806
893
  rules: ['表单页使用 width="narrow"', '不要手写 max-width CSS'],
807
894
  rulesEn: ['Use width="narrow" on form pages', 'Do not hand-write max-width CSS'],
@@ -817,14 +904,14 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
817
904
  pageTypes: ['detail'],
818
905
  keywords: ['detail', '详情', 'edit', 'header'],
819
906
  imports: ['MPageHeader', 'MButton', 'MStatus', 'MSpace'],
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>
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>
828
915
  </MPageHeader>`,
829
916
  rules: ['状态用 MStatus 放在 actions 区', '编辑为 primary,删除用 danger text'],
830
917
  rulesEn: ['Use MStatus in the actions area', 'Edit is primary; delete uses danger text'],
@@ -840,13 +927,13 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
840
927
  pageTypes: ['list', 'form', 'dashboard', 'detail', 'common'],
841
928
  keywords: ['页头', '主按钮', '新建', 'header', 'primary', 'actions', 'page-header'],
842
929
  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>
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>
850
937
  </MPageHeader>`,
851
938
  rules: ['视口内仅一个 filled primary', '次要动作用 secondary 或 text', '批量操作用 MPageToolbar,不要塞进页头'],
852
939
  rulesEn: ['Only one filled primary in the viewport', 'Lesser actions: secondary or text', 'Batch actions belong in MPageToolbar, not the header'],
@@ -870,62 +957,62 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
870
957
  'inline create',
871
958
  ],
872
959
  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
- }
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
+ }
901
988
  }`,
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>
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>
929
1016
  </MDialog>`,
930
1017
  rules: [
931
1018
  '留在列表页,不另开路由',
@@ -957,44 +1044,44 @@ async function onSave() {
957
1044
  pageTypes: ['list', 'detail', 'common'],
958
1045
  keywords: ['抽屉表单', '侧滑编辑', 'drawer form', 'form-in-drawer', 'side panel edit'],
959
1046
  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
- }
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
+ }
972
1059
  }`,
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>
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>
998
1085
  </MDrawer>`,
999
1086
  rules: ['position 常用 right;较长表单可加大 size', '字段 fluid;底栏放 #footer', '成功用 message.success'],
1000
1087
  rulesEn: ['position usually right; raise size for longer forms', 'Fields fluid; actions in #footer', 'Success via message.success'],
@@ -1010,28 +1097,28 @@ async function onSave() {
1010
1097
  pageTypes: ['list', 'detail', 'common'],
1011
1098
  keywords: ['删除', '确认', '二次确认', 'confirm', 'delete', 'ConfirmDialog', '危险'],
1012
1099
  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
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
1026
1113
  }`,
1027
- template: `<MConfirmDialog
1028
- v-model="confirmOpen"
1029
- header="确认删除"
1030
- message="删除后不可恢复,确定继续?"
1031
- accept-label="删除"
1032
- reject-label="取消"
1033
- accept-severity="danger"
1034
- @accept="onAcceptDelete"
1114
+ template: `<MConfirmDialog
1115
+ v-model="confirmOpen"
1116
+ header="确认删除"
1117
+ message="删除后不可恢复,确定继续?"
1118
+ accept-label="删除"
1119
+ reject-label="取消"
1120
+ accept-severity="danger"
1121
+ @accept="onAcceptDelete"
1035
1122
  />`,
1036
1123
  rules: ['用 MConfirmDialog,不要手写 Dialog 确定/取消', 'acceptSeverity="danger"', '成功用 message.success,不要 toast'],
1037
1124
  rulesEn: ['Use MConfirmDialog; do not hand-roll Dialog Yes/No', 'acceptSeverity="danger"', 'Success via message.success, not toast'],
@@ -1047,15 +1134,15 @@ function onAcceptDelete() {
1047
1134
  pageTypes: ['list', 'common'],
1048
1135
  keywords: ['更多', '行菜单', 'dropdown', 'row menu', 'overflow', 'actions menu'],
1049
1136
  imports: ['MDropdown', 'MButton'],
1050
- scriptSetup: `const rowMenu = [
1051
- { label: '编辑', command: () => {/* open edit */} },
1052
- { label: '复制', command: () => {/* duplicate */} },
1053
- { label: '删除', severity: 'danger', command: () => {/* askDelete */} },
1137
+ scriptSetup: `const rowMenu = [
1138
+ { label: '编辑', command: () => {/* open edit */} },
1139
+ { label: '复制', command: () => {/* duplicate */} },
1140
+ { label: '删除', severity: 'danger', command: () => {/* askDelete */} },
1054
1141
  ]`,
1055
- template: `<template #cell-actions>
1056
- <MDropdown :model="rowMenu">
1057
- <MButton severity="secondary" size="small" text icon="more-horizontal" aria-label="更多操作" />
1058
- </MDropdown>
1142
+ template: `<template #cell-actions>
1143
+ <MDropdown :model="rowMenu">
1144
+ <MButton severity="secondary" size="small" text icon="more-horizontal" aria-label="更多操作" />
1145
+ </MDropdown>
1059
1146
  </template>`,
1060
1147
  rules: ['操作很多时再用 Dropdown;2–3 个明确操作用 text 按钮', '危险项走 confirm-delete', '图标按钮必须 aria-label'],
1061
1148
  rulesEn: ['Use Dropdown when many actions; 2–3 explicit actions stay as text buttons', 'Danger → confirm-delete', 'Icon-only buttons need aria-label'],
@@ -1071,45 +1158,45 @@ function onAcceptDelete() {
1071
1158
  pageTypes: ['form', 'common'],
1072
1159
  keywords: ['向导', '分步', '步骤', 'wizard', 'stepper', 'multi-step'],
1073
1160
  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
- }
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
+ }
1095
1182
  }`,
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>
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>
1113
1200
  </MPageSection>`,
1114
1201
  rules: ['用 :steps(不是 :items)', '每步一个主任务;进入下一步前校验当前步', '完成文案与「下一步」区分'],
1115
1202
  rulesEn: ['Use :steps (not :items)', 'One job per step; validate before advancing', 'Finish label differs from Next'],
@@ -1125,50 +1212,50 @@ async function finish() {
1125
1212
  pageTypes: ['common'],
1126
1213
  keywords: ['登录', '认证', '分栏', 'login', 'auth', 'split', 'brand'],
1127
1214
  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
- }
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
+ }
1142
1229
  }`,
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>
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>
1172
1259
  </MConfigProvider>`,
1173
1260
  rules: [
1174
1261
  '品牌区用 --m-* / color-mix,默认扁平勿叠 aurora',
@@ -1210,29 +1297,29 @@ async function onSubmit() {
1210
1297
  'MConfigProvider',
1211
1298
  'zhCN',
1212
1299
  ],
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>
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>
1236
1323
  </MConfigProvider>`,
1237
1324
  rules: [
1238
1325
  '根布局用 fillViewport',
@@ -1265,19 +1352,19 @@ async function onSubmit() {
1265
1352
  'log',
1266
1353
  ],
1267
1354
  imports: ['MScrollbar', 'MCard'],
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>
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>
1276
1363
  </MCard>`,
1277
- scriptSetup: `const logLines = [
1278
- { id: '1', text: '用户 admin 登录成功' },
1279
- { id: '2', text: '导出任务已完成' },
1280
- { id: '3', text: '配置已保存' },
1364
+ scriptSetup: `const logLines = [
1365
+ { id: '1', text: '用户 admin 登录成功' },
1366
+ { id: '2', text: '导出任务已完成' },
1367
+ { id: '3', text: '配置已保存' },
1281
1368
  ]`,
1282
1369
  rules: [
1283
1370
  '整页主滚动推荐 MLayout fillViewport',