Files
geo/apps/admin-web/src/views/KolGenerateView.vue
T
root 8991edabb5
Frontend CI / Frontend (push) Successful in 2m47s
Backend CI / Backend (push) Failing after 6m50s
feat(kol): support multiple platform hints with shared tag renderer
Turn the KOL prompt platform hint into a multi-select backed by the
media platforms API, persist hints as a 、-joined string, and add a
reusable KolPlatformTags component so manage/generate/package/template/
workspace views and the generate-task drawer all render the hints
consistently with truncation and overflow handling.

Also auto-select the first available subscription prompt in
GenerateTaskDrawer when switching to the subscription-prompt target so
users land on a usable option without an extra click.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:48:04 +08:00

413 lines
10 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 setup lang="ts">
import KnowledgeGroupSelect from '@/components/KnowledgeGroupSelect.vue'
import KolDynamicForm from '@/components/kol/KolDynamicForm.vue'
import KolPlatformTags from '@/components/kol/KolPlatformTags.vue'
import { kolGenerateApi } from '@/lib/api'
import { formatError } from '@/lib/errors'
import { parseKolCardConfig } from '@/lib/kol-card-config'
import {
getKolVariableInitialValue,
isKolVariableValueEmpty,
normalizeKolVariableDefinition,
} from '@/lib/kol-placeholders'
import { useCompanyStore } from '@/stores/company'
import { LeftOutlined } from '@ant-design/icons-vue'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { message } from 'ant-design-vue'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const queryClient = useQueryClient()
const { t } = useI18n()
const companyStore = useCompanyStore()
const subscriptionPromptId = computed(() => String(route.params.subscriptionPromptId))
const sourcePage = computed<'workspace' | 'templates' | null>(() => {
if (route.query.source === 'workspace') {
return 'workspace'
}
if (route.query.source === 'templates') {
return 'templates'
}
return null
})
function resolveReturnRoute(): { name: 'workspace' } | { name: 'articles-templates' } {
if (sourcePage.value === 'workspace') {
return { name: 'workspace' }
}
return { name: 'articles-templates' }
}
const schemaQuery = useQuery({
queryKey: ['kol', 'subscription-prompt', 'schema', subscriptionPromptId],
queryFn: () => kolGenerateApi.schema(subscriptionPromptId.value),
enabled: computed(() => !!subscriptionPromptId.value),
})
const values = ref<Record<string, any>>({})
const enableWebSearch = ref(false)
const selectedKnowledgeGroupIds = ref<number[]>([])
const schema = computed(() => schemaQuery.data.value)
const cardConfig = computed(() => parseKolCardConfig(schema.value?.card_config_json))
const allowWebSearch = computed(() => cardConfig.value.allow_web_search)
const allowUserKnowledge = computed(() => cardConfig.value.allow_user_knowledge)
function fieldKey(key?: string | null, id?: string): string {
return key?.trim() || id || ''
}
// Initialize values from schema
watch(
() => schemaQuery.data.value,
(schema) => {
if (schema?.schema_json?.variables) {
const newValues: Record<string, any> = {}
schema.schema_json.variables.forEach((v) => {
newValues[fieldKey(v.key, v.id)] = getKolVariableInitialValue(
normalizeKolVariableDefinition(v),
)
})
values.value = newValues
enableWebSearch.value = false
selectedKnowledgeGroupIds.value = []
}
},
{ immediate: true },
)
const generateMutation = useMutation({
mutationFn: (variables: Record<string, any>) =>
kolGenerateApi.submit(subscriptionPromptId.value, {
variables,
enable_web_search: allowWebSearch.value ? enableWebSearch.value : undefined,
knowledge_group_ids: allowUserKnowledge.value ? selectedKnowledgeGroupIds.value : undefined,
}),
onSuccess: async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['articles'] }),
queryClient.invalidateQueries({ queryKey: ['workspace'] }),
])
message.info(t('templates.wizard.messages.queued'))
await router.replace(resolveReturnRoute())
},
onError: (error: any) => {
message.error(formatError(error) || t('common.noData'))
},
})
function handleBack() {
void router.push(resolveReturnRoute())
}
function handleSubmit() {
const schemaValue = schemaQuery.data.value
if (!schemaValue?.schema_json?.variables) return
if (!companyStore.currentBrandId) {
message.warning(t('templates.wizard.messages.selectBrandBeforeQuestion'))
return
}
const missingLabels: string[] = []
schemaValue.schema_json.variables.forEach((v) => {
const normalizedVariable = normalizeKolVariableDefinition(v)
if (
normalizedVariable.required &&
isKolVariableValueEmpty(normalizedVariable, values.value[fieldKey(v.key, v.id)])
) {
missingLabels.push(v.label)
}
})
if (missingLabels.length > 0) {
message.warning(`${t('kol.generate.fillVariables')}: ${missingLabels.join(', ')}`)
return
}
generateMutation.mutate(values.value)
}
</script>
<template>
<div class="wizard-page">
<div v-if="schemaQuery.isPending.value" class="loading-state">
<a-spin size="large" />
</div>
<template v-else-if="schema">
<header class="wizard-page__header">
<div class="header-left">
<a-button type="text" shape="circle" @click="handleBack">
<template #icon><LeftOutlined /></template>
</a-button>
<div class="header-title-box">
<h2 class="header-title">
{{ schema.package_name }}
<span class="divider">/</span>
{{ schema.prompt_name }}
</h2>
<KolPlatformTags
v-if="schema.platform_hint"
:value="schema.platform_hint"
size="small"
:truncate="false"
class="platform-badge"
/>
<p class="header-eyebrow">{{ t('kol.generate.fillVariables') }}</p>
</div>
</div>
</header>
<main class="wizard-page__main">
<section class="wizard-section">
<div class="wizard-card">
<div class="wizard-card__header" style="margin-bottom: 24px">
<div>
<h3>{{ t('kol.generate.fillVariables') }}</h3>
<p>根据设定提供相关信息</p>
</div>
</div>
<div v-if="allowWebSearch || allowUserKnowledge" class="capability-card">
<div v-if="allowWebSearch" class="capability-row">
<div class="capability-copy">
<div class="capability-label">联网搜索</div>
<div class="capability-hint">按需启用生成时可参考实时网页信息</div>
</div>
<a-switch
v-model:checked="enableWebSearch"
:disabled="generateMutation.isPending.value"
/>
</div>
<div v-if="allowUserKnowledge" class="capability-knowledge">
<label class="capability-label">知识库引用</label>
<KnowledgeGroupSelect
v-model="selectedKnowledgeGroupIds"
:disabled="generateMutation.isPending.value"
placeholder="可选,选择你自己的知识库分组辅助生成"
/>
</div>
</div>
<KolDynamicForm
v-model="values"
:variables="schema.schema_json.variables"
class="form-body"
/>
</div>
</section>
</main>
<footer class="wizard-page__footer">
<div></div>
<div class="footer-actions">
<a-button
type="primary"
:loading="generateMutation.isPending.value"
@click="handleSubmit"
>
{{ t('kol.generate.submit') }}
</a-button>
</div>
</footer>
</template>
</div>
</template>
<style scoped>
.wizard-page {
display: flex;
flex-direction: column;
min-height: calc(100vh - 120px);
background: #fff;
}
.loading-state {
display: flex;
justify-content: center;
padding: 100px;
}
.wizard-page__header {
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 32px;
border-bottom: 1px solid #edf2f7;
background: #fff;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.header-title-box {
display: flex;
flex-direction: column;
gap: 4px;
}
.header-title {
margin: 0;
color: #111827;
font-size: 20px;
font-weight: 800;
display: flex;
align-items: center;
}
.divider {
margin: 0 8px;
color: #d9d9d9;
font-weight: 400;
}
.platform-badge {
margin-top: 2px;
}
.header-eyebrow {
margin: 0;
color: #6d7c94;
font-size: 13px;
}
.wizard-page__main {
display: flex;
flex-direction: column;
margin: 0 auto;
width: 100%;
max-width: 860px;
padding: 24px 32px 64px;
}
.wizard-section {
display: flex;
flex-direction: column;
}
.wizard-card {
padding: 32px 0;
}
.wizard-card__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.wizard-card__header h3 {
margin: 0;
color: #111827;
font-size: 16px;
font-weight: 800;
display: flex;
align-items: center;
}
.wizard-card__header h3::before {
content: '';
display: inline-block;
width: 4px;
height: 16px;
border-radius: 2px;
background: #111827;
margin-right: 12px;
}
.wizard-card__header p {
margin: 6px 0 0 16px;
color: #667085;
font-size: 13px;
line-height: 1.7;
}
.capability-card {
margin-bottom: 32px;
padding: 20px;
border: 1px solid #e6edf5;
border-radius: 12px;
background: #fafcff;
}
.capability-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.capability-knowledge {
margin-top: 20px;
}
.capability-copy {
min-width: 0;
}
.capability-label {
display: block;
margin-bottom: 8px;
color: #111827;
font-size: 14px;
font-weight: 600;
}
.capability-hint {
color: #667085;
font-size: 13px;
line-height: 1.5;
}
.wizard-page__footer {
position: sticky;
bottom: 0;
z-index: 100;
margin-top: auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-top: 1px solid rgba(20, 44, 88, 0.08);
backdrop-filter: blur(14px);
background: rgba(255, 255, 255, 0.88);
}
.footer-actions {
display: flex;
gap: 12px;
}
@media (max-width: 768px) {
.wizard-page__main {
padding: 16px;
}
.wizard-card {
padding: 18px 0;
}
.wizard-page__footer {
flex-direction: column;
gap: 12px;
align-items: stretch;
}
.footer-actions {
width: 100%;
justify-content: flex-end;
}
}
</style>