mdk-skills 2.2.13 → 2.2.15
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/package.json
CHANGED
|
@@ -1,201 +1,201 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="dashboard">
|
|
3
|
-
<!-- 仓库 README -->
|
|
4
|
-
<n-collapse v-
|
|
5
|
-
<n-collapse-item title="仓库说明" name="readme">
|
|
6
|
-
<div class="markdown-content" v-html="renderedReadme" />
|
|
7
|
-
</n-collapse-item>
|
|
8
|
-
</n-collapse>
|
|
9
|
-
|
|
10
|
-
<div class="page-header">
|
|
11
|
-
<h2>技能列表</h2>
|
|
12
|
-
<n-button size="small" @click="loadSkills">
|
|
13
|
-
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
|
14
|
-
刷新
|
|
15
|
-
</n-button>
|
|
16
|
-
</div>
|
|
17
|
-
|
|
18
|
-
<n-spin :show="loading">
|
|
19
|
-
<SkillCard
|
|
20
|
-
v-for="skill in skills"
|
|
21
|
-
:key="skill.name"
|
|
22
|
-
:skill="skill"
|
|
23
|
-
@refresh="loadSkills"
|
|
24
|
-
@click="showSkillDetail(skill)"
|
|
25
|
-
/>
|
|
26
|
-
</n-spin>
|
|
27
|
-
|
|
28
|
-
<n-empty v-if="!loading && skills.length === 0" description="暂无技能数据">
|
|
29
|
-
<template #extra>
|
|
30
|
-
<n-button size="small" @click="$router.push({ name: 'Scenes' })">
|
|
31
|
-
前往场景切换
|
|
32
|
-
</n-button>
|
|
33
|
-
</template>
|
|
34
|
-
</n-empty>
|
|
35
|
-
|
|
36
|
-
<!-- 技能详情弹窗 -->
|
|
37
|
-
<n-modal
|
|
38
|
-
v-model:show="detailVisible"
|
|
39
|
-
:title="detailSkill?.name || '技能详情'"
|
|
40
|
-
preset="card"
|
|
41
|
-
style="width: 720px;"
|
|
42
|
-
content-style="max-height: 65vh; overflow-y: auto; padding: 16px 24px; background: #fff;"
|
|
43
|
-
:mask-closable="true"
|
|
44
|
-
>
|
|
45
|
-
<div v-if="detailLoading" class="detail-status">
|
|
46
|
-
<n-spin />
|
|
47
|
-
</div>
|
|
48
|
-
<div v-else-if="detailContent" class="markdown-content" v-html="renderedDetail" />
|
|
49
|
-
<n-empty v-else description="该技能没有 SKILL.md 文档" />
|
|
50
|
-
</n-modal>
|
|
51
|
-
</div>
|
|
52
|
-
</template>
|
|
53
|
-
|
|
54
|
-
<script setup>
|
|
55
|
-
import { ref, onMounted, onActivated, onUnmounted, nextTick } from "vue";
|
|
56
|
-
import { NIcon } from "naive-ui";
|
|
57
|
-
import { RefreshOutline } from "@vicons/ionicons5";
|
|
58
|
-
import { marked } from "marked";
|
|
59
|
-
import hljs from "highlight.js";
|
|
60
|
-
import SkillCard from "../components/SkillCard.vue";
|
|
61
|
-
import { getSkills, getReadme, getSkillReadme } from "../api/skills";
|
|
62
|
-
|
|
63
|
-
// marked 配置:代码高亮 + 外链安全
|
|
64
|
-
marked.use({
|
|
65
|
-
renderer: {
|
|
66
|
-
code({ text, lang }) {
|
|
67
|
-
const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
|
|
68
|
-
let highlighted;
|
|
69
|
-
try {
|
|
70
|
-
highlighted = hljs.highlight(text, { language }).value;
|
|
71
|
-
} catch {
|
|
72
|
-
highlighted = hljs.highlightAuto(text).value;
|
|
73
|
-
}
|
|
74
|
-
return `<pre><button class="copy-btn" onclick="navigator.clipboard.writeText(this.parentNode.querySelector('code').textContent);this.textContent='已复制';setTimeout(()=>this.textContent='复制',1500)">复制</button><code class="hljs language-${language}">${highlighted}</code></pre>`;
|
|
75
|
-
},
|
|
76
|
-
link({ href, text }) {
|
|
77
|
-
const isExternal = href && (href.startsWith("http://") || href.startsWith("https://"));
|
|
78
|
-
const target = isExternal ? ' target="_blank" rel="noopener noreferrer"' : "";
|
|
79
|
-
return `<a href="${href}"${target}>${text}</a>`;
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const emit = defineEmits(["refresh"]);
|
|
85
|
-
const skills = ref([]);
|
|
86
|
-
const loading = ref(false);
|
|
87
|
-
|
|
88
|
-
// README
|
|
89
|
-
const readmeContent = ref(null);
|
|
90
|
-
const renderedReadme = ref("");
|
|
91
|
-
|
|
92
|
-
// 详情弹窗
|
|
93
|
-
const detailVisible = ref(false);
|
|
94
|
-
const detailSkill = ref(null);
|
|
95
|
-
const detailLoading = ref(false);
|
|
96
|
-
const detailContent = ref(null);
|
|
97
|
-
const renderedDetail = ref("");
|
|
98
|
-
|
|
99
|
-
/** 去掉 YAML frontmatter(--- 包裹的元数据) */
|
|
100
|
-
function stripFrontmatter(text) {
|
|
101
|
-
if (!text) return text;
|
|
102
|
-
return text.replace(/^---[\s\S]*?---\s*/, "");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** 渲染 markdown,返回 HTML */
|
|
106
|
-
function renderMd(text) {
|
|
107
|
-
if (!text) return "";
|
|
108
|
-
const body = stripFrontmatter(text);
|
|
109
|
-
if (!body.trim()) return "";
|
|
110
|
-
try {
|
|
111
|
-
return marked.parse(body);
|
|
112
|
-
} catch {
|
|
113
|
-
return body;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async function loadSkills() {
|
|
118
|
-
loading.value = true;
|
|
119
|
-
try {
|
|
120
|
-
skills.value = await getSkills();
|
|
121
|
-
emit("refresh");
|
|
122
|
-
} finally {
|
|
123
|
-
loading.value = false;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function loadReadme() {
|
|
128
|
-
try {
|
|
129
|
-
const res = await getReadme();
|
|
130
|
-
if (res.content) {
|
|
131
|
-
readmeContent.value = res.content;
|
|
132
|
-
renderedReadme.value = renderMd(res.content);
|
|
133
|
-
}
|
|
134
|
-
} catch {
|
|
135
|
-
// 静默失败
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async function showSkillDetail(skill) {
|
|
140
|
-
detailSkill.value = skill;
|
|
141
|
-
detailVisible.value = true;
|
|
142
|
-
detailLoading.value = true;
|
|
143
|
-
detailContent.value = null;
|
|
144
|
-
renderedDetail.value = "";
|
|
145
|
-
try {
|
|
146
|
-
const res = await getSkillReadme(skill.name);
|
|
147
|
-
if (res.content) {
|
|
148
|
-
detailContent.value = res.content;
|
|
149
|
-
await nextTick();
|
|
150
|
-
renderedDetail.value = renderMd(res.content);
|
|
151
|
-
}
|
|
152
|
-
} catch {
|
|
153
|
-
detailContent.value = null;
|
|
154
|
-
} finally {
|
|
155
|
-
detailLoading.value = false;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// 首次加载
|
|
160
|
-
onMounted(() => {
|
|
161
|
-
loadSkills();
|
|
162
|
-
loadReadme();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// keep-alive 切回来时自动刷新
|
|
166
|
-
onActivated(loadSkills);
|
|
167
|
-
|
|
168
|
-
// 从其他窗口切回浏览器时自动刷新
|
|
169
|
-
function onFocus() {
|
|
170
|
-
if (document.visibilityState === "visible") {
|
|
171
|
-
loadSkills();
|
|
172
|
-
loadReadme();
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
onMounted(() => document.addEventListener("visibilitychange", onFocus));
|
|
176
|
-
onUnmounted(() => document.removeEventListener("visibilitychange", onFocus));
|
|
177
|
-
</script>
|
|
178
|
-
|
|
179
|
-
<style scoped>
|
|
180
|
-
.page-header {
|
|
181
|
-
display: flex;
|
|
182
|
-
align-items: center;
|
|
183
|
-
justify-content: space-between;
|
|
184
|
-
margin-bottom: 20px;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
.page-header h2 {
|
|
188
|
-
font-size: 20px;
|
|
189
|
-
font-weight: 600;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
.readme-collapse {
|
|
193
|
-
margin-bottom: 20px;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
.detail-status {
|
|
197
|
-
display: flex;
|
|
198
|
-
justify-content: center;
|
|
199
|
-
padding: 40px 0;
|
|
200
|
-
}
|
|
1
|
+
<template>
|
|
2
|
+
<div class="dashboard">
|
|
3
|
+
<!-- 仓库 README -->
|
|
4
|
+
<n-collapse v-show="readmeContent" :default-expanded-names="[]" class="readme-collapse" :animated="false">
|
|
5
|
+
<n-collapse-item title="仓库说明" name="readme">
|
|
6
|
+
<div class="markdown-content" v-html="renderedReadme" />
|
|
7
|
+
</n-collapse-item>
|
|
8
|
+
</n-collapse>
|
|
9
|
+
|
|
10
|
+
<div class="page-header">
|
|
11
|
+
<h2>技能列表</h2>
|
|
12
|
+
<n-button size="small" @click="loadSkills">
|
|
13
|
+
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
|
14
|
+
刷新
|
|
15
|
+
</n-button>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<n-spin :show="loading">
|
|
19
|
+
<SkillCard
|
|
20
|
+
v-for="skill in skills"
|
|
21
|
+
:key="skill.name"
|
|
22
|
+
:skill="skill"
|
|
23
|
+
@refresh="loadSkills"
|
|
24
|
+
@click="showSkillDetail(skill)"
|
|
25
|
+
/>
|
|
26
|
+
</n-spin>
|
|
27
|
+
|
|
28
|
+
<n-empty v-if="!loading && skills.length === 0" description="暂无技能数据">
|
|
29
|
+
<template #extra>
|
|
30
|
+
<n-button size="small" @click="$router.push({ name: 'Scenes' })">
|
|
31
|
+
前往场景切换
|
|
32
|
+
</n-button>
|
|
33
|
+
</template>
|
|
34
|
+
</n-empty>
|
|
35
|
+
|
|
36
|
+
<!-- 技能详情弹窗 -->
|
|
37
|
+
<n-modal
|
|
38
|
+
v-model:show="detailVisible"
|
|
39
|
+
:title="detailSkill?.name || '技能详情'"
|
|
40
|
+
preset="card"
|
|
41
|
+
style="width: 720px;"
|
|
42
|
+
content-style="max-height: 65vh; overflow-y: auto; padding: 16px 24px; background: #fff;"
|
|
43
|
+
:mask-closable="true"
|
|
44
|
+
>
|
|
45
|
+
<div v-if="detailLoading" class="detail-status">
|
|
46
|
+
<n-spin />
|
|
47
|
+
</div>
|
|
48
|
+
<div v-else-if="detailContent" class="markdown-content" v-html="renderedDetail" />
|
|
49
|
+
<n-empty v-else description="该技能没有 SKILL.md 文档" />
|
|
50
|
+
</n-modal>
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<script setup>
|
|
55
|
+
import { ref, onMounted, onActivated, onUnmounted, nextTick } from "vue";
|
|
56
|
+
import { NIcon } from "naive-ui";
|
|
57
|
+
import { RefreshOutline } from "@vicons/ionicons5";
|
|
58
|
+
import { marked } from "marked";
|
|
59
|
+
import hljs from "highlight.js";
|
|
60
|
+
import SkillCard from "../components/SkillCard.vue";
|
|
61
|
+
import { getSkills, getReadme, getSkillReadme } from "../api/skills";
|
|
62
|
+
|
|
63
|
+
// marked 配置:代码高亮 + 外链安全
|
|
64
|
+
marked.use({
|
|
65
|
+
renderer: {
|
|
66
|
+
code({ text, lang }) {
|
|
67
|
+
const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
|
|
68
|
+
let highlighted;
|
|
69
|
+
try {
|
|
70
|
+
highlighted = hljs.highlight(text, { language }).value;
|
|
71
|
+
} catch {
|
|
72
|
+
highlighted = hljs.highlightAuto(text).value;
|
|
73
|
+
}
|
|
74
|
+
return `<pre><button class="copy-btn" onclick="navigator.clipboard.writeText(this.parentNode.querySelector('code').textContent);this.textContent='已复制';setTimeout(()=>this.textContent='复制',1500)">复制</button><code class="hljs language-${language}">${highlighted}</code></pre>`;
|
|
75
|
+
},
|
|
76
|
+
link({ href, text }) {
|
|
77
|
+
const isExternal = href && (href.startsWith("http://") || href.startsWith("https://"));
|
|
78
|
+
const target = isExternal ? ' target="_blank" rel="noopener noreferrer"' : "";
|
|
79
|
+
return `<a href="${href}"${target}>${text}</a>`;
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const emit = defineEmits(["refresh"]);
|
|
85
|
+
const skills = ref([]);
|
|
86
|
+
const loading = ref(false);
|
|
87
|
+
|
|
88
|
+
// README
|
|
89
|
+
const readmeContent = ref(null);
|
|
90
|
+
const renderedReadme = ref("");
|
|
91
|
+
|
|
92
|
+
// 详情弹窗
|
|
93
|
+
const detailVisible = ref(false);
|
|
94
|
+
const detailSkill = ref(null);
|
|
95
|
+
const detailLoading = ref(false);
|
|
96
|
+
const detailContent = ref(null);
|
|
97
|
+
const renderedDetail = ref("");
|
|
98
|
+
|
|
99
|
+
/** 去掉 YAML frontmatter(--- 包裹的元数据) */
|
|
100
|
+
function stripFrontmatter(text) {
|
|
101
|
+
if (!text) return text;
|
|
102
|
+
return text.replace(/^---[\s\S]*?---\s*/, "");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 渲染 markdown,返回 HTML */
|
|
106
|
+
function renderMd(text) {
|
|
107
|
+
if (!text) return "";
|
|
108
|
+
const body = stripFrontmatter(text);
|
|
109
|
+
if (!body.trim()) return "";
|
|
110
|
+
try {
|
|
111
|
+
return marked.parse(body);
|
|
112
|
+
} catch {
|
|
113
|
+
return body;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function loadSkills() {
|
|
118
|
+
loading.value = true;
|
|
119
|
+
try {
|
|
120
|
+
skills.value = await getSkills();
|
|
121
|
+
emit("refresh");
|
|
122
|
+
} finally {
|
|
123
|
+
loading.value = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function loadReadme() {
|
|
128
|
+
try {
|
|
129
|
+
const res = await getReadme();
|
|
130
|
+
if (res.content) {
|
|
131
|
+
readmeContent.value = res.content;
|
|
132
|
+
renderedReadme.value = renderMd(res.content);
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// 静默失败
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function showSkillDetail(skill) {
|
|
140
|
+
detailSkill.value = skill;
|
|
141
|
+
detailVisible.value = true;
|
|
142
|
+
detailLoading.value = true;
|
|
143
|
+
detailContent.value = null;
|
|
144
|
+
renderedDetail.value = "";
|
|
145
|
+
try {
|
|
146
|
+
const res = await getSkillReadme(skill.name);
|
|
147
|
+
if (res.content) {
|
|
148
|
+
detailContent.value = res.content;
|
|
149
|
+
await nextTick();
|
|
150
|
+
renderedDetail.value = renderMd(res.content);
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
detailContent.value = null;
|
|
154
|
+
} finally {
|
|
155
|
+
detailLoading.value = false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 首次加载
|
|
160
|
+
onMounted(() => {
|
|
161
|
+
loadSkills();
|
|
162
|
+
loadReadme();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// keep-alive 切回来时自动刷新
|
|
166
|
+
onActivated(loadSkills);
|
|
167
|
+
|
|
168
|
+
// 从其他窗口切回浏览器时自动刷新
|
|
169
|
+
function onFocus() {
|
|
170
|
+
if (document.visibilityState === "visible") {
|
|
171
|
+
loadSkills();
|
|
172
|
+
loadReadme();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
onMounted(() => document.addEventListener("visibilitychange", onFocus));
|
|
176
|
+
onUnmounted(() => document.removeEventListener("visibilitychange", onFocus));
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<style scoped>
|
|
180
|
+
.page-header {
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: space-between;
|
|
184
|
+
margin-bottom: 20px;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.page-header h2 {
|
|
188
|
+
font-size: 20px;
|
|
189
|
+
font-weight: 600;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.readme-collapse {
|
|
193
|
+
margin-bottom: 20px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.detail-status {
|
|
197
|
+
display: flex;
|
|
198
|
+
justify-content: center;
|
|
199
|
+
padding: 40px 0;
|
|
200
|
+
}
|
|
201
201
|
</style>
|
|
@@ -1,439 +1,439 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="scene-switch">
|
|
3
|
-
<!-- 技能说明文档 -->
|
|
4
|
-
<n-collapse v-
|
|
5
|
-
<n-collapse-item title="技能说明" name="skills-readme">
|
|
6
|
-
<div class="markdown-content" v-html="renderedSkillsReadme" />
|
|
7
|
-
</n-collapse-item>
|
|
8
|
-
</n-collapse>
|
|
9
|
-
|
|
10
|
-
<div class="page-header">
|
|
11
|
-
<h2>场景切换</h2>
|
|
12
|
-
<n-button v-if="!readonly" size="small" type="primary" @click="openCreate">
|
|
13
|
-
<template #icon><n-icon><AddOutline /></n-icon></template>
|
|
14
|
-
新建场景
|
|
15
|
-
</n-button>
|
|
16
|
-
</div>
|
|
17
|
-
|
|
18
|
-
<n-spin :show="loading">
|
|
19
|
-
<div class="scene-grid">
|
|
20
|
-
<n-card
|
|
21
|
-
v-for="profile in filteredProfiles"
|
|
22
|
-
:key="profile.id"
|
|
23
|
-
class="scene-card"
|
|
24
|
-
:class="{ active: profile.id === activeId }"
|
|
25
|
-
:title="profile.name"
|
|
26
|
-
size="small"
|
|
27
|
-
>
|
|
28
|
-
<template #header-extra>
|
|
29
|
-
<n-space v-if="!readonly && profile.id !== 'custom' && profile.skills !== null" :size="4">
|
|
30
|
-
<n-button size="tiny" quaternary @click="openEdit(profile)">
|
|
31
|
-
<template #icon><n-icon size="16"><PencilOutline /></n-icon></template>
|
|
32
|
-
</n-button>
|
|
33
|
-
<n-popconfirm
|
|
34
|
-
:negative-text="'取消'"
|
|
35
|
-
:positive-text="'确认删除'"
|
|
36
|
-
@positive-click="onDelete(profile)"
|
|
37
|
-
>
|
|
38
|
-
<template #trigger>
|
|
39
|
-
<n-button size="tiny" quaternary>
|
|
40
|
-
<template #icon><n-icon size="16"><TrashOutline /></n-icon></template>
|
|
41
|
-
</n-button>
|
|
42
|
-
</template>
|
|
43
|
-
确定删除场景「{{ profile.name }}」?
|
|
44
|
-
</n-popconfirm>
|
|
45
|
-
</n-space>
|
|
46
|
-
</template>
|
|
47
|
-
|
|
48
|
-
<p class="scene-desc">{{ profile.description }}</p>
|
|
49
|
-
|
|
50
|
-
<template #footer>
|
|
51
|
-
<div class="scene-footer">
|
|
52
|
-
<n-tag v-if="profile.id === activeId" type="success" size="small">
|
|
53
|
-
当前场景
|
|
54
|
-
</n-tag>
|
|
55
|
-
<n-tag v-else-if="profile.skills === null" size="small">
|
|
56
|
-
自定义
|
|
57
|
-
</n-tag>
|
|
58
|
-
<n-tag v-else size="small" :bordered="false">
|
|
59
|
-
{{ profile.skills.length }} 个技能
|
|
60
|
-
</n-tag>
|
|
61
|
-
|
|
62
|
-
<n-button
|
|
63
|
-
v-if="profile.id !== activeId && profile.skills !== null"
|
|
64
|
-
size="small"
|
|
65
|
-
type="primary"
|
|
66
|
-
:loading="applying === profile.id"
|
|
67
|
-
@click="onApply(profile)"
|
|
68
|
-
>
|
|
69
|
-
应用
|
|
70
|
-
</n-button>
|
|
71
|
-
<n-button
|
|
72
|
-
v-if="profile.id !== activeId && profile.skills === null"
|
|
73
|
-
size="small"
|
|
74
|
-
@click="openCustomDialog"
|
|
75
|
-
>
|
|
76
|
-
自定义勾选
|
|
77
|
-
</n-button>
|
|
78
|
-
</div>
|
|
79
|
-
</template>
|
|
80
|
-
</n-card>
|
|
81
|
-
</div>
|
|
82
|
-
</n-spin>
|
|
83
|
-
|
|
84
|
-
<!-- 自定义勾选弹窗 -->
|
|
85
|
-
<n-modal v-model:show="showCustom" title="自定义技能组合" preset="card" style="width: 480px">
|
|
86
|
-
<n-checkbox-group v-model:value="customSelected">
|
|
87
|
-
<n-space vertical>
|
|
88
|
-
<n-checkbox
|
|
89
|
-
v-for="skill in allSkills"
|
|
90
|
-
:key="skill.name"
|
|
91
|
-
:value="skill.name"
|
|
92
|
-
:label="skill.name"
|
|
93
|
-
/>
|
|
94
|
-
</n-space>
|
|
95
|
-
</n-checkbox-group>
|
|
96
|
-
<template #footer>
|
|
97
|
-
<n-button type="primary" :loading="customLoading" @click="onApplyCustom">
|
|
98
|
-
确认安装
|
|
99
|
-
</n-button>
|
|
100
|
-
</template>
|
|
101
|
-
</n-modal>
|
|
102
|
-
|
|
103
|
-
<!-- 编辑弹窗 -->
|
|
104
|
-
<n-modal
|
|
105
|
-
v-model:show="showEditor"
|
|
106
|
-
:title="editingProfile ? '编辑场景' : '新建场景'"
|
|
107
|
-
preset="card"
|
|
108
|
-
style="width: 520px"
|
|
109
|
-
:mask-closable="false"
|
|
110
|
-
>
|
|
111
|
-
<n-form :model="formData" label-placement="top">
|
|
112
|
-
<n-form-item label="场景名称" required>
|
|
113
|
-
<n-input
|
|
114
|
-
v-model:value="formData.name"
|
|
115
|
-
placeholder="给场景起个名字"
|
|
116
|
-
:maxlength="30"
|
|
117
|
-
/>
|
|
118
|
-
</n-form-item>
|
|
119
|
-
<n-form-item label="场景描述">
|
|
120
|
-
<n-input
|
|
121
|
-
v-model:value="formData.description"
|
|
122
|
-
type="textarea"
|
|
123
|
-
:rows="2"
|
|
124
|
-
placeholder="简短描述这个场景的用途"
|
|
125
|
-
/>
|
|
126
|
-
</n-form-item>
|
|
127
|
-
<n-divider />
|
|
128
|
-
<n-form-item label="包含的技能">
|
|
129
|
-
<n-checkbox-group v-model:value="formData.skills">
|
|
130
|
-
<n-space vertical>
|
|
131
|
-
<n-checkbox
|
|
132
|
-
v-for="skill in allSkills"
|
|
133
|
-
:key="skill.name"
|
|
134
|
-
:value="skill.name"
|
|
135
|
-
:label="skill.name"
|
|
136
|
-
/>
|
|
137
|
-
</n-space>
|
|
138
|
-
</n-checkbox-group>
|
|
139
|
-
</n-form-item>
|
|
140
|
-
<n-divider />
|
|
141
|
-
<n-form-item label="始终加载(always_apply)">
|
|
142
|
-
<template v-if="formData.skills.length === 0">
|
|
143
|
-
<span class="hint-text">请先勾选包含的技能</span>
|
|
144
|
-
</template>
|
|
145
|
-
<n-checkbox-group v-else v-model:value="formData.always_apply">
|
|
146
|
-
<n-space vertical>
|
|
147
|
-
<n-checkbox
|
|
148
|
-
v-for="skillName in formData.skills"
|
|
149
|
-
:key="skillName"
|
|
150
|
-
:value="skillName"
|
|
151
|
-
:label="skillName"
|
|
152
|
-
/>
|
|
153
|
-
</n-space>
|
|
154
|
-
</n-checkbox-group>
|
|
155
|
-
</n-form-item>
|
|
156
|
-
</n-form>
|
|
157
|
-
<template #footer>
|
|
158
|
-
<n-space justify="end">
|
|
159
|
-
<n-button @click="showEditor = false">取消</n-button>
|
|
160
|
-
<n-button type="primary" :loading="saving" @click="onSave">
|
|
161
|
-
保存
|
|
162
|
-
</n-button>
|
|
163
|
-
</n-space>
|
|
164
|
-
</template>
|
|
165
|
-
</n-modal>
|
|
166
|
-
</div>
|
|
167
|
-
</template>
|
|
168
|
-
|
|
169
|
-
<script setup>
|
|
170
|
-
import { ref, onMounted, computed } from "vue";
|
|
171
|
-
import { useMessage } from "naive-ui";
|
|
172
|
-
import { NIcon } from "naive-ui";
|
|
173
|
-
import { AddOutline, PencilOutline, TrashOutline } from "@vicons/ionicons5";
|
|
174
|
-
import { marked } from "marked";
|
|
175
|
-
import hljs from "highlight.js";
|
|
176
|
-
import {
|
|
177
|
-
getProfiles,
|
|
178
|
-
applyProfile,
|
|
179
|
-
getSkills,
|
|
180
|
-
saveProfile,
|
|
181
|
-
deleteProfile,
|
|
182
|
-
installSkills,
|
|
183
|
-
getSkillsReadme,
|
|
184
|
-
} from "../api/skills";
|
|
185
|
-
|
|
186
|
-
// marked 配置:代码高亮 + 外链安全
|
|
187
|
-
marked.use({
|
|
188
|
-
renderer: {
|
|
189
|
-
code({ text, lang }) {
|
|
190
|
-
const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
|
|
191
|
-
let highlighted;
|
|
192
|
-
try {
|
|
193
|
-
highlighted = hljs.highlight(text, { language }).value;
|
|
194
|
-
} catch {
|
|
195
|
-
highlighted = hljs.highlightAuto(text).value;
|
|
196
|
-
}
|
|
197
|
-
return `<pre><button class="copy-btn" onclick="navigator.clipboard.writeText(this.parentNode.querySelector('code').textContent);this.textContent='已复制';setTimeout(()=>this.textContent='复制',1500)">复制</button><code class="hljs language-${language}">${highlighted}</code></pre>`;
|
|
198
|
-
},
|
|
199
|
-
link({ href, text }) {
|
|
200
|
-
const isExternal = href && (href.startsWith("http://") || href.startsWith("https://"));
|
|
201
|
-
const target = isExternal ? ' target="_blank" rel="noopener noreferrer"' : "";
|
|
202
|
-
return `<a href="${href}"${target}>${text}</a>`;
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
const emit = defineEmits(["refresh"]);
|
|
208
|
-
const message = useMessage();
|
|
209
|
-
|
|
210
|
-
// 技能说明 README
|
|
211
|
-
const skillsReadmeContent = ref(null);
|
|
212
|
-
const renderedSkillsReadme = ref("");
|
|
213
|
-
|
|
214
|
-
const profiles = ref([]);
|
|
215
|
-
const activeId = ref(null);
|
|
216
|
-
const allSkills = ref([]);
|
|
217
|
-
const loading = ref(false);
|
|
218
|
-
const applying = ref(null);
|
|
219
|
-
const readonly = ref(true);
|
|
220
|
-
|
|
221
|
-
// 自定义勾选
|
|
222
|
-
const showCustom = ref(false);
|
|
223
|
-
const customSelected = ref([]);
|
|
224
|
-
const customLoading = ref(false);
|
|
225
|
-
|
|
226
|
-
// 编辑器
|
|
227
|
-
const showEditor = ref(false);
|
|
228
|
-
const editingProfile = ref(null);
|
|
229
|
-
const saving = ref(false);
|
|
230
|
-
const formData = ref({
|
|
231
|
-
name: "",
|
|
232
|
-
description: "",
|
|
233
|
-
skills: [],
|
|
234
|
-
always_apply: [],
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// 过滤掉不可见的场景(如已有内置)
|
|
238
|
-
const filteredProfiles = computed(() => profiles.value);
|
|
239
|
-
|
|
240
|
-
function stripFrontmatter(text) {
|
|
241
|
-
if (!text) return text;
|
|
242
|
-
return text.replace(/^---[\s\S]*?---\s*/, "");
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function renderMd(text) {
|
|
246
|
-
if (!text) return "";
|
|
247
|
-
const body = stripFrontmatter(text);
|
|
248
|
-
if (!body.trim()) return "";
|
|
249
|
-
try {
|
|
250
|
-
return marked.parse(body);
|
|
251
|
-
} catch {
|
|
252
|
-
return body;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async function loadSkillsReadme() {
|
|
257
|
-
try {
|
|
258
|
-
const res = await getSkillsReadme();
|
|
259
|
-
if (res.content) {
|
|
260
|
-
skillsReadmeContent.value = res.content;
|
|
261
|
-
renderedSkillsReadme.value = renderMd(res.content);
|
|
262
|
-
}
|
|
263
|
-
} catch {
|
|
264
|
-
// 静默失败
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
async function loadData() {
|
|
269
|
-
loading.value = true;
|
|
270
|
-
try {
|
|
271
|
-
const res = await getProfiles();
|
|
272
|
-
profiles.value = res.profiles || [];
|
|
273
|
-
activeId.value = res.activeProfile;
|
|
274
|
-
readonly.value = res.readonly !== false;
|
|
275
|
-
allSkills.value = await getSkills();
|
|
276
|
-
} finally {
|
|
277
|
-
loading.value = false;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function openCreate() {
|
|
282
|
-
editingProfile.value = null;
|
|
283
|
-
formData.value = {
|
|
284
|
-
name: "",
|
|
285
|
-
description: "",
|
|
286
|
-
skills: [],
|
|
287
|
-
always_apply: [],
|
|
288
|
-
};
|
|
289
|
-
showEditor.value = true;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function openEdit(profile) {
|
|
293
|
-
editingProfile.value = profile;
|
|
294
|
-
formData.value = {
|
|
295
|
-
name: profile.name,
|
|
296
|
-
description: profile.description || "",
|
|
297
|
-
skills: [...(profile.skills || [])],
|
|
298
|
-
always_apply: [...(profile.always_apply || [])],
|
|
299
|
-
};
|
|
300
|
-
showEditor.value = true;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function openCustomDialog() {
|
|
304
|
-
customSelected.value = allSkills.value.filter((s) => s.enabled).map((s) => s.name);
|
|
305
|
-
showCustom.value = true;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async function onApplyCustom() {
|
|
309
|
-
customLoading.value = true;
|
|
310
|
-
try {
|
|
311
|
-
const res = await installSkills(customSelected.value);
|
|
312
|
-
if (res.ok) {
|
|
313
|
-
message.success(`已安装 ${customSelected.value.length} 个技能`);
|
|
314
|
-
activeId.value = "custom";
|
|
315
|
-
showCustom.value = false;
|
|
316
|
-
emit("refresh");
|
|
317
|
-
}
|
|
318
|
-
} catch {
|
|
319
|
-
message.error("安装失败");
|
|
320
|
-
} finally {
|
|
321
|
-
customLoading.value = false;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
async function onSave() {
|
|
326
|
-
if (!formData.value.name.trim()) {
|
|
327
|
-
message.warning("请填写场景名称");
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
saving.value = true;
|
|
331
|
-
try {
|
|
332
|
-
const data = {
|
|
333
|
-
id: editingProfile.value?.id,
|
|
334
|
-
name: formData.value.name.trim(),
|
|
335
|
-
description: formData.value.description.trim(),
|
|
336
|
-
skills: formData.value.skills,
|
|
337
|
-
always_apply: formData.value.always_apply,
|
|
338
|
-
};
|
|
339
|
-
const res = await saveProfile(data);
|
|
340
|
-
if (res.ok) {
|
|
341
|
-
profiles.value = res.profiles;
|
|
342
|
-
message.success(editingProfile.value ? "场景已更新" : "场景已创建");
|
|
343
|
-
showEditor.value = false;
|
|
344
|
-
emit("refresh");
|
|
345
|
-
} else if (res.error) {
|
|
346
|
-
message.error(res.error);
|
|
347
|
-
}
|
|
348
|
-
} catch {
|
|
349
|
-
message.error("保存失败");
|
|
350
|
-
} finally {
|
|
351
|
-
saving.value = false;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
async function onDelete(profile) {
|
|
356
|
-
try {
|
|
357
|
-
const res = await deleteProfile(profile.id);
|
|
358
|
-
if (res.ok) {
|
|
359
|
-
profiles.value = profiles.value.filter((p) => p.id !== profile.id);
|
|
360
|
-
message.success(`场景「${profile.name}」已删除`);
|
|
361
|
-
emit("refresh");
|
|
362
|
-
} else if (res.error) {
|
|
363
|
-
message.error(res.error);
|
|
364
|
-
}
|
|
365
|
-
} catch {
|
|
366
|
-
message.error("删除失败");
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
async function onApply(profile) {
|
|
371
|
-
applying.value = profile.id;
|
|
372
|
-
try {
|
|
373
|
-
const res = await applyProfile(profile.id);
|
|
374
|
-
if (res.ok) {
|
|
375
|
-
activeId.value = profile.id;
|
|
376
|
-
message.success(`已切换到「${profile.name}」`);
|
|
377
|
-
emit("refresh");
|
|
378
|
-
}
|
|
379
|
-
} catch {
|
|
380
|
-
message.error("切换失败");
|
|
381
|
-
} finally {
|
|
382
|
-
applying.value = null;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
onMounted(() => {
|
|
387
|
-
loadData();
|
|
388
|
-
loadSkillsReadme();
|
|
389
|
-
});
|
|
390
|
-
</script>
|
|
391
|
-
|
|
392
|
-
<style scoped>
|
|
393
|
-
.page-header {
|
|
394
|
-
display: flex;
|
|
395
|
-
align-items: center;
|
|
396
|
-
justify-content: space-between;
|
|
397
|
-
margin-bottom: 20px;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
.page-header h2 {
|
|
401
|
-
font-size: 20px;
|
|
402
|
-
font-weight: 600;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
.readme-collapse {
|
|
406
|
-
margin-bottom: 20px;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
.scene-grid {
|
|
410
|
-
display: grid;
|
|
411
|
-
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
412
|
-
gap: 16px;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
.scene-card {
|
|
416
|
-
transition: box-shadow 0.2s;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
.scene-card.active {
|
|
420
|
-
box-shadow: 0 0 0 2px #2080f0;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
.scene-desc {
|
|
424
|
-
font-size: 13px;
|
|
425
|
-
color: #666;
|
|
426
|
-
margin: 0;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
.scene-footer {
|
|
430
|
-
display: flex;
|
|
431
|
-
align-items: center;
|
|
432
|
-
justify-content: space-between;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
.hint-text {
|
|
436
|
-
font-size: 12px;
|
|
437
|
-
color: #999;
|
|
438
|
-
}
|
|
439
|
-
</style>
|
|
1
|
+
<template>
|
|
2
|
+
<div class="scene-switch">
|
|
3
|
+
<!-- 技能说明文档 -->
|
|
4
|
+
<n-collapse v-show="skillsReadmeContent" :default-expanded-names="[]" class="readme-collapse" :animated="false">
|
|
5
|
+
<n-collapse-item title="技能说明" name="skills-readme">
|
|
6
|
+
<div class="markdown-content" v-html="renderedSkillsReadme" />
|
|
7
|
+
</n-collapse-item>
|
|
8
|
+
</n-collapse>
|
|
9
|
+
|
|
10
|
+
<div class="page-header">
|
|
11
|
+
<h2>场景切换</h2>
|
|
12
|
+
<n-button v-if="!readonly" size="small" type="primary" @click="openCreate">
|
|
13
|
+
<template #icon><n-icon><AddOutline /></n-icon></template>
|
|
14
|
+
新建场景
|
|
15
|
+
</n-button>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<n-spin :show="loading">
|
|
19
|
+
<div class="scene-grid">
|
|
20
|
+
<n-card
|
|
21
|
+
v-for="profile in filteredProfiles"
|
|
22
|
+
:key="profile.id"
|
|
23
|
+
class="scene-card"
|
|
24
|
+
:class="{ active: profile.id === activeId }"
|
|
25
|
+
:title="profile.name"
|
|
26
|
+
size="small"
|
|
27
|
+
>
|
|
28
|
+
<template #header-extra>
|
|
29
|
+
<n-space v-if="!readonly && profile.id !== 'custom' && profile.skills !== null" :size="4">
|
|
30
|
+
<n-button size="tiny" quaternary @click="openEdit(profile)">
|
|
31
|
+
<template #icon><n-icon size="16"><PencilOutline /></n-icon></template>
|
|
32
|
+
</n-button>
|
|
33
|
+
<n-popconfirm
|
|
34
|
+
:negative-text="'取消'"
|
|
35
|
+
:positive-text="'确认删除'"
|
|
36
|
+
@positive-click="onDelete(profile)"
|
|
37
|
+
>
|
|
38
|
+
<template #trigger>
|
|
39
|
+
<n-button size="tiny" quaternary>
|
|
40
|
+
<template #icon><n-icon size="16"><TrashOutline /></n-icon></template>
|
|
41
|
+
</n-button>
|
|
42
|
+
</template>
|
|
43
|
+
确定删除场景「{{ profile.name }}」?
|
|
44
|
+
</n-popconfirm>
|
|
45
|
+
</n-space>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<p class="scene-desc">{{ profile.description }}</p>
|
|
49
|
+
|
|
50
|
+
<template #footer>
|
|
51
|
+
<div class="scene-footer">
|
|
52
|
+
<n-tag v-if="profile.id === activeId" type="success" size="small">
|
|
53
|
+
当前场景
|
|
54
|
+
</n-tag>
|
|
55
|
+
<n-tag v-else-if="profile.skills === null" size="small">
|
|
56
|
+
自定义
|
|
57
|
+
</n-tag>
|
|
58
|
+
<n-tag v-else size="small" :bordered="false">
|
|
59
|
+
{{ profile.skills.length }} 个技能
|
|
60
|
+
</n-tag>
|
|
61
|
+
|
|
62
|
+
<n-button
|
|
63
|
+
v-if="profile.id !== activeId && profile.skills !== null"
|
|
64
|
+
size="small"
|
|
65
|
+
type="primary"
|
|
66
|
+
:loading="applying === profile.id"
|
|
67
|
+
@click="onApply(profile)"
|
|
68
|
+
>
|
|
69
|
+
应用
|
|
70
|
+
</n-button>
|
|
71
|
+
<n-button
|
|
72
|
+
v-if="profile.id !== activeId && profile.skills === null"
|
|
73
|
+
size="small"
|
|
74
|
+
@click="openCustomDialog"
|
|
75
|
+
>
|
|
76
|
+
自定义勾选
|
|
77
|
+
</n-button>
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|
|
80
|
+
</n-card>
|
|
81
|
+
</div>
|
|
82
|
+
</n-spin>
|
|
83
|
+
|
|
84
|
+
<!-- 自定义勾选弹窗 -->
|
|
85
|
+
<n-modal v-model:show="showCustom" title="自定义技能组合" preset="card" style="width: 480px">
|
|
86
|
+
<n-checkbox-group v-model:value="customSelected">
|
|
87
|
+
<n-space vertical>
|
|
88
|
+
<n-checkbox
|
|
89
|
+
v-for="skill in allSkills"
|
|
90
|
+
:key="skill.name"
|
|
91
|
+
:value="skill.name"
|
|
92
|
+
:label="skill.name"
|
|
93
|
+
/>
|
|
94
|
+
</n-space>
|
|
95
|
+
</n-checkbox-group>
|
|
96
|
+
<template #footer>
|
|
97
|
+
<n-button type="primary" :loading="customLoading" @click="onApplyCustom">
|
|
98
|
+
确认安装
|
|
99
|
+
</n-button>
|
|
100
|
+
</template>
|
|
101
|
+
</n-modal>
|
|
102
|
+
|
|
103
|
+
<!-- 编辑弹窗 -->
|
|
104
|
+
<n-modal
|
|
105
|
+
v-model:show="showEditor"
|
|
106
|
+
:title="editingProfile ? '编辑场景' : '新建场景'"
|
|
107
|
+
preset="card"
|
|
108
|
+
style="width: 520px"
|
|
109
|
+
:mask-closable="false"
|
|
110
|
+
>
|
|
111
|
+
<n-form :model="formData" label-placement="top">
|
|
112
|
+
<n-form-item label="场景名称" required>
|
|
113
|
+
<n-input
|
|
114
|
+
v-model:value="formData.name"
|
|
115
|
+
placeholder="给场景起个名字"
|
|
116
|
+
:maxlength="30"
|
|
117
|
+
/>
|
|
118
|
+
</n-form-item>
|
|
119
|
+
<n-form-item label="场景描述">
|
|
120
|
+
<n-input
|
|
121
|
+
v-model:value="formData.description"
|
|
122
|
+
type="textarea"
|
|
123
|
+
:rows="2"
|
|
124
|
+
placeholder="简短描述这个场景的用途"
|
|
125
|
+
/>
|
|
126
|
+
</n-form-item>
|
|
127
|
+
<n-divider />
|
|
128
|
+
<n-form-item label="包含的技能">
|
|
129
|
+
<n-checkbox-group v-model:value="formData.skills">
|
|
130
|
+
<n-space vertical>
|
|
131
|
+
<n-checkbox
|
|
132
|
+
v-for="skill in allSkills"
|
|
133
|
+
:key="skill.name"
|
|
134
|
+
:value="skill.name"
|
|
135
|
+
:label="skill.name"
|
|
136
|
+
/>
|
|
137
|
+
</n-space>
|
|
138
|
+
</n-checkbox-group>
|
|
139
|
+
</n-form-item>
|
|
140
|
+
<n-divider />
|
|
141
|
+
<n-form-item label="始终加载(always_apply)">
|
|
142
|
+
<template v-if="formData.skills.length === 0">
|
|
143
|
+
<span class="hint-text">请先勾选包含的技能</span>
|
|
144
|
+
</template>
|
|
145
|
+
<n-checkbox-group v-else v-model:value="formData.always_apply">
|
|
146
|
+
<n-space vertical>
|
|
147
|
+
<n-checkbox
|
|
148
|
+
v-for="skillName in formData.skills"
|
|
149
|
+
:key="skillName"
|
|
150
|
+
:value="skillName"
|
|
151
|
+
:label="skillName"
|
|
152
|
+
/>
|
|
153
|
+
</n-space>
|
|
154
|
+
</n-checkbox-group>
|
|
155
|
+
</n-form-item>
|
|
156
|
+
</n-form>
|
|
157
|
+
<template #footer>
|
|
158
|
+
<n-space justify="end">
|
|
159
|
+
<n-button @click="showEditor = false">取消</n-button>
|
|
160
|
+
<n-button type="primary" :loading="saving" @click="onSave">
|
|
161
|
+
保存
|
|
162
|
+
</n-button>
|
|
163
|
+
</n-space>
|
|
164
|
+
</template>
|
|
165
|
+
</n-modal>
|
|
166
|
+
</div>
|
|
167
|
+
</template>
|
|
168
|
+
|
|
169
|
+
<script setup>
|
|
170
|
+
import { ref, onMounted, computed } from "vue";
|
|
171
|
+
import { useMessage } from "naive-ui";
|
|
172
|
+
import { NIcon } from "naive-ui";
|
|
173
|
+
import { AddOutline, PencilOutline, TrashOutline } from "@vicons/ionicons5";
|
|
174
|
+
import { marked } from "marked";
|
|
175
|
+
import hljs from "highlight.js";
|
|
176
|
+
import {
|
|
177
|
+
getProfiles,
|
|
178
|
+
applyProfile,
|
|
179
|
+
getSkills,
|
|
180
|
+
saveProfile,
|
|
181
|
+
deleteProfile,
|
|
182
|
+
installSkills,
|
|
183
|
+
getSkillsReadme,
|
|
184
|
+
} from "../api/skills";
|
|
185
|
+
|
|
186
|
+
// marked 配置:代码高亮 + 外链安全
|
|
187
|
+
marked.use({
|
|
188
|
+
renderer: {
|
|
189
|
+
code({ text, lang }) {
|
|
190
|
+
const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
|
|
191
|
+
let highlighted;
|
|
192
|
+
try {
|
|
193
|
+
highlighted = hljs.highlight(text, { language }).value;
|
|
194
|
+
} catch {
|
|
195
|
+
highlighted = hljs.highlightAuto(text).value;
|
|
196
|
+
}
|
|
197
|
+
return `<pre><button class="copy-btn" onclick="navigator.clipboard.writeText(this.parentNode.querySelector('code').textContent);this.textContent='已复制';setTimeout(()=>this.textContent='复制',1500)">复制</button><code class="hljs language-${language}">${highlighted}</code></pre>`;
|
|
198
|
+
},
|
|
199
|
+
link({ href, text }) {
|
|
200
|
+
const isExternal = href && (href.startsWith("http://") || href.startsWith("https://"));
|
|
201
|
+
const target = isExternal ? ' target="_blank" rel="noopener noreferrer"' : "";
|
|
202
|
+
return `<a href="${href}"${target}>${text}</a>`;
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const emit = defineEmits(["refresh"]);
|
|
208
|
+
const message = useMessage();
|
|
209
|
+
|
|
210
|
+
// 技能说明 README
|
|
211
|
+
const skillsReadmeContent = ref(null);
|
|
212
|
+
const renderedSkillsReadme = ref("");
|
|
213
|
+
|
|
214
|
+
const profiles = ref([]);
|
|
215
|
+
const activeId = ref(null);
|
|
216
|
+
const allSkills = ref([]);
|
|
217
|
+
const loading = ref(false);
|
|
218
|
+
const applying = ref(null);
|
|
219
|
+
const readonly = ref(true);
|
|
220
|
+
|
|
221
|
+
// 自定义勾选
|
|
222
|
+
const showCustom = ref(false);
|
|
223
|
+
const customSelected = ref([]);
|
|
224
|
+
const customLoading = ref(false);
|
|
225
|
+
|
|
226
|
+
// 编辑器
|
|
227
|
+
const showEditor = ref(false);
|
|
228
|
+
const editingProfile = ref(null);
|
|
229
|
+
const saving = ref(false);
|
|
230
|
+
const formData = ref({
|
|
231
|
+
name: "",
|
|
232
|
+
description: "",
|
|
233
|
+
skills: [],
|
|
234
|
+
always_apply: [],
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// 过滤掉不可见的场景(如已有内置)
|
|
238
|
+
const filteredProfiles = computed(() => profiles.value);
|
|
239
|
+
|
|
240
|
+
function stripFrontmatter(text) {
|
|
241
|
+
if (!text) return text;
|
|
242
|
+
return text.replace(/^---[\s\S]*?---\s*/, "");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function renderMd(text) {
|
|
246
|
+
if (!text) return "";
|
|
247
|
+
const body = stripFrontmatter(text);
|
|
248
|
+
if (!body.trim()) return "";
|
|
249
|
+
try {
|
|
250
|
+
return marked.parse(body);
|
|
251
|
+
} catch {
|
|
252
|
+
return body;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function loadSkillsReadme() {
|
|
257
|
+
try {
|
|
258
|
+
const res = await getSkillsReadme();
|
|
259
|
+
if (res.content) {
|
|
260
|
+
skillsReadmeContent.value = res.content;
|
|
261
|
+
renderedSkillsReadme.value = renderMd(res.content);
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
// 静默失败
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function loadData() {
|
|
269
|
+
loading.value = true;
|
|
270
|
+
try {
|
|
271
|
+
const res = await getProfiles();
|
|
272
|
+
profiles.value = res.profiles || [];
|
|
273
|
+
activeId.value = res.activeProfile;
|
|
274
|
+
readonly.value = res.readonly !== false;
|
|
275
|
+
allSkills.value = await getSkills();
|
|
276
|
+
} finally {
|
|
277
|
+
loading.value = false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function openCreate() {
|
|
282
|
+
editingProfile.value = null;
|
|
283
|
+
formData.value = {
|
|
284
|
+
name: "",
|
|
285
|
+
description: "",
|
|
286
|
+
skills: [],
|
|
287
|
+
always_apply: [],
|
|
288
|
+
};
|
|
289
|
+
showEditor.value = true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function openEdit(profile) {
|
|
293
|
+
editingProfile.value = profile;
|
|
294
|
+
formData.value = {
|
|
295
|
+
name: profile.name,
|
|
296
|
+
description: profile.description || "",
|
|
297
|
+
skills: [...(profile.skills || [])],
|
|
298
|
+
always_apply: [...(profile.always_apply || [])],
|
|
299
|
+
};
|
|
300
|
+
showEditor.value = true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function openCustomDialog() {
|
|
304
|
+
customSelected.value = allSkills.value.filter((s) => s.enabled).map((s) => s.name);
|
|
305
|
+
showCustom.value = true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function onApplyCustom() {
|
|
309
|
+
customLoading.value = true;
|
|
310
|
+
try {
|
|
311
|
+
const res = await installSkills(customSelected.value);
|
|
312
|
+
if (res.ok) {
|
|
313
|
+
message.success(`已安装 ${customSelected.value.length} 个技能`);
|
|
314
|
+
activeId.value = "custom";
|
|
315
|
+
showCustom.value = false;
|
|
316
|
+
emit("refresh");
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
message.error("安装失败");
|
|
320
|
+
} finally {
|
|
321
|
+
customLoading.value = false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function onSave() {
|
|
326
|
+
if (!formData.value.name.trim()) {
|
|
327
|
+
message.warning("请填写场景名称");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
saving.value = true;
|
|
331
|
+
try {
|
|
332
|
+
const data = {
|
|
333
|
+
id: editingProfile.value?.id,
|
|
334
|
+
name: formData.value.name.trim(),
|
|
335
|
+
description: formData.value.description.trim(),
|
|
336
|
+
skills: formData.value.skills,
|
|
337
|
+
always_apply: formData.value.always_apply,
|
|
338
|
+
};
|
|
339
|
+
const res = await saveProfile(data);
|
|
340
|
+
if (res.ok) {
|
|
341
|
+
profiles.value = res.profiles;
|
|
342
|
+
message.success(editingProfile.value ? "场景已更新" : "场景已创建");
|
|
343
|
+
showEditor.value = false;
|
|
344
|
+
emit("refresh");
|
|
345
|
+
} else if (res.error) {
|
|
346
|
+
message.error(res.error);
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
message.error("保存失败");
|
|
350
|
+
} finally {
|
|
351
|
+
saving.value = false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function onDelete(profile) {
|
|
356
|
+
try {
|
|
357
|
+
const res = await deleteProfile(profile.id);
|
|
358
|
+
if (res.ok) {
|
|
359
|
+
profiles.value = profiles.value.filter((p) => p.id !== profile.id);
|
|
360
|
+
message.success(`场景「${profile.name}」已删除`);
|
|
361
|
+
emit("refresh");
|
|
362
|
+
} else if (res.error) {
|
|
363
|
+
message.error(res.error);
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
message.error("删除失败");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function onApply(profile) {
|
|
371
|
+
applying.value = profile.id;
|
|
372
|
+
try {
|
|
373
|
+
const res = await applyProfile(profile.id);
|
|
374
|
+
if (res.ok) {
|
|
375
|
+
activeId.value = profile.id;
|
|
376
|
+
message.success(`已切换到「${profile.name}」`);
|
|
377
|
+
emit("refresh");
|
|
378
|
+
}
|
|
379
|
+
} catch {
|
|
380
|
+
message.error("切换失败");
|
|
381
|
+
} finally {
|
|
382
|
+
applying.value = null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
onMounted(() => {
|
|
387
|
+
loadData();
|
|
388
|
+
loadSkillsReadme();
|
|
389
|
+
});
|
|
390
|
+
</script>
|
|
391
|
+
|
|
392
|
+
<style scoped>
|
|
393
|
+
.page-header {
|
|
394
|
+
display: flex;
|
|
395
|
+
align-items: center;
|
|
396
|
+
justify-content: space-between;
|
|
397
|
+
margin-bottom: 20px;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.page-header h2 {
|
|
401
|
+
font-size: 20px;
|
|
402
|
+
font-weight: 600;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.readme-collapse {
|
|
406
|
+
margin-bottom: 20px;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.scene-grid {
|
|
410
|
+
display: grid;
|
|
411
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
412
|
+
gap: 16px;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.scene-card {
|
|
416
|
+
transition: box-shadow 0.2s;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.scene-card.active {
|
|
420
|
+
box-shadow: 0 0 0 2px #2080f0;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.scene-desc {
|
|
424
|
+
font-size: 13px;
|
|
425
|
+
color: #666;
|
|
426
|
+
margin: 0;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.scene-footer {
|
|
430
|
+
display: flex;
|
|
431
|
+
align-items: center;
|
|
432
|
+
justify-content: space-between;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.hint-text {
|
|
436
|
+
font-size: 12px;
|
|
437
|
+
color: #999;
|
|
438
|
+
}
|
|
439
|
+
</style>
|