mdk-skills 2.2.9 → 2.2.11
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/.claude/profiles.json +73 -67
- package/package.json +3 -1
- package/scripts/cli.js +552 -553
- package/scripts/core.js +15 -5
- package/scripts/web-ui/server.js +173 -8
- package/scripts/web-ui/src/App.vue +8 -2
- package/scripts/web-ui/src/api/skills.js +20 -0
- package/scripts/web-ui/src/components/SkillCard.vue +16 -8
- package/scripts/web-ui/src/components/StatusBar.vue +9 -1
- package/scripts/web-ui/src/main.js +1 -0
- package/scripts/web-ui/src/router/index.js +2 -0
- package/scripts/web-ui/src/styles/main.css +96 -0
- package/scripts/web-ui/src/views/Dashboard.vue +129 -5
- package/scripts/web-ui/src/views/ReadmeView.vue +100 -0
- package/scripts/web-ui/src/views/SceneSwitch.vue +131 -3
- package/scripts/web-ui/src/views/Settings.vue +43 -1
package/scripts/core.js
CHANGED
|
@@ -109,6 +109,17 @@ function deepMerge(userObj, pkgObj) {
|
|
|
109
109
|
return result;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* 列出技能目录下的有效技能目录(排除非目录项和隐藏目录)
|
|
114
|
+
*/
|
|
115
|
+
function listSkillDirs(skillsPath) {
|
|
116
|
+
if (!fs.existsSync(skillsPath)) return [];
|
|
117
|
+
return fs.readdirSync(skillsPath).filter((name) => {
|
|
118
|
+
const fullPath = path.join(skillsPath, name);
|
|
119
|
+
return fs.statSync(fullPath).isDirectory() && !name.startsWith(".");
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
112
123
|
/**
|
|
113
124
|
* 读取技能目录下的 .meta.json,没有则返回 null
|
|
114
125
|
*/
|
|
@@ -134,7 +145,7 @@ function installSkills(skillsSource, skillsDest, logFile) {
|
|
|
134
145
|
fs.mkdirSync(skillsDest, { recursive: true });
|
|
135
146
|
}
|
|
136
147
|
|
|
137
|
-
const skillDirs =
|
|
148
|
+
const skillDirs = listSkillDirs(skillsSource);
|
|
138
149
|
const results = [];
|
|
139
150
|
|
|
140
151
|
for (const dir of skillDirs) {
|
|
@@ -202,9 +213,7 @@ function installSettings(sourceDir, destDir, logFile) {
|
|
|
202
213
|
*/
|
|
203
214
|
function getPackageSkills(packageDir) {
|
|
204
215
|
const skillsSource = path.join(packageDir, ".claude", "skills");
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return fs.readdirSync(skillsSource).map((name) => {
|
|
216
|
+
return listSkillDirs(skillsSource).map((name) => {
|
|
208
217
|
const meta = readMeta(path.join(skillsSource, name));
|
|
209
218
|
return {
|
|
210
219
|
name,
|
|
@@ -250,7 +259,7 @@ function getUserSkills(claudeDest) {
|
|
|
250
259
|
|
|
251
260
|
const skillConfigs = settings.skills || {};
|
|
252
261
|
|
|
253
|
-
return
|
|
262
|
+
return listSkillDirs(skillsDest).map((name) => {
|
|
254
263
|
const meta = readMeta(path.join(skillsDest, name));
|
|
255
264
|
const config = skillConfigs[name] || {};
|
|
256
265
|
return {
|
|
@@ -270,6 +279,7 @@ module.exports = {
|
|
|
270
279
|
backupDir,
|
|
271
280
|
copyDirSync,
|
|
272
281
|
deepMerge,
|
|
282
|
+
listSkillDirs,
|
|
273
283
|
readMeta,
|
|
274
284
|
installSkills,
|
|
275
285
|
installSettings,
|
package/scripts/web-ui/server.js
CHANGED
|
@@ -95,9 +95,7 @@ function loadProfiles() {
|
|
|
95
95
|
|
|
96
96
|
// 应用场景
|
|
97
97
|
function applyProfile(profile) {
|
|
98
|
-
const pkgSkills =
|
|
99
|
-
? fs.readdirSync(pkgSkillsSource)
|
|
100
|
-
: [];
|
|
98
|
+
const pkgSkills = core.listSkillDirs(pkgSkillsSource);
|
|
101
99
|
const settings = readSettings();
|
|
102
100
|
|
|
103
101
|
let selected;
|
|
@@ -153,6 +151,13 @@ function applyProfile(profile) {
|
|
|
153
151
|
fs.copyFileSync(profilesSource, profilesDest);
|
|
154
152
|
}
|
|
155
153
|
|
|
154
|
+
// 复制 CLAUDE.md(不存在才装)
|
|
155
|
+
const mdSource = path.join(skillsSource, "CLAUDE.md");
|
|
156
|
+
const mdDest = path.join(projectRoot, "CLAUDE.md");
|
|
157
|
+
if (fs.existsSync(mdSource) && !fs.existsSync(mdDest)) {
|
|
158
|
+
fs.copyFileSync(mdSource, mdDest);
|
|
159
|
+
}
|
|
160
|
+
|
|
156
161
|
return {
|
|
157
162
|
selected,
|
|
158
163
|
enabled: selected.length,
|
|
@@ -168,7 +173,7 @@ function installSelectedSkills(selectedNames) {
|
|
|
168
173
|
fs.mkdirSync(skillsDest, { recursive: true });
|
|
169
174
|
}
|
|
170
175
|
|
|
171
|
-
const allPkgSkills =
|
|
176
|
+
const allPkgSkills = core.listSkillDirs(pkgSkillsSource);
|
|
172
177
|
let installedCount = 0;
|
|
173
178
|
|
|
174
179
|
for (const name of allPkgSkills) {
|
|
@@ -200,6 +205,13 @@ function installSelectedSkills(selectedNames) {
|
|
|
200
205
|
}
|
|
201
206
|
writeSettings(settings);
|
|
202
207
|
|
|
208
|
+
// 复制 CLAUDE.md(不存在才装)
|
|
209
|
+
const mdSource = path.join(skillsSource, "CLAUDE.md");
|
|
210
|
+
const mdDest = path.join(projectRoot, "CLAUDE.md");
|
|
211
|
+
if (fs.existsSync(mdSource) && !fs.existsSync(mdDest)) {
|
|
212
|
+
fs.copyFileSync(mdSource, mdDest);
|
|
213
|
+
}
|
|
214
|
+
|
|
203
215
|
return { installed: installedCount };
|
|
204
216
|
}
|
|
205
217
|
|
|
@@ -286,6 +298,50 @@ async function handleApi(req, res) {
|
|
|
286
298
|
return sendJSON(res, { ok: true, ...result });
|
|
287
299
|
}
|
|
288
300
|
|
|
301
|
+
// GET /api/readme — 获取根目录 README.md
|
|
302
|
+
if (method === "GET" && pathname === "/api/readme") {
|
|
303
|
+
const p = path.join(skillsSource, "README.md");
|
|
304
|
+
const content = fs.existsSync(p) ? fs.readFileSync(p, "utf-8") : null;
|
|
305
|
+
return sendJSON(res, { content, found: !!content });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// GET /api/skills-readme — 获取技能目录 README.md
|
|
309
|
+
if (method === "GET" && pathname === "/api/skills-readme") {
|
|
310
|
+
const p = path.join(skillsSource, ".claude", "skills", "README.md");
|
|
311
|
+
const content = fs.existsSync(p) ? fs.readFileSync(p, "utf-8") : null;
|
|
312
|
+
return sendJSON(res, { content, found: !!content });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// GET /api/claudemd — 获取 CLAUDE.md 内容
|
|
316
|
+
if (method === "GET" && pathname === "/api/claudemd") {
|
|
317
|
+
const paths = [
|
|
318
|
+
path.join(skillsSource, "CLAUDE.md"),
|
|
319
|
+
path.join(projectRoot, "CLAUDE.md"),
|
|
320
|
+
];
|
|
321
|
+
let content = null;
|
|
322
|
+
for (const p of paths) {
|
|
323
|
+
if (fs.existsSync(p)) {
|
|
324
|
+
content = fs.readFileSync(p, "utf-8");
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return sendJSON(res, { content, found: !!content });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// GET /api/skills/:name/readme — 获取技能的 SKILL.md
|
|
332
|
+
const readmeMatch = pathname.match(/^\/api\/skills\/([^/]+)\/readme$/);
|
|
333
|
+
if (method === "GET" && readmeMatch) {
|
|
334
|
+
const name = readmeMatch[1];
|
|
335
|
+
const skillDir = path.join(skillsDest, name);
|
|
336
|
+
const altDir = path.join(pkgSkillsSource, name);
|
|
337
|
+
const readmePath = [
|
|
338
|
+
path.join(skillDir, "SKILL.md"),
|
|
339
|
+
path.join(altDir, "SKILL.md"),
|
|
340
|
+
].find((p) => fs.existsSync(p));
|
|
341
|
+
const content = readmePath ? fs.readFileSync(readmePath, "utf-8") : null;
|
|
342
|
+
return sendJSON(res, { content, found: !!readmePath });
|
|
343
|
+
}
|
|
344
|
+
|
|
289
345
|
// GET /api/profiles — 获取场景列表(含只读状态)
|
|
290
346
|
if (method === "GET" && pathname === "/api/profiles") {
|
|
291
347
|
const profiles = loadProfiles();
|
|
@@ -411,12 +467,40 @@ async function handleApi(req, res) {
|
|
|
411
467
|
});
|
|
412
468
|
}
|
|
413
469
|
|
|
414
|
-
// GET /api/source —
|
|
470
|
+
// GET /api/source — 获取本地源信息(含初始化检测)
|
|
415
471
|
if (method === "GET" && pathname === "/api/source") {
|
|
416
472
|
const settings = readSettings();
|
|
473
|
+
const sourcePath = settings._skill_source || null;
|
|
474
|
+
|
|
475
|
+
let needsInit = false;
|
|
476
|
+
if (sourcePath) {
|
|
477
|
+
const claudeDir = path.join(sourcePath, ".claude");
|
|
478
|
+
if (fs.existsSync(claudeDir)) {
|
|
479
|
+
// 检查 profiles.json
|
|
480
|
+
const hasProfiles = fs.existsSync(path.join(claudeDir, "profiles.json"));
|
|
481
|
+
// 检查 settings.json
|
|
482
|
+
const hasSettings = fs.existsSync(path.join(claudeDir, "settings.json"));
|
|
483
|
+
// 检查每个 skill 目录的 .meta.json
|
|
484
|
+
const skillsDir = path.join(claudeDir, "skills");
|
|
485
|
+
let allMeta = true;
|
|
486
|
+
if (fs.existsSync(skillsDir)) {
|
|
487
|
+
for (const name of core.listSkillDirs(skillsDir)) {
|
|
488
|
+
if (!fs.existsSync(path.join(skillsDir, name, ".meta.json"))) {
|
|
489
|
+
allMeta = false;
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
needsInit = !hasProfiles || !hasSettings || !allMeta;
|
|
495
|
+
} else {
|
|
496
|
+
needsInit = true;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
417
500
|
return sendJSON(res, {
|
|
418
|
-
connected: !!
|
|
419
|
-
path:
|
|
501
|
+
connected: !!sourcePath,
|
|
502
|
+
path: sourcePath,
|
|
503
|
+
needsInit,
|
|
420
504
|
});
|
|
421
505
|
}
|
|
422
506
|
|
|
@@ -436,6 +520,13 @@ async function handleApi(req, res) {
|
|
|
436
520
|
const settings = readSettings();
|
|
437
521
|
settings._skill_source = resolved;
|
|
438
522
|
writeSettings(settings);
|
|
523
|
+
// 复制 CLAUDE.md(不存在才装)
|
|
524
|
+
const mdSource = path.join(resolved, "CLAUDE.md");
|
|
525
|
+
const mdDest = path.join(projectRoot, "CLAUDE.md");
|
|
526
|
+
if (fs.existsSync(mdSource) && !fs.existsSync(mdDest)) {
|
|
527
|
+
fs.copyFileSync(mdSource, mdDest);
|
|
528
|
+
}
|
|
529
|
+
|
|
439
530
|
refreshSource();
|
|
440
531
|
return sendJSON(res, { ok: true, path: resolved });
|
|
441
532
|
}
|
|
@@ -469,7 +560,7 @@ async function handleApi(req, res) {
|
|
|
469
560
|
if (!fs.existsSync(repoSkills))
|
|
470
561
|
fs.mkdirSync(repoSkills, { recursive: true });
|
|
471
562
|
if (fs.existsSync(skillsDest)) {
|
|
472
|
-
for (const name of
|
|
563
|
+
for (const name of core.listSkillDirs(skillsDest)) {
|
|
473
564
|
const src = path.join(skillsDest, name);
|
|
474
565
|
const dest = path.join(repoSkills, name);
|
|
475
566
|
if (fs.existsSync(dest))
|
|
@@ -506,6 +597,80 @@ async function handleApi(req, res) {
|
|
|
506
597
|
return sendJSON(res, { ok: true });
|
|
507
598
|
}
|
|
508
599
|
|
|
600
|
+
// POST /api/source/init — 初始化本地源骨架文件(缺啥补啥)
|
|
601
|
+
if (method === "POST" && pathname === "/api/source/init") {
|
|
602
|
+
const settings = readSettings();
|
|
603
|
+
const sourcePath = settings._skill_source;
|
|
604
|
+
if (!sourcePath) return sendJSON(res, { error: "未绑定本地源" }, 400);
|
|
605
|
+
|
|
606
|
+
const claudeDir = path.join(sourcePath, ".claude");
|
|
607
|
+
const skillsDir = path.join(claudeDir, "skills");
|
|
608
|
+
const profilesPath = path.join(claudeDir, "profiles.json");
|
|
609
|
+
const settingsPath = path.join(claudeDir, "settings.json");
|
|
610
|
+
|
|
611
|
+
const created = { profiles: false, settings: false, metaFiles: 0 };
|
|
612
|
+
|
|
613
|
+
// 1. profiles.json
|
|
614
|
+
if (!fs.existsSync(profilesPath)) {
|
|
615
|
+
fs.writeFileSync(
|
|
616
|
+
profilesPath,
|
|
617
|
+
JSON.stringify(
|
|
618
|
+
{
|
|
619
|
+
version: 1,
|
|
620
|
+
profiles: [
|
|
621
|
+
{
|
|
622
|
+
id: "custom",
|
|
623
|
+
name: "自定义技能组合",
|
|
624
|
+
description: "自由勾选技能项,按需个性化配置",
|
|
625
|
+
skills: null,
|
|
626
|
+
},
|
|
627
|
+
],
|
|
628
|
+
},
|
|
629
|
+
null,
|
|
630
|
+
2,
|
|
631
|
+
) + "\n",
|
|
632
|
+
"utf-8",
|
|
633
|
+
);
|
|
634
|
+
created.profiles = true;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// 2. settings.json
|
|
638
|
+
if (!fs.existsSync(settingsPath)) {
|
|
639
|
+
fs.writeFileSync(
|
|
640
|
+
settingsPath,
|
|
641
|
+
JSON.stringify({ skills: {}, always_apply_skills: [] }, null, 2) + "\n",
|
|
642
|
+
"utf-8",
|
|
643
|
+
);
|
|
644
|
+
created.settings = true;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// 3. 每个 skill 目录的 .meta.json
|
|
648
|
+
if (fs.existsSync(skillsDir)) {
|
|
649
|
+
for (const name of core.listSkillDirs(skillsDir)) {
|
|
650
|
+
const skillDir = path.join(skillsDir, name);
|
|
651
|
+
const metaPath = path.join(skillDir, ".meta.json");
|
|
652
|
+
if (!fs.existsSync(metaPath)) {
|
|
653
|
+
fs.writeFileSync(
|
|
654
|
+
metaPath,
|
|
655
|
+
JSON.stringify(
|
|
656
|
+
{
|
|
657
|
+
version: "1.0.0",
|
|
658
|
+
description: name,
|
|
659
|
+
tags: [],
|
|
660
|
+
},
|
|
661
|
+
null,
|
|
662
|
+
2,
|
|
663
|
+
) + "\n",
|
|
664
|
+
"utf-8",
|
|
665
|
+
);
|
|
666
|
+
created.metaFiles++;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return sendJSON(res, { ok: true, ...created });
|
|
672
|
+
}
|
|
673
|
+
|
|
509
674
|
// GET /api/diagnose — 健康检查
|
|
510
675
|
if (method === "GET" && pathname === "/api/diagnose") {
|
|
511
676
|
const results = [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<n-message-provider>
|
|
3
3
|
<n-notification-provider>
|
|
4
|
-
<n-config-provider :theme="theme">
|
|
4
|
+
<n-config-provider :theme="theme" :locale="zhCN" :date-locale="dateZhCN">
|
|
5
5
|
<n-layout class="app-layout">
|
|
6
6
|
<!-- 导航栏 -->
|
|
7
7
|
<n-layout-header class="app-header" bordered>
|
|
@@ -37,11 +37,12 @@
|
|
|
37
37
|
<script setup>
|
|
38
38
|
import { h, ref, onMounted, computed } from "vue";
|
|
39
39
|
import { useRouter, useRoute } from "vue-router";
|
|
40
|
-
import { NIcon } from "naive-ui";
|
|
40
|
+
import { NIcon, zhCN, dateZhCN } from "naive-ui";
|
|
41
41
|
import {
|
|
42
42
|
AppsOutline,
|
|
43
43
|
SwapHorizontalOutline,
|
|
44
44
|
CogOutline,
|
|
45
|
+
DocumentTextOutline,
|
|
45
46
|
} from "@vicons/ionicons5";
|
|
46
47
|
import StatusBar from "./components/StatusBar.vue";
|
|
47
48
|
import { getStatus } from "./api/skills";
|
|
@@ -69,6 +70,11 @@ const menuOptions = [
|
|
|
69
70
|
key: "Settings",
|
|
70
71
|
icon: () => h(NIcon, null, { default: () => h(CogOutline) }),
|
|
71
72
|
},
|
|
73
|
+
{
|
|
74
|
+
label: "CLAUDE.md",
|
|
75
|
+
key: "ClaudeMd",
|
|
76
|
+
icon: () => h(NIcon, null, { default: () => h(DocumentTextOutline) }),
|
|
77
|
+
},
|
|
72
78
|
];
|
|
73
79
|
|
|
74
80
|
const activeKey = ref(route.name || "Dashboard");
|
|
@@ -78,6 +78,26 @@ export function syncSource() {
|
|
|
78
78
|
return request("/source/sync", { method: "POST" });
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
export function initSource() {
|
|
82
|
+
return request("/source/init", { method: "POST" });
|
|
83
|
+
}
|
|
84
|
+
|
|
81
85
|
export function diagnose() {
|
|
82
86
|
return request("/diagnose");
|
|
83
87
|
}
|
|
88
|
+
|
|
89
|
+
export function getReadme() {
|
|
90
|
+
return request("/readme");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getSkillsReadme() {
|
|
94
|
+
return request("/skills-readme");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getClaudeMd() {
|
|
98
|
+
return request("/claudemd");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getSkillReadme(name) {
|
|
102
|
+
return request("/skills/" + encodeURIComponent(name) + "/readme");
|
|
103
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<n-card class="skill-card" :title="skill.name" size="small">
|
|
2
|
+
<n-card class="skill-card" :title="skill.name" size="small" @click="emit('click')">
|
|
3
3
|
<template #header-extra>
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
<span @click.stop>
|
|
5
|
+
<n-switch
|
|
6
|
+
:value="skill.enabled"
|
|
7
|
+
:loading="loading"
|
|
8
|
+
@update:value="onToggle"
|
|
9
|
+
/>
|
|
10
|
+
</span>
|
|
9
11
|
</template>
|
|
10
12
|
<div class="skill-meta">
|
|
11
13
|
<span class="skill-version">v{{ skill.version }}</span>
|
|
@@ -26,7 +28,7 @@ const props = defineProps({
|
|
|
26
28
|
skill: { type: Object, required: true },
|
|
27
29
|
});
|
|
28
30
|
|
|
29
|
-
const emit = defineEmits(["refresh"]);
|
|
31
|
+
const emit = defineEmits(["refresh", "click"]);
|
|
30
32
|
const message = useMessage();
|
|
31
33
|
const loading = ref(false);
|
|
32
34
|
|
|
@@ -49,6 +51,12 @@ async function onToggle(value) {
|
|
|
49
51
|
<style scoped>
|
|
50
52
|
.skill-card {
|
|
51
53
|
margin-bottom: 12px;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
transition: box-shadow 0.2s;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.skill-card:hover {
|
|
59
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
.skill-meta {
|
|
@@ -69,4 +77,4 @@ async function onToggle(value) {
|
|
|
69
77
|
color: #666;
|
|
70
78
|
margin: 0;
|
|
71
79
|
}
|
|
72
|
-
</style>
|
|
80
|
+
</style>
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
</span>
|
|
20
20
|
</div>
|
|
21
21
|
<div class="status-divider" />
|
|
22
|
-
<div class="status-item">
|
|
22
|
+
<div class="status-item clickable" @click="$router.push({ name: 'ClaudeMd' })">
|
|
23
23
|
<n-icon size="18" :color="data.hasClaudeMd ? '#18a058' : '#d03050'">
|
|
24
24
|
<DocumentTextOutline />
|
|
25
25
|
</n-icon>
|
|
@@ -58,6 +58,14 @@ defineProps({
|
|
|
58
58
|
color: #555;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
.status-item.clickable {
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.status-item.clickable:hover {
|
|
66
|
+
color: #2080f0;
|
|
67
|
+
}
|
|
68
|
+
|
|
61
69
|
.status-divider {
|
|
62
70
|
width: 1px;
|
|
63
71
|
height: 20px;
|
|
@@ -2,11 +2,13 @@ import { createRouter, createWebHistory } from "vue-router";
|
|
|
2
2
|
import Dashboard from "../views/Dashboard.vue";
|
|
3
3
|
import SceneSwitch from "../views/SceneSwitch.vue";
|
|
4
4
|
import Settings from "../views/Settings.vue";
|
|
5
|
+
import ReadmeView from "../views/ReadmeView.vue";
|
|
5
6
|
|
|
6
7
|
const routes = [
|
|
7
8
|
{ path: "/", name: "Dashboard", component: Dashboard, meta: { title: "仪表盘" } },
|
|
8
9
|
{ path: "/scenes", name: "Scenes", component: SceneSwitch, meta: { title: "场景切换" } },
|
|
9
10
|
{ path: "/settings", name: "Settings", component: Settings, meta: { title: "设置" } },
|
|
11
|
+
{ path: "/readme", name: "ClaudeMd", component: ReadmeView, meta: { title: "CLAUDE.md" } },
|
|
10
12
|
];
|
|
11
13
|
|
|
12
14
|
export default createRouter({
|
|
@@ -57,3 +57,99 @@ html, body, #app {
|
|
|
57
57
|
width: 100%;
|
|
58
58
|
margin: 0 auto;
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
/* ---------- markdown 渲染样式 ---------- */
|
|
62
|
+
|
|
63
|
+
.markdown-content {
|
|
64
|
+
line-height: 1.7;
|
|
65
|
+
font-size: 14px;
|
|
66
|
+
color: #333;
|
|
67
|
+
word-wrap: break-word;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.markdown-content h1 { font-size: 22px; font-weight: 700; margin: 20px 0 12px; padding-bottom: 8px; border-bottom: 1px solid #eee; }
|
|
71
|
+
.markdown-content h2 { font-size: 18px; font-weight: 700; margin: 18px 0 10px; padding-bottom: 6px; border-bottom: 1px solid #eee; }
|
|
72
|
+
.markdown-content h3 { font-size: 16px; font-weight: 600; margin: 16px 0 8px; }
|
|
73
|
+
.markdown-content h4 { font-size: 14px; font-weight: 600; margin: 14px 0 6px; }
|
|
74
|
+
.markdown-content h5, .markdown-content h6 { font-size: 13px; font-weight: 600; margin: 12px 0 6px; }
|
|
75
|
+
|
|
76
|
+
.markdown-content p { margin: 8px 0; }
|
|
77
|
+
.markdown-content ul, .markdown-content ol { margin: 8px 0; padding-left: 24px; }
|
|
78
|
+
.markdown-content li { margin: 4px 0; }
|
|
79
|
+
.markdown-content blockquote {
|
|
80
|
+
margin: 10px 0;
|
|
81
|
+
padding: 8px 16px;
|
|
82
|
+
border-left: 4px solid #2080f0;
|
|
83
|
+
background: #f0f7ff;
|
|
84
|
+
color: #555;
|
|
85
|
+
}
|
|
86
|
+
.markdown-content blockquote p { margin: 4px 0; }
|
|
87
|
+
|
|
88
|
+
.markdown-content a { color: #2080f0; text-decoration: none; }
|
|
89
|
+
.markdown-content a:hover { text-decoration: underline; }
|
|
90
|
+
|
|
91
|
+
.markdown-content hr { margin: 16px 0; border: none; border-top: 1px solid #e0e0e0; }
|
|
92
|
+
|
|
93
|
+
.markdown-content table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13px; }
|
|
94
|
+
.markdown-content th, .markdown-content td {
|
|
95
|
+
padding: 8px 12px;
|
|
96
|
+
border: 1px solid #e0e0e0;
|
|
97
|
+
text-align: left;
|
|
98
|
+
}
|
|
99
|
+
.markdown-content th { background: #f5f7fa; font-weight: 600; }
|
|
100
|
+
.markdown-content tr:nth-child(even) td { background: #fafafa; }
|
|
101
|
+
|
|
102
|
+
.markdown-content img { max-width: 100%; border-radius: 4px; margin: 8px 0; }
|
|
103
|
+
|
|
104
|
+
.markdown-content code {
|
|
105
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
106
|
+
font-size: 13px;
|
|
107
|
+
padding: 2px 6px;
|
|
108
|
+
background: #f0f0f0;
|
|
109
|
+
border-radius: 3px;
|
|
110
|
+
color: #d63384;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.markdown-content pre {
|
|
114
|
+
margin: 12px 0;
|
|
115
|
+
padding: 0;
|
|
116
|
+
border-radius: 6px;
|
|
117
|
+
overflow: hidden;
|
|
118
|
+
border: 1px solid #e8e8e8;
|
|
119
|
+
position: relative;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.markdown-content pre code {
|
|
123
|
+
display: block;
|
|
124
|
+
padding: 14px 16px;
|
|
125
|
+
background: #f8f9fa;
|
|
126
|
+
color: #333;
|
|
127
|
+
font-size: 13px;
|
|
128
|
+
line-height: 1.5;
|
|
129
|
+
overflow-x: auto;
|
|
130
|
+
tab-size: 2;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.markdown-content pre .copy-btn {
|
|
134
|
+
position: absolute;
|
|
135
|
+
top: 6px;
|
|
136
|
+
right: 6px;
|
|
137
|
+
padding: 3px 8px;
|
|
138
|
+
font-size: 11px;
|
|
139
|
+
color: #999;
|
|
140
|
+
background: #fff;
|
|
141
|
+
border: 1px solid #e0e0e0;
|
|
142
|
+
border-radius: 4px;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
opacity: 0;
|
|
145
|
+
transition: opacity 0.2s;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.markdown-content pre:hover .copy-btn {
|
|
149
|
+
opacity: 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.markdown-content pre .copy-btn:hover {
|
|
153
|
+
color: #2080f0;
|
|
154
|
+
border-color: #2080f0;
|
|
155
|
+
}
|