@morya-ui/mcp 0.2.8 → 0.2.9

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "generatedAt": "2026-09-21T01:37:39.078Z",
3
- "catalogGeneratedAt": "2026-09-21T01:37:31.355Z",
2
+ "generatedAt": "2026-09-21T09:57:51.741Z",
3
+ "catalogGeneratedAt": "2026-09-21T09:57:47.817Z",
4
4
  "components": [
5
5
  {
6
6
  "id": "Accordion",
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
3
  * 黄金样例:仪表盘页
4
- * @see DESIGN.md §3
4
+ * @see DESIGN.md · morya-ui-pages references/page-layouts.md · visual-craft § Ops polish
5
5
  */
6
6
  import {
7
7
  MBreadcrumb,
@@ -16,6 +16,7 @@ import {
16
16
  MPageHeader,
17
17
  MPagePlaceholder,
18
18
  MPageStat,
19
+ MStatus,
19
20
  MTable,
20
21
  MTag,
21
22
  zhCN,
@@ -32,7 +33,7 @@ const recentColumns = [
32
33
  { key: 'id', label: '工单号', width: 96 },
33
34
  { key: 'title', label: '标题' },
34
35
  { key: 'priority', label: '优先级', width: 96 },
35
- { key: 'status', label: '状态', width: 96 },
36
+ { key: 'status', label: '状态', width: 110 },
36
37
  ]
37
38
 
38
39
  const recentRows = [
@@ -47,11 +48,23 @@ function prioritySeverity(p: string) {
47
48
  return 'secondary'
48
49
  }
49
50
 
51
+ function priorityLabel(p: string) {
52
+ if (p === 'high') return '高'
53
+ if (p === 'medium') return '中'
54
+ return '低'
55
+ }
56
+
50
57
  function statusLabel(s: string) {
51
58
  if (s === 'open') return '待处理'
52
59
  if (s === 'progress') return '进行中'
53
60
  return '已完成'
54
61
  }
62
+
63
+ function statusSeverity(s: string) {
64
+ if (s === 'open') return 'warn'
65
+ if (s === 'progress') return 'info'
66
+ return 'success'
67
+ }
55
68
  </script>
56
69
 
57
70
  <template>
@@ -63,7 +76,10 @@ function statusLabel(s: string) {
63
76
 
64
77
  <MLayoutContent>
65
78
  <MPageContent density="spacious">
66
- <MPageHeader title="仪表盘" />
79
+ <MPageHeader
80
+ title="运营概览"
81
+ description="关注今日活跃与待处理工单,异常优先下钻。"
82
+ />
67
83
 
68
84
  <MGrid :cols="4" :x-gap="16" :y-gap="16" responsive="screen">
69
85
  <MGridItem v-for="item in stats" :key="item.label" :span="1">
@@ -77,20 +93,37 @@ function statusLabel(s: string) {
77
93
  </MGridItem>
78
94
  </MGrid>
79
95
 
80
- <MGrid :cols="2" :x-gap="16" :y-gap="16">
96
+ <MGrid :cols="2" :x-gap="16" :y-gap="16" responsive="screen">
81
97
  <MGridItem :span="1">
82
98
  <MCard title="趋势概览">
83
- <MPagePlaceholder aria-label="图表占位" description="图表区域(接入 ECharts / 业务组件)" />
99
+ <MPagePlaceholder
100
+ aria-label="图表占位"
101
+ description="接入图表组件后展示近 7 日活跃与转化。"
102
+ />
84
103
  </MCard>
85
104
  </MGridItem>
86
105
  <MGridItem :span="1">
87
106
  <MCard title="最近工单">
88
- <MTable :columns="recentColumns" :rows="recentRows" size="small" :paginator="false" bordered>
107
+ <MTable
108
+ :columns="recentColumns"
109
+ :rows="recentRows"
110
+ size="small"
111
+ :paginator="false"
112
+ bordered
113
+ row-key="id"
114
+ aria-label="最近工单"
115
+ >
89
116
  <template #cell-priority="{ value }">
90
- <MTag :value="String(value)" :severity="prioritySeverity(String(value))" />
117
+ <MTag
118
+ :value="priorityLabel(String(value))"
119
+ :severity="prioritySeverity(String(value))"
120
+ />
91
121
  </template>
92
122
  <template #cell-status="{ value }">
93
- <MTag :value="statusLabel(String(value))" severity="info" />
123
+ <MStatus
124
+ :label="statusLabel(String(value))"
125
+ :severity="statusSeverity(String(value))"
126
+ />
94
127
  </template>
95
128
  </MTable>
96
129
  </MCard>
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
3
  * 黄金样例:列表空状态(Flow)
4
- * @see DESIGN.md §3 · morya-ui-pages surfaces § Flow
4
+ * @see DESIGN.md · page-layouts.md · surfaces § Flow
5
5
  * 可嵌在列表页 MPageContent / MTable #empty 中;此处给出完整可运行骨架。
6
6
  */
7
7
  import {
@@ -53,6 +53,10 @@ import {
53
53
 
54
54
  <style scoped>
55
55
  .empty-state-shell {
56
+ display: grid;
57
+ place-items: center;
58
+ min-height: 18rem;
59
+ padding: var(--m-space-8) var(--m-space-6);
56
60
  border: 1px dashed color-mix(in srgb, var(--m-color-border) 80%, var(--m-color-primary));
57
61
  border-radius: var(--m-radius-md);
58
62
  background:
@@ -63,4 +67,21 @@ import {
63
67
  ),
64
68
  var(--m-color-surface);
65
69
  }
70
+
71
+ @media (prefers-reduced-motion: no-preference) {
72
+ .empty-state-shell {
73
+ animation: empty-in 400ms ease both;
74
+ }
75
+ }
76
+
77
+ @keyframes empty-in {
78
+ from {
79
+ opacity: 0;
80
+ transform: translateY(0.35rem);
81
+ }
82
+ to {
83
+ opacity: 1;
84
+ transform: translateY(0);
85
+ }
86
+ }
66
87
  </style>
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
3
  * 黄金样例:表单页
4
- * @see DESIGN.md §3
4
+ * @see DESIGN.md · morya-ui-pages references/page-layouts.md
5
5
  */
6
6
  import {
7
7
  MBreadcrumb,
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
3
  * 黄金样例:营销落地页(Express)
4
- * @see DESIGN.md §3 · morya-ui-pages surfaces § Express
4
+ * @see DESIGN.md · page-layouts.md · surfaces § Express
5
5
  * 首屏单一任务;控件用 M*;色彩只走 --m-*;避开 AI 默认脸。
6
6
  */
7
7
  import { MAccordion, MButton, MConfigProvider, MTag, zhCN } from 'morya-ui'
@@ -219,6 +219,36 @@ const faqTabs = [
219
219
  font-weight: 600;
220
220
  }
221
221
 
222
+ @media (prefers-reduced-motion: no-preference) {
223
+ .landing-hero__track span {
224
+ animation: landing-fade-up 480ms ease both;
225
+ }
226
+
227
+ .landing-hero__track span:nth-child(2) {
228
+ animation-delay: 60ms;
229
+ }
230
+
231
+ .landing-hero__track span:nth-child(3) {
232
+ animation-delay: 120ms;
233
+ }
234
+
235
+ .landing-hero__track span:nth-child(4) {
236
+ animation-delay: 180ms;
237
+ }
238
+ }
239
+
240
+ @keyframes landing-fade-up {
241
+ from {
242
+ opacity: 0;
243
+ transform: translateY(0.4rem);
244
+ }
245
+
246
+ to {
247
+ opacity: 1;
248
+ transform: translateY(0);
249
+ }
250
+ }
251
+
222
252
  .landing-section {
223
253
  padding: clamp(2.5rem, 6vw, 4rem) clamp(1.25rem, 4vw, 3rem);
224
254
  max-width: 72rem;
@@ -1,12 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
3
  * 黄金样例:列表页
4
- * @see DESIGN.md §3
4
+ * @see DESIGN.md · morya-ui-pages references/page-layouts.md · visual-craft § Ops polish
5
5
  */
6
6
  import {
7
7
  MBreadcrumb,
8
8
  MButton,
9
9
  MConfigProvider,
10
+ MEmpty,
10
11
  MInput,
11
12
  MLayout,
12
13
  MLayoutContent,
@@ -18,8 +19,8 @@ import {
18
19
  MPageToolbar,
19
20
  MSelect,
20
21
  MSpace,
22
+ MStatus,
21
23
  MTable,
22
- MTag,
23
24
  zhCN,
24
25
  } from 'morya-ui'
25
26
  import { ref } from 'vue'
@@ -31,6 +32,7 @@ const siderCollapsed = ref(false)
31
32
  const menuModel = [
32
33
  { key: 'users', label: '用户管理', icon: 'user', to: '/users' },
33
34
  { key: 'roles', label: '角色管理', icon: 'shield', to: '/roles' },
35
+ { key: 'settings', label: '系统设置', icon: 'settings', to: '/settings' },
34
36
  ]
35
37
 
36
38
  const statusOptions = [
@@ -41,9 +43,9 @@ const statusOptions = [
41
43
 
42
44
  const columns = [
43
45
  { key: 'name', label: '名称' },
44
- { key: 'status', label: '状态' },
45
- { key: 'updatedAt', label: '更新时间' },
46
- { key: 'actions', label: '操作', width: 128 },
46
+ { key: 'status', label: '状态', width: 120 },
47
+ { key: 'updatedAt', label: '更新时间', width: 140 },
48
+ { key: 'actions', label: '操作', width: 148 },
47
49
  ]
48
50
 
49
51
  const rows = [
@@ -104,7 +106,7 @@ const rows = [
104
106
  <MTable
105
107
  :columns="columns"
106
108
  :rows="rows"
107
- :rows-per-page="3"
109
+ :rows-per-page="5"
108
110
  paginator
109
111
  striped
110
112
  bordered
@@ -112,22 +114,33 @@ const rows = [
112
114
  aria-label="用户列表"
113
115
  >
114
116
  <template #cell-status="{ value }">
115
- <MTag :value="value === 'active' ? '启用' : '停用'" :severity="value === 'active' ? 'success' : 'secondary'" />
117
+ <MStatus
118
+ :label="value === 'active' ? '启用' : '停用'"
119
+ :severity="value === 'active' ? 'success' : 'secondary'"
120
+ />
116
121
  </template>
117
122
  <template #cell-actions>
118
123
  <MSpace>
119
- <MButton severity="secondary" size="small">
124
+ <MButton severity="secondary" size="small" text>
120
125
  编辑
121
126
  </MButton>
122
- <MButton severity="danger" size="small">
127
+ <MButton severity="danger" size="small" text>
123
128
  删除
124
129
  </MButton>
125
130
  </MSpace>
126
131
  </template>
127
132
  <template #empty>
128
- <p style="margin: 0; padding: var(--m-space-8); text-align: center; color: var(--m-color-text-muted)">
129
- 暂无用户数据
130
- </p>
133
+ <MEmpty
134
+ title="还没有用户"
135
+ description="创建第一个用户后,即可分配角色与权限。"
136
+ icon="user"
137
+ >
138
+ <template #extra>
139
+ <MButton severity="primary">
140
+ 新建用户
141
+ </MButton>
142
+ </template>
143
+ </MEmpty>
131
144
  </template>
132
145
  </MTable>
133
146
  </MPageContent>
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
3
  * 黄金样例:登录页(Account + 轻量品牌)
4
- * @see DESIGN.md §3 · morya-ui-pages surfaces § Account
4
+ * @see DESIGN.md · surfaces § Account · visual-craft § Atmosphere
5
5
  */
6
6
  import {
7
7
  MButton,
@@ -56,7 +56,6 @@ async function onSubmit() {
56
56
  <p>使用工作邮箱进入后台。</p>
57
57
  </header>
58
58
 
59
- <!-- 表单级常驻错误:token 样式告警条(字段错误优先用 errorMessage) -->
60
59
  <p v-if="formError" class="login-alert" role="alert">
61
60
  {{ formError }}
62
61
  </p>
@@ -82,7 +81,7 @@ async function onSubmit() {
82
81
  </MFormItem>
83
82
 
84
83
  <MSpace style="margin-top: var(--m-space-2)" alignment="center">
85
- <MButton type="submit" label="登录" :loading="submitting" />
84
+ <MButton type="submit" label="登录" severity="primary" :loading="submitting" />
86
85
  <MButton type="button" label="忘记密码" severity="secondary" text />
87
86
  </MSpace>
88
87
  </MForm>
@@ -102,21 +101,45 @@ async function onSubmit() {
102
101
  }
103
102
 
104
103
  .login-brand {
104
+ position: relative;
105
105
  display: flex;
106
106
  flex-direction: column;
107
107
  justify-content: flex-end;
108
108
  gap: var(--m-space-4);
109
109
  padding: clamp(2rem, 6vw, 4.5rem);
110
+ overflow: hidden;
110
111
  background:
112
+ radial-gradient(
113
+ 80% 60% at 10% 20%,
114
+ color-mix(in srgb, var(--m-color-primary) 22%, transparent),
115
+ transparent 55%
116
+ ),
111
117
  linear-gradient(
112
118
  165deg,
113
- color-mix(in srgb, var(--m-color-primary) 18%, var(--m-color-surface)) 0%,
119
+ color-mix(in srgb, var(--m-color-primary) 16%, var(--m-color-surface)) 0%,
114
120
  var(--m-color-surface) 55%,
115
121
  color-mix(in srgb, var(--m-color-border) 35%, var(--m-color-surface)) 100%
116
122
  );
117
123
  border-right: 1px solid var(--m-color-border);
118
124
  }
119
125
 
126
+ .login-brand::after {
127
+ content: '';
128
+ position: absolute;
129
+ inset: auto -10% -20% 40%;
130
+ height: 55%;
131
+ border-radius: 50%;
132
+ background: color-mix(in srgb, var(--m-color-primary) 12%, transparent);
133
+ pointer-events: none;
134
+ }
135
+
136
+ .login-brand__mark,
137
+ .login-brand__title,
138
+ .login-brand__lead {
139
+ position: relative;
140
+ z-index: 1;
141
+ }
142
+
120
143
  .login-brand__mark {
121
144
  margin: 0;
122
145
  font-size: 0.8125rem;
@@ -177,6 +200,23 @@ async function onSubmit() {
177
200
  line-height: 1.45;
178
201
  }
179
202
 
203
+ @media (prefers-reduced-motion: no-preference) {
204
+ .login-panel {
205
+ animation: login-panel-in 420ms ease both;
206
+ }
207
+ }
208
+
209
+ @keyframes login-panel-in {
210
+ from {
211
+ opacity: 0;
212
+ transform: translateY(0.5rem);
213
+ }
214
+ to {
215
+ opacity: 1;
216
+ transform: translateY(0);
217
+ }
218
+ }
219
+
180
220
  @media (max-width: 768px) {
181
221
  .login-shell {
182
222
  grid-template-columns: 1fr;
@@ -199,6 +199,16 @@ describe('@morya-ui/mcp handlers', () => {
199
199
  expect(result.id).toBe('overlay-choice');
200
200
  expect(result.options.some((option) => option.component === 'Drawer')).toBe(true);
201
201
  });
202
+ it('covers current selection, status, empty, and menu guides', () => {
203
+ const selection = read(handlers.recommendComponent({ decision: 'selection-choice' }));
204
+ expect(selection.options.map((option) => option.component)).toEqual(expect.arrayContaining(['Select', 'CascadeSelect', 'Listbox', 'SelectButton', 'Radio']));
205
+ const status = read(handlers.recommendComponent({ decision: 'status-label-choice' }));
206
+ expect(status.id).toBe('status-label-choice');
207
+ const empty = read(handlers.recommendComponent({ decision: 'empty-result-choice' }));
208
+ expect(empty.options.map((option) => option.component)).toEqual(['Empty', 'Result']);
209
+ const menu = read(handlers.recommendComponent({ decision: 'action-menu-choice' }));
210
+ expect(menu.options.some((option) => option.component === 'ContextMenu')).toBe(true);
211
+ });
202
212
  it('exposes catalog health in version metadata', () => {
203
213
  const result = read(handlers.version());
204
214
  expect(result.health.ok).toBe(true);
package/dist/decisions.js CHANGED
@@ -136,7 +136,7 @@ export const componentDecisions = [
136
136
  titleEn: 'Choosing a selection control',
137
137
  question: '选项是平面少量、层级结构、多选标签,还是需要输入搜索?',
138
138
  questionEn: 'Are options a small flat set, a hierarchy, multi-select tags, or searchable input?',
139
- keywords: ['选择', '下拉', '多选', '树选择', '搜索选择', 'select', 'treeselect', 'autocomplete', 'dropdown'],
139
+ keywords: ['选择', '下拉', '多选', '树选择', '搜索选择', '级联', '列表选择', '按钮组', 'select', 'treeselect', 'autocomplete', 'cascadeselect', 'listbox', 'selectbutton', 'radio', 'dropdown'],
140
140
  options: [
141
141
  {
142
142
  component: 'Select',
@@ -159,6 +159,34 @@ export const componentDecisions = [
159
159
  avoidWhen: ['用户只能从固定枚举中选择', '不应该允许自由输入'],
160
160
  avoidWhenEn: ['Users must choose from a fixed enum', 'Free input must not be allowed'],
161
161
  },
162
+ {
163
+ component: 'SelectButton',
164
+ when: ['选项很少且需要全部露出', '单选或多选都适合按钮组'],
165
+ whenEn: ['Only a few options, and all of them should stay visible', 'Single or multiple selection works as a button group'],
166
+ avoidWhen: ['选项超过大约 5 个', '选项很长或来自远程搜索'],
167
+ avoidWhenEn: ['More than about five options', 'Labels are long or options come from remote search'],
168
+ },
169
+ {
170
+ component: 'Radio',
171
+ when: ['少量互斥选项需要和表单文案一起阅读', '一次只能选一个'],
172
+ whenEn: ['A few mutually exclusive options should be read with the form copy', 'Only one value can be selected'],
173
+ avoidWhen: ['多选', '选项很多需要收进弹出层'],
174
+ avoidWhenEn: ['Multiple selection', 'Too many options for an always-visible list'],
175
+ },
176
+ {
177
+ component: 'Listbox',
178
+ when: ['选项需要始终以列表展示', '可筛选,但不希望收成下拉框'],
179
+ whenEn: ['Options should stay visible as a list', 'Filtering is useful, but a collapsed dropdown is not'],
180
+ avoidWhen: ['页面空间紧、只需要一个闭合的选择框'],
181
+ avoidWhenEn: ['Space is tight and a closed select is enough'],
182
+ },
183
+ {
184
+ component: 'CascadeSelect',
185
+ when: ['值要按多级分栏逐级点选', '层级是路径而不是可勾选的树'],
186
+ whenEn: ['The value is chosen column by column through levels', 'The hierarchy is a path, not a checkable tree'],
187
+ avoidWhen: ['需要勾选树节点或搜索整棵树'],
188
+ avoidWhenEn: ['Users need to check tree nodes or search the whole tree'],
189
+ },
162
190
  ],
163
191
  },
164
192
  {
@@ -452,6 +480,113 @@ export const componentDecisions = [
452
480
  },
453
481
  ],
454
482
  },
483
+ {
484
+ id: 'status-label-choice',
485
+ title: '如何选择状态与标签',
486
+ titleEn: 'Choosing status, tag, chip, or badge',
487
+ question: '这是行内业务状态、分类标签、可移除实体,还是挂在控件上的数量?',
488
+ questionEn: 'Is this an inline business status, a category label, a removable entity, or a count on another control?',
489
+ keywords: ['行内状态', '状态点', '角标', '徽章', '可关闭标签', '芯片', 'status', 'badge', 'chip', 'tag', 'closable'],
490
+ options: [
491
+ {
492
+ component: 'Status',
493
+ when: ['表格单元格或标题旁的业务状态', '需要圆点或语义图标加短文案'],
494
+ whenEn: ['Business status in a table cell or beside a title', 'A dot or semantic icon plus a short label'],
495
+ avoidWhen: ['可关闭的分类标签', '挂在按钮上的数字'],
496
+ avoidWhenEn: ['A closable category label', 'A count attached to a button'],
497
+ },
498
+ {
499
+ component: 'Tag',
500
+ when: ['分类、筛选结果或可关闭标签', '需要比 Status 更像芯片的表面'],
501
+ whenEn: ['A category, filter result, or closable label', 'A chip-like surface rather than an inline status'],
502
+ avoidWhen: ['只是一行里的轻量状态', '带图片且代表一个可移除实体'],
503
+ avoidWhenEn: ['A lightweight inline status', 'An entity with an image that can be removed'],
504
+ },
505
+ {
506
+ component: 'Chip',
507
+ when: ['短实体信息,可带图标、图片和移除'],
508
+ whenEn: ['A short entity that may include an icon, image, and remove action'],
509
+ avoidWhen: ['纯状态色点', '只是分类色块'],
510
+ avoidWhenEn: ['A status dot only', 'A category color block only'],
511
+ },
512
+ {
513
+ component: 'Badge',
514
+ when: ['角标数量或圆点,附着在按钮、头像等控件上'],
515
+ whenEn: ['A count or dot attached to a button, avatar, or other control'],
516
+ avoidWhen: ['独立成行的状态文案'],
517
+ avoidWhenEn: ['A standalone status sentence'],
518
+ },
519
+ ],
520
+ },
521
+ {
522
+ id: 'empty-result-choice',
523
+ title: '空态还是结果页',
524
+ titleEn: 'Empty state vs result page',
525
+ question: '这是没有数据,还是流程已经结束的成功、失败或 HTTP 状态?',
526
+ questionEn: 'Is there simply no data, or has a flow ended in success, failure, or an HTTP status?',
527
+ keywords: ['空状态', '无数据', '首次使用', '筛选无结果', '结果页', '404', '403', '500', 'empty', 'result'],
528
+ options: [
529
+ {
530
+ component: 'Empty',
531
+ when: ['列表没有行', '筛选没有命中', '首次使用,需要引导创建'],
532
+ whenEn: ['A list has no rows', 'Filters matched nothing', 'First use, and the next step is to create something'],
533
+ avoidWhen: ['接口失败', '无权限', '页面不存在', '提交成功回执'],
534
+ avoidWhenEn: ['The request failed', 'Permission was denied', 'The page does not exist', 'A submit-success receipt'],
535
+ },
536
+ {
537
+ component: 'Result',
538
+ when: ['提交成功或失败的终点页', '403 / 404 / 500 等阻断状态', '需要明确的下一步(返回、重试、回首页)'],
539
+ whenEn: ['A terminal success or failure page', 'A blocking 403 / 404 / 500 state', 'The next step must be explicit (back, retry, home)'],
540
+ avoidWhen: ['正常的无数据', '表格内部的空行'],
541
+ avoidWhenEn: ['A normal empty collection', 'The empty slot inside a table'],
542
+ },
543
+ ],
544
+ },
545
+ {
546
+ id: 'action-menu-choice',
547
+ title: '如何选择菜单',
548
+ titleEn: 'Choosing a menu',
549
+ question: '这是按钮上的操作项、侧栏导航、右键菜单,还是全局命令搜索?',
550
+ questionEn: 'Is this an action menu on a trigger, sider navigation, a context menu, or a global command search?',
551
+ keywords: ['动作菜单', '右键', '右键菜单', '命令面板', '导航菜单', 'context menu', 'command menu', 'menubar', '上下文菜单'],
552
+ options: [
553
+ {
554
+ component: 'Dropdown',
555
+ when: ['从按钮或图标打开一组操作', '编辑、删除、更多'],
556
+ whenEn: ['Actions open from a button or icon', 'Edit, delete, or more'],
557
+ avoidWhen: ['表单里选一个枚举值', '常驻侧栏导航'],
558
+ avoidWhenEn: ['Choosing an enum in a form', 'Persistent sider navigation'],
559
+ },
560
+ {
561
+ component: 'Menu',
562
+ when: ['后台侧栏或页面内的常驻导航', '需要选中项与路由同步'],
563
+ whenEn: ['Persistent navigation in an admin sider or page', 'Selection should stay in sync with the route'],
564
+ avoidWhen: ['一次性操作菜单', '右键弹出'],
565
+ avoidWhenEn: ['A one-shot action menu', 'A right-click popup'],
566
+ },
567
+ {
568
+ component: 'ContextMenu',
569
+ when: ['在指针位置弹出,通常由右键触发'],
570
+ whenEn: ['The menu opens at the pointer, usually from a right-click'],
571
+ avoidWhen: ['可见的导航', '表单选择'],
572
+ avoidWhenEn: ['Visible navigation', 'Form selection'],
573
+ },
574
+ {
575
+ component: 'CommandMenu',
576
+ when: ['全局搜索命令或跳转', '键盘优先,例如 Cmd/Ctrl+K'],
577
+ whenEn: ['Global command search or jump', 'Keyboard-first, such as Cmd/Ctrl+K'],
578
+ avoidWhen: ['少量固定操作放在按钮旁即可'],
579
+ avoidWhenEn: ['A few fixed actions fit next to a button'],
580
+ },
581
+ {
582
+ component: 'Menubar',
583
+ when: ['顶部应用菜单,项很多且分组'],
584
+ whenEn: ['A top application menu with many grouped items'],
585
+ avoidWhen: ['侧栏只有一层链接'],
586
+ avoidWhenEn: ['The sider is a single level of links'],
587
+ },
588
+ ],
589
+ },
455
590
  ];
456
591
  export function findDecision(name) {
457
592
  const key = name.trim().toLowerCase().replace(/[-_\s]/g, '');
@@ -61,7 +61,7 @@ const statusOptions = [
61
61
  descriptionEn: 'Standard MTable placed directly in MPageContent without an extra Card wrapper.',
62
62
  pageTypes: ['list'],
63
63
  keywords: ['表格', 'table', 'pagination', 'paginator', 'empty', 'columns', 'rows'],
64
- imports: ['MTable'],
64
+ imports: ['MTable', 'MEmpty', 'MButton'],
65
65
  scriptSetup: `const columns = [
66
66
  { key: 'name', label: '名称' },
67
67
  { key: 'status', label: '状态' },
@@ -82,13 +82,19 @@ const loading = ref(false)`,
82
82
  aria-label="数据列表"
83
83
  >
84
84
  <template #empty>
85
- <p style="margin:0;padding:var(--m-space-8);text-align:center;color:var(--m-color-text-muted)">
86
- 暂无数据
87
- </p>
85
+ <MEmpty
86
+ title="还没有数据"
87
+ description="创建第一条记录后,这里会列出结果。"
88
+ icon="inbox"
89
+ >
90
+ <template #extra>
91
+ <MButton severity="primary">新建</MButton>
92
+ </template>
93
+ </MEmpty>
88
94
  </template>
89
95
  </MTable>`,
90
- rules: ['表格直接放在 MPageContent 内', '空态用 #empty,不要留空白区域'],
91
- rulesEn: ['Place the table directly in MPageContent', 'Use #empty for zero-data states'],
96
+ rules: ['表格直接放在 MPageContent 内', '空态用 MEmpty,不要留空白或单行灰字'],
97
+ rulesEn: ['Place the table directly in MPageContent', 'Use MEmpty for zero-data states, not a blank or muted sentence'],
92
98
  avoid: ['不要用 MCard 包裹 bordered MTable'],
93
99
  avoidEn: ['Do not wrap a bordered MTable with MCard'],
94
100
  },
@@ -103,12 +109,12 @@ const loading = ref(false)`,
103
109
  imports: ['MSpace', 'MButton'],
104
110
  template: `<template #cell-actions>
105
111
  <MSpace>
106
- <MButton severity="secondary" size="small">编辑</MButton>
107
- <MButton severity="danger" size="small">删除</MButton>
112
+ <MButton severity="secondary" size="small" text>编辑</MButton>
113
+ <MButton severity="danger" size="small" text>删除</MButton>
108
114
  </MSpace>
109
115
  </template>`,
110
- rules: ['行内操作用 size="small"', '危险操作使用 severity="danger" 并配合确认弹窗'],
111
- rulesEn: ['Use size="small" for row actions', 'Use severity="danger" with confirmation for destructive actions'],
116
+ rules: ['行内操作用 size="small" + text/outlined,避免一排实心按钮', '危险操作使用 severity="danger" 并配合确认弹窗'],
117
+ rulesEn: ['Use size="small" with text/outlined row actions', 'Use severity="danger" with confirmation for destructive actions'],
112
118
  avoid: ['不要用 Dropdown 代替明确的行内按钮组,除非操作很多'],
113
119
  avoidEn: ['Do not replace explicit row buttons with Dropdown unless there are many actions'],
114
120
  },
@@ -116,8 +122,8 @@ const loading = ref(false)`,
116
122
  id: 'list-status-tag',
117
123
  title: '表格状态列 Tag',
118
124
  titleEn: 'Table status tag cell',
119
- description: '在 #cell-status 中用 MTag 展示业务状态。',
120
- descriptionEn: 'Render business status with MTag in #cell-status.',
125
+ description: '在 #cell-status 中用 MTag 展示分类标签(业务状态优先用 list-status-dot / MStatus)。',
126
+ descriptionEn: 'Render category labels with MTag in #cell-status (prefer MStatus for business status).',
121
127
  pageTypes: ['list'],
122
128
  keywords: ['状态', 'tag', 'status', 'cell-status'],
123
129
  imports: ['MTag'],
@@ -313,18 +319,22 @@ const submitting = ref(false)`,
313
319
  descriptionEn: 'Recent records in a small MTable inside MCard.',
314
320
  pageTypes: ['dashboard'],
315
321
  keywords: ['recent', '最近', 'table', 'card', 'list'],
316
- imports: ['MCard', 'MTable', 'MTag'],
322
+ imports: ['MCard', 'MTable', 'MStatus'],
317
323
  scriptSetup: `const recentColumns = [
318
324
  { key: 'id', label: '工单号', width: 96 },
319
325
  { key: 'title', label: '标题' },
320
- { key: 'status', label: '状态', width: 96 },
326
+ { key: 'status', label: '状态', width: 110 },
321
327
  ]
322
328
  const recentRows = ref<Record<string, unknown>[]>([])`,
323
329
  template: `<MCard title="最近工单">
324
- <MTable :columns="recentColumns" :rows="recentRows" size="small" :paginator="false" bordered />
330
+ <MTable :columns="recentColumns" :rows="recentRows" size="small" :paginator="false" bordered row-key="id">
331
+ <template #cell-status="{ value }">
332
+ <MStatus :label="String(value ?? '')" severity="secondary" />
333
+ </template>
334
+ </MTable>
325
335
  </MCard>`,
326
- rules: ['明细列表放 MCard 内合理', 'size="small" 适合卡片内表格'],
327
- rulesEn: ['Detail lists inside MCard are appropriate', 'Use size="small" for in-card tables'],
336
+ rules: ['明细列表放 MCard 内合理', 'size="small" 适合卡片内表格', '行内业务状态用 MStatus'],
337
+ rulesEn: ['Detail lists inside MCard are appropriate', 'Use size="small" for in-card tables', 'Use MStatus for row business status'],
328
338
  avoid: ['卡片内表格不要再外包一层 MPageContent'],
329
339
  avoidEn: ['Do not wrap in-card tables with another MPageContent'],
330
340
  },