@morya-ui/mcp 0.3.4 → 0.3.6
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 -0
- package/README.md +154 -151
- package/bin/morya-ui-mcp.js +2 -2
- package/data/catalog.json +40 -40
- package/data/example-coverage.json +2 -2
- 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 -166
- package/data/golden-pages/list-page-rail.vue +272 -270
- package/data/golden-pages/list-page.vue +222 -220
- 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__/fidelity.test.d.ts +1 -0
- package/dist/__tests__/fidelity.test.js +88 -0
- package/dist/__tests__/page-snippets.test.js +42 -5
- package/dist/__tests__/tools.test.js +49 -29
- package/dist/decisions.d.ts +2 -0
- package/dist/decisions.js +34 -6
- package/dist/golden-pages.d.ts +6 -1
- package/dist/golden-pages.js +34 -33
- package/dist/index.js +52 -9
- package/dist/page-snippets.js +1041 -280
- package/dist/patterns.d.ts +4 -2
- package/dist/patterns.js +35 -6
- package/dist/reference-brief.d.ts +55 -0
- package/dist/reference-brief.js +468 -0
- package/dist/style-presets.d.ts +13 -32
- package/dist/style-presets.js +100 -291
- package/dist/style-shells.d.ts +20 -0
- package/dist/style-shells.js +249 -0
- package/dist/tools.d.ts +21 -3
- package/dist/tools.js +496 -424
- package/package.json +11 -13
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
|
+
/>
|
|
194
|
+
</MPageFilterChips>`,
|
|
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
|
+
/>
|
|
96
329
|
</MPageFilterChips>`,
|
|
97
|
-
rules: [
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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,8 +457,8 @@ const department = ref<string | null>(null)`,
|
|
|
143
457
|
id: 'list-table',
|
|
144
458
|
title: '列表页表格',
|
|
145
459
|
titleEn: 'List page table',
|
|
146
|
-
description: '标准 MTable
|
|
147
|
-
descriptionEn: '
|
|
460
|
+
description: '标准 MTable:状态列 MStatus、数字列 align end、行操作 small text、空态带下一步。全视口主列表可加 fill;嵌入/短页不要 fill。',
|
|
461
|
+
descriptionEn: 'MTable with MStatus status, end-aligned numbers, small text row actions, next-step empty. Add fill only for full-viewport main lists.',
|
|
148
462
|
pageTypes: ['list'],
|
|
149
463
|
keywords: [
|
|
150
464
|
'表格',
|
|
@@ -157,60 +471,84 @@ const department = ref<string | null>(null)`,
|
|
|
157
471
|
'fill',
|
|
158
472
|
'撑满',
|
|
159
473
|
'分页',
|
|
474
|
+
'tabular',
|
|
475
|
+
'对齐',
|
|
160
476
|
],
|
|
161
|
-
imports: ['MTable', 'MEmpty', 'MButton'],
|
|
162
|
-
scriptSetup: `const columns = [
|
|
163
|
-
{ key: 'name', label: '名称' },
|
|
164
|
-
{ key: 'status', label: '状态' },
|
|
165
|
-
{ key: '
|
|
166
|
-
{ key: '
|
|
167
|
-
|
|
168
|
-
|
|
477
|
+
imports: ['MTable', 'MEmpty', 'MButton', 'MStatus', 'MSpace'],
|
|
478
|
+
scriptSetup: `const columns = [
|
|
479
|
+
{ key: 'name', label: '名称' },
|
|
480
|
+
{ key: 'status', label: '状态', width: 110 },
|
|
481
|
+
{ key: 'count', label: '数量', width: 96, align: 'end' as const },
|
|
482
|
+
{ key: 'updatedAt', label: '更新时间', width: 160 },
|
|
483
|
+
{ key: 'actions', label: '操作', width: 128 },
|
|
484
|
+
]
|
|
485
|
+
const rows = ref<Record<string, unknown>[]>([])
|
|
169
486
|
const loading = ref(false)`,
|
|
170
|
-
template: `<!-- 全视口主列表再加 fill;嵌入/短页去掉 fill -->
|
|
171
|
-
<MTable
|
|
172
|
-
:columns="columns"
|
|
173
|
-
:rows="rows"
|
|
174
|
-
:loading="loading"
|
|
175
|
-
fill
|
|
176
|
-
paginator
|
|
177
|
-
:rows-per-page="10"
|
|
178
|
-
striped
|
|
179
|
-
bordered
|
|
180
|
-
row-key="id"
|
|
181
|
-
aria-label="数据列表"
|
|
182
|
-
>
|
|
183
|
-
<template #
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
487
|
+
template: `<!-- 全视口主列表再加 fill;嵌入/短页去掉 fill -->
|
|
488
|
+
<MTable
|
|
489
|
+
:columns="columns"
|
|
490
|
+
:rows="rows"
|
|
491
|
+
:loading="loading"
|
|
492
|
+
fill
|
|
493
|
+
paginator
|
|
494
|
+
:rows-per-page="10"
|
|
495
|
+
striped
|
|
496
|
+
bordered
|
|
497
|
+
row-key="id"
|
|
498
|
+
aria-label="数据列表"
|
|
499
|
+
>
|
|
500
|
+
<template #cell-status="{ value }">
|
|
501
|
+
<MStatus
|
|
502
|
+
:label="value === 'active' || value === 'online' ? '启用' : '停用'"
|
|
503
|
+
:severity="value === 'active' || value === 'online' ? 'success' : 'secondary'"
|
|
504
|
+
/>
|
|
505
|
+
</template>
|
|
506
|
+
<template #cell-count="{ value }">
|
|
507
|
+
<span style="font-variant-numeric: tabular-nums">{{ value ?? '—' }}</span>
|
|
508
|
+
</template>
|
|
509
|
+
<template #cell-actions>
|
|
510
|
+
<MSpace>
|
|
511
|
+
<MButton severity="secondary" size="small" text>编辑</MButton>
|
|
512
|
+
<MButton severity="danger" size="small" text>删除</MButton>
|
|
513
|
+
</MSpace>
|
|
514
|
+
</template>
|
|
515
|
+
<template #empty>
|
|
516
|
+
<MEmpty
|
|
517
|
+
title="还没有成员"
|
|
518
|
+
description="新建第一条后,这里会列出姓名、状态与更新时间。"
|
|
519
|
+
icon="inbox"
|
|
520
|
+
>
|
|
521
|
+
<template #extra>
|
|
522
|
+
<MButton severity="primary">新建成员</MButton>
|
|
523
|
+
</template>
|
|
524
|
+
</MEmpty>
|
|
525
|
+
</template>
|
|
194
526
|
</MTable>`,
|
|
195
527
|
rules: [
|
|
196
528
|
'先判断是否适合 fill:全视口后台主列表 + 表格是主任务 → MPageContent fill + MTable fill',
|
|
197
529
|
'嵌入表、短页、整页文档滚动 → 不要 fill,内容高度即可',
|
|
198
|
-
'
|
|
530
|
+
'业务状态列用 MStatus;分类标签才用 MTag',
|
|
531
|
+
'数量/金额列 columns.align = end,并用 tabular-nums',
|
|
532
|
+
'行操作用 size="small" + text;空态文案写领域下一步,禁止「示例 / Name / No data」',
|
|
199
533
|
],
|
|
200
534
|
rulesEn: [
|
|
201
535
|
'Decide fill first: full-viewport admin main list → MPageContent fill + MTable fill',
|
|
202
536
|
'Embedded / short / document-scroll pages → skip fill',
|
|
203
|
-
'
|
|
537
|
+
'Business status → MStatus; categories → MTag',
|
|
538
|
+
'Numeric columns: align end + tabular-nums',
|
|
539
|
+
'Row actions: size="small" + text; empty copy is domain-real — no “示例 / Name / No data”',
|
|
204
540
|
],
|
|
205
541
|
avoid: [
|
|
206
542
|
'不要用 MCard 包裹 bordered MTable',
|
|
207
543
|
'不要对嵌入/短页硬套 fill',
|
|
208
544
|
'适合 fill 时不要手写 min-height / calc',
|
|
545
|
+
'不要在 #cell-status 默认用 MTag',
|
|
209
546
|
],
|
|
210
547
|
avoidEn: [
|
|
211
548
|
'Do not wrap a bordered MTable with MCard',
|
|
212
549
|
'Do not force fill on embedded or short pages',
|
|
213
550
|
'Do not hand-write min-height / calc when fill is appropriate',
|
|
551
|
+
'Do not default #cell-status to MTag',
|
|
214
552
|
],
|
|
215
553
|
},
|
|
216
554
|
{
|
|
@@ -222,11 +560,11 @@ const loading = ref(false)`,
|
|
|
222
560
|
pageTypes: ['list'],
|
|
223
561
|
keywords: ['行操作', '编辑', '删除', 'actions', 'row', 'cell'],
|
|
224
562
|
imports: ['MSpace', 'MButton'],
|
|
225
|
-
template: `<template #cell-actions>
|
|
226
|
-
<MSpace>
|
|
227
|
-
<MButton severity="secondary" size="small" text>编辑</MButton>
|
|
228
|
-
<MButton severity="danger" size="small" text>删除</MButton>
|
|
229
|
-
</MSpace>
|
|
563
|
+
template: `<template #cell-actions>
|
|
564
|
+
<MSpace>
|
|
565
|
+
<MButton severity="secondary" size="small" text>编辑</MButton>
|
|
566
|
+
<MButton severity="danger" size="small" text>删除</MButton>
|
|
567
|
+
</MSpace>
|
|
230
568
|
</template>`,
|
|
231
569
|
rules: ['行内操作用 size="small" + text/outlined,避免一排实心按钮', '危险操作使用 severity="danger" 并配合确认弹窗'],
|
|
232
570
|
rulesEn: ['Use size="small" with text/outlined row actions', 'Use severity="danger" with confirmation for destructive actions'],
|
|
@@ -242,11 +580,11 @@ const loading = ref(false)`,
|
|
|
242
580
|
pageTypes: ['list'],
|
|
243
581
|
keywords: ['分类', '标签', '可关闭', 'chip', 'category', 'tag-only'],
|
|
244
582
|
imports: ['MTag'],
|
|
245
|
-
template: `<template #cell-category="{ value }">
|
|
246
|
-
<MTag
|
|
247
|
-
:value="String(value ?? '')"
|
|
248
|
-
severity="info"
|
|
249
|
-
/>
|
|
583
|
+
template: `<template #cell-category="{ value }">
|
|
584
|
+
<MTag
|
|
585
|
+
:value="String(value ?? '')"
|
|
586
|
+
severity="info"
|
|
587
|
+
/>
|
|
250
588
|
</template>`,
|
|
251
589
|
rules: ['分类/强调态用 MTag;行内业务状态用 MStatus(list-status-dot)', '不要用 Button 颜色表达状态'],
|
|
252
590
|
rulesEn: ['Use MTag for categories; use MStatus for business status (list-status-dot)', 'Do not use Button colors for status'],
|
|
@@ -262,11 +600,11 @@ const loading = ref(false)`,
|
|
|
262
600
|
pageTypes: ['list'],
|
|
263
601
|
keywords: ['状态', 'status', 'dot', 'cell-status', '在线', '启用', '停用', '业务状态'],
|
|
264
602
|
imports: ['MStatus'],
|
|
265
|
-
template: `<template #cell-status="{ value }">
|
|
266
|
-
<MStatus
|
|
267
|
-
:label="value === 'active' || value === 'online' ? '启用' : '停用'"
|
|
268
|
-
:severity="value === 'active' || value === 'online' ? 'success' : 'secondary'"
|
|
269
|
-
/>
|
|
603
|
+
template: `<template #cell-status="{ value }">
|
|
604
|
+
<MStatus
|
|
605
|
+
:label="value === 'active' || value === 'online' ? '启用' : '停用'"
|
|
606
|
+
:severity="value === 'active' || value === 'online' ? 'success' : 'secondary'"
|
|
607
|
+
/>
|
|
270
608
|
</template>`,
|
|
271
609
|
rules: ['行内轻量业务状态优先 MStatus', '不要用 Button 颜色表达状态'],
|
|
272
610
|
rulesEn: ['Prefer MStatus for lightweight inline business status', 'Do not use Button colors for status'],
|
|
@@ -277,25 +615,33 @@ const loading = ref(false)`,
|
|
|
277
615
|
id: 'empty-block',
|
|
278
616
|
title: '空状态 MEmpty',
|
|
279
617
|
titleEn: 'Empty state with MEmpty',
|
|
280
|
-
description: '列表或内容区无数据时使用 MEmpty
|
|
281
|
-
descriptionEn: '
|
|
618
|
+
description: '列表或内容区无数据时使用 MEmpty;标题/说明写领域下一步,#extra 放唯一 primary。',
|
|
619
|
+
descriptionEn: 'MEmpty for no-data; domain next-step copy; one primary in #extra.',
|
|
282
620
|
pageTypes: ['list', 'common'],
|
|
283
621
|
keywords: ['空状态', '无数据', 'empty', 'zero state', 'no data'],
|
|
284
622
|
imports: ['MEmpty', 'MButton'],
|
|
285
|
-
template: `<MEmpty
|
|
286
|
-
title="
|
|
287
|
-
description="
|
|
288
|
-
icon="
|
|
289
|
-
>
|
|
290
|
-
<template #extra>
|
|
291
|
-
<MButton
|
|
292
|
-
<MButton
|
|
293
|
-
</template>
|
|
623
|
+
template: `<MEmpty
|
|
624
|
+
title="还没有成员"
|
|
625
|
+
description="邀请或新建第一位成员后,这里会显示状态与最近活动。"
|
|
626
|
+
icon="users"
|
|
627
|
+
>
|
|
628
|
+
<template #extra>
|
|
629
|
+
<MButton severity="primary">新建成员</MButton>
|
|
630
|
+
<MButton severity="secondary" text>导入名单</MButton>
|
|
631
|
+
</template>
|
|
294
632
|
</MEmpty>`,
|
|
295
|
-
rules: [
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
633
|
+
rules: [
|
|
634
|
+
'正常无数据用 MEmpty,不要用错误色',
|
|
635
|
+
'主 CTA 用 severity="primary",次动作用 text/secondary',
|
|
636
|
+
'文案写清「现在可以做什么」,禁止「暂无数据 / No data / 示例」',
|
|
637
|
+
],
|
|
638
|
+
rulesEn: [
|
|
639
|
+
'Use MEmpty for normal emptiness, not error colors',
|
|
640
|
+
'Primary CTA with severity="primary"; lesser actions text/secondary',
|
|
641
|
+
'Copy says what to do next — no “暂无数据 / No data / 示例”',
|
|
642
|
+
],
|
|
643
|
+
avoid: ['不要用手写 div 拼空态', '不要用 MResult 表达无数据', '不要放两个 filled primary'],
|
|
644
|
+
avoidEn: ['Do not hand-roll empty markup', 'Do not use MResult for no-data', 'Do not place two filled primaries'],
|
|
299
645
|
},
|
|
300
646
|
{
|
|
301
647
|
id: 'result-block',
|
|
@@ -306,11 +652,11 @@ const loading = ref(false)`,
|
|
|
306
652
|
pageTypes: ['common'],
|
|
307
653
|
keywords: ['结果', '成功', '失败', '404', '403', 'result', 'success', 'error'],
|
|
308
654
|
imports: ['MResult', 'MButton'],
|
|
309
|
-
template: `<MResult status="success" description="订单已创建,可在列表中查看详情。">
|
|
310
|
-
<template #extra>
|
|
311
|
-
<MButton label="查看订单" />
|
|
312
|
-
<MButton label="返回列表" severity="secondary" text />
|
|
313
|
-
</template>
|
|
655
|
+
template: `<MResult status="success" description="订单已创建,可在列表中查看详情。">
|
|
656
|
+
<template #extra>
|
|
657
|
+
<MButton label="查看订单" />
|
|
658
|
+
<MButton label="返回列表" severity="secondary" text />
|
|
659
|
+
</template>
|
|
314
660
|
</MResult>`,
|
|
315
661
|
rules: ['流程终点用 MResult', '提供明确下一步操作'],
|
|
316
662
|
rulesEn: ['Use MResult for terminal outcomes', 'Provide clear next-step actions'],
|
|
@@ -336,25 +682,52 @@ const loading = ref(false)`,
|
|
|
336
682
|
id: 'form-body',
|
|
337
683
|
title: '表单主体',
|
|
338
684
|
titleEn: 'Form body surface',
|
|
339
|
-
description: 'MPageSection variant="form"
|
|
340
|
-
descriptionEn: '
|
|
685
|
+
description: 'MPageSection variant="form" + MForm;左标签时设 label-position/label-width;常驻错误用字段 errorMessage。',
|
|
686
|
+
descriptionEn: 'MPageSection form + MForm; set label-position/label-width for left labels; persistent errors via errorMessage.',
|
|
341
687
|
pageTypes: ['form'],
|
|
342
|
-
keywords: ['表单', 'form', 'fields', 'formitem'],
|
|
343
|
-
imports: ['MPageSection', 'MForm', 'MFormItem', 'MInput'],
|
|
344
|
-
scriptSetup: `const model = reactive({ name: '' })
|
|
345
|
-
const submitting = ref(false)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
688
|
+
keywords: ['表单', 'form', 'fields', 'formitem', 'label-width', 'errorMessage'],
|
|
689
|
+
imports: ['MPageSection', 'MForm', 'MFormItem', 'MInput', 'MSelect'],
|
|
690
|
+
scriptSetup: `const model = reactive({ name: '', role: undefined as string | undefined })
|
|
691
|
+
const submitting = ref(false)
|
|
692
|
+
const roleOptions = [
|
|
693
|
+
{ label: '管理员', value: 'admin' },
|
|
694
|
+
{ label: '成员', value: 'member' },
|
|
695
|
+
]`,
|
|
696
|
+
template: `<MPageSection variant="form">
|
|
697
|
+
<MForm
|
|
698
|
+
label-position="left"
|
|
699
|
+
label-width="6rem"
|
|
700
|
+
@submit.prevent="onSubmit"
|
|
701
|
+
>
|
|
702
|
+
<MFormItem label="姓名" name="name" required>
|
|
703
|
+
<MInput v-model="model.name" placeholder="与工牌一致的姓名" fluid />
|
|
704
|
+
</MFormItem>
|
|
705
|
+
<MFormItem label="角色" name="role" required>
|
|
706
|
+
<MSelect
|
|
707
|
+
v-model="model.role"
|
|
708
|
+
:options="roleOptions"
|
|
709
|
+
placeholder="选择角色"
|
|
710
|
+
clearable
|
|
711
|
+
fluid
|
|
712
|
+
/>
|
|
713
|
+
</MFormItem>
|
|
714
|
+
<!-- 更多字段;常驻错误:error-message="请填写必填项" -->
|
|
715
|
+
</MForm>
|
|
353
716
|
</MPageSection>`,
|
|
354
|
-
rules: [
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
717
|
+
rules: [
|
|
718
|
+
'字段用 MFormItem + fluid 控件',
|
|
719
|
+
'左标签布局设 label-position="left" + label-width(约 5–7rem)',
|
|
720
|
+
'表单表面由 MPageSection 提供,不要重复 MCard',
|
|
721
|
+
'常驻错误用字段 errorMessage 或 token 样式 role="alert",不要用 <MMessage> 当内嵌 Alert',
|
|
722
|
+
],
|
|
723
|
+
rulesEn: [
|
|
724
|
+
'Use MFormItem with fluid controls',
|
|
725
|
+
'Left labels: label-position="left" + label-width (~5–7rem)',
|
|
726
|
+
'Let MPageSection provide the surface; do not duplicate MCard',
|
|
727
|
+
'Persistent errors: field errorMessage or token role="alert" — not <MMessage> as inline alert',
|
|
728
|
+
],
|
|
729
|
+
avoid: ['不要绕过 MForm 手写校验状态', '不要把错误只画成红色边框而无文案'],
|
|
730
|
+
avoidEn: ['Do not bypass MForm for validation state', 'Do not show errors as red borders without copy'],
|
|
358
731
|
},
|
|
359
732
|
{
|
|
360
733
|
id: 'form-actions',
|
|
@@ -365,11 +738,11 @@ const submitting = ref(false)`,
|
|
|
365
738
|
pageTypes: ['form'],
|
|
366
739
|
keywords: ['保存', '取消', 'submit', 'actions', 'footer'],
|
|
367
740
|
imports: ['MPageSection', 'MSpace', 'MButton'],
|
|
368
|
-
template: `<MPageSection variant="actions">
|
|
369
|
-
<MSpace>
|
|
370
|
-
<MButton native-type="submit" severity="primary" :loading="submitting">保存</MButton>
|
|
371
|
-
<MButton severity="secondary">取消</MButton>
|
|
372
|
-
</MSpace>
|
|
741
|
+
template: `<MPageSection variant="actions">
|
|
742
|
+
<MSpace>
|
|
743
|
+
<MButton native-type="submit" severity="primary" :loading="submitting">保存</MButton>
|
|
744
|
+
<MButton severity="secondary">取消</MButton>
|
|
745
|
+
</MSpace>
|
|
373
746
|
</MPageSection>`,
|
|
374
747
|
rules: ['提交按钮在前,取消 secondary', '保存中传 loading 防重复提交'],
|
|
375
748
|
rulesEn: ['Submit first, cancel secondary', 'Pass loading while saving to prevent duplicate submits'],
|
|
@@ -380,31 +753,59 @@ const submitting = ref(false)`,
|
|
|
380
753
|
id: 'dashboard-kpi-grid',
|
|
381
754
|
title: '仪表盘 KPI 栅格',
|
|
382
755
|
titleEn: 'Dashboard KPI grid',
|
|
383
|
-
description: 'MGrid + MPageStat
|
|
384
|
-
descriptionEn: '
|
|
756
|
+
description: 'MGrid + MPageStat:带 trend / trendDirection / trendSeverity;高密条可用 density="compact"。',
|
|
757
|
+
descriptionEn: 'MGrid + MPageStat with trend / trendDirection / trendSeverity; use density="compact" for dense strips.',
|
|
385
758
|
pageTypes: ['dashboard'],
|
|
386
|
-
keywords: ['kpi', '指标', 'stat', 'grid', 'metrics'],
|
|
759
|
+
keywords: ['kpi', '指标', 'stat', 'grid', 'metrics', 'trend', 'compact'],
|
|
387
760
|
imports: ['MGrid', 'MGridItem', 'MPageStat'],
|
|
388
|
-
scriptSetup: `const metrics = ref([
|
|
389
|
-
{
|
|
390
|
-
|
|
761
|
+
scriptSetup: `const metrics = ref([
|
|
762
|
+
{
|
|
763
|
+
label: '在册成员',
|
|
764
|
+
value: '12,480',
|
|
765
|
+
trend: '+8.2%',
|
|
766
|
+
trendDirection: 'up' as const,
|
|
767
|
+
trendSeverity: 'success' as const,
|
|
768
|
+
trendLabel: '较上月',
|
|
769
|
+
icon: 'users',
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
label: '今日活跃',
|
|
773
|
+
value: '1,926',
|
|
774
|
+
trend: '-3.1%',
|
|
775
|
+
trendDirection: 'down' as const,
|
|
776
|
+
trendSeverity: 'warn' as const,
|
|
777
|
+
trendLabel: '较昨日',
|
|
778
|
+
icon: 'activity',
|
|
779
|
+
},
|
|
391
780
|
])`,
|
|
392
|
-
template: `<MGrid :cols="4" :x-gap="16" :y-gap="16" responsive="screen">
|
|
393
|
-
<MGridItem v-for="metric in metrics" :key="metric.label" :span="1">
|
|
394
|
-
<MPageStat
|
|
395
|
-
:label="metric.label"
|
|
396
|
-
:value="metric.value"
|
|
397
|
-
:trend="metric.trend"
|
|
398
|
-
:trend-direction="metric.trendDirection"
|
|
399
|
-
:trend-
|
|
400
|
-
:
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
</
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
781
|
+
template: `<MGrid :cols="4" :x-gap="16" :y-gap="16" responsive="screen">
|
|
782
|
+
<MGridItem v-for="metric in metrics" :key="metric.label" :span="1">
|
|
783
|
+
<MPageStat
|
|
784
|
+
:label="metric.label"
|
|
785
|
+
:value="metric.value"
|
|
786
|
+
:trend="metric.trend"
|
|
787
|
+
:trend-direction="metric.trendDirection"
|
|
788
|
+
:trend-severity="metric.trendSeverity"
|
|
789
|
+
:trend-label="metric.trendLabel"
|
|
790
|
+
:icon="metric.icon"
|
|
791
|
+
/>
|
|
792
|
+
</MGridItem>
|
|
793
|
+
</MGrid>
|
|
794
|
+
<!-- 高密条带示例:density="compact" layout="plain" orientation="inline" -->`,
|
|
795
|
+
rules: [
|
|
796
|
+
'KPI 用 MPageStat,不要每页自定义 stat CSS',
|
|
797
|
+
'趋势务必带 trendDirection + trendSeverity(升 success / 降 warn|danger)',
|
|
798
|
+
'responsive="screen" 便于窄屏降列',
|
|
799
|
+
'密集工作台可用 density="compact";加载中用 loading',
|
|
800
|
+
],
|
|
801
|
+
rulesEn: [
|
|
802
|
+
'Use MPageStat for KPIs — no per-page stat CSS',
|
|
803
|
+
'Always set trendDirection + trendSeverity (up→success, down→warn|danger)',
|
|
804
|
+
'Use responsive="screen" for narrow layouts',
|
|
805
|
+
'Dense dashboards may use density="compact"; use loading while fetching',
|
|
806
|
+
],
|
|
807
|
+
avoid: ['不要每个指标手写 font-size/颜色', '不要只有数字没有趋势语义(用户要求趋势时)'],
|
|
808
|
+
avoidEn: ['Do not hand-write font sizes/colors per metric', 'Do not omit trend semantics when the brief asks for trends'],
|
|
408
809
|
},
|
|
409
810
|
{
|
|
410
811
|
id: 'dashboard-chart-card',
|
|
@@ -415,14 +816,14 @@ const submitting = ref(false)`,
|
|
|
415
816
|
pageTypes: ['dashboard'],
|
|
416
817
|
keywords: ['chart', '图表', 'placeholder', 'card', 'trend'],
|
|
417
818
|
imports: ['MCard', 'MEmpty', 'MSkeleton'],
|
|
418
|
-
template: `<MCard title="趋势概览" shadow="always">
|
|
419
|
-
<MSkeleton v-if="loading" height="8rem" />
|
|
420
|
-
<MEmpty
|
|
421
|
-
v-else
|
|
422
|
-
aria-label="图表占位"
|
|
423
|
-
title="暂无图表数据"
|
|
424
|
-
description="图表区域(接入 ECharts / 业务组件)"
|
|
425
|
-
/>
|
|
819
|
+
template: `<MCard title="趋势概览" shadow="always">
|
|
820
|
+
<MSkeleton v-if="loading" height="8rem" />
|
|
821
|
+
<MEmpty
|
|
822
|
+
v-else
|
|
823
|
+
aria-label="图表占位"
|
|
824
|
+
title="暂无图表数据"
|
|
825
|
+
description="图表区域(接入 ECharts / 业务组件)"
|
|
826
|
+
/>
|
|
426
827
|
</MCard>`,
|
|
427
828
|
rules: ['图表区适合 MCard,因为需要 card 标题', '加载中用 MSkeleton'],
|
|
428
829
|
rulesEn: ['Charts belong in MCard when a card title is needed', 'Use MSkeleton while loading'],
|
|
@@ -438,18 +839,18 @@ const submitting = ref(false)`,
|
|
|
438
839
|
pageTypes: ['dashboard'],
|
|
439
840
|
keywords: ['recent', '最近', 'table', 'card', 'list'],
|
|
440
841
|
imports: ['MCard', 'MTable', 'MStatus'],
|
|
441
|
-
scriptSetup: `const recentColumns = [
|
|
442
|
-
{ key: 'id', label: '工单号', width: 96 },
|
|
443
|
-
{ key: 'title', label: '标题' },
|
|
444
|
-
{ key: 'status', label: '状态', width: 110 },
|
|
445
|
-
]
|
|
842
|
+
scriptSetup: `const recentColumns = [
|
|
843
|
+
{ key: 'id', label: '工单号', width: 96 },
|
|
844
|
+
{ key: 'title', label: '标题' },
|
|
845
|
+
{ key: 'status', label: '状态', width: 110 },
|
|
846
|
+
]
|
|
446
847
|
const recentRows = ref<Record<string, unknown>[]>([])`,
|
|
447
|
-
template: `<MCard title="最近工单">
|
|
448
|
-
<MTable :columns="recentColumns" :rows="recentRows" size="small" :paginator="false" bordered row-key="id">
|
|
449
|
-
<template #cell-status="{ value }">
|
|
450
|
-
<MStatus :label="String(value ?? '')" severity="secondary" />
|
|
451
|
-
</template>
|
|
452
|
-
</MTable>
|
|
848
|
+
template: `<MCard title="最近工单">
|
|
849
|
+
<MTable :columns="recentColumns" :rows="recentRows" size="small" :paginator="false" bordered row-key="id">
|
|
850
|
+
<template #cell-status="{ value }">
|
|
851
|
+
<MStatus :label="String(value ?? '')" severity="secondary" />
|
|
852
|
+
</template>
|
|
853
|
+
</MTable>
|
|
453
854
|
</MCard>`,
|
|
454
855
|
rules: ['明细列表放 MCard 内合理', 'size="small" 适合卡片内表格', '行内业务状态用 MStatus'],
|
|
455
856
|
rulesEn: ['Detail lists inside MCard are appropriate', 'Use size="small" for in-card tables', 'Use MStatus for row business status'],
|
|
@@ -465,10 +866,10 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
|
|
|
465
866
|
pageTypes: ['list', 'common'],
|
|
466
867
|
keywords: ['page content', 'stack', 'layout content', '容器'],
|
|
467
868
|
imports: ['MPageContent'],
|
|
468
|
-
template: `<MLayoutContent>
|
|
469
|
-
<MPageContent>
|
|
470
|
-
<!-- MPageFilters → MPageToolbar → MTable -->
|
|
471
|
-
</MPageContent>
|
|
869
|
+
template: `<MLayoutContent>
|
|
870
|
+
<MPageContent>
|
|
871
|
+
<!-- MPageFilters → MPageToolbar → MTable -->
|
|
872
|
+
</MPageContent>
|
|
472
873
|
</MLayoutContent>`,
|
|
473
874
|
rules: ['所有列表区块放在 MPageContent 内', '区块顺序:筛选 → 工具栏 → 表格'],
|
|
474
875
|
rulesEn: ['Keep all list sections inside MPageContent', 'Order: filters → toolbar → table'],
|
|
@@ -484,10 +885,10 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
|
|
|
484
885
|
pageTypes: ['form', 'common'],
|
|
485
886
|
keywords: ['narrow', 'form content', 'max-width'],
|
|
486
887
|
imports: ['MPageContent'],
|
|
487
|
-
template: `<MLayoutContent>
|
|
488
|
-
<MPageContent width="narrow">
|
|
489
|
-
<!-- MPageHeader → MPageSection form → actions -->
|
|
490
|
-
</MPageContent>
|
|
888
|
+
template: `<MLayoutContent>
|
|
889
|
+
<MPageContent width="narrow">
|
|
890
|
+
<!-- MPageHeader → MPageSection form → actions -->
|
|
891
|
+
</MPageContent>
|
|
491
892
|
</MLayoutContent>`,
|
|
492
893
|
rules: ['表单页使用 width="narrow"', '不要手写 max-width CSS'],
|
|
493
894
|
rulesEn: ['Use width="narrow" on form pages', 'Do not hand-write max-width CSS'],
|
|
@@ -503,20 +904,380 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
|
|
|
503
904
|
pageTypes: ['detail'],
|
|
504
905
|
keywords: ['detail', '详情', 'edit', 'header'],
|
|
505
906
|
imports: ['MPageHeader', 'MButton', 'MStatus', 'MSpace'],
|
|
506
|
-
template: `<MPageHeader title="示例资源" description="查看摘要与属性。">
|
|
507
|
-
<template #actions>
|
|
508
|
-
<MSpace>
|
|
509
|
-
<MStatus label="正常" severity="success" />
|
|
510
|
-
<MButton severity="primary">编辑</MButton>
|
|
511
|
-
<MButton severity="danger" text>删除</MButton>
|
|
512
|
-
</MSpace>
|
|
513
|
-
</template>
|
|
907
|
+
template: `<MPageHeader title="示例资源" description="查看摘要与属性。">
|
|
908
|
+
<template #actions>
|
|
909
|
+
<MSpace>
|
|
910
|
+
<MStatus label="正常" severity="success" />
|
|
911
|
+
<MButton severity="primary">编辑</MButton>
|
|
912
|
+
<MButton severity="danger" text>删除</MButton>
|
|
913
|
+
</MSpace>
|
|
914
|
+
</template>
|
|
514
915
|
</MPageHeader>`,
|
|
515
916
|
rules: ['状态用 MStatus 放在 actions 区', '编辑为 primary,删除用 danger text'],
|
|
516
917
|
rulesEn: ['Use MStatus in the actions area', 'Edit is primary; delete uses danger text'],
|
|
517
918
|
avoid: ['不要把操作散落到多个无关区域'],
|
|
518
919
|
avoidEn: ['Do not scatter actions across unrelated areas'],
|
|
519
920
|
},
|
|
921
|
+
{
|
|
922
|
+
id: 'page-header-actions',
|
|
923
|
+
title: '页头单主操作',
|
|
924
|
+
titleEn: 'Page header with one primary action',
|
|
925
|
+
description: 'MPageHeader + #actions:视口内仅一个 filled primary,次要用 secondary/text。',
|
|
926
|
+
descriptionEn: 'MPageHeader + #actions: one filled primary in the viewport; secondary/text for lesser actions.',
|
|
927
|
+
pageTypes: ['list', 'form', 'dashboard', 'detail', 'common'],
|
|
928
|
+
keywords: ['页头', '主按钮', '新建', 'header', 'primary', 'actions', 'page-header'],
|
|
929
|
+
imports: ['MPageHeader', 'MButton', 'MSpace'],
|
|
930
|
+
template: `<MPageHeader title="用户管理" description="管理成员与角色。">
|
|
931
|
+
<template #actions>
|
|
932
|
+
<MSpace>
|
|
933
|
+
<MButton severity="secondary" text>导入</MButton>
|
|
934
|
+
<MButton severity="primary">新建用户</MButton>
|
|
935
|
+
</MSpace>
|
|
936
|
+
</template>
|
|
937
|
+
</MPageHeader>`,
|
|
938
|
+
rules: ['视口内仅一个 filled primary', '次要动作用 secondary 或 text', '批量操作用 MPageToolbar,不要塞进页头'],
|
|
939
|
+
rulesEn: ['Only one filled primary in the viewport', 'Lesser actions: secondary or text', 'Batch actions belong in MPageToolbar, not the header'],
|
|
940
|
+
avoid: ['不要在页头和筛选区各放一个实心 primary', '不要把危险删除做成页头唯一实心按钮'],
|
|
941
|
+
avoidEn: ['Do not place a solid primary in both header and filters', 'Do not make destructive delete the only solid header button'],
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
id: 'form-in-dialog',
|
|
945
|
+
title: '列表内弹窗短表单',
|
|
946
|
+
titleEn: 'Short form in dialog from a list',
|
|
947
|
+
description: '同页 MDialog + MForm + #footer;短 CRUD 默认路径(约 ≤8 字段)。',
|
|
948
|
+
descriptionEn: 'Same-page MDialog + MForm + #footer; default short CRUD path (~≤8 fields).',
|
|
949
|
+
pageTypes: ['list', 'common'],
|
|
950
|
+
keywords: [
|
|
951
|
+
'弹窗表单',
|
|
952
|
+
'新建弹窗',
|
|
953
|
+
'编辑弹窗',
|
|
954
|
+
'dialog form',
|
|
955
|
+
'form-in-dialog',
|
|
956
|
+
'modal form',
|
|
957
|
+
'inline create',
|
|
958
|
+
],
|
|
959
|
+
imports: ['MDialog', 'MForm', 'MFormItem', 'MInput', 'MSelect', 'MButton', 'MSpace', 'message'],
|
|
960
|
+
scriptSetup: `const dialogOpen = ref(false)
|
|
961
|
+
const submitting = ref(false)
|
|
962
|
+
const model = reactive({
|
|
963
|
+
name: '',
|
|
964
|
+
email: '',
|
|
965
|
+
role: undefined as string | undefined,
|
|
966
|
+
})
|
|
967
|
+
const roleOptions = [
|
|
968
|
+
{ label: '管理员', value: 'admin' },
|
|
969
|
+
{ label: '成员', value: 'member' },
|
|
970
|
+
]
|
|
971
|
+
|
|
972
|
+
function resetModel() {
|
|
973
|
+
model.name = ''
|
|
974
|
+
model.email = ''
|
|
975
|
+
model.role = undefined
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
async function onSave() {
|
|
979
|
+
submitting.value = true
|
|
980
|
+
try {
|
|
981
|
+
// await api.save(model)
|
|
982
|
+
message.success('已保存')
|
|
983
|
+
dialogOpen.value = false
|
|
984
|
+
resetModel()
|
|
985
|
+
} finally {
|
|
986
|
+
submitting.value = false
|
|
987
|
+
}
|
|
988
|
+
}`,
|
|
989
|
+
template: `<MDialog
|
|
990
|
+
v-model="dialogOpen"
|
|
991
|
+
header="新建用户"
|
|
992
|
+
width="32rem"
|
|
993
|
+
@close="resetModel"
|
|
994
|
+
>
|
|
995
|
+
<MForm @submit.prevent="onSave">
|
|
996
|
+
<MFormItem label="姓名" name="name" required>
|
|
997
|
+
<MInput v-model="model.name" placeholder="请输入姓名" fluid />
|
|
998
|
+
</MFormItem>
|
|
999
|
+
<MFormItem label="邮箱" name="email" required>
|
|
1000
|
+
<MInput v-model="model.email" type="email" placeholder="name@example.com" fluid />
|
|
1001
|
+
</MFormItem>
|
|
1002
|
+
<MFormItem label="角色" name="role" required>
|
|
1003
|
+
<MSelect v-model="model.role" :options="roleOptions" placeholder="请选择角色" fluid />
|
|
1004
|
+
</MFormItem>
|
|
1005
|
+
</MForm>
|
|
1006
|
+
<template #footer>
|
|
1007
|
+
<MSpace style="justify-content: flex-end; width: 100%">
|
|
1008
|
+
<MButton severity="secondary" text :disabled="submitting" @click="dialogOpen = false">
|
|
1009
|
+
取消
|
|
1010
|
+
</MButton>
|
|
1011
|
+
<MButton severity="primary" :loading="submitting" @click="onSave">
|
|
1012
|
+
保存
|
|
1013
|
+
</MButton>
|
|
1014
|
+
</MSpace>
|
|
1015
|
+
</template>
|
|
1016
|
+
</MDialog>`,
|
|
1017
|
+
rules: [
|
|
1018
|
+
'留在列表页,不另开路由',
|
|
1019
|
+
'宽度约 28–36rem;字段用 fluid',
|
|
1020
|
+
'取消/保存放 #footer;成功用 message.success',
|
|
1021
|
+
],
|
|
1022
|
+
rulesEn: [
|
|
1023
|
+
'Stay on the list page; do not open a new route',
|
|
1024
|
+
'Width ~28–36rem; fields use fluid',
|
|
1025
|
+
'Cancel/save in #footer; success via message.success',
|
|
1026
|
+
],
|
|
1027
|
+
avoid: [
|
|
1028
|
+
'不要用 visible 代替 v-model',
|
|
1029
|
+
'不要把长多分组表单塞进 Dialog → form-page / form-in-drawer',
|
|
1030
|
+
'不要用 toast 代替单行 message',
|
|
1031
|
+
],
|
|
1032
|
+
avoidEn: [
|
|
1033
|
+
'Do not use visible instead of v-model',
|
|
1034
|
+
'Do not stuff long multi-section forms into Dialog → form-page / form-in-drawer',
|
|
1035
|
+
'Do not use toast for a one-line message',
|
|
1036
|
+
],
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
id: 'form-in-drawer',
|
|
1040
|
+
title: '列表内侧滑表单',
|
|
1041
|
+
titleEn: 'Form in drawer from a list',
|
|
1042
|
+
description: '同页 MDrawer + MForm + #footer;比 Dialog 更长但仍需对照列表。',
|
|
1043
|
+
descriptionEn: 'Same-page MDrawer + MForm + #footer; longer than a dialog but list context still matters.',
|
|
1044
|
+
pageTypes: ['list', 'detail', 'common'],
|
|
1045
|
+
keywords: ['抽屉表单', '侧滑编辑', 'drawer form', 'form-in-drawer', 'side panel edit'],
|
|
1046
|
+
imports: ['MDrawer', 'MForm', 'MFormItem', 'MInput', 'MButton', 'MSpace', 'message'],
|
|
1047
|
+
scriptSetup: `const drawerOpen = ref(false)
|
|
1048
|
+
const submitting = ref(false)
|
|
1049
|
+
const model = reactive({ name: '', note: '' })
|
|
1050
|
+
|
|
1051
|
+
async function onSave() {
|
|
1052
|
+
submitting.value = true
|
|
1053
|
+
try {
|
|
1054
|
+
message.success('已保存')
|
|
1055
|
+
drawerOpen.value = false
|
|
1056
|
+
} finally {
|
|
1057
|
+
submitting.value = false
|
|
1058
|
+
}
|
|
1059
|
+
}`,
|
|
1060
|
+
template: `<MDrawer
|
|
1061
|
+
v-model="drawerOpen"
|
|
1062
|
+
header="编辑详情"
|
|
1063
|
+
position="right"
|
|
1064
|
+
size="28rem"
|
|
1065
|
+
@close="/* reset draft */"
|
|
1066
|
+
>
|
|
1067
|
+
<MForm @submit.prevent="onSave">
|
|
1068
|
+
<MFormItem label="名称" name="name" required>
|
|
1069
|
+
<MInput v-model="model.name" fluid />
|
|
1070
|
+
</MFormItem>
|
|
1071
|
+
<MFormItem label="备注" name="note">
|
|
1072
|
+
<MInput v-model="model.note" fluid />
|
|
1073
|
+
</MFormItem>
|
|
1074
|
+
</MForm>
|
|
1075
|
+
<template #footer>
|
|
1076
|
+
<MSpace style="justify-content: flex-end; width: 100%">
|
|
1077
|
+
<MButton severity="secondary" text :disabled="submitting" @click="drawerOpen = false">
|
|
1078
|
+
取消
|
|
1079
|
+
</MButton>
|
|
1080
|
+
<MButton severity="primary" :loading="submitting" @click="onSave">
|
|
1081
|
+
保存
|
|
1082
|
+
</MButton>
|
|
1083
|
+
</MSpace>
|
|
1084
|
+
</template>
|
|
1085
|
+
</MDrawer>`,
|
|
1086
|
+
rules: ['position 常用 right;较长表单可加大 size', '字段 fluid;底栏放 #footer', '成功用 message.success'],
|
|
1087
|
+
rulesEn: ['position usually right; raise size for longer forms', 'Fields fluid; actions in #footer', 'Success via message.success'],
|
|
1088
|
+
avoid: ['极短 2–3 字段也用 Drawer → form-in-dialog', '危险删除只用 Drawer 无确认 → confirm-delete'],
|
|
1089
|
+
avoidEn: ['Tiny 2–3 field forms → form-in-dialog', 'Destructive delete without confirm → confirm-delete'],
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
id: 'confirm-delete',
|
|
1093
|
+
title: '危险删除确认',
|
|
1094
|
+
titleEn: 'Destructive delete confirm',
|
|
1095
|
+
description: 'MConfirmDialog + message.success;列表行删除默认路径。',
|
|
1096
|
+
descriptionEn: 'MConfirmDialog + message.success; default for list-row delete.',
|
|
1097
|
+
pageTypes: ['list', 'detail', 'common'],
|
|
1098
|
+
keywords: ['删除', '确认', '二次确认', 'confirm', 'delete', 'ConfirmDialog', '危险'],
|
|
1099
|
+
imports: ['MConfirmDialog', 'message'],
|
|
1100
|
+
scriptSetup: `const confirmOpen = ref(false)
|
|
1101
|
+
const pendingId = ref<string | null>(null)
|
|
1102
|
+
|
|
1103
|
+
function askDelete(id: string) {
|
|
1104
|
+
pendingId.value = id
|
|
1105
|
+
confirmOpen.value = true
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function onAcceptDelete() {
|
|
1109
|
+
// await api.remove(pendingId.value)
|
|
1110
|
+
message.success('已删除')
|
|
1111
|
+
pendingId.value = null
|
|
1112
|
+
confirmOpen.value = false
|
|
1113
|
+
}`,
|
|
1114
|
+
template: `<MConfirmDialog
|
|
1115
|
+
v-model="confirmOpen"
|
|
1116
|
+
header="确认删除"
|
|
1117
|
+
message="删除后不可恢复,确定继续?"
|
|
1118
|
+
accept-label="删除"
|
|
1119
|
+
reject-label="取消"
|
|
1120
|
+
accept-severity="danger"
|
|
1121
|
+
@accept="onAcceptDelete"
|
|
1122
|
+
/>`,
|
|
1123
|
+
rules: ['用 MConfirmDialog,不要手写 Dialog 确定/取消', 'acceptSeverity="danger"', '成功用 message.success,不要 toast'],
|
|
1124
|
+
rulesEn: ['Use MConfirmDialog; do not hand-roll Dialog Yes/No', 'acceptSeverity="danger"', 'Success via message.success, not toast'],
|
|
1125
|
+
avoid: ['普通 Dialog 冒充删除确认', '编辑表单误用 ConfirmDialog'],
|
|
1126
|
+
avoidEn: ['Faking delete confirm with Dialog', 'Using ConfirmDialog for edit forms'],
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
id: 'row-actions-menu',
|
|
1130
|
+
title: '表格行更多操作菜单',
|
|
1131
|
+
titleEn: 'Table row overflow actions menu',
|
|
1132
|
+
description: '操作较多时用 MDropdown 收纳行内动作;少量明确操作仍用 list-row-actions。',
|
|
1133
|
+
descriptionEn: 'Use MDropdown when there are many row actions; prefer list-row-actions for a few explicit buttons.',
|
|
1134
|
+
pageTypes: ['list', 'common'],
|
|
1135
|
+
keywords: ['更多', '行菜单', 'dropdown', 'row menu', 'overflow', 'actions menu'],
|
|
1136
|
+
imports: ['MDropdown', 'MButton'],
|
|
1137
|
+
scriptSetup: `const rowMenu = [
|
|
1138
|
+
{ label: '编辑', command: () => {/* open edit */} },
|
|
1139
|
+
{ label: '复制', command: () => {/* duplicate */} },
|
|
1140
|
+
{ label: '删除', severity: 'danger', command: () => {/* askDelete */} },
|
|
1141
|
+
]`,
|
|
1142
|
+
template: `<template #cell-actions>
|
|
1143
|
+
<MDropdown :model="rowMenu">
|
|
1144
|
+
<MButton severity="secondary" size="small" text icon="more-horizontal" aria-label="更多操作" />
|
|
1145
|
+
</MDropdown>
|
|
1146
|
+
</template>`,
|
|
1147
|
+
rules: ['操作很多时再用 Dropdown;2–3 个明确操作用 text 按钮', '危险项走 confirm-delete', '图标按钮必须 aria-label'],
|
|
1148
|
+
rulesEn: ['Use Dropdown when many actions; 2–3 explicit actions stay as text buttons', 'Danger → confirm-delete', 'Icon-only buttons need aria-label'],
|
|
1149
|
+
avoid: ['表单枚举用 Dropdown → Select', '只有编辑/删除也强行收进菜单'],
|
|
1150
|
+
avoidEn: ['Form enums via Dropdown → Select', 'Forcing edit/delete into a menu when two text buttons suffice'],
|
|
1151
|
+
},
|
|
1152
|
+
{
|
|
1153
|
+
id: 'wizard-steps',
|
|
1154
|
+
title: '分步向导 Stepper + Form',
|
|
1155
|
+
titleEn: 'Wizard stepper with per-step form',
|
|
1156
|
+
description: 'MStepper :steps + 每步一个 MForm;完成可用 result-block。',
|
|
1157
|
+
descriptionEn: 'MStepper :steps + one MForm per step; finish with result-block when useful.',
|
|
1158
|
+
pageTypes: ['form', 'common'],
|
|
1159
|
+
keywords: ['向导', '分步', '步骤', 'wizard', 'stepper', 'multi-step'],
|
|
1160
|
+
imports: ['MStepper', 'MForm', 'MFormItem', 'MInput', 'MButton', 'MSpace', 'MPageSection', 'message'],
|
|
1161
|
+
scriptSetup: `const activeStep = ref(0)
|
|
1162
|
+
const submitting = ref(false)
|
|
1163
|
+
const steps = [
|
|
1164
|
+
{ label: '基本信息', description: '名称与说明' },
|
|
1165
|
+
{ label: '确认', description: '核对后创建' },
|
|
1166
|
+
]
|
|
1167
|
+
const model = reactive({ name: '' })
|
|
1168
|
+
|
|
1169
|
+
function back() {
|
|
1170
|
+
if (activeStep.value > 0) activeStep.value -= 1
|
|
1171
|
+
}
|
|
1172
|
+
function next() {
|
|
1173
|
+
if (activeStep.value < steps.length - 1) activeStep.value += 1
|
|
1174
|
+
}
|
|
1175
|
+
async function finish() {
|
|
1176
|
+
submitting.value = true
|
|
1177
|
+
try {
|
|
1178
|
+
message.success('已创建')
|
|
1179
|
+
} finally {
|
|
1180
|
+
submitting.value = false
|
|
1181
|
+
}
|
|
1182
|
+
}`,
|
|
1183
|
+
template: `<MStepper v-model="activeStep" :steps="steps" linear />
|
|
1184
|
+
<MPageSection variant="form">
|
|
1185
|
+
<MForm v-if="activeStep === 0" @submit.prevent="next">
|
|
1186
|
+
<MFormItem label="名称" name="name" required>
|
|
1187
|
+
<MInput v-model="model.name" fluid />
|
|
1188
|
+
</MFormItem>
|
|
1189
|
+
</MForm>
|
|
1190
|
+
<div v-else>
|
|
1191
|
+
<p>确认创建「{{ model.name }}」?</p>
|
|
1192
|
+
</div>
|
|
1193
|
+
</MPageSection>
|
|
1194
|
+
<MPageSection variant="actions">
|
|
1195
|
+
<MSpace>
|
|
1196
|
+
<MButton severity="secondary" text :disabled="activeStep === 0" @click="back">上一步</MButton>
|
|
1197
|
+
<MButton v-if="activeStep < steps.length - 1" severity="primary" @click="next">下一步</MButton>
|
|
1198
|
+
<MButton v-else severity="primary" :loading="submitting" @click="finish">创建</MButton>
|
|
1199
|
+
</MSpace>
|
|
1200
|
+
</MPageSection>`,
|
|
1201
|
+
rules: ['用 :steps(不是 :items)', '每步一个主任务;进入下一步前校验当前步', '完成文案与「下一步」区分'],
|
|
1202
|
+
rulesEn: ['Use :steps (not :items)', 'One job per step; validate before advancing', 'Finish label differs from Next'],
|
|
1203
|
+
avoid: ['所有字段藏在单页超长表单', '允许跳过有前置依赖的步骤'],
|
|
1204
|
+
avoidEn: ['Hiding all fields on one long page', 'Skipping steps with prerequisites'],
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
id: 'auth-split-shell',
|
|
1208
|
+
title: '登录左右分栏壳',
|
|
1209
|
+
titleEn: 'Auth split brand + form shell',
|
|
1210
|
+
description: '左品牌 / 右表单;控件 M*;失败用 role="alert" 或 errorMessage,不用 Toast。',
|
|
1211
|
+
descriptionEn: 'Brand left / form right; M* controls; failures via role="alert" or errorMessage, not Toast.',
|
|
1212
|
+
pageTypes: ['common'],
|
|
1213
|
+
keywords: ['登录', '认证', '分栏', 'login', 'auth', 'split', 'brand'],
|
|
1214
|
+
imports: ['MForm', 'MFormItem', 'MInput', 'MInputPassword', 'MButton', 'MSpace', 'MConfigProvider', 'zhCN'],
|
|
1215
|
+
scriptSetup: `const submitting = ref(false)
|
|
1216
|
+
const formError = ref('')
|
|
1217
|
+
const model = reactive({ email: '', password: '' })
|
|
1218
|
+
|
|
1219
|
+
async function onSubmit() {
|
|
1220
|
+
formError.value = ''
|
|
1221
|
+
submitting.value = true
|
|
1222
|
+
try {
|
|
1223
|
+
if (!model.email || !model.password) {
|
|
1224
|
+
formError.value = '请输入邮箱和密码后再试。'
|
|
1225
|
+
}
|
|
1226
|
+
} finally {
|
|
1227
|
+
submitting.value = false
|
|
1228
|
+
}
|
|
1229
|
+
}`,
|
|
1230
|
+
template: `<MConfigProvider :locale="zhCN">
|
|
1231
|
+
<div class="login-shell">
|
|
1232
|
+
<aside class="login-brand" aria-label="品牌">
|
|
1233
|
+
<p class="login-brand__mark">产品名</p>
|
|
1234
|
+
<h1 class="login-brand__title">一句品牌主张</h1>
|
|
1235
|
+
<p class="login-brand__lead">一句支持说明。</p>
|
|
1236
|
+
</aside>
|
|
1237
|
+
<main class="login-main">
|
|
1238
|
+
<div class="login-panel">
|
|
1239
|
+
<header class="login-panel__header">
|
|
1240
|
+
<h2>登录</h2>
|
|
1241
|
+
<p>使用工作邮箱进入。</p>
|
|
1242
|
+
</header>
|
|
1243
|
+
<p v-if="formError" class="login-alert" role="alert">{{ formError }}</p>
|
|
1244
|
+
<MForm @submit="onSubmit">
|
|
1245
|
+
<MFormItem label="邮箱" name="email" required>
|
|
1246
|
+
<MInput v-model="model.email" type="email" autocomplete="username" fluid />
|
|
1247
|
+
</MFormItem>
|
|
1248
|
+
<MFormItem label="密码" name="password" required>
|
|
1249
|
+
<MInputPassword v-model="model.password" autocomplete="current-password" fluid />
|
|
1250
|
+
</MFormItem>
|
|
1251
|
+
<MSpace style="margin-top: var(--m-space-2)" alignment="center">
|
|
1252
|
+
<MButton type="submit" label="登录" severity="primary" :loading="submitting" />
|
|
1253
|
+
<MButton type="button" label="忘记密码" severity="secondary" text />
|
|
1254
|
+
</MSpace>
|
|
1255
|
+
</MForm>
|
|
1256
|
+
</div>
|
|
1257
|
+
</main>
|
|
1258
|
+
</div>
|
|
1259
|
+
</MConfigProvider>`,
|
|
1260
|
+
rules: [
|
|
1261
|
+
'品牌区用 --m-* / color-mix,默认扁平勿叠 aurora',
|
|
1262
|
+
'失败留在表单区(role="alert" 或 errorMessage),不要 Toast',
|
|
1263
|
+
'窄屏改为单栏:表单在上或品牌收短',
|
|
1264
|
+
],
|
|
1265
|
+
rulesEn: [
|
|
1266
|
+
'Brand panel: --m-* / color-mix; flat by default — no stacked aurora',
|
|
1267
|
+
'Keep failures in the form (role="alert" or errorMessage), not Toast',
|
|
1268
|
+
'Narrow screens: single column',
|
|
1269
|
+
],
|
|
1270
|
+
avoid: [
|
|
1271
|
+
'不要把 MMessage 当内嵌 Alert',
|
|
1272
|
+
'不要引入第二套 UI 库做登录壳',
|
|
1273
|
+
'不要未询问就加玻璃/霓虹/全页渐变',
|
|
1274
|
+
],
|
|
1275
|
+
avoidEn: [
|
|
1276
|
+
'Do not use MMessage as an inline Alert',
|
|
1277
|
+
'Do not bring a second UI kit for the auth shell',
|
|
1278
|
+
'Do not add glass/neon/full-page gradients unless asked',
|
|
1279
|
+
],
|
|
1280
|
+
},
|
|
520
1281
|
{
|
|
521
1282
|
id: 'layout-app-shell',
|
|
522
1283
|
title: '后台应用骨架',
|
|
@@ -536,29 +1297,29 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
|
|
|
536
1297
|
'MConfigProvider',
|
|
537
1298
|
'zhCN',
|
|
538
1299
|
],
|
|
539
|
-
template: `<MConfigProvider :locale="zhCN">
|
|
540
|
-
<MLayout fill-viewport has-sider>
|
|
541
|
-
<MLayoutSider v-model:collapsed="siderCollapsed" bordered :collapsed-width="72">
|
|
542
|
-
<MMenu
|
|
543
|
-
:model="[
|
|
544
|
-
{ key: 'users', label: '用户管理', icon: 'user', to: '/users' },
|
|
545
|
-
{ key: 'roles', label: '角色管理', icon: 'shield', to: '/roles' },
|
|
546
|
-
]"
|
|
547
|
-
:collapsed="siderCollapsed"
|
|
548
|
-
:collapsed-width="72"
|
|
549
|
-
/>
|
|
550
|
-
</MLayoutSider>
|
|
551
|
-
<MLayout>
|
|
552
|
-
<MLayoutHeader :padding="'var(--m-space-4) var(--m-space-6)'">
|
|
553
|
-
<MBreadcrumb :model="[{ label: '首页', to: '/' }, { label: '当前页' }]" />
|
|
554
|
-
</MLayoutHeader>
|
|
555
|
-
<MLayoutContent>
|
|
556
|
-
<MPageContent>
|
|
557
|
-
<!-- page sections -->
|
|
558
|
-
</MPageContent>
|
|
559
|
-
</MLayoutContent>
|
|
560
|
-
</MLayout>
|
|
561
|
-
</MLayout>
|
|
1300
|
+
template: `<MConfigProvider :locale="zhCN">
|
|
1301
|
+
<MLayout fill-viewport has-sider>
|
|
1302
|
+
<MLayoutSider v-model:collapsed="siderCollapsed" bordered :collapsed-width="72">
|
|
1303
|
+
<MMenu
|
|
1304
|
+
:model="[
|
|
1305
|
+
{ key: 'users', label: '用户管理', icon: 'user', to: '/users' },
|
|
1306
|
+
{ key: 'roles', label: '角色管理', icon: 'shield', to: '/roles' },
|
|
1307
|
+
]"
|
|
1308
|
+
:collapsed="siderCollapsed"
|
|
1309
|
+
:collapsed-width="72"
|
|
1310
|
+
/>
|
|
1311
|
+
</MLayoutSider>
|
|
1312
|
+
<MLayout>
|
|
1313
|
+
<MLayoutHeader :padding="'var(--m-space-4) var(--m-space-6)'">
|
|
1314
|
+
<MBreadcrumb :model="[{ label: '首页', to: '/' }, { label: '当前页' }]" />
|
|
1315
|
+
</MLayoutHeader>
|
|
1316
|
+
<MLayoutContent>
|
|
1317
|
+
<MPageContent>
|
|
1318
|
+
<!-- page sections -->
|
|
1319
|
+
</MPageContent>
|
|
1320
|
+
</MLayoutContent>
|
|
1321
|
+
</MLayout>
|
|
1322
|
+
</MLayout>
|
|
562
1323
|
</MConfigProvider>`,
|
|
563
1324
|
rules: [
|
|
564
1325
|
'根布局用 fillViewport',
|
|
@@ -591,19 +1352,19 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
|
|
|
591
1352
|
'log',
|
|
592
1353
|
],
|
|
593
1354
|
imports: ['MScrollbar', 'MCard'],
|
|
594
|
-
template: `<MCard title="活动日志">
|
|
595
|
-
<MScrollbar max-height="16rem">
|
|
596
|
-
<ul style="margin:0;padding:0;list-style:none">
|
|
597
|
-
<li v-for="item in logLines" :key="item.id" style="padding:var(--m-space-2) 0;border-bottom:1px solid var(--m-color-border)">
|
|
598
|
-
{{ item.text }}
|
|
599
|
-
</li>
|
|
600
|
-
</ul>
|
|
601
|
-
</MScrollbar>
|
|
1355
|
+
template: `<MCard title="活动日志">
|
|
1356
|
+
<MScrollbar max-height="16rem">
|
|
1357
|
+
<ul style="margin:0;padding:0;list-style:none">
|
|
1358
|
+
<li v-for="item in logLines" :key="item.id" style="padding:var(--m-space-2) 0;border-bottom:1px solid var(--m-color-border)">
|
|
1359
|
+
{{ item.text }}
|
|
1360
|
+
</li>
|
|
1361
|
+
</ul>
|
|
1362
|
+
</MScrollbar>
|
|
602
1363
|
</MCard>`,
|
|
603
|
-
scriptSetup: `const logLines = [
|
|
604
|
-
{ id: '1', text: '用户 admin 登录成功' },
|
|
605
|
-
{ id: '2', text: '导出任务已完成' },
|
|
606
|
-
{ id: '3', text: '配置已保存' },
|
|
1364
|
+
scriptSetup: `const logLines = [
|
|
1365
|
+
{ id: '1', text: '用户 admin 登录成功' },
|
|
1366
|
+
{ id: '2', text: '导出任务已完成' },
|
|
1367
|
+
{ id: '3', text: '配置已保存' },
|
|
607
1368
|
]`,
|
|
608
1369
|
rules: [
|
|
609
1370
|
'整页主滚动推荐 MLayout fillViewport',
|