feat(kol): three-pane prompt editor + KolManageView UI

This commit is contained in:
2026-04-17 14:08:34 +08:00
parent d5f7cdfc5f
commit 6eb663bd6e
9 changed files with 1319 additions and 7 deletions
@@ -0,0 +1,219 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
import { message } from "ant-design-vue";
import { SaveOutlined, SendOutlined, EyeOutlined } from "@ant-design/icons-vue";
import KolVariablePanel from "./KolVariablePanel.vue";
import KolVariableConfig from "./KolVariableConfig.vue";
import KolPromptEditArea from "./KolPromptEditArea.vue";
import KolDynamicForm from "./KolDynamicForm.vue";
import { kolManageApi } from "@/lib/api";
import type { KolVariableDefinition, KolSchemaJson } from "@geo/shared-types";
import { formatError } from "@/lib/errors";
const props = defineProps<{
promptId: number;
}>();
const { t } = useI18n();
const queryClient = useQueryClient();
const content = ref("");
const variables = ref<KolVariableDefinition[]>([]);
const previewOpen = ref(false);
const previewValues = ref<Record<string, any>>({});
const aiTaskId = ref<string | null>(null);
const { data: promptDetail, isPending } = useQuery({
queryKey: computed(() => ["kol", "prompt", props.promptId]),
queryFn: () => kolManageApi.getPrompt(props.promptId),
});
watch(
() => promptDetail.value,
(detail) => {
if (detail) {
content.value = detail.latest_prompt_content ?? "";
variables.value = detail.latest_schema_json?.variables ?? [];
}
},
{ immediate: true }
);
const saveDraftMutation = useMutation({
mutationFn: (data: { content: string; variables: KolVariableDefinition[] }) =>
kolManageApi.saveDraft(props.promptId, {
content: data.content,
schema: { variables: data.variables },
card_config: {},
}),
onSuccess: () => {
message.success(t("common.save") + "成功");
queryClient.invalidateQueries({ queryKey: ["kol", "prompt", props.promptId] });
},
onError: (error) => {
message.error(formatError(error));
},
});
const publishMutation = useMutation({
mutationFn: () => kolManageApi.publishPrompt(props.promptId),
onSuccess: () => {
message.success(t("kol.manage.editor.publish") + "成功");
queryClient.invalidateQueries({ queryKey: ["kol", "prompt", props.promptId] });
},
onError: (error) => {
message.error(formatError(error));
},
});
const { data: assistTask } = useQuery({
queryKey: computed(() => ["kol", "assist", aiTaskId.value]),
queryFn: () => kolManageApi.getAssist(aiTaskId.value!),
enabled: computed(() => !!aiTaskId.value),
refetchInterval: (query) => {
const status = query.state.data?.status;
return status === "completed" || status === "failed" ? false : 2000;
},
});
watch(
() => assistTask.value,
(task) => {
if (task?.status === "completed" && task.result) {
content.value = task.result.content;
variables.value = task.result.schema.variables;
aiTaskId.value = null;
message.success("AI 处理完成");
} else if (task?.status === "failed") {
aiTaskId.value = null;
message.error(task.error_message || "AI 处理失败");
}
}
);
async function handleAiGenerate(description: string) {
try {
const res = await kolManageApi.submitAssist({
mode: "generate",
description,
prompt_id: props.promptId,
});
aiTaskId.value = res.task_id;
} catch (error) {
message.error(formatError(error));
}
}
async function handleAiOptimize() {
try {
const res = await kolManageApi.submitAssist({
mode: "optimize",
current_content: content.value,
schema: { variables: variables.value },
prompt_id: props.promptId,
});
aiTaskId.value = res.task_id;
} catch (error) {
message.error(formatError(error));
}
}
function handleSave() {
saveDraftMutation.mutate({ content: content.value, variables: variables.value });
}
function handlePublish() {
publishMutation.mutate();
}
function handlePreview() {
previewValues.value = {};
previewOpen.value = true;
}
</script>
<template>
<div class="kol-prompt-editor">
<div class="editor-header">
<div class="header-title">
<span class="prompt-name">{{ promptDetail?.name || 'Loading...' }}</span>
</div>
<div class="header-actions">
<a-button @click="handlePreview">
<template #icon><EyeOutlined /></template>
{{ t('kol.manage.editor.preview') }}
</a-button>
<a-button :loading="saveDraftMutation.isPending.value" @click="handleSave">
<template #icon><SaveOutlined /></template>
{{ t('kol.manage.editor.saveDraft') }}
</a-button>
<a-button type="primary" :loading="publishMutation.isPending.value" @click="handlePublish">
<template #icon><SendOutlined /></template>
{{ t('kol.manage.editor.publish') }}
</a-button>
</div>
</div>
<div class="editor-body">
<KolVariablePanel />
<KolPromptEditArea
v-model:content="content"
v-model:variables="variables"
:ai-task-id="aiTaskId"
@ai-generate="handleAiGenerate"
@ai-optimize="handleAiOptimize"
/>
<KolVariableConfig v-model:variables="variables" />
</div>
<a-modal
v-model:open="previewOpen"
:title="t('kol.manage.editor.preview')"
:footer="null"
width="600px"
>
<KolDynamicForm
:variables="variables"
v-model:modelValue="previewValues"
/>
</a-modal>
</div>
</template>
<style scoped>
.kol-prompt-editor {
height: 100%;
display: flex;
flex-direction: column;
}
.editor-header {
height: 56px;
padding: 0 16px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
}
.prompt-name {
font-size: 16px;
font-weight: 500;
}
.header-actions {
display: flex;
gap: 12px;
}
.editor-body {
flex: 1;
display: flex;
overflow: hidden;
background: #f0f0f0;
}
</style>