@morya-ui/mcp 0.2.2 → 0.2.3
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 +18 -4
- package/data/catalog.json +149 -5
- package/data/example-coverage.json +29 -4
- package/data/golden-pages/dashboard-page.vue +103 -0
- package/data/golden-pages/form-page.vue +103 -0
- package/data/golden-pages/list-page.vue +117 -0
- package/dist/__tests__/page-snippets.test.d.ts +1 -0
- package/dist/__tests__/page-snippets.test.js +17 -0
- package/dist/__tests__/tools.test.js +34 -1
- package/dist/catalog.js +14 -0
- package/dist/decisions.js +121 -0
- package/dist/golden-pages.d.ts +16 -0
- package/dist/golden-pages.js +67 -0
- package/dist/index.js +22 -1
- package/dist/page-snippets.d.ts +33 -0
- package/dist/page-snippets.js +405 -0
- package/dist/patterns.d.ts +20 -0
- package/dist/patterns.js +71 -18
- package/dist/tools.d.ts +54 -1
- package/dist/tools.js +262 -84
- package/package.json +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 黄金样例:列表页
|
|
4
|
+
* @see DESIGN.md §3
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
MBreadcrumb,
|
|
8
|
+
MButton,
|
|
9
|
+
MConfigProvider,
|
|
10
|
+
MInput,
|
|
11
|
+
MLayout,
|
|
12
|
+
MLayoutContent,
|
|
13
|
+
MLayoutHeader,
|
|
14
|
+
MLayoutSider,
|
|
15
|
+
MMenu,
|
|
16
|
+
MPageContent,
|
|
17
|
+
MPageFilters,
|
|
18
|
+
MPageToolbar,
|
|
19
|
+
MSelect,
|
|
20
|
+
MSpace,
|
|
21
|
+
MTable,
|
|
22
|
+
MTag,
|
|
23
|
+
zhCN,
|
|
24
|
+
} from 'morya-ui'
|
|
25
|
+
import { ref } from 'vue'
|
|
26
|
+
|
|
27
|
+
const keyword = ref('')
|
|
28
|
+
const status = ref<string | undefined>()
|
|
29
|
+
|
|
30
|
+
const statusOptions = [
|
|
31
|
+
{ label: '全部', value: '' },
|
|
32
|
+
{ label: '启用', value: 'active' },
|
|
33
|
+
{ label: '停用', value: 'inactive' },
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const columns = [
|
|
37
|
+
{ key: 'name', label: '名称' },
|
|
38
|
+
{ key: 'status', label: '状态' },
|
|
39
|
+
{ key: 'updatedAt', label: '更新时间' },
|
|
40
|
+
{ key: 'actions', label: '操作', width: 128 },
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
const rows = [
|
|
44
|
+
{ id: '1', name: '示例项目 A', status: 'active', updatedAt: '2026-09-01' },
|
|
45
|
+
{ id: '2', name: '示例项目 B', status: 'inactive', updatedAt: '2026-08-28' },
|
|
46
|
+
{ id: '3', name: '示例项目 C', status: 'active', updatedAt: '2026-08-25' },
|
|
47
|
+
{ id: '4', name: '示例项目 D', status: 'active', updatedAt: '2026-08-20' },
|
|
48
|
+
{ id: '5', name: '示例项目 E', status: 'inactive', updatedAt: '2026-08-15' },
|
|
49
|
+
]
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<MConfigProvider :locale="zhCN">
|
|
54
|
+
<MLayout has-sider fill-viewport>
|
|
55
|
+
<MLayoutSider bordered>
|
|
56
|
+
<MMenu :model="[{ label: '用户管理', key: 'users' }, { label: '角色管理', key: 'roles' }]" />
|
|
57
|
+
</MLayoutSider>
|
|
58
|
+
|
|
59
|
+
<MLayout>
|
|
60
|
+
<MLayoutHeader :padding="'var(--m-space-4) var(--m-space-6)'">
|
|
61
|
+
<MBreadcrumb :model="[{ label: '首页', to: '/' }, { label: '用户管理' }]" />
|
|
62
|
+
</MLayoutHeader>
|
|
63
|
+
|
|
64
|
+
<MLayoutContent>
|
|
65
|
+
<MPageContent>
|
|
66
|
+
<MPageFilters aria-label="筛选">
|
|
67
|
+
<MSpace wrap>
|
|
68
|
+
<MInput v-model="keyword" placeholder="搜索名称" clearable style="width: 14rem" />
|
|
69
|
+
<MSelect
|
|
70
|
+
v-model="status"
|
|
71
|
+
:options="statusOptions"
|
|
72
|
+
placeholder="状态"
|
|
73
|
+
clearable
|
|
74
|
+
style="width: 10rem"
|
|
75
|
+
/>
|
|
76
|
+
<MButton severity="primary">查询</MButton>
|
|
77
|
+
<MButton severity="secondary">重置</MButton>
|
|
78
|
+
</MSpace>
|
|
79
|
+
</MPageFilters>
|
|
80
|
+
|
|
81
|
+
<MPageToolbar title="用户管理">
|
|
82
|
+
<template #actions>
|
|
83
|
+
<MButton severity="primary">新建用户</MButton>
|
|
84
|
+
</template>
|
|
85
|
+
</MPageToolbar>
|
|
86
|
+
|
|
87
|
+
<MTable
|
|
88
|
+
:columns="columns"
|
|
89
|
+
:rows="rows"
|
|
90
|
+
:rows-per-page="3"
|
|
91
|
+
paginator
|
|
92
|
+
striped
|
|
93
|
+
bordered
|
|
94
|
+
row-key="id"
|
|
95
|
+
aria-label="用户列表"
|
|
96
|
+
>
|
|
97
|
+
<template #cell-status="{ value }">
|
|
98
|
+
<MTag :value="value === 'active' ? '启用' : '停用'" :severity="value === 'active' ? 'success' : 'secondary'" />
|
|
99
|
+
</template>
|
|
100
|
+
<template #cell-actions>
|
|
101
|
+
<MSpace>
|
|
102
|
+
<MButton severity="secondary" size="small">编辑</MButton>
|
|
103
|
+
<MButton severity="danger" size="small">删除</MButton>
|
|
104
|
+
</MSpace>
|
|
105
|
+
</template>
|
|
106
|
+
<template #empty>
|
|
107
|
+
<p style="margin: 0; padding: var(--m-space-8); text-align: center; color: var(--m-color-text-muted)">
|
|
108
|
+
暂无用户数据
|
|
109
|
+
</p>
|
|
110
|
+
</template>
|
|
111
|
+
</MTable>
|
|
112
|
+
</MPageContent>
|
|
113
|
+
</MLayoutContent>
|
|
114
|
+
</MLayout>
|
|
115
|
+
</MLayout>
|
|
116
|
+
</MConfigProvider>
|
|
117
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { filterPageSnippets, findPageSnippet, scorePageSnippet } from '../page-snippets.js';
|
|
3
|
+
describe('page snippets catalog', () => {
|
|
4
|
+
it('finds snippet by exact id', () => {
|
|
5
|
+
expect(findPageSnippet('form-actions')?.id).toBe('form-actions');
|
|
6
|
+
});
|
|
7
|
+
it('scores keyword matches for fuzzy lookup', () => {
|
|
8
|
+
const snippet = findPageSnippet('list-filters');
|
|
9
|
+
expect(scorePageSnippet(snippet, 'filters')).toBeGreaterThan(0);
|
|
10
|
+
expect(scorePageSnippet(snippet, 'search')).toBeGreaterThan(0);
|
|
11
|
+
});
|
|
12
|
+
it('filters snippets by page type', () => {
|
|
13
|
+
const result = filterPageSnippets({ pageType: 'dashboard' });
|
|
14
|
+
expect(result.items.some((item) => item.id === 'dashboard-kpi-grid')).toBe(true);
|
|
15
|
+
expect(result.items.some((item) => item.id === 'list-filters')).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -82,8 +82,38 @@ describe('@morya-ui/mcp handlers', () => {
|
|
|
82
82
|
}));
|
|
83
83
|
expect(result.matchedPattern).toBe('dashboard');
|
|
84
84
|
expect(result.scaffold.files.component).toContain('MGrid');
|
|
85
|
+
expect(result.scaffold.files.component).toContain('MPageStat');
|
|
85
86
|
expect(result.scaffold.files.component).toContain('MSkeleton');
|
|
86
87
|
});
|
|
88
|
+
it('returns a golden page source sample', () => {
|
|
89
|
+
const result = read(handlers.getGoldenPage({ page: 'list-page' }));
|
|
90
|
+
expect(result.id).toBe('list-page');
|
|
91
|
+
expect(result.source).toContain('MPageContent');
|
|
92
|
+
expect(result.source).toContain('MPageFilters');
|
|
93
|
+
});
|
|
94
|
+
it('returns a page section snippet by keyword', () => {
|
|
95
|
+
const result = read(handlers.getPageSnippet({ section: 'filters' }));
|
|
96
|
+
expect(result.id).toBe('list-filters');
|
|
97
|
+
expect(result.template).toContain('MPageFilters');
|
|
98
|
+
expect(result.imports).toContain('MPageFilters');
|
|
99
|
+
});
|
|
100
|
+
it('lists page snippets filtered by page type', () => {
|
|
101
|
+
const result = read(handlers.listPageSnippets({ pageType: 'form', limit: 10 }));
|
|
102
|
+
expect(result.items.some((item) => item.id === 'form-actions')).toBe(true);
|
|
103
|
+
expect(result.items.some((item) => item.id === 'page-content-form')).toBe(true);
|
|
104
|
+
expect(result.items.some((item) => item.id === 'list-filters')).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
it('searches page snippets scope', () => {
|
|
107
|
+
const result = read(handlers.search({ query: 'toolbar', scope: 'snippets', limit: 5 }));
|
|
108
|
+
expect(result.items.some((item) => item.type === 'snippet' && item.id === 'list-toolbar')).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
it('flags double-border page composition issues', () => {
|
|
111
|
+
const result = read(handlers.validatePage({
|
|
112
|
+
code: '<MLayoutContent><MCard><MTable bordered /></MCard></MLayoutContent>',
|
|
113
|
+
}));
|
|
114
|
+
expect(result.ok).toBe(false);
|
|
115
|
+
expect(result.issues.some((issue) => issue.type === 'double-border')).toBe(true);
|
|
116
|
+
});
|
|
87
117
|
it('lists component decision guides when query is omitted', () => {
|
|
88
118
|
const result = read(handlers.recommendComponent({ limit: 5 }));
|
|
89
119
|
expect(result.kind).toBe('decisions');
|
|
@@ -103,7 +133,10 @@ describe('@morya-ui/mcp handlers', () => {
|
|
|
103
133
|
expect(result.counts.decisions).toBeGreaterThan(0);
|
|
104
134
|
expect(result.counts.resources).toBeGreaterThan(100);
|
|
105
135
|
expect(result.counts.resourceTemplates).toBe(3);
|
|
106
|
-
expect(result.tools).toHaveLength(
|
|
136
|
+
expect(result.tools).toHaveLength(18);
|
|
137
|
+
expect(result.tools).toContain('get_golden_page');
|
|
138
|
+
expect(result.tools).toContain('get_page_snippet');
|
|
139
|
+
expect(result.tools).toContain('validate_page');
|
|
107
140
|
expect(result.tools).not.toContain('create_page');
|
|
108
141
|
});
|
|
109
142
|
});
|
package/dist/catalog.js
CHANGED
|
@@ -25,6 +25,20 @@ export const componentAliases = {
|
|
|
25
25
|
layoutcontent: 'Layout',
|
|
26
26
|
layoutfooter: 'Layout',
|
|
27
27
|
griditem: 'Grid',
|
|
28
|
+
pagecontent: 'Page',
|
|
29
|
+
pagefilters: 'Page',
|
|
30
|
+
pagetoolbar: 'Page',
|
|
31
|
+
pageheader: 'Page',
|
|
32
|
+
pagesection: 'Page',
|
|
33
|
+
pagestat: 'Page',
|
|
34
|
+
pageplaceholder: 'Page',
|
|
35
|
+
mpagecontent: 'Page',
|
|
36
|
+
mpagefilters: 'Page',
|
|
37
|
+
mpagetoolbar: 'Page',
|
|
38
|
+
mpageheader: 'Page',
|
|
39
|
+
mpagesection: 'Page',
|
|
40
|
+
mpagestat: 'Page',
|
|
41
|
+
mpageplaceholder: 'Page',
|
|
28
42
|
};
|
|
29
43
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
30
44
|
export function loadCatalog() {
|
package/dist/decisions.js
CHANGED
|
@@ -137,6 +137,127 @@ export const componentDecisions = [
|
|
|
137
137
|
},
|
|
138
138
|
],
|
|
139
139
|
},
|
|
140
|
+
{
|
|
141
|
+
id: 'layout-spacing-choice',
|
|
142
|
+
title: '如何选择页面间距组件',
|
|
143
|
+
titleEn: 'Choosing page spacing/layout components',
|
|
144
|
+
question: '控件组、页面区块、栅格之间需要哪种间距与对齐方式?',
|
|
145
|
+
questionEn: 'What kind of spacing and alignment is needed between controls, page sections, or grid areas?',
|
|
146
|
+
keywords: ['间距', '布局', 'gap', 'flex', 'space', 'page', 'toolbar', 'filters', 'spacing', 'layout', 'stack'],
|
|
147
|
+
options: [
|
|
148
|
+
{
|
|
149
|
+
component: 'PageContent',
|
|
150
|
+
when: ['页面主内容区需要统一的 padding 和垂直 gap', '列表/表单/仪表盘区块纵向堆叠'],
|
|
151
|
+
whenEn: ['The main content area needs consistent padding and vertical gap', 'List, form, or dashboard sections stack vertically'],
|
|
152
|
+
avoidWhen: ['只是两个按钮之间的水平间距'],
|
|
153
|
+
avoidWhenEn: ['Only horizontal spacing between two buttons is needed'],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
component: 'Flex',
|
|
157
|
+
when: ['同一行控件需要 gap 和对齐', '工具栏内操作组、筛选控件组'],
|
|
158
|
+
whenEn: ['Controls on one row need gap and alignment', 'Toolbar action groups or filter control rows'],
|
|
159
|
+
avoidWhen: ['整个页面区块的 padding 和垂直 rhythm'],
|
|
160
|
+
avoidWhenEn: ['Padding and vertical rhythm for whole page sections'],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
component: 'Space',
|
|
164
|
+
when: ['简单 wrap 控件组且不需要 justify 语义', '表格行内操作按钮组'],
|
|
165
|
+
whenEn: ['A simple wrapped control group without justify semantics', 'Inline row action button groups'],
|
|
166
|
+
avoidWhen: ['页面级标题与主操作两端对齐'],
|
|
167
|
+
avoidWhenEn: ['Page-level title and primary action need space-between alignment'],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
component: 'Grid',
|
|
171
|
+
when: ['KPI 卡片、仪表盘双栏、响应式列布局'],
|
|
172
|
+
whenEn: ['KPI cards, dashboard two-column layouts, responsive columns'],
|
|
173
|
+
avoidWhen: ['单个筛选行或表单字段列'],
|
|
174
|
+
avoidWhenEn: ['A single filter row or form field column'],
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'page-section-choice',
|
|
180
|
+
title: '如何选择页面区块组件',
|
|
181
|
+
titleEn: 'Choosing page section components',
|
|
182
|
+
question: '这是筛选区、标题区、表单表面、KPI 还是图表占位?',
|
|
183
|
+
questionEn: 'Is this a filter bar, page header, form surface, KPI metric, or chart placeholder?',
|
|
184
|
+
keywords: ['筛选', '工具栏', '标题', '表单', 'kpi', '占位', 'page filters', 'toolbar', 'header', 'stat', 'placeholder'],
|
|
185
|
+
options: [
|
|
186
|
+
{
|
|
187
|
+
component: 'PageFilters',
|
|
188
|
+
when: ['列表页或搜索页的筛选/查询区域', '需要统一浅色背景与边框'],
|
|
189
|
+
whenEn: ['Filter or search region on list pages', 'A consistent muted background and border is needed'],
|
|
190
|
+
avoidWhen: ['普通表单字段分组', 'KPI 指标展示'],
|
|
191
|
+
avoidWhenEn: ['Regular form field grouping', 'KPI metric display'],
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
component: 'PageToolbar',
|
|
195
|
+
when: ['页面标题 + 右侧主操作', '列表页新建按钮区域'],
|
|
196
|
+
whenEn: ['Page title plus primary actions on the right', 'Create button area on list pages'],
|
|
197
|
+
avoidWhen: ['带长描述的表单引导区'],
|
|
198
|
+
avoidWhenEn: ['Form intro areas with long descriptions'],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
component: 'PageHeader',
|
|
202
|
+
when: ['页面标题 + 描述文案', '表单页/详情页引导'],
|
|
203
|
+
whenEn: ['Page title plus descriptive copy', 'Form or detail page intros'],
|
|
204
|
+
avoidWhen: ['只有标题和单个主按钮的列表工具栏'],
|
|
205
|
+
avoidWhenEn: ['List toolbars with only a title and one primary button'],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
component: 'PageSection',
|
|
209
|
+
when: ['表单主体表面 (variant="form")', '底栏操作区 (variant="actions")'],
|
|
210
|
+
whenEn: ['Form body surface (variant="form")', 'Footer actions (variant="actions")'],
|
|
211
|
+
avoidWhen: ['列表页筛选区', 'KPI 卡片'],
|
|
212
|
+
avoidWhenEn: ['List page filter areas', 'KPI cards'],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
component: 'PageStat',
|
|
216
|
+
when: ['仪表盘 KPI 单指标卡', '需要 label/value/trend/icon 结构'],
|
|
217
|
+
whenEn: ['Dashboard KPI cards', 'Needs label/value/trend/icon structure'],
|
|
218
|
+
avoidWhen: ['普通内容分组', '表格或表单'],
|
|
219
|
+
avoidWhenEn: ['Generic content grouping', 'Tables or forms'],
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
component: 'PagePlaceholder',
|
|
223
|
+
when: ['图表、地图或媒体区域尚未接入', '需要 dashed 占位表面'],
|
|
224
|
+
whenEn: ['Charts, maps, or media areas are not wired yet', 'A dashed placeholder surface is needed'],
|
|
225
|
+
avoidWhen: ['真实数据表格或表单'],
|
|
226
|
+
avoidWhenEn: ['Real data tables or forms'],
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'surface-nesting-choice',
|
|
232
|
+
title: '如何避免双边框与多余容器',
|
|
233
|
+
titleEn: 'Avoiding double borders and redundant wrappers',
|
|
234
|
+
question: '内容是否已经被 Page 组件或 Table 提供了边界?',
|
|
235
|
+
questionEn: 'Does Page or Table already provide the boundary for this content?',
|
|
236
|
+
keywords: ['边框', '双边框', 'card', 'nested', 'border', 'wrapper', 'table', 'filters'],
|
|
237
|
+
options: [
|
|
238
|
+
{
|
|
239
|
+
component: 'PageContent + MTable',
|
|
240
|
+
when: ['列表页数据表格', 'MPageFilters 已提供筛选区边界'],
|
|
241
|
+
whenEn: ['List page data tables', 'MPageFilters already provides the filter boundary'],
|
|
242
|
+
avoidWhen: ['需要独立卡片标题的内容模块'],
|
|
243
|
+
avoidWhenEn: ['Content modules that need an independent card title'],
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
component: 'MCard',
|
|
247
|
+
when: ['仪表盘中的图表区/明细区', '需要 card 标题的内容模块'],
|
|
248
|
+
whenEn: ['Chart or detail areas on dashboards', 'Content modules that need a card title'],
|
|
249
|
+
avoidWhen: ['列表页表格外层', '筛选区外层'],
|
|
250
|
+
avoidWhenEn: ['Wrapping list page tables', 'Wrapping filter regions'],
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
component: 'PageSection',
|
|
254
|
+
when: ['表单页主体或 actions 底栏', '需要统一 form surface'],
|
|
255
|
+
whenEn: ['Form page bodies or action footers', 'A unified form surface is needed'],
|
|
256
|
+
avoidWhen: ['再套 MCard variant form 导致双边框'],
|
|
257
|
+
avoidWhenEn: ['Wrapping again with MCard and causing double borders'],
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
},
|
|
140
261
|
];
|
|
141
262
|
export function findDecision(name) {
|
|
142
263
|
const key = name.trim().toLowerCase().replace(/[-_\s]/g, '');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface GoldenPageRecord {
|
|
2
|
+
id: string;
|
|
3
|
+
file: string;
|
|
4
|
+
title: string;
|
|
5
|
+
titleEn: string;
|
|
6
|
+
patternId: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const goldenPageCatalog: GoldenPageRecord[];
|
|
9
|
+
export declare function listGoldenPages(): GoldenPageRecord[];
|
|
10
|
+
export declare function findGoldenPage(id: string): GoldenPageRecord | undefined;
|
|
11
|
+
export declare function readGoldenPageSource(id: string): {
|
|
12
|
+
record: GoldenPageRecord;
|
|
13
|
+
source: string;
|
|
14
|
+
} | null;
|
|
15
|
+
export declare function goldenPagesAvailable(): boolean;
|
|
16
|
+
export declare function listGoldenPageFilesOnDisk(): string[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export const goldenPageCatalog = [
|
|
5
|
+
{
|
|
6
|
+
id: 'list-page',
|
|
7
|
+
file: 'list-page.vue',
|
|
8
|
+
title: '列表页黄金样例',
|
|
9
|
+
titleEn: 'List page golden sample',
|
|
10
|
+
patternId: 'admin-list',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'form-page',
|
|
14
|
+
file: 'form-page.vue',
|
|
15
|
+
title: '表单页黄金样例',
|
|
16
|
+
titleEn: 'Form page golden sample',
|
|
17
|
+
patternId: 'form-page',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'dashboard-page',
|
|
21
|
+
file: 'dashboard-page.vue',
|
|
22
|
+
title: '仪表盘黄金样例',
|
|
23
|
+
titleEn: 'Dashboard golden sample',
|
|
24
|
+
patternId: 'dashboard',
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
const bundledDir = join(pkgRoot, 'data/golden-pages');
|
|
29
|
+
const repoDir = join(pkgRoot, '../../ai-design-config/docs/golden-pages');
|
|
30
|
+
function resolveGoldenPagesDir() {
|
|
31
|
+
if (existsSync(bundledDir))
|
|
32
|
+
return bundledDir;
|
|
33
|
+
if (existsSync(repoDir))
|
|
34
|
+
return repoDir;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
export function listGoldenPages() {
|
|
38
|
+
return goldenPageCatalog;
|
|
39
|
+
}
|
|
40
|
+
export function findGoldenPage(id) {
|
|
41
|
+
const key = id.trim().toLowerCase().replace(/\.vue$/i, '').replace(/[-_\s]/g, '');
|
|
42
|
+
return goldenPageCatalog.find((item) => item.id.replace(/[-_\s]/g, '') === key);
|
|
43
|
+
}
|
|
44
|
+
export function readGoldenPageSource(id) {
|
|
45
|
+
const record = findGoldenPage(id);
|
|
46
|
+
if (!record)
|
|
47
|
+
return null;
|
|
48
|
+
const dir = resolveGoldenPagesDir();
|
|
49
|
+
if (!dir)
|
|
50
|
+
return null;
|
|
51
|
+
const path = join(dir, record.file);
|
|
52
|
+
if (!existsSync(path))
|
|
53
|
+
return null;
|
|
54
|
+
return { record, source: readFileSync(path, 'utf8').replace(/^\uFEFF/, '') };
|
|
55
|
+
}
|
|
56
|
+
export function goldenPagesAvailable() {
|
|
57
|
+
const dir = resolveGoldenPagesDir();
|
|
58
|
+
if (!dir)
|
|
59
|
+
return false;
|
|
60
|
+
return goldenPageCatalog.every((item) => existsSync(join(dir, item.file)));
|
|
61
|
+
}
|
|
62
|
+
export function listGoldenPageFilesOnDisk() {
|
|
63
|
+
const dir = resolveGoldenPagesDir();
|
|
64
|
+
if (!dir)
|
|
65
|
+
return [];
|
|
66
|
+
return readdirSync(dir).filter((name) => name.endsWith('.vue'));
|
|
67
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -33,7 +33,7 @@ register('list', 'List morya-ui components, guides, examples, categories, or pag
|
|
|
33
33
|
}, async (args) => handlers.list(args));
|
|
34
34
|
register('search', 'Search components, guides, API text, examples, page patterns, and component decision guides.', {
|
|
35
35
|
query: z.string().min(1),
|
|
36
|
-
scope: z.enum(['all', 'components', 'guides', 'api', 'examples', 'patterns', 'decisions']).optional(),
|
|
36
|
+
scope: z.enum(['all', 'components', 'guides', 'api', 'examples', 'patterns', 'decisions', 'snippets']).optional(),
|
|
37
37
|
mode: z.string().optional(),
|
|
38
38
|
limit: z.number().int().min(1).max(50).optional(),
|
|
39
39
|
offset: z.number().int().min(0).optional(),
|
|
@@ -101,6 +101,27 @@ register('recommend_component', 'List, read, or recommend component selection gu
|
|
|
101
101
|
limit: z.number().int().min(1).max(200).optional(),
|
|
102
102
|
offset: z.number().int().min(0).optional(),
|
|
103
103
|
}, async (args) => handlers.recommendComponent(args));
|
|
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).', {
|
|
106
|
+
page: z.string().min(1),
|
|
107
|
+
mode: z.string().optional(),
|
|
108
|
+
}, async (args) => handlers.getGoldenPage(args));
|
|
109
|
+
register('validate_page', 'Validate a generated page for composition, spacing, and border anti-patterns.', {
|
|
110
|
+
code: z.string().min(1),
|
|
111
|
+
mode: z.string().optional(),
|
|
112
|
+
}, async (args) => handlers.validatePage(args));
|
|
113
|
+
register('list_page_snippets', 'List reusable page section snippets for local edits (filters, toolbar, form actions, KPI grid, etc.).', {
|
|
114
|
+
query: z.string().optional(),
|
|
115
|
+
pageType: z.string().optional(),
|
|
116
|
+
mode: z.string().optional(),
|
|
117
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
118
|
+
offset: z.number().int().min(0).optional(),
|
|
119
|
+
}, async (args) => handlers.listPageSnippets(args));
|
|
120
|
+
register('get_page_snippet', 'Read a standard page section snippet by id or keyword (e.g. filters, toolbar, form-actions). Pass includeScript: true for import + template bundle.', {
|
|
121
|
+
section: z.string().min(1),
|
|
122
|
+
mode: z.string().optional(),
|
|
123
|
+
includeScript: z.boolean().optional(),
|
|
124
|
+
}, async (args) => handlers.getPageSnippet(args));
|
|
104
125
|
register('version', 'Return MCP package, library version, and catalog status.', {}, async () => handlers.version());
|
|
105
126
|
const transport = new StdioServerTransport();
|
|
106
127
|
await server.connect(transport);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type PageSnippetPageType = 'list' | 'form' | 'dashboard' | 'detail' | 'common';
|
|
2
|
+
export interface PageSnippet {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
titleEn: string;
|
|
6
|
+
description: string;
|
|
7
|
+
descriptionEn: string;
|
|
8
|
+
pageTypes: PageSnippetPageType[];
|
|
9
|
+
keywords: string[];
|
|
10
|
+
imports: string[];
|
|
11
|
+
scriptSetup?: string;
|
|
12
|
+
template: string;
|
|
13
|
+
rules: string[];
|
|
14
|
+
rulesEn: string[];
|
|
15
|
+
avoid: string[];
|
|
16
|
+
avoidEn: string[];
|
|
17
|
+
}
|
|
18
|
+
export declare const pageSnippets: PageSnippet[];
|
|
19
|
+
export declare function findPageSnippet(id: string): PageSnippet | undefined;
|
|
20
|
+
export declare function scorePageSnippet(snippet: PageSnippet, query: string): number;
|
|
21
|
+
export declare function filterPageSnippets(args: {
|
|
22
|
+
query?: string;
|
|
23
|
+
pageType?: string;
|
|
24
|
+
limit?: number;
|
|
25
|
+
offset?: number;
|
|
26
|
+
}): {
|
|
27
|
+
total: number;
|
|
28
|
+
items: PageSnippet[];
|
|
29
|
+
offset: number;
|
|
30
|
+
limit: number;
|
|
31
|
+
hasMore: boolean;
|
|
32
|
+
nextOffset: number | undefined;
|
|
33
|
+
};
|