@morya-ui/mcp 0.2.3 → 0.2.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/README.md +1 -1
- package/data/catalog.json +1871 -1017
- package/data/example-coverage.json +100 -9
- package/data/golden-pages/dashboard-page.vue +1 -1
- package/data/golden-pages/empty-state.vue +66 -0
- package/data/golden-pages/form-page.vue +7 -3
- package/data/golden-pages/landing-page.vue +328 -0
- package/data/golden-pages/list-page.vue +16 -6
- package/data/golden-pages/login-page.vue +191 -0
- package/dist/__tests__/resources.test.js +1 -1
- package/dist/__tests__/tools.test.js +56 -3
- package/dist/decisions.js +70 -0
- package/dist/golden-pages.js +22 -1
- package/dist/index.js +1 -1
- package/dist/page-snippets.js +115 -2
- package/dist/patterns.d.ts +41 -7
- package/dist/patterns.js +315 -33
- package/dist/resources.d.ts +1 -1
- package/dist/tools.js +153 -56
- package/package.json +1 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 黄金样例:登录页(Account + 轻量品牌)
|
|
4
|
+
* @see DESIGN.md §3 · morya-ui-pages surfaces § Account
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
MButton,
|
|
8
|
+
MConfigProvider,
|
|
9
|
+
MForm,
|
|
10
|
+
MFormItem,
|
|
11
|
+
MInput,
|
|
12
|
+
MInputPassword,
|
|
13
|
+
MSpace,
|
|
14
|
+
zhCN,
|
|
15
|
+
} from 'morya-ui'
|
|
16
|
+
import { reactive, ref } from 'vue'
|
|
17
|
+
|
|
18
|
+
const submitting = ref(false)
|
|
19
|
+
const formError = ref('')
|
|
20
|
+
|
|
21
|
+
const model = reactive({
|
|
22
|
+
email: '',
|
|
23
|
+
password: '',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
async function onSubmit() {
|
|
27
|
+
formError.value = ''
|
|
28
|
+
submitting.value = true
|
|
29
|
+
try {
|
|
30
|
+
// await api.login(model)
|
|
31
|
+
if (!model.email || !model.password) {
|
|
32
|
+
formError.value = '请输入邮箱和密码后再试。'
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
} finally {
|
|
36
|
+
submitting.value = false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<MConfigProvider :locale="zhCN">
|
|
43
|
+
<div class="login-shell">
|
|
44
|
+
<aside class="login-brand" aria-label="品牌">
|
|
45
|
+
<p class="login-brand__mark">青禾书房</p>
|
|
46
|
+
<h1 class="login-brand__title">把好书留在手边</h1>
|
|
47
|
+
<p class="login-brand__lead">
|
|
48
|
+
店员与会员共用同一套账户。登录后继续进货、上架与会员服务。
|
|
49
|
+
</p>
|
|
50
|
+
</aside>
|
|
51
|
+
|
|
52
|
+
<main class="login-main">
|
|
53
|
+
<div class="login-panel">
|
|
54
|
+
<header class="login-panel__header">
|
|
55
|
+
<h2>登录</h2>
|
|
56
|
+
<p>使用工作邮箱进入后台。</p>
|
|
57
|
+
</header>
|
|
58
|
+
|
|
59
|
+
<!-- 表单级常驻错误:token 样式告警条(字段错误优先用 errorMessage) -->
|
|
60
|
+
<p v-if="formError" class="login-alert" role="alert">
|
|
61
|
+
{{ formError }}
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
<MForm @submit="onSubmit">
|
|
65
|
+
<MFormItem label="邮箱" name="email" required>
|
|
66
|
+
<MInput
|
|
67
|
+
v-model="model.email"
|
|
68
|
+
type="email"
|
|
69
|
+
placeholder="name@qinghe.example"
|
|
70
|
+
autocomplete="username"
|
|
71
|
+
fluid
|
|
72
|
+
/>
|
|
73
|
+
</MFormItem>
|
|
74
|
+
|
|
75
|
+
<MFormItem label="密码" name="password" required>
|
|
76
|
+
<MInputPassword
|
|
77
|
+
v-model="model.password"
|
|
78
|
+
placeholder="请输入密码"
|
|
79
|
+
autocomplete="current-password"
|
|
80
|
+
fluid
|
|
81
|
+
/>
|
|
82
|
+
</MFormItem>
|
|
83
|
+
|
|
84
|
+
<MSpace style="margin-top: var(--m-space-2)" alignment="center">
|
|
85
|
+
<MButton type="submit" label="登录" :loading="submitting" />
|
|
86
|
+
<MButton type="button" label="忘记密码" severity="secondary" text />
|
|
87
|
+
</MSpace>
|
|
88
|
+
</MForm>
|
|
89
|
+
</div>
|
|
90
|
+
</main>
|
|
91
|
+
</div>
|
|
92
|
+
</MConfigProvider>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<style scoped>
|
|
96
|
+
.login-shell {
|
|
97
|
+
display: grid;
|
|
98
|
+
grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
|
|
99
|
+
min-height: 100vh;
|
|
100
|
+
background: var(--m-color-surface);
|
|
101
|
+
color: var(--m-color-text);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.login-brand {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
justify-content: flex-end;
|
|
108
|
+
gap: var(--m-space-4);
|
|
109
|
+
padding: clamp(2rem, 6vw, 4.5rem);
|
|
110
|
+
background:
|
|
111
|
+
linear-gradient(
|
|
112
|
+
165deg,
|
|
113
|
+
color-mix(in srgb, var(--m-color-primary) 18%, var(--m-color-surface)) 0%,
|
|
114
|
+
var(--m-color-surface) 55%,
|
|
115
|
+
color-mix(in srgb, var(--m-color-border) 35%, var(--m-color-surface)) 100%
|
|
116
|
+
);
|
|
117
|
+
border-right: 1px solid var(--m-color-border);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.login-brand__mark {
|
|
121
|
+
margin: 0;
|
|
122
|
+
font-size: 0.8125rem;
|
|
123
|
+
letter-spacing: 0.14em;
|
|
124
|
+
text-transform: uppercase;
|
|
125
|
+
color: var(--m-color-text-muted);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.login-brand__title {
|
|
129
|
+
margin: 0;
|
|
130
|
+
max-width: 12em;
|
|
131
|
+
font-size: clamp(2rem, 4vw, 3rem);
|
|
132
|
+
font-weight: 650;
|
|
133
|
+
line-height: 1.15;
|
|
134
|
+
letter-spacing: -0.03em;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.login-brand__lead {
|
|
138
|
+
margin: 0;
|
|
139
|
+
max-width: 28rem;
|
|
140
|
+
color: var(--m-color-text-muted);
|
|
141
|
+
line-height: 1.6;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.login-main {
|
|
145
|
+
display: grid;
|
|
146
|
+
place-items: center;
|
|
147
|
+
padding: var(--m-space-6);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.login-panel {
|
|
151
|
+
width: min(100%, 22rem);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.login-panel__header {
|
|
155
|
+
margin-bottom: var(--m-space-5);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.login-panel__header h2 {
|
|
159
|
+
margin: 0 0 var(--m-space-2);
|
|
160
|
+
font-size: 1.5rem;
|
|
161
|
+
font-weight: 650;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.login-panel__header p {
|
|
165
|
+
margin: 0;
|
|
166
|
+
color: var(--m-color-text-muted);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.login-alert {
|
|
170
|
+
margin: 0 0 var(--m-space-4);
|
|
171
|
+
padding: var(--m-space-3) var(--m-space-4);
|
|
172
|
+
border: 1px solid color-mix(in srgb, var(--m-color-danger) 40%, var(--m-color-border));
|
|
173
|
+
border-radius: var(--m-radius-md);
|
|
174
|
+
background: color-mix(in srgb, var(--m-color-danger) 10%, var(--m-color-surface));
|
|
175
|
+
color: var(--m-color-danger);
|
|
176
|
+
font-size: 0.875rem;
|
|
177
|
+
line-height: 1.45;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@media (max-width: 768px) {
|
|
181
|
+
.login-shell {
|
|
182
|
+
grid-template-columns: 1fr;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.login-brand {
|
|
186
|
+
border-right: 0;
|
|
187
|
+
border-bottom: 1px solid var(--m-color-border);
|
|
188
|
+
min-height: 12rem;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { countCatalogResources, countCatalogResourceTemplates } from '../resources.js';
|
|
2
3
|
import { createToolHandlers } from '../tools.js';
|
|
3
|
-
import { countCatalogResourceTemplates, countCatalogResources } from '../resources.js';
|
|
4
4
|
describe('@morya-ui/mcp resources', () => {
|
|
5
5
|
const handlers = createToolHandlers();
|
|
6
6
|
it('registers static resources and resource templates', () => {
|
|
@@ -107,12 +107,65 @@ describe('@morya-ui/mcp handlers', () => {
|
|
|
107
107
|
const result = read(handlers.search({ query: 'toolbar', scope: 'snippets', limit: 5 }));
|
|
108
108
|
expect(result.items.some((item) => item.type === 'snippet' && item.id === 'list-toolbar')).toBe(true);
|
|
109
109
|
});
|
|
110
|
-
it('
|
|
110
|
+
it('suggests double-border page composition as advisory standard', () => {
|
|
111
111
|
const result = read(handlers.validatePage({
|
|
112
112
|
code: '<MLayoutContent><MCard><MTable bordered /></MCard></MLayoutContent>',
|
|
113
113
|
}));
|
|
114
|
-
expect(result.ok).toBe(
|
|
115
|
-
expect(result.
|
|
114
|
+
expect(result.ok).toBe(true);
|
|
115
|
+
expect(result.advisory).toBe(true);
|
|
116
|
+
expect(result.suggestions.some((item) => item.type === 'double-border')).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
it('suggests MScrollbar over inline overflow scroll styles', () => {
|
|
119
|
+
const result = read(handlers.validatePage({
|
|
120
|
+
code: '<div style="overflow:auto;height:20rem"><MTable /></div>',
|
|
121
|
+
}));
|
|
122
|
+
expect(result.ok).toBe(true);
|
|
123
|
+
expect(result.suggestions.some((item) => item.type === 'native-scroll' && item.standardId === 'scroll')).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
it('suggests MScrollbar over overflow in style blocks', () => {
|
|
126
|
+
const result = read(handlers.validatePage({
|
|
127
|
+
code: '<template><div class="panel">x</div></template><style>.panel { max-height: 20rem; overflow-y: auto; }</style>',
|
|
128
|
+
}));
|
|
129
|
+
expect(result.ok).toBe(true);
|
|
130
|
+
expect(result.suggestions.some((item) => item.type === 'native-scroll')).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
it('returns scrollable-panel snippet for scroll queries', () => {
|
|
133
|
+
const result = read(handlers.getPageSnippet({ section: 'scrollable-panel' }));
|
|
134
|
+
expect(result.id).toBe('scrollable-panel');
|
|
135
|
+
expect(result.template).toContain('MScrollbar');
|
|
136
|
+
expect(result.imports).toContain('MScrollbar');
|
|
137
|
+
});
|
|
138
|
+
it('keeps recommend_page focused on pattern matching', () => {
|
|
139
|
+
const result = read(handlers.recommendPage({ intent: '用户列表页', pageType: 'list' }));
|
|
140
|
+
expect(result.matchedPattern).toBe('admin-list');
|
|
141
|
+
expect(result.nextStep).toContain('get_design_rules');
|
|
142
|
+
expect(result.pageStandards).toBeUndefined();
|
|
143
|
+
expect(result.scrollStandards).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
it('keeps get_golden_page focused on source code', () => {
|
|
146
|
+
const result = read(handlers.getGoldenPage({ page: 'list-page' }));
|
|
147
|
+
expect(result.source).toContain('MLayout');
|
|
148
|
+
expect(result.nextStep).toContain('get_design_rules');
|
|
149
|
+
expect(result.pageStandards).toBeUndefined();
|
|
150
|
+
expect(result.scrollStandards).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
it('exposes page standards from get_design_rules', () => {
|
|
153
|
+
const result = read(handlers.getDesignRules({ mode: 'zh-CN' }));
|
|
154
|
+
expect(result.meta.nature).toBe('recommended');
|
|
155
|
+
expect(result.standards.some((item) => item.id === 'scroll')).toBe(true);
|
|
156
|
+
expect(result.standards.some((item) => item.id === 'feedback')).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
it('reads page scroll decision guide by id', () => {
|
|
159
|
+
const result = read(handlers.recommendComponent({ decision: 'page-scroll-choice' }));
|
|
160
|
+
expect(result.id).toBe('page-scroll-choice');
|
|
161
|
+
expect(result.options.some((option) => option.component === 'MScrollbar')).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
it('suggests redundant wrappers around MPage blocks', () => {
|
|
164
|
+
const result = read(handlers.validatePage({
|
|
165
|
+
code: '<MPageContent><div style="padding:24px"><MPageFilters /></div></MPageContent>',
|
|
166
|
+
}));
|
|
167
|
+
expect(result.ok).toBe(true);
|
|
168
|
+
expect(result.suggestions.some((item) => item.type === 'redundant-wrapper' && item.standardId === 'page-sections')).toBe(true);
|
|
116
169
|
});
|
|
117
170
|
it('lists component decision guides when query is omitted', () => {
|
|
118
171
|
const result = read(handlers.recommendComponent({ limit: 5 }));
|
package/dist/decisions.js
CHANGED
|
@@ -227,6 +227,76 @@ export const componentDecisions = [
|
|
|
227
227
|
},
|
|
228
228
|
],
|
|
229
229
|
},
|
|
230
|
+
{
|
|
231
|
+
id: 'page-scroll-choice',
|
|
232
|
+
title: '页面滚动如何选择',
|
|
233
|
+
titleEn: 'Choosing page scroll strategy',
|
|
234
|
+
question: '这是整页滚动、组件内置滚动,还是业务手写的局部滚动区?',
|
|
235
|
+
questionEn: 'Is this whole-page scroll, built-in component scroll, or a hand-written local scroll region?',
|
|
236
|
+
keywords: [
|
|
237
|
+
'scroll',
|
|
238
|
+
'scrollbar',
|
|
239
|
+
'滚动',
|
|
240
|
+
'滚动条',
|
|
241
|
+
'overflow',
|
|
242
|
+
'页面滚动',
|
|
243
|
+
'layout scroll',
|
|
244
|
+
'panel scroll',
|
|
245
|
+
],
|
|
246
|
+
options: [
|
|
247
|
+
{
|
|
248
|
+
component: 'MLayout fillViewport',
|
|
249
|
+
when: [
|
|
250
|
+
'整页后台列表/表单/仪表盘',
|
|
251
|
+
'需要 Header + Content + 可选 Sider 的应用骨架',
|
|
252
|
+
'页面主滚动应随 Layout 主题化',
|
|
253
|
+
],
|
|
254
|
+
whenEn: [
|
|
255
|
+
'Full admin list/form/dashboard pages',
|
|
256
|
+
'App shell with header, content, and optional sider',
|
|
257
|
+
'Main page scroll should follow the layout theme',
|
|
258
|
+
],
|
|
259
|
+
avoidWhen: ['单个卡片内部的小块内容', '需要业务自行控制滚动的 Dialog/Drawer 内容'],
|
|
260
|
+
avoidWhenEn: ['Small regions inside a single card', 'Dialog/Drawer bodies where apps control scrolling'],
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
component: 'MScrollbar',
|
|
264
|
+
when: [
|
|
265
|
+
'业务自行限高的卡片正文、侧栏、日志列表,且希望主题化滚动条',
|
|
266
|
+
'Dialog / Drawer 等内容区需要主题滚动时由业务显式包一层',
|
|
267
|
+
'组件未内置滚动、又需要统一滚动外观时',
|
|
268
|
+
],
|
|
269
|
+
whenEn: [
|
|
270
|
+
'App-owned capped regions (card bodies, side panels, logs) that want themed scrollbars',
|
|
271
|
+
'Dialog / Drawer content where the app opts into themed scrolling',
|
|
272
|
+
'No built-in component scroll, but a consistent scrollbar look is desired',
|
|
273
|
+
],
|
|
274
|
+
avoidWhen: [
|
|
275
|
+
'Dialog / Drawer / Popover / Splitter 等用户内容插槽被组件库强行包滚动',
|
|
276
|
+
'Textarea 等原生控件自身的滚动',
|
|
277
|
+
'MTable / MLayoutContent / Select 弹出层等已内置滚动的区域',
|
|
278
|
+
],
|
|
279
|
+
avoidWhenEn: [
|
|
280
|
+
'Library-forced scroll around user content slots such as Dialog, Drawer, Popover, or Splitter',
|
|
281
|
+
'Native control scrolling such as Textarea',
|
|
282
|
+
'Areas that already scroll internally (MTable, MLayoutContent, Select popups)',
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
component: 'Built-in (no extra wrapper)',
|
|
287
|
+
when: [
|
|
288
|
+
'MLayout、MTable、MVirtualScroller、菜单/下拉面板等已内置 MScrollbar',
|
|
289
|
+
'浮层菜单与子菜单(Dropdown/ContextMenu/TieredMenu/Menu flyout)',
|
|
290
|
+
],
|
|
291
|
+
whenEn: [
|
|
292
|
+
'MLayout, MTable, MVirtualScroller, and menu/select panels already scroll internally',
|
|
293
|
+
'Overlay menus and nested flyouts (Dropdown/ContextMenu/TieredMenu/Menu flyout)',
|
|
294
|
+
],
|
|
295
|
+
avoidWhen: ['在已内置滚动的组件外再包一层滚动容器'],
|
|
296
|
+
avoidWhenEn: ['Wrapping another scroll container around built-in scroll chrome'],
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
},
|
|
230
300
|
{
|
|
231
301
|
id: 'surface-nesting-choice',
|
|
232
302
|
title: '如何避免双边框与多余容器',
|
package/dist/golden-pages.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
export const goldenPageCatalog = [
|
|
@@ -23,6 +23,27 @@ export const goldenPageCatalog = [
|
|
|
23
23
|
titleEn: 'Dashboard golden sample',
|
|
24
24
|
patternId: 'dashboard',
|
|
25
25
|
},
|
|
26
|
+
{
|
|
27
|
+
id: 'login-page',
|
|
28
|
+
file: 'login-page.vue',
|
|
29
|
+
title: '登录页黄金样例',
|
|
30
|
+
titleEn: 'Login page golden sample',
|
|
31
|
+
patternId: 'auth-page',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'landing-page',
|
|
35
|
+
file: 'landing-page.vue',
|
|
36
|
+
title: '营销落地页黄金样例',
|
|
37
|
+
titleEn: 'Marketing landing golden sample',
|
|
38
|
+
patternId: 'marketing-landing',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'empty-state',
|
|
42
|
+
file: 'empty-state.vue',
|
|
43
|
+
title: '空状态黄金样例',
|
|
44
|
+
titleEn: 'Empty state golden sample',
|
|
45
|
+
patternId: 'empty-state',
|
|
46
|
+
},
|
|
26
47
|
];
|
|
27
48
|
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
49
|
const bundledDir = join(pkgRoot, 'data/golden-pages');
|
package/dist/index.js
CHANGED
|
@@ -102,7 +102,7 @@ register('recommend_component', 'List, read, or recommend component selection gu
|
|
|
102
102
|
offset: z.number().int().min(0).optional(),
|
|
103
103
|
}, async (args) => handlers.recommendComponent(args));
|
|
104
104
|
register('list_golden_pages', 'List golden page samples for list, form, and dashboard layouts.', { mode: z.string().optional() }, async (args) => handlers.listGoldenPageCatalog(args));
|
|
105
|
-
register('get_golden_page', 'Read a golden page Vue source sample (list-page, form-page, dashboard-page).', {
|
|
105
|
+
register('get_golden_page', 'Read a golden page Vue source sample (list-page, form-page, dashboard-page, login-page, landing-page, empty-state).', {
|
|
106
106
|
page: z.string().min(1),
|
|
107
107
|
mode: z.string().optional(),
|
|
108
108
|
}, async (args) => handlers.getGoldenPage(args));
|
package/dist/page-snippets.js
CHANGED
|
@@ -127,11 +127,75 @@ const loading = ref(false)`,
|
|
|
127
127
|
:severity="value === 'active' ? 'success' : 'secondary'"
|
|
128
128
|
/>
|
|
129
129
|
</template>`,
|
|
130
|
-
rules: ['
|
|
131
|
-
rulesEn: ['Use MTag for status, not Button colors'],
|
|
130
|
+
rules: ['分类/强调态用 MTag;更轻的圆点+文案用 MStatus', '不要用 Button 颜色表达状态'],
|
|
131
|
+
rulesEn: ['Use MTag for chip-like status; use MStatus for lighter dot+label', 'Do not use Button colors for status'],
|
|
132
132
|
avoid: ['不要用裸文本颜色区分状态'],
|
|
133
133
|
avoidEn: ['Do not rely on raw text color for status'],
|
|
134
134
|
},
|
|
135
|
+
{
|
|
136
|
+
id: 'list-status-dot',
|
|
137
|
+
title: '表格状态列 Status',
|
|
138
|
+
titleEn: 'Table status dot cell',
|
|
139
|
+
description: '在 #cell-status 中用 MStatus 展示轻量业务状态。',
|
|
140
|
+
descriptionEn: 'Render lightweight business status with MStatus in #cell-status.',
|
|
141
|
+
pageTypes: ['list'],
|
|
142
|
+
keywords: ['状态', 'status', 'dot', 'cell-status', '在线'],
|
|
143
|
+
imports: ['MStatus'],
|
|
144
|
+
template: `<template #cell-status="{ value }">
|
|
145
|
+
<MStatus
|
|
146
|
+
:label="value === 'online' ? '在线' : '离线'"
|
|
147
|
+
:severity="value === 'online' ? 'success' : 'secondary'"
|
|
148
|
+
/>
|
|
149
|
+
</template>`,
|
|
150
|
+
rules: ['行内轻量状态优先 MStatus', '不要用 Button 颜色表达状态'],
|
|
151
|
+
rulesEn: ['Prefer MStatus for lightweight inline status', 'Do not use Button colors for status'],
|
|
152
|
+
avoid: ['不要用裸文本颜色区分状态'],
|
|
153
|
+
avoidEn: ['Do not rely on raw text color for status'],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'empty-block',
|
|
157
|
+
title: '空状态 MEmpty',
|
|
158
|
+
titleEn: 'Empty state with MEmpty',
|
|
159
|
+
description: '列表或内容区无数据时使用 MEmpty,操作放在 #extra。',
|
|
160
|
+
descriptionEn: 'Use MEmpty for no-data regions; put actions in #extra.',
|
|
161
|
+
pageTypes: ['list', 'common'],
|
|
162
|
+
keywords: ['空状态', '无数据', 'empty', 'zero state', 'no data'],
|
|
163
|
+
imports: ['MEmpty', 'MButton'],
|
|
164
|
+
template: `<MEmpty
|
|
165
|
+
title="还没有数据"
|
|
166
|
+
description="创建第一条记录后即可在此查看。"
|
|
167
|
+
icon="database"
|
|
168
|
+
>
|
|
169
|
+
<template #extra>
|
|
170
|
+
<MButton label="新建" />
|
|
171
|
+
<MButton label="导入" severity="secondary" text />
|
|
172
|
+
</template>
|
|
173
|
+
</MEmpty>`,
|
|
174
|
+
rules: ['正常无数据用 MEmpty,不要用错误色', '主 CTA 用 primary,次动作用 text/secondary'],
|
|
175
|
+
rulesEn: ['Use MEmpty for normal emptiness, not error colors', 'Primary CTA + secondary/text for lesser actions'],
|
|
176
|
+
avoid: ['不要用手写 div 拼空态', '不要用 MResult 表达无数据'],
|
|
177
|
+
avoidEn: ['Do not hand-roll empty markup', 'Do not use MResult for no-data'],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'result-block',
|
|
181
|
+
title: '结果页 MResult',
|
|
182
|
+
titleEn: 'Result page with MResult',
|
|
183
|
+
description: '提交成功、失败或 403/404/500 使用 MResult。',
|
|
184
|
+
descriptionEn: 'Use MResult for submit outcomes and 403/404/500 pages.',
|
|
185
|
+
pageTypes: ['common'],
|
|
186
|
+
keywords: ['结果', '成功', '失败', '404', '403', 'result', 'success', 'error'],
|
|
187
|
+
imports: ['MResult', 'MButton'],
|
|
188
|
+
template: `<MResult status="success" description="订单已创建,可在列表中查看详情。">
|
|
189
|
+
<template #extra>
|
|
190
|
+
<MButton label="查看订单" />
|
|
191
|
+
<MButton label="返回列表" severity="secondary" text />
|
|
192
|
+
</template>
|
|
193
|
+
</MResult>`,
|
|
194
|
+
rules: ['流程终点用 MResult', '提供明确下一步操作'],
|
|
195
|
+
rulesEn: ['Use MResult for terminal outcomes', 'Provide clear next-step actions'],
|
|
196
|
+
avoid: ['不要用 MEmpty 表达 403/404/失败', '不要只靠颜色表达结果'],
|
|
197
|
+
avoidEn: ['Do not use MEmpty for 403/404/failure', 'Do not rely on color alone'],
|
|
198
|
+
},
|
|
135
199
|
{
|
|
136
200
|
id: 'form-header',
|
|
137
201
|
title: '表单页标题区',
|
|
@@ -348,6 +412,55 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
|
|
|
348
412
|
avoid: ['不要手写 min-height: 100vh'],
|
|
349
413
|
avoidEn: ['Do not hand-write min-height: 100vh'],
|
|
350
414
|
},
|
|
415
|
+
{
|
|
416
|
+
id: 'scrollable-panel',
|
|
417
|
+
title: '局部可滚动面板',
|
|
418
|
+
titleEn: 'Scrollable local panel',
|
|
419
|
+
description: '业务自行限高滚动时的可选写法:显式使用 MScrollbar。组件内置滚动区无需再包。',
|
|
420
|
+
descriptionEn: 'Optional pattern when the app wants a themed capped scroll region. Do not wrap component-owned scroll chrome again.',
|
|
421
|
+
pageTypes: ['list', 'form', 'dashboard', 'detail', 'common'],
|
|
422
|
+
keywords: [
|
|
423
|
+
'scroll',
|
|
424
|
+
'scrollbar',
|
|
425
|
+
'滚动',
|
|
426
|
+
'滚动条',
|
|
427
|
+
'overflow',
|
|
428
|
+
'max-height',
|
|
429
|
+
'panel',
|
|
430
|
+
'card body',
|
|
431
|
+
'sidebar',
|
|
432
|
+
'log',
|
|
433
|
+
],
|
|
434
|
+
imports: ['MScrollbar', 'MCard'],
|
|
435
|
+
template: `<MCard title="活动日志">
|
|
436
|
+
<MScrollbar max-height="16rem">
|
|
437
|
+
<ul style="margin:0;padding:0;list-style:none">
|
|
438
|
+
<li v-for="item in logLines" :key="item.id" style="padding:var(--m-space-2) 0;border-bottom:1px solid var(--m-color-border)">
|
|
439
|
+
{{ item.text }}
|
|
440
|
+
</li>
|
|
441
|
+
</ul>
|
|
442
|
+
</MScrollbar>
|
|
443
|
+
</MCard>`,
|
|
444
|
+
scriptSetup: `const logLines = [
|
|
445
|
+
{ id: '1', text: '用户 admin 登录成功' },
|
|
446
|
+
{ id: '2', text: '导出任务已完成' },
|
|
447
|
+
{ id: '3', text: '配置已保存' },
|
|
448
|
+
]`,
|
|
449
|
+
rules: [
|
|
450
|
+
'整页主滚动推荐 MLayout fillViewport',
|
|
451
|
+
'固定高度用 height;仅超出时才滚动用 max-height',
|
|
452
|
+
'MTable / Layout / 菜单下拉等通常已内置滚动,一般无需再包一层',
|
|
453
|
+
'Dialog / Drawer 内容滚动由业务自行决定,需要主题滚动时再包 MScrollbar',
|
|
454
|
+
],
|
|
455
|
+
rulesEn: [
|
|
456
|
+
'Main page scroll: prefer MLayout fillViewport',
|
|
457
|
+
'Use height for fixed viewports; use max-height when scroll should appear only on overflow',
|
|
458
|
+
'MTable / Layout / menu popups usually scroll internally; an extra wrapper is often unnecessary',
|
|
459
|
+
'Dialog / Drawer content scrolling is app-owned; wrap MScrollbar only when themed scroll is desired',
|
|
460
|
+
],
|
|
461
|
+
avoid: ['整页壳写 overflow:auto', '重复包裹已内置滚动的组件'],
|
|
462
|
+
avoidEn: ['overflow:auto on the page shell', 'Double-wrapping components that already scroll'],
|
|
463
|
+
},
|
|
351
464
|
];
|
|
352
465
|
export function findPageSnippet(id) {
|
|
353
466
|
const key = id.trim().toLowerCase().replace(/[-_\s]/g, '');
|
package/dist/patterns.d.ts
CHANGED
|
@@ -20,7 +20,29 @@ export interface PagePattern {
|
|
|
20
20
|
avoid: string[];
|
|
21
21
|
}
|
|
22
22
|
export declare const pagePatterns: PagePattern[];
|
|
23
|
+
export interface PageStandard {
|
|
24
|
+
id: string;
|
|
25
|
+
title: string;
|
|
26
|
+
titleEn: string;
|
|
27
|
+
recommend: string[];
|
|
28
|
+
recommendEn: string[];
|
|
29
|
+
discouraged?: string[];
|
|
30
|
+
discouragedEn?: string[];
|
|
31
|
+
mcp?: {
|
|
32
|
+
snippet?: string;
|
|
33
|
+
decision?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Advisory page-writing standards for MCP and human authors — not enforced blockers. */
|
|
37
|
+
export declare const pageStandards: PageStandard[];
|
|
23
38
|
export declare const designRules: {
|
|
39
|
+
readonly meta: {
|
|
40
|
+
readonly nature: "recommended";
|
|
41
|
+
readonly noteZh: "standards 为页面书写标准(推荐实践);特殊场景可偏离。validate_page 仅给出参考建议。";
|
|
42
|
+
readonly noteEn: "standards are recommended page-writing practices; deviations are fine when justified. validate_page is advisory only.";
|
|
43
|
+
};
|
|
44
|
+
/** Page writing standards — single source for layout, scroll, tokens, feedback, a11y, etc. */
|
|
45
|
+
readonly standards: PageStandard[];
|
|
24
46
|
readonly tokens: {
|
|
25
47
|
readonly colors: readonly ["--m-color-primary", "--m-color-surface", "--m-color-text", "--m-color-border"];
|
|
26
48
|
readonly spacing: "--m-space-*";
|
|
@@ -29,8 +51,8 @@ export declare const designRules: {
|
|
|
29
51
|
readonly motion: "--m-motion-fast/normal";
|
|
30
52
|
};
|
|
31
53
|
readonly composition: {
|
|
32
|
-
readonly workflow: readonly ["For full pages: recommend_page
|
|
33
|
-
readonly snippets: readonly ["list-filters", "list-toolbar", "list-table", "list-row-actions", "form-header", "form-body", "form-actions", "dashboard-kpi-grid", "dashboard-chart-card", "layout-app-shell"];
|
|
54
|
+
readonly workflow: readonly ["For full pages: recommend_page → get_golden_page → get_design_rules.", "For local edits: get_page_snippet(section) for filters/toolbar/form-actions/KPI/scrollable-panel blocks.", "Use MLayout fillViewport as the app shell; put MPageContent inside MLayoutContent.", "Prefer MPage* components over scoped CSS for filters, toolbars, headers, form surfaces, and KPI cards.", "Use MSpace or MFlex for control groups inside MPageFilters; use MPageToolbar for title + primary action."];
|
|
55
|
+
readonly snippets: readonly ["list-filters", "list-toolbar", "list-table", "list-row-actions", "form-header", "form-body", "form-actions", "dashboard-kpi-grid", "dashboard-chart-card", "layout-app-shell", "scrollable-panel"];
|
|
34
56
|
readonly pageStack: {
|
|
35
57
|
readonly list: readonly ["MLayout", "MLayoutHeader", "MLayoutContent", "MPageContent", "MPageFilters", "MPageToolbar", "MTable"];
|
|
36
58
|
readonly form: readonly ["MLayout", "MLayoutHeader", "MLayoutContent", "MPageContent", "MPageHeader", "MPageSection", "MForm"];
|
|
@@ -46,7 +68,7 @@ export declare const designRules: {
|
|
|
46
68
|
readonly do: readonly ["MLayoutHeader bordered (default)", "MPageFilters for filter regions", "MPageSection variant=\"form\" for forms", "MTable bordered when table needs grid lines"];
|
|
47
69
|
readonly avoid: readonly ["MCard wrapping MTable that is already bordered", "Nested MCard with bordered=true on both levels", "Hand-written section borders when MPageFilters or MPageSection applies", "Double borders on filter + table wrapper"];
|
|
48
70
|
};
|
|
49
|
-
readonly goldenPages: readonly ["list-page", "form-page", "dashboard-page"];
|
|
71
|
+
readonly goldenPages: readonly ["list-page", "form-page", "dashboard-page", "login-page", "landing-page", "empty-state"];
|
|
50
72
|
};
|
|
51
73
|
readonly actions: {
|
|
52
74
|
readonly primary: {
|
|
@@ -68,13 +90,15 @@ export declare const designRules: {
|
|
|
68
90
|
};
|
|
69
91
|
};
|
|
70
92
|
readonly status: {
|
|
71
|
-
readonly
|
|
93
|
+
readonly preferred: "MStatus";
|
|
94
|
+
readonly chip: "MTag";
|
|
72
95
|
readonly mapping: {
|
|
73
96
|
readonly active: "success";
|
|
74
97
|
readonly pending: "warn";
|
|
75
98
|
readonly disabled: "secondary";
|
|
76
99
|
readonly error: "danger";
|
|
77
100
|
};
|
|
101
|
+
readonly note: "行内轻量状态用 MStatus;芯片/可关闭标签用 MTag";
|
|
78
102
|
};
|
|
79
103
|
readonly feedback: {
|
|
80
104
|
readonly default: "message";
|
|
@@ -88,13 +112,23 @@ export declare const designRules: {
|
|
|
88
112
|
readonly when: readonly ["同时需要标题与补充说明", "后台任务/批量结果含统计", "异步通知感、角落堆叠"];
|
|
89
113
|
readonly avoid: readonly ["仅有单行文案时不要使用 Toast"];
|
|
90
114
|
};
|
|
115
|
+
readonly empty: {
|
|
116
|
+
readonly component: "MEmpty";
|
|
117
|
+
readonly when: readonly ["列表无数据", "筛选无结果", "首次使用"];
|
|
118
|
+
readonly avoid: readonly ["不要用错误色表达正常空态", "不要用 MResult 表达无数据"];
|
|
119
|
+
};
|
|
120
|
+
readonly result: {
|
|
121
|
+
readonly component: "MResult";
|
|
122
|
+
readonly when: readonly ["提交成功/失败页", "403 / 404 / 500", "流程终点回执"];
|
|
123
|
+
readonly avoid: readonly ["不要用手写图标+文案替代 MResult", "不要用 MEmpty 表达阻断错误"];
|
|
124
|
+
};
|
|
91
125
|
readonly inlineMessage: {
|
|
92
|
-
readonly
|
|
93
|
-
readonly when: readonly ["
|
|
126
|
+
readonly api: "field errorMessage | token-styled role=alert";
|
|
127
|
+
readonly when: readonly ["登录/表单区常驻错误:优先字段 errorMessage", "表单级总结:使用 --m-* 样式的 role=alert 条(见 login-page 黄金样例)", "MMessage 组件当前主要为 message 服务宿主,勿臆造 severity 子节点 API"];
|
|
94
128
|
};
|
|
95
129
|
readonly doc: "docs/feedback-message-vs-toast.md";
|
|
96
130
|
};
|
|
97
|
-
readonly global: readonly ["优先使用组件库组件和 --m-* Token
|
|
131
|
+
readonly global: readonly ["优先使用组件库组件和 --m-* Token", "操作反馈默认 message;仅一行文案时优先于 toast", "空态用 MEmpty;结果/阻断页用 MResult;行内状态优先 MStatus", "图标按钮建议提供 aria-label;表单字段建议有可见 label", "浮层默认 Teleport 到 body;有明确布局约束时再改 appendTo", "优先使用组件 documented variant,少写深层 CSS 覆盖"];
|
|
98
132
|
};
|
|
99
133
|
export declare function findPattern(input: string): PagePattern | undefined;
|
|
100
134
|
export declare function scorePattern(pattern: PagePattern, query: string): number;
|