@morya-ui/mcp 0.4.4 → 0.5.0

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>查询</MButton>
36
- <MButton type="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>查询</MButton>
36
+ <MButton type="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>查询</MButton>
109
- <MButton type="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>查询</MButton>
109
+ <MButton type="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>查询</MButton>
304
- <MButton type="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>查询</MButton>
304
+ <MButton type="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 size="small">查询</MButton>
394
- <MButton size="small" type="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 size="small">查询</MButton>
394
+ <MButton size="small" type="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 type="primary">新建用户</MButton>
428
- </template>
425
+ template: `<MPageHeader title="用户管理" description="维护账号、角色与权限。">
426
+ <template #actions>
427
+ <MButton type="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 type="text">导出</MButton>
448
- <MButton danger type="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 type="text">导出</MButton>
448
+ <MButton danger type="text">删除</MButton>
449
+ </template>
450
450
  </MPageToolbar>`,
451
451
  rules: ['左侧用 default 插槽展示已选数量', '批量操作用 type="text" / variant="outlined",danger 仅 destructive', '放在 FilterChips 之后、表格之前'],
452
452
  rulesEn: ['Use the default slot for selection count', 'Batch actions use type="text" / variant="outlined"; danger only for destructive ops', 'Place after FilterChips and before the table'],
@@ -475,54 +475,54 @@ function resetFilters() {
475
475
  '对齐',
476
476
  ],
477
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>[]>([])
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>[]>([])
486
486
  const loading = ref(false)`,
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
- :type="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 size="small" type="text">编辑</MButton>
512
- <MButton danger size="small" type="text">删除</MButton>
513
- </MSpace>
514
- </template>
515
- <template #empty>
516
- <MEmpty
517
- title="还没有成员"
518
- description="新建第一条后,这里会列出姓名、状态与更新时间。"
519
- icon="inbox"
520
- >
521
- <template #extra>
522
- <MButton type="primary">新建成员</MButton>
523
- </template>
524
- </MEmpty>
525
- </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
+ :type="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 size="small" type="text">编辑</MButton>
512
+ <MButton danger size="small" type="text">删除</MButton>
513
+ </MSpace>
514
+ </template>
515
+ <template #empty>
516
+ <MEmpty
517
+ title="还没有成员"
518
+ description="新建第一条后,这里会列出姓名、状态与更新时间。"
519
+ icon="inbox"
520
+ >
521
+ <template #extra>
522
+ <MButton type="primary">新建成员</MButton>
523
+ </template>
524
+ </MEmpty>
525
+ </template>
526
526
  </MTable>`,
527
527
  rules: [
528
528
  '先判断是否适合 fill:全视口后台主列表 + 表格是主任务 → MPageContent fill + MTable fill',
@@ -560,11 +560,11 @@ const loading = ref(false)`,
560
560
  pageTypes: ['list'],
561
561
  keywords: ['行操作', '编辑', '删除', 'actions', 'row', 'cell'],
562
562
  imports: ['MSpace', 'MButton'],
563
- template: `<template #cell-actions>
564
- <MSpace>
565
- <MButton size="small" type="text">编辑</MButton>
566
- <MButton danger size="small" type="text">删除</MButton>
567
- </MSpace>
563
+ template: `<template #cell-actions>
564
+ <MSpace>
565
+ <MButton size="small" type="text">编辑</MButton>
566
+ <MButton danger size="small" type="text">删除</MButton>
567
+ </MSpace>
568
568
  </template>`,
569
569
  rules: ['行内操作用 size="small" + type="text" / variant="outlined",避免一排实心按钮', '危险操作使用 danger 并配合确认弹窗'],
570
570
  rulesEn: ['Use size="small" with type="text" / variant="outlined" row actions', 'Use danger with confirmation for destructive actions'],
@@ -580,11 +580,11 @@ const loading = ref(false)`,
580
580
  pageTypes: ['list'],
581
581
  keywords: ['分类', '标签', '可关闭', 'chip', 'category', 'tag-only'],
582
582
  imports: ['MTag'],
583
- template: `<template #cell-category="{ value }">
584
- <MTag
585
- :value="String(value ?? '')"
586
- type="info"
587
- />
583
+ template: `<template #cell-category="{ value }">
584
+ <MTag
585
+ :value="String(value ?? '')"
586
+ type="info"
587
+ />
588
588
  </template>`,
589
589
  rules: ['分类/强调态用 MTag;行内业务状态用 MStatus(list-status-dot)', '不要用 Button 颜色表达状态'],
590
590
  rulesEn: ['Use MTag for categories; use MStatus for business status (list-status-dot)', 'Do not use Button colors for status'],
@@ -600,11 +600,11 @@ const loading = ref(false)`,
600
600
  pageTypes: ['list'],
601
601
  keywords: ['状态', 'status', 'dot', 'cell-status', '在线', '启用', '停用', '业务状态'],
602
602
  imports: ['MStatus'],
603
- template: `<template #cell-status="{ value }">
604
- <MStatus
605
- :label="value === 'active' || value === 'online' ? '启用' : '停用'"
606
- :type="value === 'active' || value === 'online' ? 'success' : 'secondary'"
607
- />
603
+ template: `<template #cell-status="{ value }">
604
+ <MStatus
605
+ :label="value === 'active' || value === 'online' ? '启用' : '停用'"
606
+ :type="value === 'active' || value === 'online' ? 'success' : 'secondary'"
607
+ />
608
608
  </template>`,
609
609
  rules: ['行内轻量业务状态优先 MStatus', '不要用 Button 颜色表达状态'],
610
610
  rulesEn: ['Prefer MStatus for lightweight inline business status', 'Do not use Button colors for status'],
@@ -620,15 +620,15 @@ const loading = ref(false)`,
620
620
  pageTypes: ['list', 'common'],
621
621
  keywords: ['空状态', '无数据', 'empty', 'zero state', 'no data', 'illustration'],
622
622
  imports: ['MEmpty', 'MButton'],
623
- template: `<MEmpty
624
- title="还没有成员"
625
- description="邀请或新建第一位成员后,这里会显示状态与最近活动。"
626
- illustration="no-content"
627
- >
628
- <template #extra>
629
- <MButton type="primary">新建成员</MButton>
630
- <MButton type="text">导入名单</MButton>
631
- </template>
623
+ template: `<MEmpty
624
+ title="还没有成员"
625
+ description="邀请或新建第一位成员后,这里会显示状态与最近活动。"
626
+ illustration="no-content"
627
+ >
628
+ <template #extra>
629
+ <MButton type="primary">新建成员</MButton>
630
+ <MButton type="text">导入名单</MButton>
631
+ </template>
632
632
  </MEmpty>`,
633
633
  rules: [
634
634
  '正常无数据用 MEmpty,不要用错误色',
@@ -654,11 +654,11 @@ const loading = ref(false)`,
654
654
  pageTypes: ['common'],
655
655
  keywords: ['结果', '成功', '失败', '404', '403', 'result', 'success', 'error'],
656
656
  imports: ['MResult', 'MButton'],
657
- template: `<MResult status="success" description="订单已创建,可在列表中查看详情。">
658
- <template #extra>
659
- <MButton label="查看订单" />
660
- <MButton label="返回列表" type="text" />
661
- </template>
657
+ template: `<MResult status="success" description="订单已创建,可在列表中查看详情。">
658
+ <template #extra>
659
+ <MButton label="查看订单" />
660
+ <MButton label="返回列表" type="text" />
661
+ </template>
662
662
  </MResult>`,
663
663
  rules: ['流程终点用 MResult', '提供明确下一步操作'],
664
664
  rulesEn: ['Use MResult for terminal outcomes', 'Provide clear next-step actions'],
@@ -689,32 +689,32 @@ const loading = ref(false)`,
689
689
  pageTypes: ['form'],
690
690
  keywords: ['表单', 'form', 'fields', 'formitem', 'label-width', 'errorMessage'],
691
691
  imports: ['MPageSection', 'MForm', 'MFormItem', 'MInput', 'MSelect'],
692
- scriptSetup: `const model = reactive({ name: '', role: undefined as string | undefined })
693
- const submitting = ref(false)
694
- const roleOptions = [
695
- { label: '管理员', value: 'admin' },
696
- { label: '成员', value: 'member' },
692
+ scriptSetup: `const model = reactive({ name: '', role: undefined as string | undefined })
693
+ const submitting = ref(false)
694
+ const roleOptions = [
695
+ { label: '管理员', value: 'admin' },
696
+ { label: '成员', value: 'member' },
697
697
  ]`,
698
- template: `<MPageSection variant="form">
699
- <MForm
700
- label-position="left"
701
- label-width="6rem"
702
- @submit.prevent="onSubmit"
703
- >
704
- <MFormItem label="姓名" name="name" required>
705
- <MInput v-model="model.name" placeholder="与工牌一致的姓名" fluid />
706
- </MFormItem>
707
- <MFormItem label="角色" name="role" required>
708
- <MSelect
709
- v-model="model.role"
710
- :options="roleOptions"
711
- placeholder="选择角色"
712
- clearable
713
- fluid
714
- />
715
- </MFormItem>
716
- <!-- 更多字段;常驻错误:error-message="请填写必填项" -->
717
- </MForm>
698
+ template: `<MPageSection variant="form">
699
+ <MForm
700
+ label-position="left"
701
+ label-width="6rem"
702
+ @submit.prevent="onSubmit"
703
+ >
704
+ <MFormItem label="姓名" name="name" required>
705
+ <MInput v-model="model.name" placeholder="与工牌一致的姓名" fluid />
706
+ </MFormItem>
707
+ <MFormItem label="角色" name="role" required>
708
+ <MSelect
709
+ v-model="model.role"
710
+ :options="roleOptions"
711
+ placeholder="选择角色"
712
+ clearable
713
+ fluid
714
+ />
715
+ </MFormItem>
716
+ <!-- 更多字段;常驻错误:error-message="请填写必填项" -->
717
+ </MForm>
718
718
  </MPageSection>`,
719
719
  rules: [
720
720
  '字段用 MFormItem + fluid 控件',
@@ -740,11 +740,11 @@ const roleOptions = [
740
740
  pageTypes: ['form'],
741
741
  keywords: ['保存', '取消', 'submit', 'actions', 'footer'],
742
742
  imports: ['MPageSection', 'MSpace', 'MButton'],
743
- template: `<MPageSection variant="actions">
744
- <MSpace>
745
- <MButton html-type="submit" type="primary" :loading="submitting">保存</MButton>
746
- <MButton>取消</MButton>
747
- </MSpace>
743
+ template: `<MPageSection variant="actions">
744
+ <MSpace>
745
+ <MButton html-type="submit" type="primary" :loading="submitting">保存</MButton>
746
+ <MButton>取消</MButton>
747
+ </MSpace>
748
748
  </MPageSection>`,
749
749
  rules: ['提交按钮在前,取消 secondary', '保存中传 loading 防重复提交'],
750
750
  rulesEn: ['Submit first, cancel secondary', 'Pass loading while saving to prevent duplicate submits'],
@@ -760,39 +760,39 @@ const roleOptions = [
760
760
  pageTypes: ['dashboard'],
761
761
  keywords: ['kpi', '指标', 'stat', 'grid', 'metrics', 'trend', 'compact'],
762
762
  imports: ['MGrid', 'MGridItem', 'MPageStat'],
763
- scriptSetup: `const metrics = ref([
764
- {
765
- label: '在册成员',
766
- value: '12,480',
767
- trend: '+8.2%',
768
- trendDirection: 'up' as const,
769
- trendType: 'success' as const,
770
- trendLabel: '较上月',
771
- icon: 'users',
772
- },
773
- {
774
- label: '今日活跃',
775
- value: '1,926',
776
- trend: '-3.1%',
777
- trendDirection: 'down' as const,
778
- trendType: 'warning' as const,
779
- trendLabel: '较昨日',
780
- icon: 'activity',
781
- },
763
+ scriptSetup: `const metrics = ref([
764
+ {
765
+ label: '在册成员',
766
+ value: '12,480',
767
+ trend: '+8.2%',
768
+ trendDirection: 'up' as const,
769
+ trendType: 'success' as const,
770
+ trendLabel: '较上月',
771
+ icon: 'users',
772
+ },
773
+ {
774
+ label: '今日活跃',
775
+ value: '1,926',
776
+ trend: '-3.1%',
777
+ trendDirection: 'down' as const,
778
+ trendType: 'warning' as const,
779
+ trendLabel: '较昨日',
780
+ icon: 'activity',
781
+ },
782
782
  ])`,
783
- template: `<MGrid :cols="4" :x-gap="16" :y-gap="16" responsive="screen">
784
- <MGridItem v-for="metric in metrics" :key="metric.label" :span="1">
785
- <MPageStat
786
- :label="metric.label"
787
- :value="metric.value"
788
- :trend="metric.trend"
789
- :trend-direction="metric.trendDirection"
790
- :trend-type="metric.trendType"
791
- :trend-label="metric.trendLabel"
792
- :icon="metric.icon"
793
- />
794
- </MGridItem>
795
- </MGrid>
783
+ template: `<MGrid :cols="4" :x-gap="16" :y-gap="16" responsive="screen">
784
+ <MGridItem v-for="metric in metrics" :key="metric.label" :span="1">
785
+ <MPageStat
786
+ :label="metric.label"
787
+ :value="metric.value"
788
+ :trend="metric.trend"
789
+ :trend-direction="metric.trendDirection"
790
+ :trend-type="metric.trendType"
791
+ :trend-label="metric.trendLabel"
792
+ :icon="metric.icon"
793
+ />
794
+ </MGridItem>
795
+ </MGrid>
796
796
  <!-- 高密条带示例:density="compact" layout="plain" orientation="inline" -->`,
797
797
  rules: [
798
798
  'KPI 用 MPageStat,不要每页自定义 stat CSS',
@@ -818,15 +818,15 @@ const roleOptions = [
818
818
  pageTypes: ['dashboard'],
819
819
  keywords: ['chart', '图表', 'placeholder', 'card', 'trend'],
820
820
  imports: ['MCard', 'MEmpty', 'MSkeleton'],
821
- template: `<MCard title="趋势概览" shadow="always">
822
- <MSkeleton v-if="loading" height="8rem" />
823
- <MEmpty
824
- v-else
825
- aria-label="图表占位"
826
- title="还没有趋势数据"
827
- description="接入图表后将展示近 7 日活跃与转化。"
828
- illustration="activity-low"
829
- />
821
+ template: `<MCard title="趋势概览" shadow="always">
822
+ <MSkeleton v-if="loading" height="8rem" />
823
+ <MEmpty
824
+ v-else
825
+ aria-label="图表占位"
826
+ title="还没有趋势数据"
827
+ description="接入图表后将展示近 7 日活跃与转化。"
828
+ illustration="activity-low"
829
+ />
830
830
  </MCard>`,
831
831
  rules: ['图表区适合 MCard,因为需要 card 标题', '加载中用 MSkeleton', '占位空态用 illustration,避免裸 icon'],
832
832
  rulesEn: [
@@ -846,18 +846,18 @@ const roleOptions = [
846
846
  pageTypes: ['dashboard'],
847
847
  keywords: ['recent', '最近', 'table', 'card', 'list'],
848
848
  imports: ['MCard', 'MTable', 'MStatus'],
849
- scriptSetup: `const recentColumns = [
850
- { key: 'id', label: '工单号', width: 96 },
851
- { key: 'title', label: '标题' },
852
- { key: 'status', label: '状态', width: 110 },
853
- ]
849
+ scriptSetup: `const recentColumns = [
850
+ { key: 'id', label: '工单号', width: 96 },
851
+ { key: 'title', label: '标题' },
852
+ { key: 'status', label: '状态', width: 110 },
853
+ ]
854
854
  const recentRows = ref<Record<string, unknown>[]>([])`,
855
- template: `<MCard title="最近工单">
856
- <MTable :columns="recentColumns" :rows="recentRows" size="small" :paginator="false" bordered row-key="id">
857
- <template #cell-status="{ value }">
858
- <MStatus :label="String(value ?? '')" default / type="text" />
859
- </template>
860
- </MTable>
855
+ template: `<MCard title="最近工单">
856
+ <MTable :columns="recentColumns" :rows="recentRows" size="small" :paginator="false" bordered row-key="id">
857
+ <template #cell-status="{ value }">
858
+ <MStatus :label="String(value ?? '')" default / type="text" />
859
+ </template>
860
+ </MTable>
861
861
  </MCard>`,
862
862
  rules: ['明细列表放 MCard 内合理', 'size="small" 适合卡片内表格', '行内业务状态用 MStatus'],
863
863
  rulesEn: ['Detail lists inside MCard are appropriate', 'Use size="small" for in-card tables', 'Use MStatus for row business status'],
@@ -873,10 +873,10 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
873
873
  pageTypes: ['list', 'common'],
874
874
  keywords: ['page content', 'stack', 'layout content', '容器'],
875
875
  imports: ['MPageContent'],
876
- template: `<MLayoutContent>
877
- <MPageContent>
878
- <!-- MPageFilters → MPageToolbar → MTable -->
879
- </MPageContent>
876
+ template: `<MLayoutContent>
877
+ <MPageContent>
878
+ <!-- MPageFilters → MPageToolbar → MTable -->
879
+ </MPageContent>
880
880
  </MLayoutContent>`,
881
881
  rules: ['所有列表区块放在 MPageContent 内', '区块顺序:筛选 → 工具栏 → 表格'],
882
882
  rulesEn: ['Keep all list sections inside MPageContent', 'Order: filters → toolbar → table'],
@@ -892,10 +892,10 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
892
892
  pageTypes: ['form', 'common'],
893
893
  keywords: ['narrow', 'form content', 'max-width'],
894
894
  imports: ['MPageContent'],
895
- template: `<MLayoutContent>
896
- <MPageContent width="narrow">
897
- <!-- MPageHeader → MPageSection form → actions -->
898
- </MPageContent>
895
+ template: `<MLayoutContent>
896
+ <MPageContent width="narrow">
897
+ <!-- MPageHeader → MPageSection form → actions -->
898
+ </MPageContent>
899
899
  </MLayoutContent>`,
900
900
  rules: ['表单页使用 width="narrow"', '不要手写 max-width CSS'],
901
901
  rulesEn: ['Use width="narrow" on form pages', 'Do not hand-write max-width CSS'],
@@ -911,14 +911,14 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
911
911
  pageTypes: ['detail'],
912
912
  keywords: ['detail', '详情', 'edit', 'header'],
913
913
  imports: ['MPageHeader', 'MButton', 'MStatus', 'MSpace'],
914
- template: `<MPageHeader title="林晓" description="查看账号摘要、属性与近期活动。">
915
- <template #actions>
916
- <MSpace>
917
- <MStatus label="启用" type="success" />
918
- <MButton type="primary">编辑</MButton>
919
- <MButton danger type="text">删除</MButton>
920
- </MSpace>
921
- </template>
914
+ template: `<MPageHeader title="林晓" description="查看账号摘要、属性与近期活动。">
915
+ <template #actions>
916
+ <MSpace>
917
+ <MStatus label="启用" type="success" />
918
+ <MButton type="primary">编辑</MButton>
919
+ <MButton danger type="text">删除</MButton>
920
+ </MSpace>
921
+ </template>
922
922
  </MPageHeader>`,
923
923
  rules: ['状态用 MStatus 放在 actions 区', '编辑为 primary,删除用 danger text'],
924
924
  rulesEn: ['Use MStatus in the actions area', 'Edit is primary; delete uses danger text'],
@@ -934,15 +934,15 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
934
934
  pageTypes: ['detail'],
935
935
  keywords: ['descriptions', '属性', '详情字段', 'detail props', 'dl'],
936
936
  imports: ['MDescriptions', 'MDescriptionsItem', 'MStatus'],
937
- template: `<MDescriptions title="基本信息" bordered :column="2">
938
- <MDescriptionsItem label="姓名">林晓</MDescriptionsItem>
939
- <MDescriptionsItem label="邮箱">linxiao@example.com</MDescriptionsItem>
940
- <MDescriptionsItem label="角色">成员</MDescriptionsItem>
941
- <MDescriptionsItem label="部门">产品设计</MDescriptionsItem>
942
- <MDescriptionsItem label="入职日期">2025-03-12</MDescriptionsItem>
943
- <MDescriptionsItem label="状态">
944
- <MStatus label="启用" type="success" />
945
- </MDescriptionsItem>
937
+ template: `<MDescriptions title="基本信息" bordered :column="2">
938
+ <MDescriptionsItem label="姓名">林晓</MDescriptionsItem>
939
+ <MDescriptionsItem label="邮箱">linxiao@example.com</MDescriptionsItem>
940
+ <MDescriptionsItem label="角色">成员</MDescriptionsItem>
941
+ <MDescriptionsItem label="部门">产品设计</MDescriptionsItem>
942
+ <MDescriptionsItem label="入职日期">2025-03-12</MDescriptionsItem>
943
+ <MDescriptionsItem label="状态">
944
+ <MStatus label="启用" type="success" />
945
+ </MDescriptionsItem>
946
946
  </MDescriptions>`,
947
947
  rules: [
948
948
  '属性网格用 MDescriptions,不要手写 <dl> / 嵌套表格',
@@ -966,13 +966,13 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
966
966
  pageTypes: ['list', 'form', 'dashboard', 'detail', 'common'],
967
967
  keywords: ['页头', '主按钮', '新建', 'header', 'primary', 'actions', 'page-header'],
968
968
  imports: ['MPageHeader', 'MButton', 'MSpace'],
969
- template: `<MPageHeader title="用户管理" description="管理成员与角色。">
970
- <template #actions>
971
- <MSpace>
972
- <MButton type="text">导入</MButton>
973
- <MButton type="primary">新建用户</MButton>
974
- </MSpace>
975
- </template>
969
+ template: `<MPageHeader title="用户管理" description="管理成员与角色。">
970
+ <template #actions>
971
+ <MSpace>
972
+ <MButton type="text">导入</MButton>
973
+ <MButton type="primary">新建用户</MButton>
974
+ </MSpace>
975
+ </template>
976
976
  </MPageHeader>`,
977
977
  rules: ['视口内仅一个 filled primary', '次要动作用 secondary 或 text', '批量操作用 MPageToolbar,不要塞进页头'],
978
978
  rulesEn: ['Only one filled primary in the viewport', 'Lesser actions: secondary or text', 'Batch actions belong in MPageToolbar, not the header'],
@@ -996,62 +996,62 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
996
996
  'inline create',
997
997
  ],
998
998
  imports: ['MDialog', 'MForm', 'MFormItem', 'MInput', 'MSelect', 'MButton', 'MSpace', 'message'],
999
- scriptSetup: `const dialogOpen = ref(false)
1000
- const submitting = ref(false)
1001
- const model = reactive({
1002
- name: '',
1003
- email: '',
1004
- role: undefined as string | undefined,
1005
- })
1006
- const roleOptions = [
1007
- { label: '管理员', value: 'admin' },
1008
- { label: '成员', value: 'member' },
1009
- ]
1010
-
1011
- function resetModel() {
1012
- model.name = ''
1013
- model.email = ''
1014
- model.role = undefined
1015
- }
1016
-
1017
- async function onSave() {
1018
- submitting.value = true
1019
- try {
1020
- // await api.save(model)
1021
- message.success('已保存')
1022
- dialogOpen.value = false
1023
- resetModel()
1024
- } finally {
1025
- submitting.value = false
1026
- }
999
+ scriptSetup: `const dialogOpen = ref(false)
1000
+ const submitting = ref(false)
1001
+ const model = reactive({
1002
+ name: '',
1003
+ email: '',
1004
+ role: undefined as string | undefined,
1005
+ })
1006
+ const roleOptions = [
1007
+ { label: '管理员', value: 'admin' },
1008
+ { label: '成员', value: 'member' },
1009
+ ]
1010
+
1011
+ function resetModel() {
1012
+ model.name = ''
1013
+ model.email = ''
1014
+ model.role = undefined
1015
+ }
1016
+
1017
+ async function onSave() {
1018
+ submitting.value = true
1019
+ try {
1020
+ // await api.save(model)
1021
+ message.success('已保存')
1022
+ dialogOpen.value = false
1023
+ resetModel()
1024
+ } finally {
1025
+ submitting.value = false
1026
+ }
1027
1027
  }`,
1028
- template: `<MDialog
1029
- v-model="dialogOpen"
1030
- header="新建用户"
1031
- width="32rem"
1032
- @close="resetModel"
1033
- >
1034
- <MForm @submit.prevent="onSave">
1035
- <MFormItem label="姓名" name="name" required>
1036
- <MInput v-model="model.name" placeholder="请输入姓名" fluid />
1037
- </MFormItem>
1038
- <MFormItem label="邮箱" name="email" required>
1039
- <MInput v-model="model.email" type="email" placeholder="name@example.com" fluid />
1040
- </MFormItem>
1041
- <MFormItem label="角色" name="role" required>
1042
- <MSelect v-model="model.role" :options="roleOptions" placeholder="请选择角色" fluid />
1043
- </MFormItem>
1044
- </MForm>
1045
- <template #footer>
1046
- <MSpace style="justify-content: flex-end; width: 100%">
1047
- <MButton type="text" :disabled="submitting" @click="dialogOpen = false">
1048
- 取消
1049
- </MButton>
1050
- <MButton type="primary" :loading="submitting" @click="onSave">
1051
- 保存
1052
- </MButton>
1053
- </MSpace>
1054
- </template>
1028
+ template: `<MDialog
1029
+ v-model="dialogOpen"
1030
+ header="新建用户"
1031
+ width="32rem"
1032
+ @close="resetModel"
1033
+ >
1034
+ <MForm @submit.prevent="onSave">
1035
+ <MFormItem label="姓名" name="name" required>
1036
+ <MInput v-model="model.name" placeholder="请输入姓名" fluid />
1037
+ </MFormItem>
1038
+ <MFormItem label="邮箱" name="email" required>
1039
+ <MInput v-model="model.email" type="email" placeholder="name@example.com" fluid />
1040
+ </MFormItem>
1041
+ <MFormItem label="角色" name="role" required>
1042
+ <MSelect v-model="model.role" :options="roleOptions" placeholder="请选择角色" fluid />
1043
+ </MFormItem>
1044
+ </MForm>
1045
+ <template #footer>
1046
+ <MSpace style="justify-content: flex-end; width: 100%">
1047
+ <MButton type="text" :disabled="submitting" @click="dialogOpen = false">
1048
+ 取消
1049
+ </MButton>
1050
+ <MButton type="primary" :loading="submitting" @click="onSave">
1051
+ 保存
1052
+ </MButton>
1053
+ </MSpace>
1054
+ </template>
1055
1055
  </MDialog>`,
1056
1056
  rules: [
1057
1057
  '留在列表页,不另开路由',
@@ -1083,44 +1083,44 @@ async function onSave() {
1083
1083
  pageTypes: ['list', 'detail', 'common'],
1084
1084
  keywords: ['抽屉表单', '侧滑编辑', 'drawer form', 'form-in-drawer', 'side panel edit'],
1085
1085
  imports: ['MDrawer', 'MForm', 'MFormItem', 'MInput', 'MButton', 'MSpace', 'message'],
1086
- scriptSetup: `const drawerOpen = ref(false)
1087
- const submitting = ref(false)
1088
- const model = reactive({ name: '', note: '' })
1089
-
1090
- async function onSave() {
1091
- submitting.value = true
1092
- try {
1093
- message.success('已保存')
1094
- drawerOpen.value = false
1095
- } finally {
1096
- submitting.value = false
1097
- }
1086
+ scriptSetup: `const drawerOpen = ref(false)
1087
+ const submitting = ref(false)
1088
+ const model = reactive({ name: '', note: '' })
1089
+
1090
+ async function onSave() {
1091
+ submitting.value = true
1092
+ try {
1093
+ message.success('已保存')
1094
+ drawerOpen.value = false
1095
+ } finally {
1096
+ submitting.value = false
1097
+ }
1098
1098
  }`,
1099
- template: `<MDrawer
1100
- v-model="drawerOpen"
1101
- header="编辑详情"
1102
- position="right"
1103
- size="28rem"
1104
- @close="/* reset draft */"
1105
- >
1106
- <MForm @submit.prevent="onSave">
1107
- <MFormItem label="名称" name="name" required>
1108
- <MInput v-model="model.name" fluid />
1109
- </MFormItem>
1110
- <MFormItem label="备注" name="note">
1111
- <MInput v-model="model.note" fluid />
1112
- </MFormItem>
1113
- </MForm>
1114
- <template #footer>
1115
- <MSpace style="justify-content: flex-end; width: 100%">
1116
- <MButton type="text" :disabled="submitting" @click="drawerOpen = false">
1117
- 取消
1118
- </MButton>
1119
- <MButton type="primary" :loading="submitting" @click="onSave">
1120
- 保存
1121
- </MButton>
1122
- </MSpace>
1123
- </template>
1099
+ template: `<MDrawer
1100
+ v-model="drawerOpen"
1101
+ header="编辑详情"
1102
+ position="right"
1103
+ size="28rem"
1104
+ @close="/* reset draft */"
1105
+ >
1106
+ <MForm @submit.prevent="onSave">
1107
+ <MFormItem label="名称" name="name" required>
1108
+ <MInput v-model="model.name" fluid />
1109
+ </MFormItem>
1110
+ <MFormItem label="备注" name="note">
1111
+ <MInput v-model="model.note" fluid />
1112
+ </MFormItem>
1113
+ </MForm>
1114
+ <template #footer>
1115
+ <MSpace style="justify-content: flex-end; width: 100%">
1116
+ <MButton type="text" :disabled="submitting" @click="drawerOpen = false">
1117
+ 取消
1118
+ </MButton>
1119
+ <MButton type="primary" :loading="submitting" @click="onSave">
1120
+ 保存
1121
+ </MButton>
1122
+ </MSpace>
1123
+ </template>
1124
1124
  </MDrawer>`,
1125
1125
  rules: ['position 常用 right;较长表单可加大 size', '字段 fluid;底栏放 #footer', '成功用 message.success'],
1126
1126
  rulesEn: ['position usually right; raise size for longer forms', 'Fields fluid; actions in #footer', 'Success via message.success'],
@@ -1136,28 +1136,28 @@ async function onSave() {
1136
1136
  pageTypes: ['list', 'detail', 'common'],
1137
1137
  keywords: ['删除', '确认', '二次确认', 'confirm', 'delete', 'ConfirmDialog', '危险'],
1138
1138
  imports: ['MConfirmDialog', 'message'],
1139
- scriptSetup: `const confirmOpen = ref(false)
1140
- const pendingId = ref<string | null>(null)
1141
-
1142
- function askDelete(id: string) {
1143
- pendingId.value = id
1144
- confirmOpen.value = true
1145
- }
1146
-
1147
- function onAcceptDelete() {
1148
- // await api.remove(pendingId.value)
1149
- message.success('已删除')
1150
- pendingId.value = null
1151
- confirmOpen.value = false
1139
+ scriptSetup: `const confirmOpen = ref(false)
1140
+ const pendingId = ref<string | null>(null)
1141
+
1142
+ function askDelete(id: string) {
1143
+ pendingId.value = id
1144
+ confirmOpen.value = true
1145
+ }
1146
+
1147
+ function onAcceptDelete() {
1148
+ // await api.remove(pendingId.value)
1149
+ message.success('已删除')
1150
+ pendingId.value = null
1151
+ confirmOpen.value = false
1152
1152
  }`,
1153
- template: `<MConfirmDialog
1154
- v-model="confirmOpen"
1155
- header="确认删除"
1156
- message="删除后不可恢复,确定继续?"
1157
- accept-label="删除"
1158
- reject-label="取消"
1159
- accept-danger
1160
- @accept="onAcceptDelete"
1153
+ template: `<MConfirmDialog
1154
+ v-model="confirmOpen"
1155
+ header="确认删除"
1156
+ message="删除后不可恢复,确定继续?"
1157
+ accept-label="删除"
1158
+ reject-label="取消"
1159
+ accept-danger
1160
+ @accept="onAcceptDelete"
1161
1161
  />`,
1162
1162
  rules: ['用 MConfirmDialog,不要手写 Dialog 确定/取消', 'acceptColor="danger"', '成功用 message.success,不要 toast'],
1163
1163
  rulesEn: ['Use MConfirmDialog; do not hand-roll Dialog Yes/No', 'acceptColor="danger"', 'Success via message.success, not toast'],
@@ -1173,15 +1173,15 @@ function onAcceptDelete() {
1173
1173
  pageTypes: ['list', 'common'],
1174
1174
  keywords: ['更多', '行菜单', 'dropdown', 'row menu', 'overflow', 'actions menu'],
1175
1175
  imports: ['MDropdown', 'MButton'],
1176
- scriptSetup: `const rowMenu = [
1177
- { label: '编辑', command: () => {/* open edit */} },
1178
- { label: '复制', command: () => {/* duplicate */} },
1179
- { label: '删除', command: () => {/* askDelete */} },
1176
+ scriptSetup: `const rowMenu = [
1177
+ { label: '编辑', command: () => {/* open edit */} },
1178
+ { label: '复制', command: () => {/* duplicate */} },
1179
+ { label: '删除', command: () => {/* askDelete */} },
1180
1180
  ]`,
1181
- template: `<template #cell-actions>
1182
- <MDropdown :model="rowMenu">
1183
- <MButton size="small" type="text" icon="more-horizontal" aria-label="更多操作" />
1184
- </MDropdown>
1181
+ template: `<template #cell-actions>
1182
+ <MDropdown :model="rowMenu">
1183
+ <MButton size="small" type="text" icon="more-horizontal" aria-label="更多操作" />
1184
+ </MDropdown>
1185
1185
  </template>`,
1186
1186
  rules: ['操作很多时再用 Dropdown;2–3 个明确操作用 text 按钮', '危险项走 confirm-delete', '图标按钮必须 aria-label'],
1187
1187
  rulesEn: ['Use Dropdown when many actions; 2–3 explicit actions stay as text buttons', 'Danger → confirm-delete', 'Icon-only buttons need aria-label'],
@@ -1197,45 +1197,45 @@ function onAcceptDelete() {
1197
1197
  pageTypes: ['form', 'common'],
1198
1198
  keywords: ['向导', '分步', '步骤', 'wizard', 'stepper', 'multi-step'],
1199
1199
  imports: ['MStepper', 'MForm', 'MFormItem', 'MInput', 'MButton', 'MSpace', 'MPageSection', 'message'],
1200
- scriptSetup: `const activeStep = ref(0)
1201
- const submitting = ref(false)
1202
- const steps = [
1203
- { label: '基本信息', description: '名称与说明' },
1204
- { label: '确认', description: '核对后创建' },
1205
- ]
1206
- const model = reactive({ name: '' })
1207
-
1208
- function back() {
1209
- if (activeStep.value > 0) activeStep.value -= 1
1210
- }
1211
- function next() {
1212
- if (activeStep.value < steps.length - 1) activeStep.value += 1
1213
- }
1214
- async function finish() {
1215
- submitting.value = true
1216
- try {
1217
- message.success('已创建')
1218
- } finally {
1219
- submitting.value = false
1220
- }
1200
+ scriptSetup: `const activeStep = ref(0)
1201
+ const submitting = ref(false)
1202
+ const steps = [
1203
+ { label: '基本信息', description: '名称与说明' },
1204
+ { label: '确认', description: '核对后创建' },
1205
+ ]
1206
+ const model = reactive({ name: '' })
1207
+
1208
+ function back() {
1209
+ if (activeStep.value > 0) activeStep.value -= 1
1210
+ }
1211
+ function next() {
1212
+ if (activeStep.value < steps.length - 1) activeStep.value += 1
1213
+ }
1214
+ async function finish() {
1215
+ submitting.value = true
1216
+ try {
1217
+ message.success('已创建')
1218
+ } finally {
1219
+ submitting.value = false
1220
+ }
1221
1221
  }`,
1222
- template: `<MStepper v-model="activeStep" :steps="steps" linear />
1223
- <MPageSection variant="form">
1224
- <MForm v-if="activeStep === 0" @submit.prevent="next">
1225
- <MFormItem label="名称" name="name" required>
1226
- <MInput v-model="model.name" fluid />
1227
- </MFormItem>
1228
- </MForm>
1229
- <div v-else>
1230
- <p>确认创建「{{ model.name }}」?</p>
1231
- </div>
1232
- </MPageSection>
1233
- <MPageSection variant="actions">
1234
- <MSpace>
1235
- <MButton type="text" :disabled="activeStep === 0" @click="back">上一步</MButton>
1236
- <MButton v-if="activeStep < steps.length - 1" type="primary" @click="next">下一步</MButton>
1237
- <MButton v-else type="primary" :loading="submitting" @click="finish">创建</MButton>
1238
- </MSpace>
1222
+ template: `<MStepper v-model="activeStep" :steps="steps" linear />
1223
+ <MPageSection variant="form">
1224
+ <MForm v-if="activeStep === 0" @submit.prevent="next">
1225
+ <MFormItem label="名称" name="name" required>
1226
+ <MInput v-model="model.name" fluid />
1227
+ </MFormItem>
1228
+ </MForm>
1229
+ <div v-else>
1230
+ <p>确认创建「{{ model.name }}」?</p>
1231
+ </div>
1232
+ </MPageSection>
1233
+ <MPageSection variant="actions">
1234
+ <MSpace>
1235
+ <MButton type="text" :disabled="activeStep === 0" @click="back">上一步</MButton>
1236
+ <MButton v-if="activeStep < steps.length - 1" type="primary" @click="next">下一步</MButton>
1237
+ <MButton v-else type="primary" :loading="submitting" @click="finish">创建</MButton>
1238
+ </MSpace>
1239
1239
  </MPageSection>`,
1240
1240
  rules: ['用 :steps(不是 :items)', '每步一个主任务;进入下一步前校验当前步', '完成文案与「下一步」区分'],
1241
1241
  rulesEn: ['Use :steps (not :items)', 'One job per step; validate before advancing', 'Finish label differs from Next'],
@@ -1251,51 +1251,51 @@ async function finish() {
1251
1251
  pageTypes: ['common'],
1252
1252
  keywords: ['登录', '认证', '分栏', 'login', 'auth', 'split', 'brand', 'filled', 'themeConfig'],
1253
1253
  imports: ['MForm', 'MFormItem', 'MInput', 'MInputPassword', 'MButton', 'MSpace', 'MConfigProvider', 'zhCN'],
1254
- scriptSetup: `const submitting = ref(false)
1255
- const formError = ref('')
1256
- const model = reactive({ email: '', password: '' })
1257
- const themeConfig = { seed: { colorPrimary: '#0b6e4f' } }
1258
-
1259
- async function onSubmit() {
1260
- formError.value = ''
1261
- submitting.value = true
1262
- try {
1263
- if (!model.email || !model.password) {
1264
- formError.value = '请输入邮箱和密码后再试。'
1265
- }
1266
- } finally {
1267
- submitting.value = false
1268
- }
1254
+ scriptSetup: `const submitting = ref(false)
1255
+ const formError = ref('')
1256
+ const model = reactive({ email: '', password: '' })
1257
+ const themeConfig = { seed: { colorPrimary: '#0b6e4f' } }
1258
+
1259
+ async function onSubmit() {
1260
+ formError.value = ''
1261
+ submitting.value = true
1262
+ try {
1263
+ if (!model.email || !model.password) {
1264
+ formError.value = '请输入邮箱和密码后再试。'
1265
+ }
1266
+ } finally {
1267
+ submitting.value = false
1268
+ }
1269
1269
  }`,
1270
- template: `<MConfigProvider :locale="zhCN" input-variant="filled" :theme-config="themeConfig">
1271
- <div class="login-shell">
1272
- <aside class="login-brand" aria-label="品牌">
1273
- <p class="login-brand__mark">产品名</p>
1274
- <h1 class="login-brand__title">一句品牌主张</h1>
1275
- <p class="login-brand__lead">一句支持说明。</p>
1276
- </aside>
1277
- <main class="login-main">
1278
- <div class="login-panel">
1279
- <header class="login-panel__header">
1280
- <h2>登录</h2>
1281
- <p>使用工作邮箱进入。</p>
1282
- </header>
1283
- <p v-if="formError" class="login-alert" role="alert">{{ formError }}</p>
1284
- <MForm @submit="onSubmit">
1285
- <MFormItem label="邮箱" name="email" required>
1286
- <MInput v-model="model.email" type="email" autocomplete="username" fluid />
1287
- </MFormItem>
1288
- <MFormItem label="密码" name="password" required>
1289
- <MInputPassword v-model="model.password" autocomplete="current-password" fluid />
1290
- </MFormItem>
1291
- <MSpace style="margin-top: var(--m-space-2)" alignment="center">
1292
- <MButton html-type="submit" label="登录" type="primary" :loading="submitting" />
1293
- <MButton html-type="button" label="忘记密码" type="text" />
1294
- </MSpace>
1295
- </MForm>
1296
- </div>
1297
- </main>
1298
- </div>
1270
+ template: `<MConfigProvider :locale="zhCN" input-variant="filled" :theme-config="themeConfig">
1271
+ <div class="login-shell">
1272
+ <aside class="login-brand" aria-label="品牌">
1273
+ <p class="login-brand__mark">产品名</p>
1274
+ <h1 class="login-brand__title">一句品牌主张</h1>
1275
+ <p class="login-brand__lead">一句支持说明。</p>
1276
+ </aside>
1277
+ <main class="login-main">
1278
+ <div class="login-panel">
1279
+ <header class="login-panel__header">
1280
+ <h2>登录</h2>
1281
+ <p>使用工作邮箱进入。</p>
1282
+ </header>
1283
+ <p v-if="formError" class="login-alert" role="alert">{{ formError }}</p>
1284
+ <MForm @submit="onSubmit">
1285
+ <MFormItem label="邮箱" name="email" required>
1286
+ <MInput v-model="model.email" type="email" autocomplete="username" fluid />
1287
+ </MFormItem>
1288
+ <MFormItem label="密码" name="password" required>
1289
+ <MInputPassword v-model="model.password" autocomplete="current-password" fluid />
1290
+ </MFormItem>
1291
+ <MSpace style="margin-top: var(--m-space-2)" alignment="center">
1292
+ <MButton html-type="submit" label="登录" type="primary" :loading="submitting" />
1293
+ <MButton html-type="button" label="忘记密码" type="text" />
1294
+ </MSpace>
1295
+ </MForm>
1296
+ </div>
1297
+ </main>
1298
+ </div>
1299
1299
  </MConfigProvider>`,
1300
1300
  rules: [
1301
1301
  '认证表单默认 input-variant="filled";品牌色走 themeConfig.seed,不要页级 hex',
@@ -1339,29 +1339,29 @@ async function onSubmit() {
1339
1339
  'MConfigProvider',
1340
1340
  'zhCN',
1341
1341
  ],
1342
- template: `<MConfigProvider :locale="zhCN">
1343
- <MLayout fill-viewport has-sider>
1344
- <MLayoutSider v-model:collapsed="siderCollapsed" bordered :collapsed-width="72">
1345
- <MMenu
1346
- :model="[
1347
- { key: 'users', label: '用户管理', icon: 'user', to: '/users' },
1348
- { key: 'roles', label: '角色管理', icon: 'shield', to: '/roles' },
1349
- ]"
1350
- :collapsed="siderCollapsed"
1351
- :collapsed-width="72"
1352
- />
1353
- </MLayoutSider>
1354
- <MLayout>
1355
- <MLayoutHeader :padding="'var(--m-space-4) var(--m-space-6)'">
1356
- <MBreadcrumb :model="[{ label: '首页', to: '/' }, { label: '当前页' }]" />
1357
- </MLayoutHeader>
1358
- <MLayoutContent>
1359
- <MPageContent>
1360
- <!-- page sections -->
1361
- </MPageContent>
1362
- </MLayoutContent>
1363
- </MLayout>
1364
- </MLayout>
1342
+ template: `<MConfigProvider :locale="zhCN">
1343
+ <MLayout fill-viewport has-sider>
1344
+ <MLayoutSider v-model:collapsed="siderCollapsed" bordered :collapsed-width="72">
1345
+ <MMenu
1346
+ :model="[
1347
+ { key: 'users', label: '用户管理', icon: 'user', to: '/users' },
1348
+ { key: 'roles', label: '角色管理', icon: 'shield', to: '/roles' },
1349
+ ]"
1350
+ :collapsed="siderCollapsed"
1351
+ :collapsed-width="72"
1352
+ />
1353
+ </MLayoutSider>
1354
+ <MLayout>
1355
+ <MLayoutHeader :padding="'var(--m-space-4) var(--m-space-6)'">
1356
+ <MBreadcrumb :model="[{ label: '首页', to: '/' }, { label: '当前页' }]" />
1357
+ </MLayoutHeader>
1358
+ <MLayoutContent>
1359
+ <MPageContent>
1360
+ <!-- page sections -->
1361
+ </MPageContent>
1362
+ </MLayoutContent>
1363
+ </MLayout>
1364
+ </MLayout>
1365
1365
  </MConfigProvider>`,
1366
1366
  rules: [
1367
1367
  '根布局用 fillViewport',
@@ -1394,19 +1394,19 @@ async function onSubmit() {
1394
1394
  'log',
1395
1395
  ],
1396
1396
  imports: ['MScrollbar', 'MCard'],
1397
- template: `<MCard title="活动日志">
1398
- <MScrollbar max-height="16rem">
1399
- <ul style="margin:0;padding:0;list-style:none">
1400
- <li v-for="item in logLines" :key="item.id" style="padding:var(--m-space-2) 0;border-bottom:1px solid var(--m-color-border)">
1401
- {{ item.text }}
1402
- </li>
1403
- </ul>
1404
- </MScrollbar>
1397
+ template: `<MCard title="活动日志">
1398
+ <MScrollbar max-height="16rem">
1399
+ <ul style="margin:0;padding:0;list-style:none">
1400
+ <li v-for="item in logLines" :key="item.id" style="padding:var(--m-space-2) 0;border-bottom:1px solid var(--m-color-border)">
1401
+ {{ item.text }}
1402
+ </li>
1403
+ </ul>
1404
+ </MScrollbar>
1405
1405
  </MCard>`,
1406
- scriptSetup: `const logLines = [
1407
- { id: '1', text: '用户 admin 登录成功' },
1408
- { id: '2', text: '导出任务已完成' },
1409
- { id: '3', text: '配置已保存' },
1406
+ scriptSetup: `const logLines = [
1407
+ { id: '1', text: '用户 admin 登录成功' },
1408
+ { id: '2', text: '导出任务已完成' },
1409
+ { id: '3', text: '配置已保存' },
1410
1410
  ]`,
1411
1411
  rules: [
1412
1412
  '整页主滚动推荐 MLayout fillViewport',