mdk-skills 2.4.0 → 2.4.2

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,1697 +0,0 @@
1
- <template>
2
- <div class="dashboard">
3
- <div class="page-header">
4
- <h2>技能列表</h2>
5
- <div class="header-actions">
6
- <n-select
7
- v-model:value="sortBy"
8
- :options="sortOptions"
9
- size="small"
10
- style="width: 120px"
11
- />
12
- <n-button size="small" @click="loadSkills">
13
- <template #icon
14
- ><n-icon><RefreshOutline /></n-icon
15
- ></template>
16
- 刷新
17
- </n-button>
18
- <n-button v-if="!needsSetup" size="small" @click="openPullModal">
19
- 拉取
20
- </n-button>
21
- </div>
22
- </div>
23
-
24
- <div class="tag-filter" v-if="allTags.length > 0">
25
- <n-tag
26
- size="small"
27
- :bordered="false"
28
- :type="selectedTags.length === 0 ? 'primary' : 'default'"
29
- style="cursor: pointer"
30
- @click="selectedTags = []"
31
- >
32
- 全部
33
- </n-tag>
34
- <n-tag
35
- v-for="tag in allTags"
36
- :key="tag"
37
- size="small"
38
- :bordered="false"
39
- :type="selectedTags.includes(tag) ? 'primary' : 'default'"
40
- style="cursor: pointer"
41
- @click="toggleTag(tag)"
42
- >
43
- {{ tag }}
44
- </n-tag>
45
- </div>
46
-
47
- <div class="search-bar" v-if="!needsSetup && skills.length > 0">
48
- <n-input
49
- v-model:value="searchQuery"
50
- placeholder="搜索技能名称或描述..."
51
- size="small"
52
- clearable
53
- >
54
- <template #prefix>
55
- <n-icon><SearchOutline /></n-icon>
56
- </template>
57
- </n-input>
58
- </div>
59
-
60
- <n-spin :show="loading">
61
- <!-- 引导卡片:未设置技能目录时显示 -->
62
- <n-card v-if="needsSetup" class="setup-card" size="small">
63
- <div class="setup-card-content">
64
- <div class="setup-icon">
65
- <n-icon size="48" color="#2080f0"><FolderOpenOutline /></n-icon>
66
- </div>
67
- <h3 class="setup-title">欢迎使用 mdk-skills</h3>
68
- <p class="setup-desc">
69
- 请先设置技能目录,指向包含 <code>.claude/skills</code> 的仓库路径。
70
- 设置后即可浏览和安装技能。
71
- </p>
72
- <n-button type="primary" size="small" @click="$router.push({ name: 'Settings' })">
73
- 前往设置
74
- </n-button>
75
- </div>
76
- </n-card>
77
-
78
- <div v-for="group in groupedSkills" :key="group.key" class="skill-group" :class="'group-' + group.type">
79
- <div class="group-header" @click="toggleGroup(group.key)">
80
- <span class="fold-arrow" :class="{ open: groupOpen[group.key] }">▶</span>
81
- <span class="group-indicator" :class="'indicator-' + group.type"></span>
82
- <span class="group-label">
83
- <template v-if="group.type === 'remote'">
84
- <span v-if="editingAlias === group.key" class="alias-edit-inline" @click.stop>
85
- <n-input v-model:value="editAliasValue" size="tiny" style="width:200px" autofocus @blur="saveAlias(group)" @keyup.enter="saveAlias(group)" @keyup.escape="editingAlias = null" />
86
- </span>
87
- <span v-else>
88
- {{ sourceNames[group.url] || group.url }}
89
- </span>
90
- <span class="alias-edit-trigger" @click.stop="startEditAlias(group)">
91
- <n-icon size="13" class="alias-edit-icon"><PencilOutline /></n-icon>
92
- </span>
93
- </template>
94
- <template v-else>本地技能</template>
95
- </span>
96
- <span class="group-count">{{ group.skills.length }} 个</span>
97
- <span v-if="group.type === 'remote'" @click.stop>
98
- <n-dropdown trigger="click" :options="getBatchMenuOptions(group)" @select="(key) => onBatchMenuSelect(key, group)">
99
- <n-button size="tiny" class="group-menu-btn">
100
- <template #icon><n-icon><EllipsisHorizontalOutline /></n-icon></template>
101
- </n-button>
102
- </n-dropdown>
103
- </span>
104
- </div>
105
- <div class="group-body" v-show="groupOpen[group.key]">
106
- <SkillCard
107
- v-for="skill in group.skills"
108
- :key="skill.name"
109
- :skill="skill"
110
- :usages="getUsage(skill.name)"
111
- @refresh="loadSkills"
112
- @click="showSkillDetail(skill)"
113
- />
114
- </div>
115
- </div>
116
- </n-spin>
117
-
118
- <n-empty v-if="!loading && skills.length === 0 && !needsSetup" description="暂无技能数据">
119
- <template #extra>
120
- <n-button size="small" @click="openPullModal">去拉取技能</n-button>
121
- </template>
122
- </n-empty>
123
-
124
- <n-empty v-if="!loading && skills.length > 0 && displaySkills.length === 0" description="没有匹配的技能">
125
- <template #extra>
126
- <n-button size="small" @click="selectedTags = []; searchQuery = ''">清除筛选</n-button>
127
- </template>
128
- </n-empty>
129
-
130
- <!-- 仓库 README(页面底部) -->
131
- <div class="readme-fold" v-if="readmeContent">
132
- <div class="fold-header" @click="readmeOpen = !readmeOpen">
133
- <span class="fold-arrow" :class="{ open: readmeOpen }">▶</span>
134
- 仓库说明
135
- </div>
136
- <div class="fold-body" v-show="readmeOpen">
137
- <div class="markdown-content" v-html="renderedReadme" />
138
- </div>
139
- </div>
140
-
141
- <!-- 技能详情弹窗 -->
142
- <ModalComp
143
- :show="detailVisible"
144
- :title="detailSkill?.name || '技能详情'"
145
- width="720px"
146
- :mask-closable="true"
147
- @update:show="handleDetailClose"
148
- >
149
- <div class="detail-body">
150
- <!-- 编辑面板 -->
151
- <div class="edit-panel" v-if="detailSkill">
152
- <div class="edit-row">
153
- <span class="edit-label">版本</span>
154
- <n-input v-model:value="editVersion" size="small" style="width: 120px" />
155
- <span v-if="detailSkill?._updateCount > 0" class="update-count-hint">r{{ detailSkill._updateCount }}</span>
156
- </div>
157
- <div class="edit-row">
158
- <span class="edit-label">描述</span>
159
- <n-input v-model:value="editDescription" type="textarea" size="small" :rows="2" />
160
- </div>
161
- <div class="edit-row">
162
- <span class="edit-label">标签</span>
163
- <n-dynamic-tags v-model:value="editTags" size="small" />
164
- </div>
165
- <div class="edit-actions">
166
- <n-button size="small" type="primary" @click="saveMeta" :loading="savingMeta">保存</n-button>
167
- <n-button size="small" type="error" @click="handleDelete">删除技能</n-button>
168
- </div>
169
- <!-- 来源信息 -->
170
- <div v-if="skillSource" class="source-info">
171
- <template v-if="skillSource.type === 'remote'">
172
- <div class="source-row">
173
- <span class="source-label">来源</span>
174
- <span class="source-value">{{ sourceNames[skillSource.url] || skillSource.url }}</span>
175
- </div>
176
- <div class="source-row">
177
- <span class="source-label">拉取时间</span>
178
- <span class="source-value">{{ new Date(skillSource.pulledAt).toLocaleString() }}</span>
179
- </div>
180
- <div class="source-actions">
181
- <n-button size="small" @click="handleUpdate" :loading="updatingSkill">检查更新</n-button>
182
- </div>
183
- </template>
184
- <template v-else>
185
- <div class="source-row">
186
- <span class="source-label">类型</span>
187
- <span class="source-value local-tag">本地技能</span>
188
- </div>
189
- <div v-if="skillSource.path" class="source-row">
190
- <span class="source-label">路径</span>
191
- <span class="source-value path-text">{{ skillSource.path }}</span>
192
- </div>
193
- <div class="source-actions">
194
- <n-button size="small" @click="openDir">打开目录</n-button>
195
- </div>
196
- <div class="local-hint">
197
- 本地技能暂不支持远程更新。推送到 Git 仓库后可使用"拉取"功能分享给团队。
198
- </div>
199
- </template>
200
- </div>
201
- <n-divider style="margin: 12px 0" />
202
- </div>
203
-
204
- <div v-if="detailLoading" class="detail-status">
205
- <n-spin />
206
- </div>
207
- <div
208
- v-else-if="detailContent"
209
- class="markdown-content"
210
- v-html="renderedDetail"
211
- />
212
- <n-empty v-else description="该技能没有 SKILL.md 文档" />
213
- </div>
214
- </ModalComp>
215
-
216
- <!-- 拉取弹窗(两阶段:预览 → 选择 → 拉取) -->
217
- <ModalComp
218
- :show="showPullModal"
219
- title="从远程仓库拉取"
220
- width="600px"
221
- :mask-closable="true"
222
- @update:show="(v) => { if (!v) { showPullModal = false; pulling = false; previewing = false; cancelTask(); cancelPull(pullUrl).catch(() => {}); } }"
223
- >
224
- <div class="pull-modal-body">
225
- <!-- URL 输入 -->
226
- <div class="pull-input-row">
227
- <n-input v-model:value="pullUrl" placeholder="输入 GitHub 仓库地址..." size="small" @keyup.enter="handlePreview" />
228
- <n-button size="small" type="primary" @click="handlePreview" :loading="previewing">预览</n-button>
229
- <n-button size="small" @click="toggleSourceManager">管理源</n-button>
230
- </div>
231
-
232
- <!-- 别名管理 -->
233
- <div v-if="showSourceManager" class="source-manager">
234
- <div class="source-manager-header">
235
- <span class="source-manager-title">远程源别名</span>
236
- <n-button size="tiny" text @click="showSourceManager = false">关闭</n-button>
237
- </div>
238
- <div class="source-manager-list">
239
- <div v-for="url in knownUrlsList" :key="url" class="source-manager-row">
240
- <span class="source-manager-url" :title="url">{{ url }}</span>
241
- <n-input v-model:value="editSourceNames[url]" placeholder="输入别名..." size="tiny" style="width:180px" clearable />
242
- </div>
243
- <div v-if="knownUrlsList.length === 0" class="source-manager-empty">暂无已拉取的远程源</div>
244
- </div>
245
- <div class="source-manager-actions">
246
- <n-button size="small" type="primary" @click="doSaveSourceNames" :loading="savingSourceNames">保存</n-button>
247
- </div>
248
- </div>
249
-
250
- <!-- 错误提示 -->
251
- <div v-if="pullError" class="pull-error">
252
- <n-alert type="error" :bordered="false" closable @close="pullError = ''">{{ pullError }}</n-alert>
253
- </div>
254
-
255
- <!-- 预览中 -->
256
- <div v-if="previewing" class="pull-status">
257
- <n-spin size="small" />
258
- <span>正在获取远程技能列表...</span>
259
- </div>
260
-
261
- <!-- 预览结果:技能列表 + 勾选 -->
262
- <div v-if="previewDone && !pulling && !pullResult" class="preview-result">
263
- <div class="preview-info">
264
- 发现 <strong>{{ previewSkillList.length }}</strong> 个技能:
265
- <n-button size="tiny" text @click="togglePullAll">全选/取消</n-button>
266
- </div>
267
- <n-input
268
- v-model:value="pullSearch"
269
- placeholder="搜索技能..."
270
- size="small"
271
- clearable
272
- class="pull-search-input"
273
- />
274
- <div class="preview-list">
275
- <n-checkbox
276
- v-for="name in filteredPullSkills"
277
- :key="name"
278
- v-model:checked="pullCheck[name]"
279
- >
280
- {{ name }}
281
- <n-tag v-if="existingSkillNames.has(name)" size="tiny" type="success" :bordered="false" class="installed-tag">已安装</n-tag>
282
- </n-checkbox>
283
- </div>
284
- <div class="preview-actions">
285
- <n-button size="small" @click="showPullModal = false">取消</n-button>
286
- <n-button size="small" type="primary" @click="handlePull" :loading="pulling" :disabled="selectedPullCount === 0">
287
- 拉取选中 ({{ selectedPullCount }})
288
- </n-button>
289
- </div>
290
- </div>
291
-
292
- <!-- 拉取中 -->
293
- <div v-if="pulling" class="pull-status">
294
- <n-spin size="small" />
295
- <span>正在拉取选中技能...</span>
296
- </div>
297
-
298
- <!-- 拉取结果 -->
299
- <div v-if="pullResult" class="pull-result">
300
- <div class="pull-result-line">
301
- <span class="pull-result-label">已导入:</span>
302
- <n-tag v-for="name in pullResult.imported" :key="name" size="small" type="success" style="margin: 2px">{{ name }}</n-tag>
303
- </div>
304
- <div v-if="pullResult.skipped?.length" class="pull-result-line">
305
- <span class="pull-result-label">跳过:</span>
306
- <n-tag v-for="name in pullResult.skipped" :key="name" size="small" style="margin: 2px">{{ name }}</n-tag>
307
- <span class="pull-result-hint">(无 SKILL.md)</span>
308
- </div>
309
- <div class="preview-actions" style="margin-top: 12px;">
310
- <n-button size="small" type="primary" @click="showPullModal = false">完成</n-button>
311
- </div>
312
- </div>
313
- </div>
314
- </ModalComp>
315
-
316
- <!-- 同源兄弟更新提示 -->
317
- <ModalComp
318
- :show="!!updatedSiblings"
319
- title="同源技能更新"
320
- width="480px"
321
- :mask-closable="true"
322
- @update:show="(v) => { if (!v) updatedSiblings = null; }"
323
- >
324
- <div v-if="updatedSiblings" class="siblings-body">
325
- <p class="siblings-desc">
326
- <strong>{{ updatedSiblings.name }}</strong> 已更新。同仓库还有以下技能,是否一起更新?
327
- </p>
328
- <div class="siblings-list">
329
- <n-checkbox
330
- v-for="name in updatedSiblings.siblings"
331
- :key="name"
332
- v-model:checked="siblingCheck[name]"
333
- >
334
- {{ name }}
335
- </n-checkbox>
336
- </div>
337
- <div class="siblings-actions">
338
- <n-button size="small" @click="updatedSiblings = null">跳过</n-button>
339
- <n-button size="small" type="primary" @click="handleSiblingUpdate" :loading="updatingSiblings">
340
- 更新选中
341
- </n-button>
342
- </div>
343
- </div>
344
- </ModalComp>
345
-
346
- <!-- 全部更新弹窗 -->
347
- <ModalComp
348
- :show="showBatchModal"
349
- :title="batchPhase === 'confirm' ? '批量更新确认' : batchPhase === 'executing' ? '正在更新...' : batchPhase === 'done' ? '更新完成' : '更新失败'"
350
- width="480px"
351
- :mask-closable="batchPhase !== 'executing'"
352
- @update:show="(v) => { if (!v) { showBatchModal = false; if (batchPhase === 'executing') cancelTask(); } }"
353
- >
354
- <div class="batch-modal-body">
355
- <template v-if="batchPhase === 'confirm'">
356
- <p class="batch-desc">确定更新 "{{ batchGroup?.url }}" 下的 {{ batchGroup?.skills?.length }} 个技能吗?</p>
357
- <div class="batch-actions">
358
- <n-button size="small" @click="showBatchModal = false">取消</n-button>
359
- <n-button size="small" type="primary" @click="doBatchUpdate">开始更新</n-button>
360
- </div>
361
- </template>
362
- <template v-if="batchPhase === 'executing'">
363
- <div class="batch-executing">
364
- <n-spin size="small" />
365
- <span>正在更新,请稍候...</span>
366
- </div>
367
- </template>
368
- <template v-if="batchPhase === 'done'">
369
- <div class="batch-result" v-if="batchResult">
370
- <div v-if="batchResult.updated?.length > 0" class="batch-result-line">
371
- <span class="batch-result-label">已更新:</span>
372
- <n-tag v-for="name in batchResult.updated" :key="name" size="small" type="success">{{ name }}</n-tag>
373
- </div>
374
- <div v-if="batchResult.unchanged?.length > 0" class="batch-result-line">
375
- <span class="batch-result-label">已是最新:</span>
376
- <n-tag v-for="name in batchResult.unchanged" :key="name" size="small">{{ name }}</n-tag>
377
- </div>
378
- <div v-if="batchResult.notFound?.length > 0" class="batch-result-line">
379
- <span class="batch-result-label">未找到:</span>
380
- <n-tag v-for="name in batchResult.notFound" :key="name" size="small" type="warning">{{ name }}</n-tag>
381
- </div>
382
- </div>
383
- <div class="batch-actions">
384
- <n-button size="small" type="primary" @click="showBatchModal = false">完成</n-button>
385
- </div>
386
- </template>
387
- <template v-if="batchPhase === 'error'">
388
- <n-alert type="error" :bordered="false">{{ batchError }}</n-alert>
389
- <div class="batch-actions" style="margin-top: 12px;">
390
- <n-button size="small" type="primary" @click="showBatchModal = false">关闭</n-button>
391
- </div>
392
- </template>
393
- </div>
394
- </ModalComp>
395
- </div>
396
- </template>
397
-
398
- <script setup>
399
- import { ref, computed, onMounted, onActivated, onUnmounted, nextTick } from "vue";
400
- import { NIcon, useMessage, createDiscreteApi, darkTheme } from "naive-ui";
401
- import { RefreshOutline, FolderOpenOutline, SearchOutline, PencilOutline, EllipsisHorizontalOutline } from "@vicons/ionicons5";
402
- import { marked } from "marked";
403
- import hljs from "highlight.js";
404
- import SkillCard from "../components/SkillCard.vue";
405
- import ModalComp from "../components/ModalComp.vue";
406
- import { getSkills, getReadme, getSkillReadme, updateSkillMeta, deleteSkill, pullSkills, cancelPull, installSkills, getSkillSource, updateSkill, batchUpdateSkills, openSkillDir, cancelTask, getSourceNames, saveSourceNames, bulkToggleSkills, bulkDeleteSkills } from "../api/skills";
407
- import { sortSkills, getUsageMap, recordUsage } from "../utils/usage";
408
-
409
- // marked 配置:代码高亮 + 外链安全
410
- marked.use({
411
- renderer: {
412
- code({ text, lang }) {
413
- const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
414
- let highlighted;
415
- try {
416
- highlighted = hljs.highlight(text, { language }).value;
417
- } catch {
418
- highlighted = hljs.highlightAuto(text).value;
419
- }
420
- 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>`;
421
- },
422
- link({ href, text }) {
423
- const isExternal =
424
- href && (href.startsWith("http://") || href.startsWith("https://"));
425
- const target = isExternal
426
- ? ' target="_blank" rel="noopener noreferrer"'
427
- : "";
428
- return `<a href="${href}"${target}>${text}</a>`;
429
- },
430
- },
431
- });
432
-
433
- const emit = defineEmits(["refresh"]);
434
- const message = useMessage();
435
-
436
- function getDialog() {
437
- const isDark = document.documentElement.getAttribute("data-theme") === "dark";
438
- return createDiscreteApi(["dialog"], {
439
- configProviderProps: { theme: isDark ? darkTheme : undefined },
440
- }).dialog;
441
- }
442
- const skills = ref([]);
443
- const loading = ref(false);
444
- const needsSetup = ref(false);
445
- const sourceNames = ref({});
446
-
447
- // README
448
- const readmeContent = ref(null);
449
- const renderedReadme = ref("");
450
- const readmeOpen = ref(false);
451
-
452
- // 详情弹窗
453
- const detailVisible = ref(false);
454
- const detailSkill = ref(null);
455
- const detailLoading = ref(false);
456
- const detailContent = ref(null);
457
- const renderedDetail = ref("");
458
-
459
- // 编辑面板
460
- const editVersion = ref("");
461
- const editDescription = ref("");
462
- const editTags = ref([]);
463
- const savingMeta = ref(false);
464
- const originalMeta = ref(null);
465
- const isMetaDirty = computed(() => {
466
- if (!originalMeta.value) return false;
467
- return editVersion.value !== originalMeta.value.version
468
- || editDescription.value !== originalMeta.value.description
469
- || JSON.stringify([...editTags.value].sort()) !== JSON.stringify([...originalMeta.value.tags].sort());
470
- });
471
-
472
- // 分组折叠
473
- const groupOpen = ref({});
474
- const toggleGroup = (key) => {
475
- groupOpen.value[key] = !groupOpen.value[key];
476
- };
477
-
478
- // 技能来源 & 更新
479
- const skillSource = ref(null);
480
- const updatingSkill = ref(false);
481
-
482
- // 同源兄弟提示
483
- const updatedSiblings = ref(null);
484
- const siblingCheck = ref({});
485
- const updatingSiblings = ref(false);
486
-
487
- // 全部更新弹窗
488
- const showBatchModal = ref(false);
489
- const batchPhase = ref("confirm");
490
- const batchGroup = ref(null);
491
- const batchResult = ref(null);
492
- const batchError = ref("");
493
-
494
- async function handleSiblingUpdate() {
495
- const selected = Object.keys(siblingCheck.value).filter((k) => siblingCheck.value[k]);
496
- if (selected.length === 0) {
497
- message.info("未选择任何技能");
498
- updatedSiblings.value = null;
499
- return;
500
- }
501
- updatingSiblings.value = true;
502
- try {
503
- const res = await batchUpdateSkills(selected, updatedSiblings.value.url);
504
- if (res.ok) {
505
- message.success(`${res.updated.length} 个技能已更新`);
506
- await loadSkills();
507
- }
508
- } catch {
509
- message.error("更新失败");
510
- } finally {
511
- updatingSiblings.value = false;
512
- updatedSiblings.value = null;
513
- }
514
- }
515
-
516
- // 别名编辑(分组 header 原地编辑)
517
- const editingAlias = ref(null);
518
- const editAliasValue = ref("");
519
-
520
- // 源管理(拉取弹窗内)
521
- const showSourceManager = ref(false);
522
- const knownUrlsList = ref([]);
523
- const editSourceNames = ref({});
524
- const savingSourceNames = ref(false);
525
-
526
- // 搜索
527
- const searchQuery = ref("");
528
-
529
- // 标签筛选 & 排序
530
- const selectedTags = ref([]);
531
- const sortBy = ref("name");
532
- const sortOptions = [
533
- { label: "按名称", value: "name" },
534
- { label: "按频率", value: "frequency" },
535
- { label: "最近使用", value: "recent" },
536
- ];
537
-
538
-
539
- // 拉取(两阶段:预览 → 选择 → 拉取)
540
- const pullUrl = ref("");
541
- const previewing = ref(false);
542
- const previewDone = ref(false);
543
- const previewSkillList = ref([]);
544
- const pullCheck = ref({});
545
- const pulling = ref(false);
546
- const pullResult = ref(null);
547
- const pullError = ref("");
548
- const showPullModal = ref(false);
549
- const pullSearch = ref("");
550
-
551
- const selectedPullCount = computed(() => {
552
- return Object.keys(pullCheck.value).filter(k => pullCheck.value[k]).length;
553
- });
554
-
555
- const filteredPullSkills = computed(() => {
556
- if (!pullSearch.value) return previewSkillList.value;
557
- const q = pullSearch.value.toLowerCase();
558
- return previewSkillList.value.filter((n) => n.toLowerCase().includes(q));
559
- });
560
-
561
- function openPullModal() {
562
- const savedY = window.scrollY;
563
- showPullModal.value = true;
564
- pullUrl.value = "";
565
- previewDone.value = false;
566
- previewing.value = false;
567
- previewSkillList.value = [];
568
- pullCheck.value = {};
569
- pullResult.value = null;
570
- pullError.value = "";
571
- showSourceManager.value = false;
572
- loadSourceNames();
573
- nextTick(() => window.scrollTo(0, savedY));
574
- }
575
-
576
- function togglePullAll() {
577
- const checked = Object.values(pullCheck.value).some(v => !v);
578
- for (const key of Object.keys(pullCheck.value)) {
579
- pullCheck.value[key] = checked;
580
- }
581
- }
582
-
583
- async function handlePreview() {
584
- if (!pullUrl.value.trim()) {
585
- pullError.value = "请输入仓库地址";
586
- return;
587
- }
588
- previewing.value = true;
589
- previewDone.value = false;
590
- pullResult.value = null;
591
- pullError.value = "";
592
- try {
593
- const res = await pullSkills(pullUrl.value.trim());
594
- if (!showPullModal.value) return;
595
- if (res.ok) {
596
- previewSkillList.value = res.skills || [];
597
- const check = {};
598
- (res.skills || []).forEach(n => { check[n] = true; });
599
- pullCheck.value = check;
600
- previewDone.value = true;
601
- } else {
602
- pullError.value = res.error || "预览失败";
603
- }
604
- } catch (e) {
605
- if (!showPullModal.value) return;
606
- pullError.value = "预览失败,请检查地址或网络";
607
- } finally {
608
- previewing.value = false;
609
- }
610
- }
611
-
612
- async function handlePull() {
613
- const selected = Object.keys(pullCheck.value).filter(k => pullCheck.value[k]);
614
- if (selected.length === 0) {
615
- pullError.value = "请至少选择一个技能";
616
- return;
617
- }
618
- pulling.value = true;
619
- pullError.value = "";
620
- try {
621
- const res = await pullSkills(pullUrl.value.trim(), selected);
622
- if (!showPullModal.value) return;
623
- if (res.ok) {
624
- pullResult.value = { imported: res.imported, skipped: res.skipped || [] };
625
- // 自动安装到项目目录
626
- await installSkills(selected);
627
- message.success(`已拉取并安装 ${selected.length} 个技能`);
628
- // 记录使用
629
- selected.forEach(recordUsage);
630
- await loadSkills();
631
- } else {
632
- pullError.value = res.error || "拉取失败";
633
- }
634
- } catch (e) {
635
- if (!showPullModal.value) return;
636
- pullError.value = "拉取失败,请检查地址或网络";
637
- } finally {
638
- pulling.value = false;
639
- }
640
- }
641
- const existingSkillNames = computed(() => new Set(skills.value.map(s => s.name)));
642
-
643
- const allTags = computed(() => {
644
- const set = new Set();
645
- skills.value.forEach((s) => (s.tags || []).forEach((t) => set.add(t)));
646
- return [...set].sort();
647
- });
648
-
649
- const displaySkills = computed(() => {
650
- let filtered = skills.value;
651
- if (selectedTags.value.length > 0) {
652
- filtered = skills.value.filter((s) =>
653
- selectedTags.value.every((t) => (s.tags || []).includes(t))
654
- );
655
- }
656
- if (searchQuery.value) {
657
- const q = searchQuery.value.toLowerCase();
658
- filtered = filtered.filter((s) =>
659
- s.name.toLowerCase().includes(q)
660
- || (s.description || "").toLowerCase().includes(q)
661
- );
662
- }
663
- return sortSkills(filtered, sortBy.value);
664
- });
665
-
666
- const groupedSkills = computed(() => {
667
- const groups = {};
668
- for (const skill of displaySkills.value) {
669
- const isRemote = skill.source?.type === "remote";
670
- const key = isRemote ? skill.source.url : "__local__";
671
- if (!groups[key]) {
672
- groups[key] = {
673
- key,
674
- type: isRemote ? "remote" : "local",
675
- url: skill.source?.url || "",
676
- skills: [],
677
- };
678
- }
679
- groups[key].skills.push(skill);
680
- }
681
- // 默认展开所有分组
682
- for (const key of Object.keys(groups)) {
683
- if (groupOpen.value[key] === undefined) groupOpen.value[key] = true;
684
- }
685
- // 远程分组排前面,本地排后面
686
- return Object.values(groups).sort((a, b) => {
687
- if (a.type === "remote" && b.type === "local") return -1;
688
- if (a.type === "local" && b.type === "remote") return 1;
689
- return 0;
690
- });
691
- });
692
-
693
- function toggleTag(tag) {
694
- const idx = selectedTags.value.indexOf(tag);
695
- if (idx === -1) selectedTags.value.push(tag);
696
- else selectedTags.value.splice(idx, 1);
697
- }
698
-
699
- function getUsage(name) {
700
- const usage = getUsageMap();
701
- return usage[name]?.count || 0;
702
- }
703
-
704
- /** 去掉 YAML frontmatter(--- 包裹的元数据) */
705
- function stripFrontmatter(text) {
706
- if (!text) return text;
707
- return text.replace(/^---[\s\S]*?---\s*/, "");
708
- }
709
-
710
- /** 渲染 markdown,返回 HTML */
711
- function renderMd(text) {
712
- if (!text) return "";
713
- const body = stripFrontmatter(text);
714
- if (!body.trim()) return "";
715
- try {
716
- return marked.parse(body);
717
- } catch {
718
- return body;
719
- }
720
- }
721
-
722
- async function loadSkills() {
723
- loading.value = true;
724
- needsSetup.value = false;
725
- try {
726
- const res = await getSkills();
727
- if (Array.isArray(res)) {
728
- skills.value = res;
729
- emit("refresh");
730
- } else if (res.error) {
731
- needsSetup.value = true;
732
- skills.value = [];
733
- }
734
- } catch {
735
- skills.value = [];
736
- } finally {
737
- loading.value = false;
738
- }
739
- }
740
-
741
- async function loadReadme() {
742
- try {
743
- const res = await getReadme();
744
- if (res.content) {
745
- renderedReadme.value = renderMd(res.content);
746
- readmeContent.value = res.content;
747
- }
748
- } catch {
749
- // 静默失败
750
- }
751
- }
752
-
753
- async function showSkillDetail(skill) {
754
- const savedY = window.scrollY;
755
- detailSkill.value = skill;
756
- detailVisible.value = true;
757
- detailLoading.value = true;
758
- detailContent.value = null;
759
- renderedDetail.value = "";
760
- skillSource.value = null;
761
- editVersion.value = skill.version || "1.0.0";
762
- editDescription.value = skill.description || "";
763
- editTags.value = [...(skill.tags || [])];
764
- originalMeta.value = {
765
- version: editVersion.value,
766
- description: editDescription.value,
767
- tags: [...editTags.value],
768
- };
769
- nextTick(() => window.scrollTo(0, savedY));
770
- // 查来源
771
- try {
772
- const res = await getSkillSource(skill.name);
773
- skillSource.value = res;
774
- } catch { /* ignore */ }
775
- try {
776
- const res = await getSkillReadme(skill.name);
777
- if (res.content) {
778
- detailContent.value = res.content;
779
- await nextTick();
780
- renderedDetail.value = renderMd(res.content);
781
- }
782
- } catch {
783
- detailContent.value = null;
784
- } finally {
785
- detailLoading.value = false;
786
- }
787
- }
788
-
789
- function handleDetailClose(v) {
790
- if (!v) {
791
- if (isMetaDirty.value) {
792
- getDialog().warning({
793
- title: "未保存的修改",
794
- content: "编辑面板中有未保存的修改,确定关闭吗?",
795
- positiveText: "确定关闭",
796
- negativeText: "取消",
797
- onPositiveClick: () => {
798
- closeDetailModal();
799
- },
800
- });
801
- return;
802
- }
803
- closeDetailModal();
804
- }
805
- }
806
-
807
- function closeDetailModal() {
808
- detailVisible.value = false;
809
- updatingSkill.value = false;
810
- detailLoading.value = false;
811
- cancelTask();
812
- }
813
-
814
- async function handleUpdate() {
815
- if (!detailSkill.value) return;
816
- updatingSkill.value = true;
817
- try {
818
- const res = await updateSkill(detailSkill.value.name);
819
- if (!detailVisible.value) return;
820
- if (res.ok) {
821
- if (res.updated === false) {
822
- message.info("已是最新版本,无需更新");
823
- } else {
824
- message.success("技能已更新到最新版本");
825
- if (res.siblings && res.siblings.length > 0) {
826
- updatedSiblings.value = {
827
- name: detailSkill.value.name,
828
- url: skillSource.value?.url || "",
829
- siblings: res.siblings,
830
- };
831
- const sel = {};
832
- res.siblings.forEach((n) => { sel[n] = true; });
833
- siblingCheck.value = sel;
834
- }
835
- if (res.updatedAt) {
836
- skillSource.value = { type: "remote", url: res.url, pulledAt: res.updatedAt };
837
- }
838
- }
839
- await loadSkills();
840
- } else {
841
- message.error(res.error || "更新失败");
842
- }
843
- } catch {
844
- if (!detailVisible.value) return;
845
- message.error("更新失败");
846
- } finally {
847
- updatingSkill.value = false;
848
- }
849
- }
850
-
851
- async function batchUpdateGroup(group) {
852
- const savedY = window.scrollY;
853
- batchGroup.value = group;
854
- batchResult.value = null;
855
- batchError.value = "";
856
- batchPhase.value = "confirm";
857
- showBatchModal.value = true;
858
- nextTick(() => window.scrollTo(0, savedY));
859
- }
860
-
861
- async function doBatchUpdate() {
862
- batchPhase.value = "executing";
863
- try {
864
- const names = batchGroup.value.skills.map((s) => s.name);
865
- const res = await batchUpdateSkills(names, batchGroup.value.url);
866
- if (res.ok) {
867
- batchResult.value = { updated: res.updated, unchanged: res.unchanged, notFound: res.notFound };
868
- batchPhase.value = "done";
869
- await loadSkills();
870
- } else {
871
- batchError.value = res.error || "批量更新失败";
872
- batchPhase.value = "error";
873
- }
874
- } catch {
875
- batchError.value = "批量更新失败";
876
- batchPhase.value = "error";
877
- }
878
- }
879
-
880
- function openDir() {
881
- if (!detailSkill.value) return;
882
- openSkillDir(detailSkill.value.name);
883
- message.info("已打开技能目录");
884
- }
885
-
886
- async function saveMeta() {
887
- if (!detailSkill.value) return;
888
- savingMeta.value = true;
889
- try {
890
- const res = await updateSkillMeta(detailSkill.value.name, {
891
- version: editVersion.value,
892
- description: editDescription.value,
893
- tags: editTags.value,
894
- });
895
- if (res.ok) {
896
- message.success("技能信息已更新");
897
- detailSkill.value.version = editVersion.value;
898
- detailSkill.value.description = editDescription.value;
899
- detailSkill.value.tags = [...editTags.value];
900
- originalMeta.value = {
901
- version: editVersion.value,
902
- description: editDescription.value,
903
- tags: [...editTags.value],
904
- };
905
- await loadSkills();
906
- }
907
- } catch {
908
- message.error("保存失败");
909
- } finally {
910
- savingMeta.value = false;
911
- }
912
- }
913
-
914
- async function handleDelete() {
915
- if (!detailSkill.value) return;
916
- const ok = window.confirm(`确定要删除技能 "${detailSkill.value.name}" 吗?
917
- 将同时删除源目录和项目目录中的文件。`);
918
- if (!ok) return;
919
- try {
920
- const res = await deleteSkill(detailSkill.value.name);
921
- if (res.ok) {
922
- message.success(`技能 "${detailSkill.value.name}" 已删除`);
923
- detailVisible.value = false;
924
- await loadSkills();
925
- } else if (res.locked) {
926
- message.warning(res.error || `技能 "${detailSkill.value.name}" 被其他程序占用,无法删除`);
927
- }
928
- } catch {
929
- message.error("删除失败");
930
- }
931
- }
932
-
933
- // ---------- P1 新功能 ----------
934
-
935
- // 加载源别名
936
- async function loadSourceNames() {
937
- try {
938
- const res = await getSourceNames();
939
- sourceNames.value = res.names || {};
940
- knownUrlsList.value = res.knownUrls || [];
941
- } catch { /* 静默 */ }
942
- }
943
-
944
- // 切换别名管理面板
945
- function toggleSourceManager() {
946
- showSourceManager.value = !showSourceManager.value;
947
- if (showSourceManager.value) {
948
- editSourceNames.value = { ...sourceNames.value };
949
- loadSourceNames();
950
- }
951
- }
952
-
953
- // 保存别名
954
- async function doSaveSourceNames() {
955
- savingSourceNames.value = true;
956
- try {
957
- const names = {};
958
- for (const [url, alias] of Object.entries(editSourceNames.value)) {
959
- if (alias && alias.trim()) names[url] = alias.trim();
960
- }
961
- const res = await saveSourceNames(names);
962
- if (res.ok) {
963
- sourceNames.value = names;
964
- message.success("别名已保存");
965
- showSourceManager.value = false;
966
- }
967
- } catch {
968
- message.error("保存失败");
969
- } finally {
970
- savingSourceNames.value = false;
971
- }
972
- }
973
-
974
- // 分组 header 别名原地编辑
975
- function startEditAlias(group) {
976
- editingAlias.value = group.key;
977
- editAliasValue.value = sourceNames.value[group.url] || "";
978
- }
979
-
980
- async function saveAlias(group) {
981
- if (!editingAlias.value) return;
982
- const url = group.url;
983
- const alias = editAliasValue.value.trim();
984
- const names = { ...sourceNames.value };
985
- if (alias) {
986
- names[url] = alias;
987
- } else {
988
- delete names[url];
989
- }
990
- editingAlias.value = null;
991
- try {
992
- const res = await saveSourceNames(names);
993
- if (res.ok) sourceNames.value = names;
994
- } catch { /* 静默 */ }
995
- }
996
-
997
- // 分组批量操作菜单
998
- function getBatchMenuOptions(group) {
999
- return [
1000
- { label: "全部启用", key: "enable" },
1001
- { label: "全部禁用", key: "disable" },
1002
- { label: "全部更新", key: "update" },
1003
- { type: "divider" },
1004
- { label: "删除该源所有技能", key: "delete" },
1005
- ];
1006
- }
1007
-
1008
- async function onBatchMenuSelect(key, group) {
1009
- const names = group.skills.map((s) => s.name);
1010
- switch (key) {
1011
- case "enable":
1012
- try {
1013
- const r1 = await bulkToggleSkills(names, true);
1014
- if (r1.locked?.length) {
1015
- message.warning(`已启用 ${names.length - r1.locked.length} 个技能,${r1.locked.length} 个被占用`);
1016
- } else {
1017
- message.success(`${names.length} 个技能已启用`);
1018
- }
1019
- await loadSkills();
1020
- } catch {
1021
- message.error("操作失败");
1022
- }
1023
- break;
1024
- case "disable":
1025
- try {
1026
- const r2 = await bulkToggleSkills(names, false);
1027
- if (r2.locked?.length) {
1028
- message.warning(`已停用 ${names.length - r2.locked.length} 个技能,${r2.locked.length} 个被占用`);
1029
- } else {
1030
- message.success(`${names.length} 个技能已停用`);
1031
- }
1032
- await loadSkills();
1033
- } catch {
1034
- message.error("操作失败");
1035
- }
1036
- break;
1037
- case "update":
1038
- await batchUpdateGroup(group);
1039
- break;
1040
- case "delete":
1041
- getDialog().warning({
1042
- title: "删除确认",
1043
- content: `确定要删除该源下的 ${names.length} 个技能吗?将同时删除源目录和项目目录中的文件。`,
1044
- positiveText: "确定删除",
1045
- negativeText: "取消",
1046
- onPositiveClick: async () => {
1047
- try {
1048
- const r3 = await bulkDeleteSkills(names);
1049
- if (r3.locked?.length) {
1050
- message.warning(`已删除 ${r3.deleted?.length || 0} 个技能,${r3.locked.length} 个被占用`);
1051
- } else {
1052
- message.success(`${names.length} 个技能已删除`);
1053
- }
1054
- await loadSkills();
1055
- } catch {
1056
- message.error("删除失败");
1057
- }
1058
- },
1059
- });
1060
- break;
1061
- }
1062
- }
1063
-
1064
- // 首次加载
1065
- onMounted(() => {
1066
- loadSkills();
1067
- loadReadme();
1068
- loadSourceNames();
1069
- });
1070
-
1071
- // keep-alive 切回来时自动刷新
1072
- onActivated(loadSkills);
1073
-
1074
- // 从其他窗口切回浏览器时自动刷新
1075
- function onFocus() {
1076
- if (document.visibilityState === "visible") {
1077
- loadSkills();
1078
- loadReadme();
1079
- }
1080
- }
1081
- onMounted(() => document.addEventListener("visibilitychange", onFocus));
1082
- onUnmounted(() => document.removeEventListener("visibilitychange", onFocus));
1083
- </script>
1084
-
1085
- <style scoped>
1086
- .dashboard {
1087
- overflow-anchor: auto;
1088
- }
1089
-
1090
- .page-header {
1091
- display: flex;
1092
- align-items: center;
1093
- justify-content: space-between;
1094
- margin-bottom: 4px;
1095
- flex-wrap: wrap;
1096
- gap: 8px;
1097
- }
1098
-
1099
- .page-header h2 {
1100
- font-size: 20px;
1101
- font-weight: 600;
1102
- }
1103
-
1104
- .header-actions {
1105
- display: flex;
1106
- align-items: center;
1107
- gap: 8px;
1108
- }
1109
-
1110
- .tag-filter {
1111
- display: flex;
1112
- align-items: center;
1113
- gap: 6px;
1114
- margin-bottom: 16px;
1115
- flex-wrap: wrap;
1116
- }
1117
-
1118
- .search-bar {
1119
- margin-bottom: 12px;
1120
- }
1121
-
1122
- .detail-status {
1123
- display: flex;
1124
- justify-content: center;
1125
- padding: 40px 0;
1126
- }
1127
-
1128
- .detail-body {
1129
- max-height: 65vh;
1130
- overflow-y: auto;
1131
- }
1132
-
1133
- .edit-panel {
1134
- margin-bottom: 4px;
1135
- }
1136
-
1137
- .edit-row {
1138
- display: flex;
1139
- align-items: flex-start;
1140
- gap: 10px;
1141
- margin-bottom: 10px;
1142
- }
1143
-
1144
- .edit-label {
1145
- width: 50px;
1146
- font-size: 13px;
1147
- color: #888;
1148
- line-height: 30px;
1149
- flex-shrink: 0;
1150
- }
1151
-
1152
- .update-count-hint {
1153
- font-size: 12px;
1154
- color: #e68a00;
1155
- font-family: monospace;
1156
- margin-left: 8px;
1157
- }
1158
-
1159
- .pull-modal-body {
1160
- min-height: 100px;
1161
- }
1162
-
1163
- .pull-status {
1164
- display: flex;
1165
- align-items: center;
1166
- gap: 10px;
1167
- padding: 24px 0;
1168
- justify-content: center;
1169
- color: #666;
1170
- font-size: 13px;
1171
- }
1172
-
1173
- .preview-result {
1174
- margin-top: 8px;
1175
- }
1176
-
1177
- .pull-search-input {
1178
- margin-bottom: 8px;
1179
- }
1180
-
1181
- .preview-info {
1182
- font-size: 13px;
1183
- margin-bottom: 10px;
1184
- color: #555;
1185
- display: flex;
1186
- align-items: center;
1187
- gap: 8px;
1188
- }
1189
-
1190
- .preview-list {
1191
- display: grid;
1192
- grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
1193
- gap: 6px;
1194
- margin-bottom: 14px;
1195
- padding: 10px;
1196
- background: rgba(0, 0, 0, 0.02);
1197
- border-radius: 6px;
1198
- max-height: 300px;
1199
- overflow-y: auto;
1200
- }
1201
-
1202
- .preview-actions {
1203
- display: flex;
1204
- justify-content: flex-end;
1205
- gap: 8px;
1206
- }
1207
-
1208
- .pull-section {
1209
- margin-top: 12px;
1210
- }
1211
-
1212
- .pull-input-row {
1213
- display: flex;
1214
- gap: 8px;
1215
- margin-bottom: 8px;
1216
- }
1217
-
1218
- .pull-input-row .n-input {
1219
- flex: 1;
1220
- }
1221
-
1222
- .pull-error {
1223
- margin-bottom: 8px;
1224
- }
1225
-
1226
- .pull-result {
1227
- margin-top: 8px;
1228
- }
1229
-
1230
- .pull-result-line {
1231
- display: flex;
1232
- align-items: center;
1233
- flex-wrap: wrap;
1234
- gap: 4px;
1235
- margin-bottom: 8px;
1236
- font-size: 13px;
1237
- }
1238
-
1239
- .pull-result-label {
1240
- color: #888;
1241
- flex-shrink: 0;
1242
- }
1243
-
1244
- .pull-result-hint {
1245
- color: #999;
1246
- font-size: 12px;
1247
- }
1248
-
1249
- .install-panel {
1250
- margin-top: 12px;
1251
- padding: 12px;
1252
- background: rgba(0, 0, 0, 0.02);
1253
- border-radius: 6px;
1254
- }
1255
-
1256
- .install-panel-label {
1257
- font-size: 13px;
1258
- margin-bottom: 8px;
1259
- color: #666;
1260
- }
1261
-
1262
- .install-check-list {
1263
- margin-bottom: 10px;
1264
- }
1265
-
1266
- .install-btn {
1267
- float: right;
1268
- }
1269
-
1270
- .edit-actions {
1271
- display: flex;
1272
- justify-content: flex-end;
1273
- gap: 8px;
1274
- }
1275
-
1276
- .alias-edit-trigger {
1277
- display: inline-flex;
1278
- align-items: center;
1279
- cursor: pointer;
1280
- opacity: 0;
1281
- transition: opacity 0.15s;
1282
- vertical-align: middle;
1283
- }
1284
-
1285
- .group-header:hover .alias-edit-trigger {
1286
- opacity: 1;
1287
- }
1288
-
1289
- .alias-edit-icon {
1290
- color: #999;
1291
- }
1292
-
1293
- .alias-edit-inline {
1294
- display: inline-flex;
1295
- align-items: center;
1296
- }
1297
-
1298
- .group-menu-btn {
1299
- flex-shrink: 0;
1300
- }
1301
-
1302
- /* 源别名管理 */
1303
- .source-manager {
1304
- margin-top: 8px;
1305
- padding: 10px;
1306
- background: rgba(0, 0, 0, 0.02);
1307
- border-radius: 6px;
1308
- }
1309
-
1310
- .source-manager-header {
1311
- display: flex;
1312
- align-items: center;
1313
- justify-content: space-between;
1314
- margin-bottom: 8px;
1315
- }
1316
-
1317
- .source-manager-title {
1318
- font-size: 13px;
1319
- font-weight: 600;
1320
- }
1321
-
1322
- .source-manager-list {
1323
- max-height: 240px;
1324
- overflow-y: auto;
1325
- display: flex;
1326
- flex-direction: column;
1327
- gap: 6px;
1328
- }
1329
-
1330
- .source-manager-row {
1331
- display: flex;
1332
- align-items: center;
1333
- gap: 8px;
1334
- }
1335
-
1336
- .source-manager-url {
1337
- font-size: 11px;
1338
- font-family: monospace;
1339
- color: #888;
1340
- flex: 1;
1341
- overflow: hidden;
1342
- text-overflow: ellipsis;
1343
- white-space: nowrap;
1344
- }
1345
-
1346
- .source-manager-empty {
1347
- font-size: 12px;
1348
- color: #999;
1349
- padding: 12px 0;
1350
- text-align: center;
1351
- }
1352
-
1353
- .source-manager-actions {
1354
- display: flex;
1355
- justify-content: flex-end;
1356
- margin-top: 8px;
1357
- }
1358
-
1359
- .installed-tag {
1360
- margin-left: 4px;
1361
- }
1362
-
1363
- /* 分组折叠 */
1364
- .skill-group {
1365
- margin-bottom: 4px;
1366
- }
1367
-
1368
- .group-header {
1369
- display: flex;
1370
- align-items: center;
1371
- gap: 8px;
1372
- padding: 8px 4px;
1373
- cursor: pointer;
1374
- border-radius: 4px;
1375
- user-select: none;
1376
- }
1377
-
1378
- .group-header:hover {
1379
- background: rgba(0, 0, 0, 0.03);
1380
- }
1381
-
1382
- .fold-arrow {
1383
- font-size: 10px;
1384
- transition: transform 0.2s;
1385
- color: #999;
1386
- flex-shrink: 0;
1387
- }
1388
-
1389
- .fold-arrow.open {
1390
- transform: rotate(90deg);
1391
- }
1392
-
1393
- .group-label {
1394
- font-size: 13px;
1395
- font-weight: 600;
1396
- color: inherit;
1397
- overflow: hidden;
1398
- text-overflow: ellipsis;
1399
- white-space: nowrap;
1400
- flex: 1;
1401
- min-width: 0;
1402
- }
1403
-
1404
- .group-count {
1405
- font-size: 12px;
1406
- color: #999;
1407
- flex-shrink: 0;
1408
- }
1409
-
1410
- .group-update-btn {
1411
- flex-shrink: 0;
1412
- }
1413
-
1414
- .group-body {
1415
- display: grid;
1416
- grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
1417
- gap: 8px;
1418
- }
1419
-
1420
- /* 底部 README */
1421
- .readme-fold {
1422
- margin-top: 24px;
1423
- border-top: 1px solid rgba(128, 128, 128, 0.15);
1424
- padding-top: 12px;
1425
- }
1426
- .setup-card {
1427
- margin-bottom: 20px;
1428
- }
1429
-
1430
- .setup-card-content {
1431
- display: flex;
1432
- flex-direction: column;
1433
- align-items: center;
1434
- text-align: center;
1435
- padding: 32px 16px;
1436
- gap: 12px;
1437
- }
1438
-
1439
- .setup-icon {
1440
- margin-bottom: 4px;
1441
- }
1442
-
1443
- .setup-title {
1444
- font-size: 18px;
1445
- font-weight: 600;
1446
- margin: 0;
1447
- }
1448
-
1449
- .setup-desc {
1450
- font-size: 13px;
1451
- color: #666;
1452
- margin: 0;
1453
- max-width: 420px;
1454
- line-height: 1.6;
1455
- }
1456
-
1457
- .setup-desc code {
1458
- font-size: 12px;
1459
- background: rgba(0, 0, 0, 0.06);
1460
- padding: 1px 6px;
1461
- border-radius: 3px;
1462
- }
1463
-
1464
- /* 分组类型视觉区分 */
1465
- .skill-group.group-remote {
1466
- border-left: 3px solid #2080f0;
1467
- padding-left: 8px;
1468
- margin-bottom: 12px;
1469
- }
1470
-
1471
- .skill-group.group-local {
1472
- border-left: 3px solid #18a058;
1473
- padding-left: 8px;
1474
- margin-bottom: 12px;
1475
- }
1476
-
1477
- .group-indicator {
1478
- display: inline-block;
1479
- width: 6px;
1480
- height: 6px;
1481
- border-radius: 50%;
1482
- flex-shrink: 0;
1483
- }
1484
-
1485
- .indicator-remote {
1486
- background: #2080f0;
1487
- }
1488
-
1489
- .indicator-local {
1490
- background: #18a058;
1491
- }
1492
-
1493
- /* 来源信息 */
1494
- .source-info {
1495
- margin-bottom: 4px;
1496
- }
1497
-
1498
- .source-row {
1499
- display: flex;
1500
- align-items: center;
1501
- gap: 10px;
1502
- margin-bottom: 6px;
1503
- font-size: 13px;
1504
- }
1505
-
1506
- .source-label {
1507
- width: 70px;
1508
- color: #888;
1509
- flex-shrink: 0;
1510
- }
1511
-
1512
- .source-value {
1513
- word-break: break-all;
1514
- }
1515
-
1516
- .local-tag {
1517
- display: inline-block;
1518
- padding: 1px 8px;
1519
- border-radius: 3px;
1520
- font-size: 12px;
1521
- background: #e8f5e9;
1522
- color: #2e7d32;
1523
- }
1524
-
1525
- .path-text {
1526
- font-family: monospace;
1527
- font-size: 12px;
1528
- color: #666;
1529
- }
1530
-
1531
- .source-actions {
1532
- display: flex;
1533
- gap: 8px;
1534
- margin-top: 8px;
1535
- }
1536
-
1537
- .local-hint {
1538
- margin-top: 8px;
1539
- font-size: 12px;
1540
- color: #999;
1541
- line-height: 1.5;
1542
- padding: 8px;
1543
- background: rgba(0, 0, 0, 0.02);
1544
- border-radius: 4px;
1545
- }
1546
-
1547
- /* 同源兄弟 */
1548
- .siblings-body {
1549
- min-height: 60px;
1550
- }
1551
-
1552
- .siblings-desc {
1553
- font-size: 13px;
1554
- margin-bottom: 12px;
1555
- line-height: 1.6;
1556
- }
1557
-
1558
- .siblings-list {
1559
- display: flex;
1560
- flex-direction: column;
1561
- gap: 8px;
1562
- margin-bottom: 16px;
1563
- }
1564
-
1565
- .siblings-actions {
1566
- display: flex;
1567
- justify-content: flex-end;
1568
- gap: 8px;
1569
- }
1570
-
1571
- /* 全部更新弹窗 */
1572
- .batch-modal-body {
1573
- min-height: 60px;
1574
- }
1575
-
1576
- .batch-desc {
1577
- font-size: 13px;
1578
- line-height: 1.6;
1579
- margin-bottom: 16px;
1580
- }
1581
-
1582
- .batch-actions {
1583
- display: flex;
1584
- justify-content: flex-end;
1585
- gap: 8px;
1586
- }
1587
-
1588
- .batch-executing {
1589
- display: flex;
1590
- align-items: center;
1591
- gap: 12px;
1592
- padding: 24px 0;
1593
- justify-content: center;
1594
- color: #666;
1595
- font-size: 13px;
1596
- }
1597
-
1598
- .batch-result {
1599
- margin-bottom: 16px;
1600
- }
1601
-
1602
- .batch-result-line {
1603
- display: flex;
1604
- align-items: center;
1605
- flex-wrap: wrap;
1606
- gap: 6px;
1607
- margin-bottom: 10px;
1608
- font-size: 13px;
1609
- }
1610
-
1611
- .batch-result-label {
1612
- color: #888;
1613
- flex-shrink: 0;
1614
- }
1615
- </style>
1616
-
1617
- <style>
1618
- [data-theme="dark"] .edit-label,
1619
- [data-theme="dark"] .source-label,
1620
- [data-theme="dark"] .pull-result-label,
1621
- [data-theme="dark"] .batch-result-label {
1622
- color: #9399b2 !important;
1623
- }
1624
- [data-theme="dark"] .update-count-hint {
1625
- color: #fbbf24;
1626
- }
1627
- [data-theme="dark"] .pull-status,
1628
- [data-theme="dark"] .batch-executing,
1629
- [data-theme="dark"] .install-panel-label {
1630
- color: #a6adc8;
1631
- }
1632
- [data-theme="dark"] .preview-info {
1633
- color: #a6adc8;
1634
- }
1635
- [data-theme="dark"] .pull-result-hint,
1636
- [data-theme="dark"] .group-count,
1637
- [data-theme="dark"] .local-hint {
1638
- color: #6c7086;
1639
- }
1640
- [data-theme="dark"] .path-text {
1641
- color: #a6adc8;
1642
- }
1643
- [data-theme="dark"] .preview-list,
1644
- [data-theme="dark"] .install-panel,
1645
- [data-theme="dark"] .local-hint {
1646
- background: rgba(255, 255, 255, 0.03);
1647
- }
1648
- [data-theme="dark"] .group-header:hover {
1649
- background: rgba(255, 255, 255, 0.04);
1650
- }
1651
- [data-theme="dark"] .local-tag {
1652
- background: rgba(74, 222, 128, 0.15);
1653
- color: #4ade80;
1654
- }
1655
- [data-theme="dark"] .skill-group.group-remote {
1656
- border-left-color: #6a8cff;
1657
- }
1658
- [data-theme="dark"] .skill-group.group-local {
1659
- border-left-color: #4ade80;
1660
- }
1661
- [data-theme="dark"] .indicator-remote {
1662
- background: #6a8cff;
1663
- }
1664
- [data-theme="dark"] .indicator-local {
1665
- background: #4ade80;
1666
- }
1667
- [data-theme="dark"] .setup-desc {
1668
- color: #a6adc8;
1669
- }
1670
- [data-theme="dark"] .setup-desc code {
1671
- background: rgba(255, 255, 255, 0.08);
1672
- }
1673
- [data-theme="dark"] .readme-fold {
1674
- border-top-color: rgba(255, 255, 255, 0.08);
1675
- }
1676
- [data-theme="dark"] .source-manager {
1677
- background: rgba(255, 255, 255, 0.03);
1678
- }
1679
- [data-theme="dark"] .source-manager-url {
1680
- color: #6c7086;
1681
- }
1682
- [data-theme="dark"] .alias-edit-icon {
1683
- color: #6c7086;
1684
- }
1685
- [data-theme="dark"] .source-value {
1686
- color: #cdd6f4 !important;
1687
- }
1688
- [data-theme="dark"] .siblings-desc {
1689
- color: #cdd6f4 !important;
1690
- }
1691
- [data-theme="dark"] .batch-desc {
1692
- color: #cdd6f4 !important;
1693
- }
1694
- [data-theme="dark"] .source-manager-title {
1695
- color: #e4e4ef !important;
1696
- }
1697
- </style>