@morya-ui/mcp 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
1
  export const pageSnippets = [
2
2
  {
3
3
  id: 'list-filters',
4
- title: '列表页筛选区',
5
- titleEn: 'List page filter bar',
6
- description: '在 MPageContent 内追加或替换筛选区,使用 MPageFilters + MSpace 包裹控件。',
7
- descriptionEn: 'Add or replace a filter region inside MPageContent using MPageFilters and MSpace.',
4
+ title: '列表页基础筛选栏',
5
+ titleEn: 'Basic list filter bar',
6
+ description: '关键词 + 1–2 个枚举 + 查询/重置。条件少、无需已选摘要时用本片段;有次要条件用 list-filters-collapsible;默认列表推荐 list-filters-stack。',
7
+ descriptionEn: 'Keyword + 1–2 enums + query/reset. Use for few filters without chips; secondary fields → list-filters-collapsible; default list → list-filters-stack.',
8
8
  pageTypes: ['list'],
9
- keywords: ['筛选', '搜索', '查询', 'filters', 'filter', 'search', 'keyword', 'reset'],
9
+ keywords: ['筛选', '搜索', '查询', 'filters', 'filter', 'search', 'keyword', 'reset', '基础筛选'],
10
10
  imports: ['MPageFilters', 'MSpace', 'MInput', 'MSelect', 'MButton'],
11
11
  scriptSetup: `const keyword = ref('')
12
12
  const status = ref<string | undefined>()
@@ -14,7 +14,12 @@ const statusOptions = [
14
14
  { label: '全部', value: '' },
15
15
  { label: '启用', value: 'active' },
16
16
  { label: '停用', value: 'inactive' },
17
- ]`,
17
+ ]
18
+
19
+ function resetFilters() {
20
+ keyword.value = ''
21
+ status.value = undefined
22
+ }`,
18
23
  template: `<MPageFilters aria-label="筛选" variant="filled">
19
24
  <MSpace wrap>
20
25
  <MInput v-model="keyword" placeholder="搜索名称" clearable style="width: 14rem" />
@@ -25,27 +30,64 @@ const statusOptions = [
25
30
  clearable
26
31
  style="width: 10rem"
27
32
  />
28
- <MButton severity="secondary">查询</MButton>
29
- <MButton severity="secondary" text>重置</MButton>
30
33
  </MSpace>
34
+ <template #actions>
35
+ <MButton severity="secondary">查询</MButton>
36
+ <MButton severity="secondary" text @click="resetFilters">重置</MButton>
37
+ </template>
31
38
  </MPageFilters>`,
32
- rules: ['放在 MPageHeader 之后、表格之前', 'viewport 内仅一个 primary(通常在 Header)', '不要外包 MCard'],
33
- rulesEn: ['Place after MPageHeader and before the table', 'Keep one primary in the viewport (usually in Header)', 'Do not wrap with MCard'],
34
- avoid: ['不要手写 .page-filters 类', '不要用 MCard 包筛选区'],
35
- avoidEn: ['Do not hand-write .page-filters classes', 'Do not wrap filters in MCard'],
39
+ rules: [
40
+ '放在 MPageHeader 之后、表格(或 FilterChips)之前',
41
+ '查询/重置放 #actions(尾部簇);viewport 内 primary 通常在 Header',
42
+ 'variant 默认 filled;dense craft 可用 plain + size="small"',
43
+ '不要外包 MCard',
44
+ ],
45
+ rulesEn: [
46
+ 'Place after MPageHeader and before the table (or FilterChips)',
47
+ 'Query/reset in #actions (trailing cluster); viewport primary usually in Header',
48
+ 'Default variant filled; dense craft may use plain + size="small"',
49
+ 'Do not wrap with MCard',
50
+ ],
51
+ avoid: [
52
+ '不要手写 .page-filters 类',
53
+ '不要用 MCard 包筛选区',
54
+ '条件 ≥3 且含次要字段时不要硬用本片段 → list-filters-collapsible / list-filters-stack',
55
+ ],
56
+ avoidEn: [
57
+ 'Do not hand-write .page-filters classes',
58
+ 'Do not wrap filters in MCard',
59
+ 'When ≥3 filters with secondary fields → list-filters-collapsible / list-filters-stack',
60
+ ],
36
61
  },
37
62
  {
38
63
  id: 'list-filters-collapsible',
39
- title: '列表页折叠筛选',
40
- titleEn: 'Collapsible list filters',
41
- description: 'MPageFilters collapsible + #advanced 隐藏次要字段。',
42
- descriptionEn: 'Hide secondary fields with collapsible MPageFilters and an #advanced slot.',
64
+ title: '列表页折叠高级筛选',
65
+ titleEn: 'Collapsible advanced list filters',
66
+ description: '常用条件在 default,次要条件在 #advanced;对齐 list-page 黄金结构。通常再接 list-filter-chips,或直接用 list-filters-stack。',
67
+ descriptionEn: 'Primary filters in default, secondary in #advanced (list-page structure). Usually pair with list-filter-chips, or use list-filters-stack.',
43
68
  pageTypes: ['list'],
44
- keywords: ['折叠', '高级筛选', 'collapse', 'advanced', 'expand', 'filter'],
69
+ keywords: ['折叠', '高级筛选', 'collapse', 'advanced', 'expand', 'filter', '次要条件'],
45
70
  imports: ['MPageFilters', 'MSpace', 'MInput', 'MSelect', 'MButton'],
46
71
  scriptSetup: `const filtersExpanded = ref(false)
47
72
  const keyword = ref('')
48
- const department = ref<string | null>(null)`,
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
+ }`,
49
91
  template: `<MPageFilters
50
92
  v-model:expanded="filtersExpanded"
51
93
  aria-label="筛选"
@@ -54,14 +96,23 @@ const department = ref<string | null>(null)`,
54
96
  >
55
97
  <MSpace wrap>
56
98
  <MInput v-model="keyword" placeholder="搜索名称" clearable style="width: 14rem" />
57
- <MButton severity="secondary">查询</MButton>
58
- <MButton severity="secondary" text>重置</MButton>
99
+ <MSelect
100
+ v-model="status"
101
+ :options="statusOptions"
102
+ placeholder="状态"
103
+ clearable
104
+ style="width: 10rem"
105
+ />
59
106
  </MSpace>
107
+ <template #actions>
108
+ <MButton severity="secondary">查询</MButton>
109
+ <MButton severity="secondary" text @click="resetFilters">重置</MButton>
110
+ </template>
60
111
  <template #advanced>
61
112
  <MSpace wrap>
62
113
  <MSelect
63
114
  v-model="department"
64
- :options="[{ label: '研发', value: '研发' }, { label: '运营', value: '运营' }]"
115
+ :options="departmentOptions"
65
116
  placeholder="部门"
66
117
  clearable
67
118
  style="width: 10rem"
@@ -69,21 +120,68 @@ const department = ref<string | null>(null)`,
69
120
  </MSpace>
70
121
  </template>
71
122
  </MPageFilters>`,
72
- rules: ['常用条件放 default,次要条件放 #advanced', '查询/重置保持 secondary'],
73
- rulesEn: ['Keep primary filters in default; secondary filters in #advanced', 'Query/reset stay secondary'],
74
- avoid: ['不要用 MCard 包整个筛选区'],
75
- avoidEn: ['Do not wrap the whole filter bar in MCard'],
123
+ rules: [
124
+ '常用条件放 default,次要条件放 #advanced',
125
+ '查询/重置放 #actions;toggle 自带「高级筛选/收起」+ chevron',
126
+ '必须 collapsible + v-model:expanded + #advanced',
127
+ '重置应清掉 advanced 字段',
128
+ ],
129
+ rulesEn: [
130
+ 'Primary filters in default; secondary in #advanced',
131
+ 'Query/reset in #actions; toggle ships Advanced/Collapse + chevron',
132
+ 'Require collapsible + v-model:expanded + #advanced',
133
+ 'Reset clears advanced fields too',
134
+ ],
135
+ avoid: [
136
+ '不要用 MCard 包整个筛选区',
137
+ '不要只有 #advanced 却不设 collapsible',
138
+ '不要把查询按钮放进 #advanced',
139
+ ],
140
+ avoidEn: [
141
+ 'Do not wrap the whole filter bar in MCard',
142
+ 'Do not provide #advanced without collapsible',
143
+ 'Do not put Query inside #advanced',
144
+ ],
76
145
  },
77
146
  {
78
147
  id: 'list-filter-chips',
79
- title: '列表页已选筛选',
148
+ title: '列表页已选筛选 Chips',
80
149
  titleEn: 'Active filter chips',
81
- description: 'MPageFilterChips + MTag closable 展示并清除已选条件。',
82
- descriptionEn: 'Show and clear active filters with MPageFilterChips and closable MTag.',
150
+ description: 'MPageFilterChips + closable MTag;放在 MPageFilters 与表格之间。需配合筛选 ref + activeFilters computed;完整栈见 list-filters-stack。',
151
+ descriptionEn: 'MPageFilterChips + closable MTag between filters and table. Needs filter refs + activeFilters computed; full stack → list-filters-stack.',
83
152
  pageTypes: ['list'],
84
- keywords: ['已选', 'chip', 'tag', 'active', 'clear', '筛选'],
153
+ keywords: ['已选', 'chip', 'tag', 'active', 'clear', '筛选', 'FilterChips', '已选筛选'],
85
154
  imports: ['MPageFilterChips', 'MTag'],
86
- template: `<MPageFilterChips label="已选" aria-label="已选筛选">
155
+ scriptSetup: `const keyword = ref('')
156
+ const status = ref<string | undefined>()
157
+ const department = ref<string | null>(null)
158
+ const statusOptions = [
159
+ { label: '全部', value: '' },
160
+ { label: '启用', value: 'active' },
161
+ { label: '停用', value: 'inactive' },
162
+ ]
163
+
164
+ const activeFilters = computed(() => {
165
+ const items: Array<{ key: string; label: string }> = []
166
+ if (keyword.value.trim()) {
167
+ items.push({ key: 'keyword', label: \`关键词:\${keyword.value.trim()}\` })
168
+ }
169
+ if (status.value) {
170
+ const label = statusOptions.find((o) => o.value === status.value)?.label ?? status.value
171
+ items.push({ key: 'status', label: \`状态:\${label}\` })
172
+ }
173
+ if (department.value) {
174
+ items.push({ key: 'department', label: \`部门:\${department.value}\` })
175
+ }
176
+ return items
177
+ })
178
+
179
+ function clearFilter(key: string) {
180
+ if (key === 'keyword') keyword.value = ''
181
+ if (key === 'status') status.value = undefined
182
+ if (key === 'department') department.value = null
183
+ }`,
184
+ template: `<MPageFilterChips v-if="activeFilters.length" label="已选" aria-label="已选筛选">
87
185
  <MTag
88
186
  v-for="item in activeFilters"
89
187
  :key="item.key"
@@ -94,10 +192,226 @@ const department = ref<string | null>(null)`,
94
192
  @close="clearFilter(item.key)"
95
193
  />
96
194
  </MPageFilterChips>`,
97
- rules: ['放在 MPageFilters 之后、表格之前', '用业务 computed 生成 activeFilters'],
98
- rulesEn: ['Place after MPageFilters and before the table', 'Derive activeFilters from filter state'],
99
- avoid: ['不要手写 pill 样式或 dashed 壳'],
100
- avoidEn: ['Do not hand-write pill or dashed shells'],
195
+ rules: [
196
+ '放在 MPageFilters 之后、表格之前',
197
+ '用 computed 从筛选状态生成 activeFilters;关闭 Tag 时写回对应 ref',
198
+ '无已选条件时用 v-if 隐藏整块',
199
+ ],
200
+ rulesEn: [
201
+ 'Place after MPageFilters and before the table',
202
+ 'Derive activeFilters from filter state; closing a Tag writes back the ref',
203
+ 'Hide the whole block with v-if when empty',
204
+ ],
205
+ avoid: [
206
+ '不要手写 pill 样式或 dashed 壳',
207
+ '不要用 MTag 代替筛选控件本身',
208
+ '不要把 Chips 塞进 MPageFilters 默认槽(可用 #active,但更常见是下方独立 Chips)',
209
+ ],
210
+ avoidEn: [
211
+ 'Do not hand-write pill or dashed shells',
212
+ 'Do not replace filter controls with MTag',
213
+ 'Do not put chips in the PageFilters default slot (optional #active; prefer sibling Chips)',
214
+ ],
215
+ },
216
+ {
217
+ id: 'list-filters-stack',
218
+ title: '列表筛选完整栈(折叠 + 已选)',
219
+ titleEn: 'Full list filter stack (collapsible + chips)',
220
+ description: '默认推荐的列表筛选组合:MPageFilters collapsible + #advanced + MPageFilterChips。对齐 list-page / list-page-rail 黄金结构;dense 无高级条件时可改用 list-filters(plain/small)。',
221
+ descriptionEn: 'Default recommended list filter composition: collapsible MPageFilters + #advanced + MPageFilterChips. Mirrors list-page / list-page-rail; dense with few fields may use list-filters (plain/small).',
222
+ pageTypes: ['list'],
223
+ keywords: [
224
+ '筛选组合',
225
+ '筛选栈',
226
+ '高级筛选',
227
+ '已选筛选',
228
+ 'filter stack',
229
+ 'filters stack',
230
+ 'collapsible',
231
+ 'chips',
232
+ 'list-page',
233
+ '完整筛选',
234
+ ],
235
+ imports: [
236
+ 'MPageFilters',
237
+ 'MPageFilterChips',
238
+ 'MSpace',
239
+ 'MInput',
240
+ 'MSelect',
241
+ 'MButton',
242
+ 'MTag',
243
+ ],
244
+ scriptSetup: `const filtersExpanded = ref(false)
245
+ const keyword = ref('')
246
+ const status = ref<string | undefined>()
247
+ const department = ref<string | null>(null)
248
+
249
+ const statusOptions = [
250
+ { label: '全部', value: '' },
251
+ { label: '启用', value: 'active' },
252
+ { label: '停用', value: 'inactive' },
253
+ ]
254
+ const departmentOptions = [
255
+ { label: '研发', value: '研发' },
256
+ { label: '运营', value: '运营' },
257
+ { label: '市场', value: '市场' },
258
+ ]
259
+
260
+ const activeFilters = computed(() => {
261
+ const items: Array<{ key: string; label: string }> = []
262
+ if (keyword.value.trim()) {
263
+ items.push({ key: 'keyword', label: \`关键词:\${keyword.value.trim()}\` })
264
+ }
265
+ if (status.value) {
266
+ const label = statusOptions.find((o) => o.value === status.value)?.label ?? status.value
267
+ items.push({ key: 'status', label: \`状态:\${label}\` })
268
+ }
269
+ if (department.value) {
270
+ items.push({ key: 'department', label: \`部门:\${department.value}\` })
271
+ }
272
+ return items
273
+ })
274
+
275
+ function clearFilter(key: string) {
276
+ if (key === 'keyword') keyword.value = ''
277
+ if (key === 'status') status.value = undefined
278
+ if (key === 'department') department.value = null
279
+ }
280
+
281
+ function resetFilters() {
282
+ keyword.value = ''
283
+ status.value = undefined
284
+ department.value = null
285
+ }`,
286
+ template: `<MPageFilters
287
+ v-model:expanded="filtersExpanded"
288
+ aria-label="筛选"
289
+ variant="filled"
290
+ collapsible
291
+ >
292
+ <MSpace wrap>
293
+ <MInput v-model="keyword" placeholder="搜索名称" clearable style="width: 14rem" />
294
+ <MSelect
295
+ v-model="status"
296
+ :options="statusOptions"
297
+ placeholder="状态"
298
+ clearable
299
+ style="width: 10rem"
300
+ />
301
+ </MSpace>
302
+ <template #actions>
303
+ <MButton severity="secondary">查询</MButton>
304
+ <MButton severity="secondary" text @click="resetFilters">重置</MButton>
305
+ </template>
306
+ <template #advanced>
307
+ <MSpace wrap>
308
+ <MSelect
309
+ v-model="department"
310
+ :options="departmentOptions"
311
+ placeholder="部门"
312
+ clearable
313
+ style="width: 10rem"
314
+ />
315
+ </MSpace>
316
+ </template>
317
+ </MPageFilters>
318
+
319
+ <MPageFilterChips v-if="activeFilters.length" label="已选" aria-label="已选筛选">
320
+ <MTag
321
+ v-for="item in activeFilters"
322
+ :key="item.key"
323
+ :value="item.label"
324
+ size="small"
325
+ bordered
326
+ closable
327
+ @close="clearFilter(item.key)"
328
+ />
329
+ </MPageFilterChips>`,
330
+ rules: [
331
+ '顺序:Header → Filters → FilterChips →(可选 Toolbar)→ Table',
332
+ '查询/重置放 #actions;toggle 为「高级筛选/收起」+ chevron',
333
+ '重置与 Chip @close 必须写回同一套筛选 ref',
334
+ 'dense/compact 且无高级条件:可降级为 list-filters-dense',
335
+ ],
336
+ rulesEn: [
337
+ 'Order: Header → Filters → FilterChips → (optional Toolbar) → Table',
338
+ 'Query/reset in #actions; toggle is Advanced/Collapse + chevron',
339
+ 'Reset and Chip @close must write the same filter refs',
340
+ 'Dense/compact without advanced fields: fall back to list-filters-dense',
341
+ ],
342
+ avoid: [
343
+ '不要拆成互不共享状态的 Filters 与 Chips',
344
+ '不要用 MCard 包筛选栈',
345
+ '不要用手写 div 模拟已选条',
346
+ ],
347
+ avoidEn: [
348
+ 'Do not split Filters and Chips with disconnected state',
349
+ 'Do not wrap the filter stack in MCard',
350
+ 'Do not hand-roll an active-filter bar',
351
+ ],
352
+ },
353
+ {
354
+ id: 'list-filters-dense',
355
+ title: '列表页紧凑筛选栏',
356
+ titleEn: 'Dense list filter bar',
357
+ description: 'list-page-dense craft:variant="plain" + size="small",字段少、无折叠/Chips。监控/告警等高密度列表用。',
358
+ descriptionEn: 'list-page-dense craft: variant="plain" + size="small", few fields, no collapse/chips. For dense monitoring/alert lists.',
359
+ pageTypes: ['list'],
360
+ keywords: ['dense', 'compact', '紧凑', 'plain', '监控', '告警', '小尺寸筛选'],
361
+ imports: ['MPageFilters', 'MSpace', 'MInput', 'MSelect', 'MButton'],
362
+ scriptSetup: `const keyword = ref('')
363
+ const status = ref<string | undefined>()
364
+ const statusOptions = [
365
+ { label: '全部', value: '' },
366
+ { label: '启用', value: 'active' },
367
+ { label: '停用', value: 'inactive' },
368
+ ]
369
+
370
+ function resetFilters() {
371
+ keyword.value = ''
372
+ status.value = undefined
373
+ }`,
374
+ template: `<MPageFilters aria-label="筛选" variant="plain">
375
+ <MSpace wrap>
376
+ <MInput
377
+ v-model="keyword"
378
+ placeholder="搜索"
379
+ clearable
380
+ size="small"
381
+ style="width: 11rem"
382
+ />
383
+ <MSelect
384
+ v-model="status"
385
+ :options="statusOptions"
386
+ placeholder="状态"
387
+ clearable
388
+ size="small"
389
+ style="width: 8rem"
390
+ />
391
+ </MSpace>
392
+ <template #actions>
393
+ <MButton severity="secondary" size="small">查询</MButton>
394
+ <MButton severity="secondary" size="small" text @click="resetFilters">重置</MButton>
395
+ </template>
396
+ </MPageFilters>`,
397
+ rules: [
398
+ '仅在 dense/compact craft 使用;默认列表用 list-filters-stack',
399
+ '控件统一 size="small";Filters 用 variant="plain"',
400
+ '查询/重置放 #actions,仍为 secondary',
401
+ ],
402
+ rulesEn: [
403
+ 'Only for dense/compact craft; default lists use list-filters-stack',
404
+ 'Controls size="small"; Filters variant="plain"',
405
+ 'Query/reset in #actions; stay secondary',
406
+ ],
407
+ avoid: [
408
+ '不要在普通 soft 列表默认套 dense 筛选',
409
+ '不要在 dense 栏再叠一排大号 filled Filters',
410
+ ],
411
+ avoidEn: [
412
+ 'Do not default soft lists to dense filters',
413
+ 'Do not stack a large filled Filters bar on dense craft',
414
+ ],
101
415
  },
102
416
  {
103
417
  id: 'list-toolbar',
@@ -122,8 +436,8 @@ const department = ref<string | null>(null)`,
122
436
  id: 'list-batch-toolbar',
123
437
  title: '列表页批量操作条',
124
438
  titleEn: 'List batch action row',
125
- description: '表格多选时的批量操作,放在筛选区之后(可选)。',
126
- descriptionEn: 'Optional batch actions after filters when rows are selectable.',
439
+ description: '表格多选时的批量操作,放在筛选区(及 FilterChips)之后(可选)。',
440
+ descriptionEn: 'Optional batch actions after filters (and FilterChips) when rows are selectable.',
127
441
  pageTypes: ['list'],
128
442
  keywords: ['批量', '已选', 'toolbar', 'batch', 'export', 'delete'],
129
443
  imports: ['MPageToolbar', 'MButton'],
@@ -134,8 +448,8 @@ const department = ref<string | null>(null)`,
134
448
  <MButton severity="danger" text>删除</MButton>
135
449
  </template>
136
450
  </MPageToolbar>`,
137
- rules: ['左侧用 default 插槽展示已选数量', '批量操作用 text/outlined,danger 仅 destructive'],
138
- rulesEn: ['Use the default slot for selection count', 'Batch actions use text/outlined; danger only for destructive ops'],
451
+ rules: ['左侧用 default 插槽展示已选数量', '批量操作用 text/outlined,danger 仅 destructive', '放在 FilterChips 之后、表格之前'],
452
+ rulesEn: ['Use the default slot for selection count', 'Batch actions use text/outlined; danger only for destructive ops', 'Place after FilterChips and before the table'],
139
453
  avoid: ['不要用 Toolbar title 充当页面标题'],
140
454
  avoidEn: ['Do not use Toolbar title as the page heading'],
141
455
  },
@@ -517,6 +831,366 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
517
831
  avoid: ['不要把操作散落到多个无关区域'],
518
832
  avoidEn: ['Do not scatter actions across unrelated areas'],
519
833
  },
834
+ {
835
+ id: 'page-header-actions',
836
+ title: '页头单主操作',
837
+ titleEn: 'Page header with one primary action',
838
+ description: 'MPageHeader + #actions:视口内仅一个 filled primary,次要用 secondary/text。',
839
+ descriptionEn: 'MPageHeader + #actions: one filled primary in the viewport; secondary/text for lesser actions.',
840
+ pageTypes: ['list', 'form', 'dashboard', 'detail', 'common'],
841
+ keywords: ['页头', '主按钮', '新建', 'header', 'primary', 'actions', 'page-header'],
842
+ imports: ['MPageHeader', 'MButton', 'MSpace'],
843
+ template: `<MPageHeader title="用户管理" description="管理成员与角色。">
844
+ <template #actions>
845
+ <MSpace>
846
+ <MButton severity="secondary" text>导入</MButton>
847
+ <MButton severity="primary">新建用户</MButton>
848
+ </MSpace>
849
+ </template>
850
+ </MPageHeader>`,
851
+ rules: ['视口内仅一个 filled primary', '次要动作用 secondary 或 text', '批量操作用 MPageToolbar,不要塞进页头'],
852
+ rulesEn: ['Only one filled primary in the viewport', 'Lesser actions: secondary or text', 'Batch actions belong in MPageToolbar, not the header'],
853
+ avoid: ['不要在页头和筛选区各放一个实心 primary', '不要把危险删除做成页头唯一实心按钮'],
854
+ avoidEn: ['Do not place a solid primary in both header and filters', 'Do not make destructive delete the only solid header button'],
855
+ },
856
+ {
857
+ id: 'form-in-dialog',
858
+ title: '列表内弹窗短表单',
859
+ titleEn: 'Short form in dialog from a list',
860
+ description: '同页 MDialog + MForm + #footer;短 CRUD 默认路径(约 ≤8 字段)。',
861
+ descriptionEn: 'Same-page MDialog + MForm + #footer; default short CRUD path (~≤8 fields).',
862
+ pageTypes: ['list', 'common'],
863
+ keywords: [
864
+ '弹窗表单',
865
+ '新建弹窗',
866
+ '编辑弹窗',
867
+ 'dialog form',
868
+ 'form-in-dialog',
869
+ 'modal form',
870
+ 'inline create',
871
+ ],
872
+ imports: ['MDialog', 'MForm', 'MFormItem', 'MInput', 'MSelect', 'MButton', 'MSpace', 'message'],
873
+ scriptSetup: `const dialogOpen = ref(false)
874
+ const submitting = ref(false)
875
+ const model = reactive({
876
+ name: '',
877
+ email: '',
878
+ role: undefined as string | undefined,
879
+ })
880
+ const roleOptions = [
881
+ { label: '管理员', value: 'admin' },
882
+ { label: '成员', value: 'member' },
883
+ ]
884
+
885
+ function resetModel() {
886
+ model.name = ''
887
+ model.email = ''
888
+ model.role = undefined
889
+ }
890
+
891
+ async function onSave() {
892
+ submitting.value = true
893
+ try {
894
+ // await api.save(model)
895
+ message.success('已保存')
896
+ dialogOpen.value = false
897
+ resetModel()
898
+ } finally {
899
+ submitting.value = false
900
+ }
901
+ }`,
902
+ template: `<MDialog
903
+ v-model="dialogOpen"
904
+ header="新建用户"
905
+ width="32rem"
906
+ @close="resetModel"
907
+ >
908
+ <MForm @submit.prevent="onSave">
909
+ <MFormItem label="姓名" name="name" required>
910
+ <MInput v-model="model.name" placeholder="请输入姓名" fluid />
911
+ </MFormItem>
912
+ <MFormItem label="邮箱" name="email" required>
913
+ <MInput v-model="model.email" type="email" placeholder="name@example.com" fluid />
914
+ </MFormItem>
915
+ <MFormItem label="角色" name="role" required>
916
+ <MSelect v-model="model.role" :options="roleOptions" placeholder="请选择角色" fluid />
917
+ </MFormItem>
918
+ </MForm>
919
+ <template #footer>
920
+ <MSpace style="justify-content: flex-end; width: 100%">
921
+ <MButton severity="secondary" text :disabled="submitting" @click="dialogOpen = false">
922
+ 取消
923
+ </MButton>
924
+ <MButton severity="primary" :loading="submitting" @click="onSave">
925
+ 保存
926
+ </MButton>
927
+ </MSpace>
928
+ </template>
929
+ </MDialog>`,
930
+ rules: [
931
+ '留在列表页,不另开路由',
932
+ '宽度约 28–36rem;字段用 fluid',
933
+ '取消/保存放 #footer;成功用 message.success',
934
+ ],
935
+ rulesEn: [
936
+ 'Stay on the list page; do not open a new route',
937
+ 'Width ~28–36rem; fields use fluid',
938
+ 'Cancel/save in #footer; success via message.success',
939
+ ],
940
+ avoid: [
941
+ '不要用 visible 代替 v-model',
942
+ '不要把长多分组表单塞进 Dialog → form-page / form-in-drawer',
943
+ '不要用 toast 代替单行 message',
944
+ ],
945
+ avoidEn: [
946
+ 'Do not use visible instead of v-model',
947
+ 'Do not stuff long multi-section forms into Dialog → form-page / form-in-drawer',
948
+ 'Do not use toast for a one-line message',
949
+ ],
950
+ },
951
+ {
952
+ id: 'form-in-drawer',
953
+ title: '列表内侧滑表单',
954
+ titleEn: 'Form in drawer from a list',
955
+ description: '同页 MDrawer + MForm + #footer;比 Dialog 更长但仍需对照列表。',
956
+ descriptionEn: 'Same-page MDrawer + MForm + #footer; longer than a dialog but list context still matters.',
957
+ pageTypes: ['list', 'detail', 'common'],
958
+ keywords: ['抽屉表单', '侧滑编辑', 'drawer form', 'form-in-drawer', 'side panel edit'],
959
+ imports: ['MDrawer', 'MForm', 'MFormItem', 'MInput', 'MButton', 'MSpace', 'message'],
960
+ scriptSetup: `const drawerOpen = ref(false)
961
+ const submitting = ref(false)
962
+ const model = reactive({ name: '', note: '' })
963
+
964
+ async function onSave() {
965
+ submitting.value = true
966
+ try {
967
+ message.success('已保存')
968
+ drawerOpen.value = false
969
+ } finally {
970
+ submitting.value = false
971
+ }
972
+ }`,
973
+ template: `<MDrawer
974
+ v-model="drawerOpen"
975
+ header="编辑详情"
976
+ position="right"
977
+ size="28rem"
978
+ @close="/* reset draft */"
979
+ >
980
+ <MForm @submit.prevent="onSave">
981
+ <MFormItem label="名称" name="name" required>
982
+ <MInput v-model="model.name" fluid />
983
+ </MFormItem>
984
+ <MFormItem label="备注" name="note">
985
+ <MInput v-model="model.note" fluid />
986
+ </MFormItem>
987
+ </MForm>
988
+ <template #footer>
989
+ <MSpace style="justify-content: flex-end; width: 100%">
990
+ <MButton severity="secondary" text :disabled="submitting" @click="drawerOpen = false">
991
+ 取消
992
+ </MButton>
993
+ <MButton severity="primary" :loading="submitting" @click="onSave">
994
+ 保存
995
+ </MButton>
996
+ </MSpace>
997
+ </template>
998
+ </MDrawer>`,
999
+ rules: ['position 常用 right;较长表单可加大 size', '字段 fluid;底栏放 #footer', '成功用 message.success'],
1000
+ rulesEn: ['position usually right; raise size for longer forms', 'Fields fluid; actions in #footer', 'Success via message.success'],
1001
+ avoid: ['极短 2–3 字段也用 Drawer → form-in-dialog', '危险删除只用 Drawer 无确认 → confirm-delete'],
1002
+ avoidEn: ['Tiny 2–3 field forms → form-in-dialog', 'Destructive delete without confirm → confirm-delete'],
1003
+ },
1004
+ {
1005
+ id: 'confirm-delete',
1006
+ title: '危险删除确认',
1007
+ titleEn: 'Destructive delete confirm',
1008
+ description: 'MConfirmDialog + message.success;列表行删除默认路径。',
1009
+ descriptionEn: 'MConfirmDialog + message.success; default for list-row delete.',
1010
+ pageTypes: ['list', 'detail', 'common'],
1011
+ keywords: ['删除', '确认', '二次确认', 'confirm', 'delete', 'ConfirmDialog', '危险'],
1012
+ imports: ['MConfirmDialog', 'message'],
1013
+ scriptSetup: `const confirmOpen = ref(false)
1014
+ const pendingId = ref<string | null>(null)
1015
+
1016
+ function askDelete(id: string) {
1017
+ pendingId.value = id
1018
+ confirmOpen.value = true
1019
+ }
1020
+
1021
+ function onAcceptDelete() {
1022
+ // await api.remove(pendingId.value)
1023
+ message.success('已删除')
1024
+ pendingId.value = null
1025
+ confirmOpen.value = false
1026
+ }`,
1027
+ template: `<MConfirmDialog
1028
+ v-model="confirmOpen"
1029
+ header="确认删除"
1030
+ message="删除后不可恢复,确定继续?"
1031
+ accept-label="删除"
1032
+ reject-label="取消"
1033
+ accept-severity="danger"
1034
+ @accept="onAcceptDelete"
1035
+ />`,
1036
+ rules: ['用 MConfirmDialog,不要手写 Dialog 确定/取消', 'acceptSeverity="danger"', '成功用 message.success,不要 toast'],
1037
+ rulesEn: ['Use MConfirmDialog; do not hand-roll Dialog Yes/No', 'acceptSeverity="danger"', 'Success via message.success, not toast'],
1038
+ avoid: ['普通 Dialog 冒充删除确认', '编辑表单误用 ConfirmDialog'],
1039
+ avoidEn: ['Faking delete confirm with Dialog', 'Using ConfirmDialog for edit forms'],
1040
+ },
1041
+ {
1042
+ id: 'row-actions-menu',
1043
+ title: '表格行更多操作菜单',
1044
+ titleEn: 'Table row overflow actions menu',
1045
+ description: '操作较多时用 MDropdown 收纳行内动作;少量明确操作仍用 list-row-actions。',
1046
+ descriptionEn: 'Use MDropdown when there are many row actions; prefer list-row-actions for a few explicit buttons.',
1047
+ pageTypes: ['list', 'common'],
1048
+ keywords: ['更多', '行菜单', 'dropdown', 'row menu', 'overflow', 'actions menu'],
1049
+ imports: ['MDropdown', 'MButton'],
1050
+ scriptSetup: `const rowMenu = [
1051
+ { label: '编辑', command: () => {/* open edit */} },
1052
+ { label: '复制', command: () => {/* duplicate */} },
1053
+ { label: '删除', severity: 'danger', command: () => {/* askDelete */} },
1054
+ ]`,
1055
+ template: `<template #cell-actions>
1056
+ <MDropdown :model="rowMenu">
1057
+ <MButton severity="secondary" size="small" text icon="more-horizontal" aria-label="更多操作" />
1058
+ </MDropdown>
1059
+ </template>`,
1060
+ rules: ['操作很多时再用 Dropdown;2–3 个明确操作用 text 按钮', '危险项走 confirm-delete', '图标按钮必须 aria-label'],
1061
+ rulesEn: ['Use Dropdown when many actions; 2–3 explicit actions stay as text buttons', 'Danger → confirm-delete', 'Icon-only buttons need aria-label'],
1062
+ avoid: ['表单枚举用 Dropdown → Select', '只有编辑/删除也强行收进菜单'],
1063
+ avoidEn: ['Form enums via Dropdown → Select', 'Forcing edit/delete into a menu when two text buttons suffice'],
1064
+ },
1065
+ {
1066
+ id: 'wizard-steps',
1067
+ title: '分步向导 Stepper + Form',
1068
+ titleEn: 'Wizard stepper with per-step form',
1069
+ description: 'MStepper :steps + 每步一个 MForm;完成可用 result-block。',
1070
+ descriptionEn: 'MStepper :steps + one MForm per step; finish with result-block when useful.',
1071
+ pageTypes: ['form', 'common'],
1072
+ keywords: ['向导', '分步', '步骤', 'wizard', 'stepper', 'multi-step'],
1073
+ imports: ['MStepper', 'MForm', 'MFormItem', 'MInput', 'MButton', 'MSpace', 'MPageSection', 'message'],
1074
+ scriptSetup: `const activeStep = ref(0)
1075
+ const submitting = ref(false)
1076
+ const steps = [
1077
+ { label: '基本信息', description: '名称与说明' },
1078
+ { label: '确认', description: '核对后创建' },
1079
+ ]
1080
+ const model = reactive({ name: '' })
1081
+
1082
+ function back() {
1083
+ if (activeStep.value > 0) activeStep.value -= 1
1084
+ }
1085
+ function next() {
1086
+ if (activeStep.value < steps.length - 1) activeStep.value += 1
1087
+ }
1088
+ async function finish() {
1089
+ submitting.value = true
1090
+ try {
1091
+ message.success('已创建')
1092
+ } finally {
1093
+ submitting.value = false
1094
+ }
1095
+ }`,
1096
+ template: `<MStepper v-model="activeStep" :steps="steps" linear />
1097
+ <MPageSection variant="form">
1098
+ <MForm v-if="activeStep === 0" @submit.prevent="next">
1099
+ <MFormItem label="名称" name="name" required>
1100
+ <MInput v-model="model.name" fluid />
1101
+ </MFormItem>
1102
+ </MForm>
1103
+ <div v-else>
1104
+ <p>确认创建「{{ model.name }}」?</p>
1105
+ </div>
1106
+ </MPageSection>
1107
+ <MPageSection variant="actions">
1108
+ <MSpace>
1109
+ <MButton severity="secondary" text :disabled="activeStep === 0" @click="back">上一步</MButton>
1110
+ <MButton v-if="activeStep < steps.length - 1" severity="primary" @click="next">下一步</MButton>
1111
+ <MButton v-else severity="primary" :loading="submitting" @click="finish">创建</MButton>
1112
+ </MSpace>
1113
+ </MPageSection>`,
1114
+ rules: ['用 :steps(不是 :items)', '每步一个主任务;进入下一步前校验当前步', '完成文案与「下一步」区分'],
1115
+ rulesEn: ['Use :steps (not :items)', 'One job per step; validate before advancing', 'Finish label differs from Next'],
1116
+ avoid: ['所有字段藏在单页超长表单', '允许跳过有前置依赖的步骤'],
1117
+ avoidEn: ['Hiding all fields on one long page', 'Skipping steps with prerequisites'],
1118
+ },
1119
+ {
1120
+ id: 'auth-split-shell',
1121
+ title: '登录左右分栏壳',
1122
+ titleEn: 'Auth split brand + form shell',
1123
+ description: '左品牌 / 右表单;控件 M*;失败用 role="alert" 或 errorMessage,不用 Toast。',
1124
+ descriptionEn: 'Brand left / form right; M* controls; failures via role="alert" or errorMessage, not Toast.',
1125
+ pageTypes: ['common'],
1126
+ keywords: ['登录', '认证', '分栏', 'login', 'auth', 'split', 'brand'],
1127
+ imports: ['MForm', 'MFormItem', 'MInput', 'MInputPassword', 'MButton', 'MSpace', 'MConfigProvider', 'zhCN'],
1128
+ scriptSetup: `const submitting = ref(false)
1129
+ const formError = ref('')
1130
+ const model = reactive({ email: '', password: '' })
1131
+
1132
+ async function onSubmit() {
1133
+ formError.value = ''
1134
+ submitting.value = true
1135
+ try {
1136
+ if (!model.email || !model.password) {
1137
+ formError.value = '请输入邮箱和密码后再试。'
1138
+ }
1139
+ } finally {
1140
+ submitting.value = false
1141
+ }
1142
+ }`,
1143
+ template: `<MConfigProvider :locale="zhCN">
1144
+ <div class="login-shell">
1145
+ <aside class="login-brand" aria-label="品牌">
1146
+ <p class="login-brand__mark">产品名</p>
1147
+ <h1 class="login-brand__title">一句品牌主张</h1>
1148
+ <p class="login-brand__lead">一句支持说明。</p>
1149
+ </aside>
1150
+ <main class="login-main">
1151
+ <div class="login-panel">
1152
+ <header class="login-panel__header">
1153
+ <h2>登录</h2>
1154
+ <p>使用工作邮箱进入。</p>
1155
+ </header>
1156
+ <p v-if="formError" class="login-alert" role="alert">{{ formError }}</p>
1157
+ <MForm @submit="onSubmit">
1158
+ <MFormItem label="邮箱" name="email" required>
1159
+ <MInput v-model="model.email" type="email" autocomplete="username" fluid />
1160
+ </MFormItem>
1161
+ <MFormItem label="密码" name="password" required>
1162
+ <MInputPassword v-model="model.password" autocomplete="current-password" fluid />
1163
+ </MFormItem>
1164
+ <MSpace style="margin-top: var(--m-space-2)" alignment="center">
1165
+ <MButton type="submit" label="登录" severity="primary" :loading="submitting" />
1166
+ <MButton type="button" label="忘记密码" severity="secondary" text />
1167
+ </MSpace>
1168
+ </MForm>
1169
+ </div>
1170
+ </main>
1171
+ </div>
1172
+ </MConfigProvider>`,
1173
+ rules: [
1174
+ '品牌区用 --m-* / color-mix,默认扁平勿叠 aurora',
1175
+ '失败留在表单区(role="alert" 或 errorMessage),不要 Toast',
1176
+ '窄屏改为单栏:表单在上或品牌收短',
1177
+ ],
1178
+ rulesEn: [
1179
+ 'Brand panel: --m-* / color-mix; flat by default — no stacked aurora',
1180
+ 'Keep failures in the form (role="alert" or errorMessage), not Toast',
1181
+ 'Narrow screens: single column',
1182
+ ],
1183
+ avoid: [
1184
+ '不要把 MMessage 当内嵌 Alert',
1185
+ '不要引入第二套 UI 库做登录壳',
1186
+ '不要未询问就加玻璃/霓虹/全页渐变',
1187
+ ],
1188
+ avoidEn: [
1189
+ 'Do not use MMessage as an inline Alert',
1190
+ 'Do not bring a second UI kit for the auth shell',
1191
+ 'Do not add glass/neon/full-page gradients unless asked',
1192
+ ],
1193
+ },
520
1194
  {
521
1195
  id: 'layout-app-shell',
522
1196
  title: '后台应用骨架',