mdk-skills 2.1.11 → 2.1.13

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.
@@ -26,9 +26,10 @@
26
26
  }
27
27
  },
28
28
  "always_apply_skills": [
29
- "frontend-design",
30
29
  "vue",
30
+ "frontend-design",
31
31
  "frontend-code-review",
32
32
  "ui-ux-pro-max"
33
- ]
33
+ ],
34
+ "_active_profile": "vue3-frontend"
34
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdk-skills",
3
- "version": "2.1.11",
3
+ "version": "2.1.13",
4
4
  "description": "mdk-engineer - 沉稳靠谱的前端开发助手 Claude Skills 配置包,一键注入 .claude/ 技能目录和 CLAUDE.md 人设配置",
5
5
  "author": "XiaoMa",
6
6
  "license": "MIT",
@@ -18,6 +18,13 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@inquirer/prompts": "^8.4.2",
21
- "chalk": "^5.6.2"
21
+ "@vicons/ionicons5": "^0.13.0",
22
+ "@vicons/material": "^0.13.0",
23
+ "@vitejs/plugin-vue": "^6.0.6",
24
+ "chalk": "^5.6.2",
25
+ "naive-ui": "^2.44.1",
26
+ "vite": "^8.0.11",
27
+ "vue": "^3.5.34",
28
+ "vue-router": "^5.0.6"
22
29
  }
23
30
  }
package/scripts/cli.js CHANGED
@@ -15,7 +15,7 @@ const projectRoot = (() => {
15
15
  }
16
16
  })();
17
17
  const packageDir = path.join(__dirname, "..");
18
- const skillsSource = getSkillsSource(projectRoot, packageDir);
18
+ let skillsSource = getSkillsSource(projectRoot, packageDir);
19
19
  const claudeDest = path.join(projectRoot, ".claude");
20
20
  const skillsDest = path.join(claudeDest, "skills");
21
21
  const settingsPath = path.join(claudeDest, "settings.json");
@@ -487,7 +487,7 @@ function cmdDisconnect() {
487
487
 
488
488
  // ---------- 主入口 ----------
489
489
 
490
- const COMMANDS = ["list", "connect", "sync", "disconnect"];
490
+ const COMMANDS = ["list", "connect", "sync", "disconnect", "ui"];
491
491
 
492
492
  async function main() {
493
493
  const command = process.argv[2];
@@ -511,18 +511,30 @@ async function main() {
511
511
  npx mdk-skills connect 绑定本地技能源仓库路径
512
512
  npx mdk-skills sync 将项目中的技能修改推送到仓库
513
513
  npx mdk-skills disconnect 解绑本地源,恢复使用 npm 包内置技能
514
+ npx mdk-skills ui 启动 Web 管理面板
515
+ npx mdk-skills --connect 绑定本地仓库后直接进入场景选择
514
516
  npx mdk-skills --help 显示帮助
515
517
 
516
518
  首次运行选择场景后自动配置技能和 CLAUDE.md
517
519
  再次运行可切换场景或进入自定义模式微调
518
520
 
519
521
  示例:
522
+ npx mdk-skills --connect D:/dev/mdk-skills
520
523
  npx mdk-skills connect D:/dev/mdk-skills
521
524
  npx mdk-skills sync
522
525
  npx mdk-skills disconnect
523
526
  npx mdk-skills
524
527
  npx mdk-skills list
525
528
  `);
529
+ } else if (command === "--connect") {
530
+ cmdConnect(args[0]);
531
+ skillsSource = getSkillsSource(projectRoot, packageDir);
532
+ const profiles = loadProfiles();
533
+ if (profiles) {
534
+ await startSceneSelection();
535
+ } else {
536
+ await startInteractiveMenu();
537
+ }
526
538
  } else if (command === "list") {
527
539
  await cmdList();
528
540
  } else if (command === "connect") {
@@ -531,6 +543,8 @@ async function main() {
531
543
  cmdSync();
532
544
  } else if (command === "disconnect") {
533
545
  cmdDisconnect();
546
+ } else if (command === "ui") {
547
+ require("./web-ui/server");
534
548
  } else {
535
549
  console.log(` 未知命令: ${command}\n 可用: ${COMMANDS.join(", ")}\n`);
536
550
  process.exit(1);
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>mdk-skills 管理面板</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.js"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,461 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { createServer: createViteServer } = require("vite");
4
+ const core = require("../core");
5
+
6
+ // ---------- 路径(与 cli.js 保持一致) ----------
7
+
8
+ const projectRoot = (() => {
9
+ try {
10
+ return fs.realpathSync.native(process.cwd());
11
+ } catch {
12
+ return process.cwd();
13
+ }
14
+ })();
15
+ const packageDir = path.join(__dirname, "..", "..");
16
+ const skillsSource = core.getSkillsSource(projectRoot, packageDir);
17
+ const claudeDest = path.join(projectRoot, ".claude");
18
+ const skillsDest = path.join(claudeDest, "skills");
19
+ const settingsPath = path.join(claudeDest, "settings.json");
20
+ const pkgSkillsSource = path.join(skillsSource, ".claude", "skills");
21
+
22
+ // ---------- 工具函数 ----------
23
+
24
+ function readSettings() {
25
+ if (!fs.existsSync(settingsPath))
26
+ return { skills: {}, always_apply_skills: [] };
27
+ try {
28
+ return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
29
+ } catch {
30
+ return { skills: {}, always_apply_skills: [] };
31
+ }
32
+ }
33
+
34
+ function writeSettings(settings) {
35
+ if (!fs.existsSync(claudeDest)) {
36
+ fs.mkdirSync(claudeDest, { recursive: true });
37
+ }
38
+ fs.writeFileSync(
39
+ settingsPath,
40
+ JSON.stringify(settings, null, 2) + "\n",
41
+ "utf-8",
42
+ );
43
+ }
44
+
45
+ function sendJSON(res, data, status = 200) {
46
+ res.writeHead(status, { "Content-Type": "application/json" });
47
+ res.end(JSON.stringify(data));
48
+ }
49
+
50
+ function parseBody(req) {
51
+ return new Promise((resolve) => {
52
+ let body = "";
53
+ req.on("data", (chunk) => (body += chunk));
54
+ req.on("end", () => {
55
+ try {
56
+ resolve(JSON.parse(body));
57
+ } catch {
58
+ resolve({});
59
+ }
60
+ });
61
+ });
62
+ }
63
+
64
+ // 手动递归拷贝目录
65
+ function copyDirSync(src, dest) {
66
+ fs.mkdirSync(dest, { recursive: true });
67
+ for (const item of fs.readdirSync(src)) {
68
+ const srcPath = path.join(src, item);
69
+ const destPath = path.join(dest, item);
70
+ if (fs.statSync(srcPath).isDirectory()) {
71
+ copyDirSync(srcPath, destPath);
72
+ } else {
73
+ fs.copyFileSync(srcPath, destPath);
74
+ }
75
+ }
76
+ }
77
+
78
+ // 读取 profiles.json
79
+ function loadProfiles() {
80
+ const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
81
+ if (!fs.existsSync(profilesPath)) return null;
82
+ try {
83
+ return JSON.parse(fs.readFileSync(profilesPath, "utf-8")).profiles;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ // 应用场景
90
+ function applyProfile(profile) {
91
+ const pkgSkills = fs.existsSync(pkgSkillsSource)
92
+ ? fs.readdirSync(pkgSkillsSource)
93
+ : [];
94
+ const settings = readSettings();
95
+
96
+ let selected;
97
+ if (profile.skills === null) {
98
+ return { selected: [], message: "自定义场景,不做自动切换" };
99
+ } else if (profile.skills.length === 0) {
100
+ selected = [];
101
+ } else {
102
+ selected = profile.skills.filter((s) => pkgSkills.includes(s));
103
+ }
104
+
105
+ // 安装/删除技能
106
+ if (!fs.existsSync(skillsDest)) {
107
+ fs.mkdirSync(skillsDest, { recursive: true });
108
+ }
109
+ for (const name of pkgSkills) {
110
+ const src = path.join(pkgSkillsSource, name);
111
+ const dest = path.join(skillsDest, name);
112
+ if (selected.includes(name)) {
113
+ if (!fs.existsSync(dest)) {
114
+ try {
115
+ copyDirSync(src, dest);
116
+ } catch (err) {
117
+ console.error(`安装技能 "${name}" 失败:`, err.message);
118
+ }
119
+ }
120
+ } else {
121
+ if (fs.existsSync(dest)) {
122
+ fs.rmSync(dest, { recursive: true, force: true });
123
+ }
124
+ }
125
+ }
126
+
127
+ // 更新 settings
128
+ if (!settings.skills) settings.skills = {};
129
+ for (const name of pkgSkills) {
130
+ const enabled = selected.includes(name);
131
+ if (!settings.skills[name]) settings.skills[name] = {};
132
+ settings.skills[name].enabled = enabled;
133
+ }
134
+
135
+ if (profile.always_apply) {
136
+ settings.always_apply_skills = profile.always_apply;
137
+ }
138
+
139
+ settings._active_profile = profile.id;
140
+ writeSettings(settings);
141
+
142
+ // 复制 profiles.json
143
+ const profilesSource = path.join(skillsSource, ".claude", "profiles.json");
144
+ const profilesDest = path.join(claudeDest, "profiles.json");
145
+ if (fs.existsSync(profilesSource)) {
146
+ fs.copyFileSync(profilesSource, profilesDest);
147
+ }
148
+
149
+ return {
150
+ selected,
151
+ enabled: selected.length,
152
+ disabled: pkgSkills.length - selected.length,
153
+ };
154
+ }
155
+
156
+ // 安装勾选的技能
157
+ function installSelectedSkills(selectedNames) {
158
+ if (!fs.existsSync(pkgSkillsSource)) return { installed: 0 };
159
+
160
+ if (!fs.existsSync(skillsDest)) {
161
+ fs.mkdirSync(skillsDest, { recursive: true });
162
+ }
163
+
164
+ const allPkgSkills = fs.readdirSync(pkgSkillsSource);
165
+ let installedCount = 0;
166
+
167
+ for (const name of allPkgSkills) {
168
+ const src = path.join(pkgSkillsSource, name);
169
+ const dest = path.join(skillsDest, name);
170
+
171
+ if (selectedNames.includes(name)) {
172
+ if (!fs.existsSync(dest)) {
173
+ try {
174
+ copyDirSync(src, dest);
175
+ installedCount++;
176
+ } catch (err) {
177
+ console.error(`安装技能 "${name}" 失败:`, err.message);
178
+ }
179
+ }
180
+ } else {
181
+ if (fs.existsSync(dest)) {
182
+ fs.rmSync(dest, { recursive: true, force: true });
183
+ }
184
+ }
185
+ }
186
+
187
+ const settings = readSettings();
188
+ if (!settings.skills) settings.skills = {};
189
+ for (const name of allPkgSkills) {
190
+ const enabled = selectedNames.includes(name);
191
+ if (!settings.skills[name]) settings.skills[name] = {};
192
+ settings.skills[name].enabled = enabled;
193
+ }
194
+ writeSettings(settings);
195
+
196
+ return { installed: installedCount };
197
+ }
198
+
199
+ // ---------- API 路由 ----------
200
+
201
+ async function handleApi(req, res) {
202
+ const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
203
+ const pathname = url.pathname;
204
+ const method = req.method;
205
+
206
+ // CORS
207
+ res.setHeader("Access-Control-Allow-Origin", "*");
208
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
209
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
210
+
211
+ if (method === "OPTIONS") {
212
+ res.writeHead(204);
213
+ res.end();
214
+ return;
215
+ }
216
+
217
+ try {
218
+ // GET /api/skills — 获取所有技能列表(含启用状态)
219
+ if (method === "GET" && pathname === "/api/skills") {
220
+ const pkgSkills = core.getPackageSkills(skillsSource);
221
+ const userSkills = core.getUserSkills(claudeDest);
222
+ const userMap = new Map(userSkills.map((s) => [s.name, s]));
223
+ const result = pkgSkills.map((s) => ({
224
+ ...s,
225
+ enabled: userMap.has(s.name) ? userMap.get(s.name).enabled : false,
226
+ installed: userMap.has(s.name),
227
+ }));
228
+ return sendJSON(res, result);
229
+ }
230
+
231
+ // POST /api/skills/toggle — 开关技能
232
+ if (method === "POST" && pathname === "/api/skills/toggle") {
233
+ const body = await parseBody(req);
234
+ const { name, enabled } = body;
235
+ if (!name) return sendJSON(res, { error: "缺少技能名" }, 400);
236
+ const settings = readSettings();
237
+ if (!settings.skills) settings.skills = {};
238
+ if (!settings.skills[name]) settings.skills[name] = {};
239
+ settings.skills[name].enabled = !!enabled;
240
+ writeSettings(settings);
241
+ return sendJSON(res, { ok: true, name, enabled: !!enabled });
242
+ }
243
+
244
+ // POST /api/skills/install — 自定义勾选安装
245
+ if (method === "POST" && pathname === "/api/skills/install") {
246
+ const body = await parseBody(req);
247
+ const { selected } = body;
248
+ if (!Array.isArray(selected))
249
+ return sendJSON(res, { error: "参数错误" }, 400);
250
+ const result = installSelectedSkills(selected);
251
+ return sendJSON(res, { ok: true, ...result });
252
+ }
253
+
254
+ // GET /api/profiles — 获取场景列表
255
+ if (method === "GET" && pathname === "/api/profiles") {
256
+ const profiles = loadProfiles();
257
+ const settings = readSettings();
258
+ return sendJSON(res, {
259
+ profiles: profiles || [],
260
+ activeProfile: settings._active_profile || null,
261
+ });
262
+ }
263
+
264
+ // POST /api/profiles/apply — 应用场景
265
+ if (method === "POST" && pathname === "/api/profiles/apply") {
266
+ const body = await parseBody(req);
267
+ const { profileId } = body;
268
+ if (!profileId) return sendJSON(res, { error: "缺少场景 ID" }, 400);
269
+ const profiles = loadProfiles();
270
+ const profile = profiles?.find((p) => p.id === profileId);
271
+ if (!profile)
272
+ return sendJSON(res, { error: "场景不存在: " + profileId }, 404);
273
+ const result = applyProfile(profile);
274
+ return sendJSON(res, { ok: true, ...result });
275
+ }
276
+
277
+ // GET /api/status — 当前状态总览
278
+ if (method === "GET" && pathname === "/api/status") {
279
+ const settings = readSettings();
280
+ const userSkills = core.getUserSkills(claudeDest);
281
+ const profiles = loadProfiles();
282
+ const activeProfile = profiles?.find(
283
+ (p) => p.id === settings._active_profile,
284
+ );
285
+ const enabledCount = userSkills.filter((s) => s.enabled).length;
286
+ const mdExists = fs.existsSync(path.join(projectRoot, "CLAUDE.md"));
287
+ return sendJSON(res, {
288
+ enabledCount,
289
+ totalCount: userSkills.length,
290
+ activeProfile: activeProfile
291
+ ? { id: activeProfile.id, name: activeProfile.name }
292
+ : null,
293
+ sourcePath: settings._skill_source || null,
294
+ hasClaudeMd: mdExists,
295
+ });
296
+ }
297
+
298
+ // GET /api/source — 获取本地源信息
299
+ if (method === "GET" && pathname === "/api/source") {
300
+ const settings = readSettings();
301
+ return sendJSON(res, {
302
+ connected: !!settings._skill_source,
303
+ path: settings._skill_source || null,
304
+ });
305
+ }
306
+
307
+ // POST /api/source/connect — 绑定本地源
308
+ if (method === "POST" && pathname === "/api/source/connect") {
309
+ const body = await parseBody(req);
310
+ const repoPath = body.path;
311
+ if (!repoPath)
312
+ return sendJSON(res, { error: "缺少仓库路径" }, 400);
313
+ const resolved = path.resolve(repoPath);
314
+ if (!fs.existsSync(path.join(resolved, ".claude", "skills"))) {
315
+ return sendJSON(res, { error: `路径 "${resolved}" 下没有 .claude/skills/` }, 400);
316
+ }
317
+ const settings = readSettings();
318
+ settings._skill_source = resolved;
319
+ writeSettings(settings);
320
+ return sendJSON(res, { ok: true, path: resolved });
321
+ }
322
+
323
+ // POST /api/source/disconnect — 解绑本地源
324
+ if (method === "POST" && pathname === "/api/source/disconnect") {
325
+ const settings = readSettings();
326
+ if (!settings._skill_source) {
327
+ return sendJSON(res, { error: "当前未绑定任何技能源" }, 400);
328
+ }
329
+ delete settings._skill_source;
330
+ writeSettings(settings);
331
+ return sendJSON(res, { ok: true });
332
+ }
333
+
334
+ // POST /api/source/sync — 同步到本地源
335
+ if (method === "POST" && pathname === "/api/source/sync") {
336
+ const settings = readSettings();
337
+ const sourcePath = settings._skill_source;
338
+ if (!sourcePath)
339
+ return sendJSON(res, { error: "尚未绑定技能源" }, 400);
340
+ if (!fs.existsSync(path.join(sourcePath, ".claude"))) {
341
+ return sendJSON(res, { error: "绑定的路径已失效" }, 400);
342
+ }
343
+
344
+ const repoClaude = path.join(sourcePath, ".claude");
345
+ core.backupDir(repoClaude);
346
+
347
+ // 技能反推
348
+ const repoSkills = path.join(sourcePath, ".claude", "skills");
349
+ if (!fs.existsSync(repoSkills))
350
+ fs.mkdirSync(repoSkills, { recursive: true });
351
+ if (fs.existsSync(skillsDest)) {
352
+ for (const name of fs.readdirSync(skillsDest)) {
353
+ const src = path.join(skillsDest, name);
354
+ const dest = path.join(repoSkills, name);
355
+ if (fs.existsSync(dest))
356
+ fs.rmSync(dest, { recursive: true, force: true });
357
+ copyDirSync(src, dest);
358
+ }
359
+ }
360
+
361
+ // profiles.json 反推
362
+ const projectProfiles = path.join(claudeDest, "profiles.json");
363
+ if (fs.existsSync(projectProfiles)) {
364
+ fs.copyFileSync(
365
+ projectProfiles,
366
+ path.join(sourcePath, ".claude", "profiles.json"),
367
+ );
368
+ }
369
+
370
+ // settings.json 反推
371
+ const cleanSettings = { ...settings };
372
+ delete cleanSettings._skill_source;
373
+ delete cleanSettings._active_profile;
374
+ fs.writeFileSync(
375
+ path.join(sourcePath, ".claude", "settings.json"),
376
+ JSON.stringify(cleanSettings, null, 2) + "\n",
377
+ "utf-8",
378
+ );
379
+
380
+ // CLAUDE.md 反推
381
+ const projectMd = path.join(projectRoot, "CLAUDE.md");
382
+ if (fs.existsSync(projectMd)) {
383
+ fs.copyFileSync(projectMd, path.join(sourcePath, "CLAUDE.md"));
384
+ }
385
+
386
+ return sendJSON(res, { ok: true });
387
+ }
388
+
389
+ // GET /api/diagnose — 健康检查
390
+ if (method === "GET" && pathname === "/api/diagnose") {
391
+ const results = [];
392
+ const pkgSkills = core.getPackageSkills(skillsSource);
393
+ for (const skill of pkgSkills) {
394
+ const skillDir = path.join(skillsDest, skill.name);
395
+ const issues = [];
396
+ if (!fs.existsSync(skillDir)) {
397
+ issues.push("未安装");
398
+ } else {
399
+ const metaPath = path.join(skillDir, ".meta.json");
400
+ if (!fs.existsSync(metaPath)) {
401
+ issues.push("缺少 .meta.json");
402
+ } else {
403
+ try {
404
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
405
+ if (!meta.version) issues.push("版本号缺失");
406
+ } catch {
407
+ issues.push(".meta.json 格式错误");
408
+ }
409
+ }
410
+ }
411
+ results.push({
412
+ name: skill.name,
413
+ healthy: issues.length === 0,
414
+ issues,
415
+ });
416
+ }
417
+ return sendJSON(res, results);
418
+ }
419
+
420
+ // 404
421
+ sendJSON(res, { error: "Not Found: " + pathname }, 404);
422
+ } catch (err) {
423
+ console.error("API 错误:", err);
424
+ sendJSON(res, { error: err.message }, 500);
425
+ }
426
+ }
427
+
428
+ // ---------- 启动 ----------
429
+
430
+ async function startUI() {
431
+ const vite = await createViteServer({
432
+ root: __dirname,
433
+ server: { port: 3344, open: false },
434
+ plugins: [
435
+ {
436
+ name: "api-routes",
437
+ configureServer(viteServer) {
438
+ viteServer.middlewares.use((req, res, next) => {
439
+ if (req.url.startsWith("/api")) {
440
+ handleApi(req, res);
441
+ } else {
442
+ next();
443
+ }
444
+ });
445
+ },
446
+ },
447
+ ],
448
+ });
449
+
450
+ await vite.listen();
451
+ const port = vite.config.server.port;
452
+ console.log(`\n 🖥️ mdk-skills Web UI`);
453
+ console.log(` ─────────────────────`);
454
+ console.log(` 地址: http://localhost:${port}\n`);
455
+ console.log(` 按 Ctrl+C 停止服务\n`);
456
+ }
457
+
458
+ startUI().catch((err) => {
459
+ console.error("启动失败:", err);
460
+ process.exit(1);
461
+ });
@@ -0,0 +1,98 @@
1
+ <template>
2
+ <n-message-provider>
3
+ <n-notification-provider>
4
+ <n-config-provider :theme="theme">
5
+ <n-layout class="app-layout">
6
+ <!-- 导航栏 -->
7
+ <n-layout-header class="app-header" bordered>
8
+ <div class="header-inner">
9
+ <div class="header-left">
10
+ <span class="logo">mdk-skills</span>
11
+ <n-menu
12
+ v-model:value="activeKey"
13
+ mode="horizontal"
14
+ :options="menuOptions"
15
+ @update:value="onMenuChange"
16
+ />
17
+ </div>
18
+ <n-tag v-if="statusData" :type="statusData.sourcePath ? 'success' : 'default'" size="small">
19
+ {{ statusData.sourcePath ? "本地源" : "npm 源" }}
20
+ </n-tag>
21
+ </div>
22
+ </n-layout-header>
23
+
24
+ <!-- 状态条 -->
25
+ <StatusBar v-if="statusData" :data="statusData" />
26
+
27
+ <!-- 内容区 -->
28
+ <n-layout-content class="app-content">
29
+ <router-view @refresh="loadStatus" />
30
+ </n-layout-content>
31
+ </n-layout>
32
+ </n-config-provider>
33
+ </n-notification-provider>
34
+ </n-message-provider>
35
+ </template>
36
+
37
+ <script setup>
38
+ import { h, ref, onMounted, computed } from "vue";
39
+ import { useRouter, useRoute } from "vue-router";
40
+ import { NIcon, useMessage } from "naive-ui";
41
+ import {
42
+ DashboardOutlined,
43
+ SwapOutlined,
44
+ SettingOutlined,
45
+ } from "@vicons/material";
46
+ import StatusBar from "./components/StatusBar.vue";
47
+ import { getStatus } from "./api/skills";
48
+
49
+ const router = useRouter();
50
+ const route = useRoute();
51
+ const message = useMessage();
52
+
53
+ const statusData = ref(null);
54
+
55
+ const theme = ref(null);
56
+
57
+ const menuOptions = [
58
+ {
59
+ label: "仪表盘",
60
+ key: "Dashboard",
61
+ icon: () => h(NIcon, null, { default: () => h(DashboardOutlined) }),
62
+ },
63
+ {
64
+ label: "场景切换",
65
+ key: "Scenes",
66
+ icon: () => h(NIcon, null, { default: () => h(SwapOutlined) }),
67
+ },
68
+ {
69
+ label: "设置",
70
+ key: "Settings",
71
+ icon: () => h(NIcon, null, { default: () => h(SettingOutlined) }),
72
+ },
73
+ ];
74
+
75
+ const activeKey = ref(route.name || "Dashboard");
76
+
77
+ function onMenuChange(key) {
78
+ router.push({ name: key });
79
+ }
80
+
81
+ router.afterEach((to) => {
82
+ activeKey.value = to.name;
83
+ });
84
+
85
+ async function loadStatus() {
86
+ try {
87
+ statusData.value = await getStatus();
88
+ } catch {
89
+ // 静默失败
90
+ }
91
+ }
92
+
93
+ onMounted(loadStatus);
94
+ </script>
95
+
96
+ <style>
97
+ @import "./styles/main.css";
98
+ </style>