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