Files
AlphaPulse/web/src/pages/skills/index.vue
T
2026-10-15 18:44:46 +08:00

173 lines
6.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
import type { SkillNode, SkillVersion } from "@/apis/skills"
import { listSkillTree, listSkillVersions, patchSkillNode, publishSkillVersion, saveSkillDraft } from "@/apis/skills"
import SkillTree from "@/components/SkillTree.vue"
import SkillVersionDrawer from "@/components/SkillVersionDrawer.vue"
import { renderMarkdown } from "@/utils/markdown"
const nodes = ref<SkillNode[]>([])
const selectedId = ref<number | null>(null)
const selected = computed(() => nodes.value.find(n => n.id === selectedId.value) ?? null)
// 编辑区:以最新版本为底稿
const latest = ref<SkillVersion | null>(null)
const content = ref("")
const dirty = computed(() => selected.value?.kind === "skill" && content.value !== (latest.value?.content ?? ""))
const saving = ref(false)
const historyVisible = ref(false)
const preview = computed(() => renderMarkdown(content.value))
async function loadTree() {
nodes.value = await listSkillTree()
if (selectedId.value !== null && !selected.value)
selectedId.value = null
}
async function loadContent() {
if (selected.value?.kind !== "skill") {
latest.value = null
content.value = ""
return
}
latest.value = (await listSkillVersions(selected.value.id))[0] ?? null
content.value = latest.value?.content ?? ""
}
async function select(node: SkillNode) {
if (node.id === selectedId.value)
return
if (dirty.value)
await ElMessageBox.confirm("当前 skill 有未保存的修改,切换后会丢失。继续?", "未保存", { type: "warning" })
selectedId.value = node.id
await loadContent()
}
async function saveDraft(): Promise<SkillVersion | null> {
if (!selected.value || !dirty.value)
return latest.value
saving.value = true
try {
latest.value = await saveSkillDraft(selected.value.id, content.value)
ElMessage.success(`已保存草稿 v${latest.value.version_no}`)
await loadTree()
return latest.value
} finally {
saving.value = false
}
}
async function publishLatest() {
const version = await saveDraft()
if (!version || !selected.value)
return
await ElMessageBox.confirm(`发布 v${version.version_no}?发布后 Chat 下一轮起按此版本判断。`, "发布确认", { type: "warning" })
await publishSkillVersion(selected.value.id, version.id)
ElMessage.success(`已发布 v${version.version_no}`)
await loadTree()
}
async function updateMeta(body: { priority?: number, enabled?: boolean }) {
if (!selected.value)
return
await patchSkillNode(selected.value.id, body)
await loadTree()
}
async function onPublished() {
historyVisible.value = false
await loadTree()
}
// 离开页面前提醒未保存的修改
function beforeUnload(e: BeforeUnloadEvent) {
if (dirty.value)
e.preventDefault()
}
onMounted(() => window.addEventListener("beforeunload", beforeUnload))
onBeforeUnmount(() => window.removeEventListener("beforeunload", beforeUnload))
onBeforeRouteLeave(async () => {
if (dirty.value)
await ElMessageBox.confirm("当前 skill 有未保存的修改,离开后会丢失。继续?", "未保存", { type: "warning" })
})
loadTree()
</script>
<template>
<div class="h-full flex gap-3">
<el-card class="w-80 shrink-0" body-class="h-full box-border" shadow="never">
<SkillTree :nodes="nodes" :selected-id="selectedId" editable @select="select" @changed="loadTree" />
</el-card>
<el-card class="min-w-0 flex flex-1 flex-col" body-class="min-h-0 flex-1 flex flex-col gap-3" shadow="never">
<el-empty v-if="!selected" description="在左侧选择或新建一个 skill" />
<template v-else-if="selected.kind === 'folder'">
<div class="text-lg font-bold">
📁 {{ selected.path }}
</div>
<div class="flex items-center gap-2 text-sm">
启用
<el-switch :model-value="selected.enabled" @change="v => updateMeta({ enabled: Boolean(v) })" />
<span class="text-xs text-[var(--el-text-color-secondary)]">停用文件夹后,其中所有 skill 都不会进入 Chat 的判断</span>
</div>
</template>
<template v-else>
<div class="flex flex-wrap items-center gap-3">
<span class="text-lg font-bold">📄 {{ selected.path }}</span>
<el-tag v-if="selected.published_version_no === null" type="info">
未发布
</el-tag>
<el-tag v-else type="success">
已发布 v{{ selected.published_version_no }}
</el-tag>
<el-tag v-if="selected.has_draft" type="warning">
最新 v{{ selected.latest_version_no }} 未发布
</el-tag>
<el-tag v-if="dirty" type="danger">
有未保存的修改
</el-tag>
<div class="ml-auto flex items-center gap-2 text-sm">
优先级
<el-input-number
:model-value="selected.priority" :min="-100" :max="100" size="small" controls-position="right" class="w-24"
@change="v => v !== undefined && v !== null && updateMeta({ priority: v })"
/>
启用
<el-switch :model-value="selected.enabled" @change="v => updateMeta({ enabled: Boolean(v) })" />
</div>
</div>
<div class="text-xs text-[var(--el-text-color-secondary)]">
优先级越高越先读取,冲突时以优先级高的为准。只有「已发布」的版本会进入 Chat 的判断;保存只生成草稿。
</div>
<div class="min-h-0 flex flex-1 gap-3">
<textarea
v-model="content" placeholder="用 Markdown 写下投资经验、判断流程或输出规范"
class="box-border flex-1 resize-none border rounded border-solid border-[var(--el-border-color)] p-3 text-sm leading-6 font-mono outline-none focus:border-[var(--el-color-primary)]"
/>
<el-scrollbar class="flex-1 border rounded border-solid border-[var(--el-border-color-lighter)]">
<!-- markdown-it 已关闭 html,内容中的标签按文本显示 -->
<div class="markdown-body p-3" v-html="preview" />
</el-scrollbar>
</div>
<div class="flex gap-2">
<el-button :disabled="!dirty" :loading="saving" @click="saveDraft">
保存草稿
</el-button>
<el-button type="primary" :disabled="!dirty && !selected.has_draft" :loading="saving" @click="publishLatest">
{{ dirty ? "保存并发布" : "发布最新版本" }}
</el-button>
<el-button @click="historyVisible = true">
版本历史 / 回退
</el-button>
</div>
</template>
</el-card>
<SkillVersionDrawer v-model="historyVisible" :skill="selected" @published="onPublished" />
</div>
</template>