@morya-ui/mcp 0.2.8 → 0.3.0
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 +2 -2
- package/data/catalog.json +529 -248
- package/data/example-coverage.json +67 -39
- package/data/golden-pages/dashboard-page.vue +41 -8
- package/data/golden-pages/detail-page.vue +178 -0
- package/data/golden-pages/empty-state.vue +22 -1
- package/data/golden-pages/form-in-dialog.vue +166 -0
- package/data/golden-pages/form-page.vue +1 -1
- package/data/golden-pages/landing-page.vue +31 -1
- package/data/golden-pages/list-page.vue +25 -12
- package/data/golden-pages/login-page.vue +44 -4
- package/data/golden-pages/result-page.vue +84 -0
- package/data/golden-pages/settings-page.vue +163 -0
- package/data/golden-pages/wizard-form.vue +182 -0
- package/dist/__tests__/tools.test.js +97 -4
- package/dist/decisions.js +136 -1
- package/dist/golden-pages.js +35 -0
- package/dist/index.js +1 -1
- package/dist/page-snippets.js +46 -36
- package/dist/patterns.d.ts +7 -5
- package/dist/patterns.js +29 -8
- package/dist/tools.js +181 -58
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 黄金样例:结果 / 阻断页(System)
|
|
4
|
+
* 默认展示 403;换成 success / 404 / 500 时只改 status + 文案 + #footer 动作。
|
|
5
|
+
* 无数据空态请用 empty-state / MEmpty,不要用 MResult。
|
|
6
|
+
* @see DESIGN.md · surfaces § System · MResult API(操作用 #footer)
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
MBreadcrumb,
|
|
10
|
+
MButton,
|
|
11
|
+
MConfigProvider,
|
|
12
|
+
MLayout,
|
|
13
|
+
MLayoutContent,
|
|
14
|
+
MLayoutHeader,
|
|
15
|
+
MPageContent,
|
|
16
|
+
MResult,
|
|
17
|
+
MSpace,
|
|
18
|
+
zhCN,
|
|
19
|
+
} from 'morya-ui'
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<MConfigProvider :locale="zhCN">
|
|
24
|
+
<MLayout fill-viewport>
|
|
25
|
+
<MLayoutHeader padding="var(--m-space-4) var(--m-space-6)">
|
|
26
|
+
<MBreadcrumb
|
|
27
|
+
:model="[
|
|
28
|
+
{ label: '首页', to: '/' },
|
|
29
|
+
{ label: '项目管理', to: '/projects' },
|
|
30
|
+
{ label: '无权限' },
|
|
31
|
+
]"
|
|
32
|
+
/>
|
|
33
|
+
</MLayoutHeader>
|
|
34
|
+
|
|
35
|
+
<MLayoutContent>
|
|
36
|
+
<MPageContent>
|
|
37
|
+
<div class="result-shell">
|
|
38
|
+
<MResult
|
|
39
|
+
status="403"
|
|
40
|
+
title="没有权限查看该项目"
|
|
41
|
+
description="当前账号无权访问「青禾书房」工作区。可返回上一页,或联系管理员开通权限。"
|
|
42
|
+
size="large"
|
|
43
|
+
>
|
|
44
|
+
<template #footer>
|
|
45
|
+
<MSpace>
|
|
46
|
+
<MButton severity="primary">
|
|
47
|
+
返回上一页
|
|
48
|
+
</MButton>
|
|
49
|
+
<MButton severity="secondary" text>
|
|
50
|
+
联系管理员
|
|
51
|
+
</MButton>
|
|
52
|
+
</MSpace>
|
|
53
|
+
</template>
|
|
54
|
+
</MResult>
|
|
55
|
+
</div>
|
|
56
|
+
</MPageContent>
|
|
57
|
+
</MLayoutContent>
|
|
58
|
+
</MLayout>
|
|
59
|
+
</MConfigProvider>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<style scoped>
|
|
63
|
+
.result-shell {
|
|
64
|
+
display: grid;
|
|
65
|
+
place-items: center;
|
|
66
|
+
min-height: min(28rem, 70vh);
|
|
67
|
+
padding: var(--m-space-8) var(--m-space-4);
|
|
68
|
+
border-radius: var(--m-radius-lg);
|
|
69
|
+
background:
|
|
70
|
+
radial-gradient(
|
|
71
|
+
70% 55% at 50% 0%,
|
|
72
|
+
color-mix(in srgb, var(--m-color-primary) 10%, transparent),
|
|
73
|
+
transparent 60%
|
|
74
|
+
),
|
|
75
|
+
var(--m-color-surface);
|
|
76
|
+
border: 1px dashed color-mix(in srgb, var(--m-color-border) 80%, transparent);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@media (prefers-reduced-motion: reduce) {
|
|
80
|
+
.result-shell {
|
|
81
|
+
background: var(--m-color-surface);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 黄金样例:设置 / 配置页
|
|
4
|
+
* 窄栏 + Tabs 分组 + MPageSection form;保存用 message 单行回执。
|
|
5
|
+
* @see DESIGN.md · page-layouts.md § Settings · visual-craft § Ops polish
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
MBreadcrumb,
|
|
9
|
+
MButton,
|
|
10
|
+
MConfigProvider,
|
|
11
|
+
MForm,
|
|
12
|
+
MFormItem,
|
|
13
|
+
MInput,
|
|
14
|
+
MLayout,
|
|
15
|
+
MLayoutContent,
|
|
16
|
+
MLayoutHeader,
|
|
17
|
+
MPageContent,
|
|
18
|
+
MPageHeader,
|
|
19
|
+
MPageSection,
|
|
20
|
+
MSelect,
|
|
21
|
+
MSpace,
|
|
22
|
+
MSwitch,
|
|
23
|
+
MTabs,
|
|
24
|
+
message,
|
|
25
|
+
zhCN,
|
|
26
|
+
} from 'morya-ui'
|
|
27
|
+
import { reactive, ref } from 'vue'
|
|
28
|
+
|
|
29
|
+
const activeTab = ref('profile')
|
|
30
|
+
const submitting = ref(false)
|
|
31
|
+
|
|
32
|
+
const tabs = [
|
|
33
|
+
{ label: '资料', value: 'profile' },
|
|
34
|
+
{ label: '通知', value: 'notify' },
|
|
35
|
+
{ label: '安全', value: 'security' },
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
const profile = reactive({
|
|
39
|
+
displayName: '林晓',
|
|
40
|
+
email: 'linxiao@example.com',
|
|
41
|
+
language: 'zh-CN' as string | undefined,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const notify = reactive({
|
|
45
|
+
emailDigest: true,
|
|
46
|
+
productUpdates: false,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const languageOptions = [
|
|
50
|
+
{ label: '简体中文', value: 'zh-CN' },
|
|
51
|
+
{ label: 'English', value: 'en-US' },
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
async function saveProfile() {
|
|
55
|
+
submitting.value = true
|
|
56
|
+
try {
|
|
57
|
+
// await api.saveProfile(profile)
|
|
58
|
+
message.success('资料已保存')
|
|
59
|
+
} finally {
|
|
60
|
+
submitting.value = false
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function saveNotify() {
|
|
65
|
+
submitting.value = true
|
|
66
|
+
try {
|
|
67
|
+
// await api.saveNotify(notify)
|
|
68
|
+
message.success('通知偏好已保存')
|
|
69
|
+
} finally {
|
|
70
|
+
submitting.value = false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<MConfigProvider :locale="zhCN">
|
|
77
|
+
<MLayout fill-viewport>
|
|
78
|
+
<MLayoutHeader padding="var(--m-space-4) var(--m-space-6)">
|
|
79
|
+
<MBreadcrumb :model="[{ label: '首页', to: '/' }, { label: '系统设置' }]" />
|
|
80
|
+
</MLayoutHeader>
|
|
81
|
+
|
|
82
|
+
<MLayoutContent>
|
|
83
|
+
<MPageContent width="narrow">
|
|
84
|
+
<MPageHeader
|
|
85
|
+
title="系统设置"
|
|
86
|
+
description="按域分组配置;切换 Tab 时保留未保存输入,保存成功用 message 一句话回执。"
|
|
87
|
+
/>
|
|
88
|
+
|
|
89
|
+
<MTabs v-model="activeTab" :tabs="tabs">
|
|
90
|
+
<template #default="{ activeValue }">
|
|
91
|
+
<MPageSection v-if="activeValue === 'profile'" variant="form" title="个人资料">
|
|
92
|
+
<MForm @submit.prevent="saveProfile">
|
|
93
|
+
<MFormItem label="显示名称" name="displayName" required>
|
|
94
|
+
<MInput v-model="profile.displayName" fluid />
|
|
95
|
+
</MFormItem>
|
|
96
|
+
<MFormItem label="工作邮箱" name="email" required>
|
|
97
|
+
<MInput v-model="profile.email" type="email" fluid />
|
|
98
|
+
</MFormItem>
|
|
99
|
+
<MFormItem label="界面语言" name="language">
|
|
100
|
+
<MSelect
|
|
101
|
+
v-model="profile.language"
|
|
102
|
+
:options="languageOptions"
|
|
103
|
+
placeholder="选择语言"
|
|
104
|
+
fluid
|
|
105
|
+
/>
|
|
106
|
+
</MFormItem>
|
|
107
|
+
<MPageSection variant="actions">
|
|
108
|
+
<MSpace>
|
|
109
|
+
<MButton native-type="submit" severity="primary" :loading="submitting">
|
|
110
|
+
保存资料
|
|
111
|
+
</MButton>
|
|
112
|
+
<MButton severity="secondary" text native-type="button">
|
|
113
|
+
恢复默认
|
|
114
|
+
</MButton>
|
|
115
|
+
</MSpace>
|
|
116
|
+
</MPageSection>
|
|
117
|
+
</MForm>
|
|
118
|
+
</MPageSection>
|
|
119
|
+
|
|
120
|
+
<MPageSection v-else-if="activeValue === 'notify'" variant="form" title="通知">
|
|
121
|
+
<MForm @submit.prevent="saveNotify">
|
|
122
|
+
<MFormItem label="邮件摘要" name="emailDigest">
|
|
123
|
+
<MSwitch v-model="notify.emailDigest" />
|
|
124
|
+
</MFormItem>
|
|
125
|
+
<MFormItem label="产品动态" name="productUpdates">
|
|
126
|
+
<MSwitch v-model="notify.productUpdates" />
|
|
127
|
+
</MFormItem>
|
|
128
|
+
<MPageSection variant="actions">
|
|
129
|
+
<MButton native-type="submit" severity="primary" :loading="submitting">
|
|
130
|
+
保存通知
|
|
131
|
+
</MButton>
|
|
132
|
+
</MPageSection>
|
|
133
|
+
</MForm>
|
|
134
|
+
</MPageSection>
|
|
135
|
+
|
|
136
|
+
<MPageSection v-else variant="form" title="安全">
|
|
137
|
+
<p class="settings-hint">
|
|
138
|
+
修改密码、两步验证等敏感操作放在此区;危险动作使用 severity="danger" 并配合确认。
|
|
139
|
+
</p>
|
|
140
|
+
<MSpace>
|
|
141
|
+
<MButton severity="secondary">
|
|
142
|
+
修改密码
|
|
143
|
+
</MButton>
|
|
144
|
+
<MButton severity="danger" text>
|
|
145
|
+
注销账号
|
|
146
|
+
</MButton>
|
|
147
|
+
</MSpace>
|
|
148
|
+
</MPageSection>
|
|
149
|
+
</template>
|
|
150
|
+
</MTabs>
|
|
151
|
+
</MPageContent>
|
|
152
|
+
</MLayoutContent>
|
|
153
|
+
</MLayout>
|
|
154
|
+
</MConfigProvider>
|
|
155
|
+
</template>
|
|
156
|
+
|
|
157
|
+
<style scoped>
|
|
158
|
+
.settings-hint {
|
|
159
|
+
margin: 0 0 var(--m-space-4);
|
|
160
|
+
color: var(--m-color-text-muted);
|
|
161
|
+
font-size: var(--m-font-size-sm);
|
|
162
|
+
}
|
|
163
|
+
</style>
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 黄金样例:分步向导(wizard-form)
|
|
4
|
+
* MStepper + 每步一个主任务;完成态可接 result-page。
|
|
5
|
+
* @see DESIGN.md · surfaces § Flow · pattern wizard-form
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
MBreadcrumb,
|
|
9
|
+
MButton,
|
|
10
|
+
MConfigProvider,
|
|
11
|
+
MForm,
|
|
12
|
+
MFormItem,
|
|
13
|
+
MInput,
|
|
14
|
+
MInputTags,
|
|
15
|
+
MLayout,
|
|
16
|
+
MLayoutContent,
|
|
17
|
+
MLayoutHeader,
|
|
18
|
+
MPageContent,
|
|
19
|
+
MPageHeader,
|
|
20
|
+
MPageSection,
|
|
21
|
+
MSelect,
|
|
22
|
+
MSpace,
|
|
23
|
+
MStepper,
|
|
24
|
+
message,
|
|
25
|
+
zhCN,
|
|
26
|
+
} from 'morya-ui'
|
|
27
|
+
import { computed, reactive, ref } from 'vue'
|
|
28
|
+
|
|
29
|
+
const activeStep = ref(0)
|
|
30
|
+
const submitting = ref(false)
|
|
31
|
+
|
|
32
|
+
const steps = [
|
|
33
|
+
{ label: '工作空间', description: '名称与地区' },
|
|
34
|
+
{ label: '邀请成员', description: '可选邮箱' },
|
|
35
|
+
{ label: '确认', description: '核对后创建' },
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
const model = reactive({
|
|
39
|
+
name: '',
|
|
40
|
+
region: undefined as string | undefined,
|
|
41
|
+
invites: [] as string[],
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const regionOptions = [
|
|
45
|
+
{ label: '中国大陆', value: 'cn' },
|
|
46
|
+
{ label: '亚太', value: 'apac' },
|
|
47
|
+
{ label: '北美', value: 'na' },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
const canNext = computed(() => {
|
|
51
|
+
if (activeStep.value === 0) return Boolean(model.name && model.region)
|
|
52
|
+
return true
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
function back() {
|
|
56
|
+
if (activeStep.value > 0) activeStep.value -= 1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function next() {
|
|
60
|
+
if (!canNext.value) return
|
|
61
|
+
if (activeStep.value < steps.length - 1) activeStep.value += 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function finish() {
|
|
65
|
+
submitting.value = true
|
|
66
|
+
try {
|
|
67
|
+
// await api.createWorkspace(model)
|
|
68
|
+
message.success('工作空间已创建')
|
|
69
|
+
// 成功页可镜像 get_golden_page('result-page'),将 status 改为 success
|
|
70
|
+
} finally {
|
|
71
|
+
submitting.value = false
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<template>
|
|
77
|
+
<MConfigProvider :locale="zhCN">
|
|
78
|
+
<MLayout fill-viewport>
|
|
79
|
+
<MLayoutHeader padding="var(--m-space-4) var(--m-space-6)">
|
|
80
|
+
<MBreadcrumb
|
|
81
|
+
:model="[
|
|
82
|
+
{ label: '首页', to: '/' },
|
|
83
|
+
{ label: '工作空间', to: '/workspaces' },
|
|
84
|
+
{ label: '创建' },
|
|
85
|
+
]"
|
|
86
|
+
/>
|
|
87
|
+
</MLayoutHeader>
|
|
88
|
+
|
|
89
|
+
<MLayoutContent>
|
|
90
|
+
<MPageContent width="narrow">
|
|
91
|
+
<MPageHeader
|
|
92
|
+
title="创建工作空间"
|
|
93
|
+
description="三步完成;每步只做一件事。Stepper 使用 steps(不是 items)。"
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<MStepper v-model="activeStep" :steps="steps" linear />
|
|
97
|
+
|
|
98
|
+
<MPageSection variant="form">
|
|
99
|
+
<MForm v-if="activeStep === 0" @submit.prevent="next">
|
|
100
|
+
<MFormItem label="名称" name="name" required>
|
|
101
|
+
<MInput v-model="model.name" placeholder="例如:青禾书房" fluid />
|
|
102
|
+
</MFormItem>
|
|
103
|
+
<MFormItem label="地区" name="region" required>
|
|
104
|
+
<MSelect
|
|
105
|
+
v-model="model.region"
|
|
106
|
+
:options="regionOptions"
|
|
107
|
+
placeholder="选择地区"
|
|
108
|
+
fluid
|
|
109
|
+
/>
|
|
110
|
+
</MFormItem>
|
|
111
|
+
</MForm>
|
|
112
|
+
|
|
113
|
+
<MForm v-else-if="activeStep === 1" @submit.prevent="next">
|
|
114
|
+
<MFormItem label="邀请邮箱" name="invites">
|
|
115
|
+
<MInputTags
|
|
116
|
+
v-model="model.invites"
|
|
117
|
+
placeholder="输入邮箱后回车"
|
|
118
|
+
/>
|
|
119
|
+
</MFormItem>
|
|
120
|
+
<p class="wizard-hint">
|
|
121
|
+
可跳过;稍后也可在成员管理中邀请。
|
|
122
|
+
</p>
|
|
123
|
+
</MForm>
|
|
124
|
+
|
|
125
|
+
<div v-else class="wizard-summary">
|
|
126
|
+
<p><strong>名称</strong> {{ model.name || '—' }}</p>
|
|
127
|
+
<p><strong>地区</strong> {{ model.region || '—' }}</p>
|
|
128
|
+
<p><strong>邀请</strong> {{ model.invites.length ? model.invites.join('、') : '暂无' }}</p>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<MPageSection variant="actions">
|
|
132
|
+
<MSpace>
|
|
133
|
+
<MButton
|
|
134
|
+
severity="secondary"
|
|
135
|
+
text
|
|
136
|
+
:disabled="activeStep === 0 || submitting"
|
|
137
|
+
@click="back"
|
|
138
|
+
>
|
|
139
|
+
上一步
|
|
140
|
+
</MButton>
|
|
141
|
+
<MButton
|
|
142
|
+
v-if="activeStep < steps.length - 1"
|
|
143
|
+
severity="primary"
|
|
144
|
+
:disabled="!canNext"
|
|
145
|
+
@click="next"
|
|
146
|
+
>
|
|
147
|
+
下一步
|
|
148
|
+
</MButton>
|
|
149
|
+
<MButton
|
|
150
|
+
v-else
|
|
151
|
+
severity="primary"
|
|
152
|
+
:loading="submitting"
|
|
153
|
+
@click="finish"
|
|
154
|
+
>
|
|
155
|
+
创建
|
|
156
|
+
</MButton>
|
|
157
|
+
</MSpace>
|
|
158
|
+
</MPageSection>
|
|
159
|
+
</MPageSection>
|
|
160
|
+
</MPageContent>
|
|
161
|
+
</MLayoutContent>
|
|
162
|
+
</MLayout>
|
|
163
|
+
</MConfigProvider>
|
|
164
|
+
</template>
|
|
165
|
+
|
|
166
|
+
<style scoped>
|
|
167
|
+
.wizard-hint,
|
|
168
|
+
.wizard-summary {
|
|
169
|
+
margin: 0;
|
|
170
|
+
color: var(--m-color-text-muted);
|
|
171
|
+
font-size: var(--m-font-size-sm);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.wizard-summary {
|
|
175
|
+
display: grid;
|
|
176
|
+
gap: var(--m-space-2);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.wizard-summary p {
|
|
180
|
+
margin: 0;
|
|
181
|
+
}
|
|
182
|
+
</style>
|
|
@@ -74,16 +74,60 @@ describe('@morya-ui/mcp handlers', () => {
|
|
|
74
74
|
}));
|
|
75
75
|
expect(result.matchedPattern).toBe('admin-list');
|
|
76
76
|
});
|
|
77
|
-
it('returns
|
|
77
|
+
it('returns golden-page source as scaffold when available', () => {
|
|
78
78
|
const result = read(handlers.recommendPage({
|
|
79
79
|
intent: '生产监控仪表盘',
|
|
80
80
|
pageType: 'dashboard',
|
|
81
81
|
includeScaffold: true,
|
|
82
82
|
}));
|
|
83
83
|
expect(result.matchedPattern).toBe('dashboard');
|
|
84
|
+
expect(result.scaffold.source).toBe('golden-page');
|
|
85
|
+
expect(result.scaffold.goldenPage).toBe('dashboard-page');
|
|
84
86
|
expect(result.scaffold.files.component).toContain('MGrid');
|
|
85
87
|
expect(result.scaffold.files.component).toContain('MPageStat');
|
|
86
|
-
expect(result.scaffold.files.component).toContain('
|
|
88
|
+
expect(result.scaffold.files.component).toContain('黄金样例');
|
|
89
|
+
expect(result.nextStep).toContain('validate_usage');
|
|
90
|
+
});
|
|
91
|
+
it('prefers list-status-dot for status cell queries', () => {
|
|
92
|
+
const result = read(handlers.getPageSnippet({ section: '状态' }));
|
|
93
|
+
expect(result.id).toBe('list-status-dot');
|
|
94
|
+
expect(result.template).toContain('MStatus');
|
|
95
|
+
});
|
|
96
|
+
it('returns result-page golden sample with MResult footer slot', () => {
|
|
97
|
+
const result = read(handlers.getGoldenPage({ page: 'result-page' }));
|
|
98
|
+
expect(result.id).toBe('result-page');
|
|
99
|
+
expect(result.source).toContain('MResult');
|
|
100
|
+
expect(result.source).toContain('status="403"');
|
|
101
|
+
expect(result.source).toContain('<template #footer>');
|
|
102
|
+
expect(result.source).not.toContain('<template #extra>');
|
|
103
|
+
});
|
|
104
|
+
it('returns settings-page and wizard-form golden samples with correct APIs', () => {
|
|
105
|
+
const settings = read(handlers.getGoldenPage({ page: 'settings-page' }));
|
|
106
|
+
expect(settings.id).toBe('settings-page');
|
|
107
|
+
expect(settings.source).toContain('MTabs');
|
|
108
|
+
expect(settings.source).toContain(':tabs=');
|
|
109
|
+
expect(settings.source).not.toContain(':items=');
|
|
110
|
+
const wizard = read(handlers.getGoldenPage({ page: 'wizard-form' }));
|
|
111
|
+
expect(wizard.id).toBe('wizard-form');
|
|
112
|
+
expect(wizard.source).toContain('MStepper');
|
|
113
|
+
expect(wizard.source).toContain(':steps=');
|
|
114
|
+
expect(wizard.source).not.toContain(':items=');
|
|
115
|
+
});
|
|
116
|
+
it('scaffolds settings from golden page source', () => {
|
|
117
|
+
const result = read(handlers.recommendPage({
|
|
118
|
+
intent: '系统设置页',
|
|
119
|
+
pageType: 'settings',
|
|
120
|
+
includeScaffold: true,
|
|
121
|
+
}));
|
|
122
|
+
expect(result.scaffold.source).toBe('golden-page');
|
|
123
|
+
expect(result.scaffold.goldenPage).toBe('settings-page');
|
|
124
|
+
expect(result.scaffold.files.component).toContain(':tabs=');
|
|
125
|
+
});
|
|
126
|
+
it('flags MTabs items and MStepper items as contract advisories', () => {
|
|
127
|
+
const tabs = read(handlers.validatePage({ code: `<MTabs :items="[]" />` }));
|
|
128
|
+
expect(tabs.suggestions.some((item) => item.type === 'tabs-items-prop')).toBe(true);
|
|
129
|
+
const stepper = read(handlers.validatePage({ code: `<MStepper :items="['a']" />` }));
|
|
130
|
+
expect(stepper.suggestions.some((item) => item.type === 'stepper-items-prop')).toBe(true);
|
|
87
131
|
});
|
|
88
132
|
it('returns a golden page source sample', () => {
|
|
89
133
|
const result = read(handlers.getGoldenPage({ page: 'list-page' }));
|
|
@@ -138,14 +182,15 @@ describe('@morya-ui/mcp handlers', () => {
|
|
|
138
182
|
it('keeps recommend_page focused on pattern matching', () => {
|
|
139
183
|
const result = read(handlers.recommendPage({ intent: '用户列表页', pageType: 'list' }));
|
|
140
184
|
expect(result.matchedPattern).toBe('admin-list');
|
|
141
|
-
expect(result.nextStep).toContain('
|
|
185
|
+
expect(result.nextStep).toContain('validate_usage');
|
|
186
|
+
expect(result.nextStep).toContain('get_golden_page');
|
|
142
187
|
expect(result.pageStandards).toBeUndefined();
|
|
143
188
|
expect(result.scrollStandards).toBeUndefined();
|
|
144
189
|
});
|
|
145
190
|
it('keeps get_golden_page focused on source code', () => {
|
|
146
191
|
const result = read(handlers.getGoldenPage({ page: 'list-page' }));
|
|
147
192
|
expect(result.source).toContain('MLayout');
|
|
148
|
-
expect(result.nextStep).toContain('
|
|
193
|
+
expect(result.nextStep).toContain('validate_usage');
|
|
149
194
|
expect(result.pageStandards).toBeUndefined();
|
|
150
195
|
expect(result.scrollStandards).toBeUndefined();
|
|
151
196
|
});
|
|
@@ -188,6 +233,44 @@ describe('@morya-ui/mcp handlers', () => {
|
|
|
188
233
|
expect(result.ok).toBe(true);
|
|
189
234
|
expect(result.suggestions.some((item) => item.type === 'menu-missing-icons')).toBe(false);
|
|
190
235
|
});
|
|
236
|
+
it('flags MTable data prop as contract advisory', () => {
|
|
237
|
+
const result = read(handlers.validatePage({
|
|
238
|
+
code: '<MTable :columns="columns" :data="rows" />',
|
|
239
|
+
}));
|
|
240
|
+
expect(result.ok).toBe(true);
|
|
241
|
+
expect(result.suggestions.some((item) => item.type === 'table-data-prop')).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
it('flags MMessage severity as inline-alert misuse', () => {
|
|
244
|
+
const result = read(handlers.validatePage({
|
|
245
|
+
code: '<MMessage severity="error">登录失败</MMessage>',
|
|
246
|
+
}));
|
|
247
|
+
expect(result.ok).toBe(true);
|
|
248
|
+
expect(result.suggestions.some((item) => item.type === 'mmessage-as-alert')).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
it('flags one-line toast string as message preference', () => {
|
|
251
|
+
const result = read(handlers.validatePage({
|
|
252
|
+
code: `toast.success('已保存')`,
|
|
253
|
+
}));
|
|
254
|
+
expect(result.ok).toBe(true);
|
|
255
|
+
expect(result.suggestions.some((item) => item.type === 'toast-one-liner')).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
it('flags MTag in cell-status when MStatus is missing', () => {
|
|
258
|
+
const result = read(handlers.validatePage({
|
|
259
|
+
code: `<template #cell-status="{ value }"><MTag :value="value" /></template>`,
|
|
260
|
+
}));
|
|
261
|
+
expect(result.ok).toBe(true);
|
|
262
|
+
expect(result.suggestions.some((item) => item.type === 'status-cell-tag')).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
it('returns detail-page and form-in-dialog golden samples', () => {
|
|
265
|
+
const detail = read(handlers.getGoldenPage({ page: 'detail-page' }));
|
|
266
|
+
expect(detail.id).toBe('detail-page');
|
|
267
|
+
expect(detail.source).toContain('MPageHeader');
|
|
268
|
+
expect(detail.source).toContain('MStatus');
|
|
269
|
+
const dialog = read(handlers.getGoldenPage({ page: 'form-in-dialog' }));
|
|
270
|
+
expect(dialog.id).toBe('form-in-dialog');
|
|
271
|
+
expect(dialog.source).toContain('MDialog');
|
|
272
|
+
expect(dialog.source).toContain('message.success');
|
|
273
|
+
});
|
|
191
274
|
it('lists component decision guides when query is omitted', () => {
|
|
192
275
|
const result = read(handlers.recommendComponent({ limit: 5 }));
|
|
193
276
|
expect(result.kind).toBe('decisions');
|
|
@@ -199,6 +282,16 @@ describe('@morya-ui/mcp handlers', () => {
|
|
|
199
282
|
expect(result.id).toBe('overlay-choice');
|
|
200
283
|
expect(result.options.some((option) => option.component === 'Drawer')).toBe(true);
|
|
201
284
|
});
|
|
285
|
+
it('covers current selection, status, empty, and menu guides', () => {
|
|
286
|
+
const selection = read(handlers.recommendComponent({ decision: 'selection-choice' }));
|
|
287
|
+
expect(selection.options.map((option) => option.component)).toEqual(expect.arrayContaining(['Select', 'CascadeSelect', 'Listbox', 'SelectButton', 'Radio']));
|
|
288
|
+
const status = read(handlers.recommendComponent({ decision: 'status-label-choice' }));
|
|
289
|
+
expect(status.id).toBe('status-label-choice');
|
|
290
|
+
const empty = read(handlers.recommendComponent({ decision: 'empty-result-choice' }));
|
|
291
|
+
expect(empty.options.map((option) => option.component)).toEqual(['Empty', 'Result']);
|
|
292
|
+
const menu = read(handlers.recommendComponent({ decision: 'action-menu-choice' }));
|
|
293
|
+
expect(menu.options.some((option) => option.component === 'ContextMenu')).toBe(true);
|
|
294
|
+
});
|
|
202
295
|
it('exposes catalog health in version metadata', () => {
|
|
203
296
|
const result = read(handlers.version());
|
|
204
297
|
expect(result.health.ok).toBe(true);
|