2026-04-02 00:31:28 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-05-01 20:39:09 +08:00
|
|
|
|
import { InfoCircleOutlined } from '@ant-design/icons-vue'
|
|
|
|
|
|
import type { PromptRule, PromptRuleGroup } from '@geo/shared-types'
|
|
|
|
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
|
|
|
|
|
import { message } from 'ant-design-vue'
|
|
|
|
|
|
import { computed, reactive, watch } from 'vue'
|
|
|
|
|
|
import { useI18n } from 'vue-i18n'
|
2026-04-02 00:31:28 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
import KnowledgeGroupSelect from '@/components/KnowledgeGroupSelect.vue'
|
|
|
|
|
|
import { promptRulesApi } from '@/lib/api'
|
|
|
|
|
|
import { formatError } from '@/lib/errors'
|
2026-05-20 18:09:53 +08:00
|
|
|
|
import { useCompanyStore } from '@/stores/company'
|
2026-04-02 00:31:28 +08:00
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
2026-05-01 20:39:09 +08:00
|
|
|
|
open: boolean
|
|
|
|
|
|
rule?: PromptRule | null
|
|
|
|
|
|
}>()
|
2026-04-02 00:31:28 +08:00
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
2026-05-01 20:39:09 +08:00
|
|
|
|
'update:open': [value: boolean]
|
|
|
|
|
|
saved: [ruleId: number]
|
|
|
|
|
|
}>()
|
2026-04-02 00:31:28 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const queryClient = useQueryClient()
|
|
|
|
|
|
const { t } = useI18n()
|
2026-05-20 18:09:53 +08:00
|
|
|
|
const companyStore = useCompanyStore()
|
2026-04-02 00:31:28 +08:00
|
|
|
|
|
|
|
|
|
|
const form = reactive({
|
2026-05-01 20:39:09 +08:00
|
|
|
|
name: '',
|
|
|
|
|
|
prompt_content: '',
|
|
|
|
|
|
scene: '',
|
|
|
|
|
|
default_tone: '',
|
2026-04-02 00:31:28 +08:00
|
|
|
|
default_word_count: undefined as number | undefined,
|
|
|
|
|
|
group_id: undefined as number | undefined,
|
2026-04-05 17:14:13 +08:00
|
|
|
|
knowledge_group_ids: [] as number[],
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-04-02 00:31:28 +08:00
|
|
|
|
|
|
|
|
|
|
const groupsQuery = useQuery({
|
2026-05-20 18:09:53 +08:00
|
|
|
|
queryKey: computed(() => ['promptRules', 'groups', companyStore.currentBrandId]),
|
|
|
|
|
|
enabled: computed(() => props.open && Boolean(companyStore.currentBrandId)),
|
2026-04-02 00:31:28 +08:00
|
|
|
|
queryFn: () => promptRulesApi.listGroups(),
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-04-02 00:31:28 +08:00
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.open,
|
|
|
|
|
|
(open) => {
|
|
|
|
|
|
if (!open) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-02 00:31:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const rule = props.rule
|
|
|
|
|
|
form.name = rule?.name ?? ''
|
|
|
|
|
|
form.prompt_content = rule?.prompt_content ?? ''
|
|
|
|
|
|
form.scene = rule?.scene ?? ''
|
|
|
|
|
|
form.default_tone = rule?.default_tone ?? ''
|
|
|
|
|
|
form.default_word_count = rule?.default_word_count ?? undefined
|
|
|
|
|
|
form.group_id = rule?.group_id ?? undefined
|
|
|
|
|
|
form.knowledge_group_ids = rule?.knowledge_group_ids ?? []
|
2026-04-02 00:31:28 +08:00
|
|
|
|
},
|
2026-05-01 20:39:09 +08:00
|
|
|
|
)
|
2026-04-02 00:31:28 +08:00
|
|
|
|
|
2026-05-20 18:09:53 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => companyStore.currentBrandId,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
if (props.open) {
|
|
|
|
|
|
emit('update:open', false)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-04-02 00:31:28 +08:00
|
|
|
|
const createMutation = useMutation({
|
|
|
|
|
|
mutationFn: () =>
|
|
|
|
|
|
promptRulesApi.create({
|
|
|
|
|
|
group_id: form.group_id ?? null,
|
|
|
|
|
|
name: form.name.trim(),
|
|
|
|
|
|
prompt_content: form.prompt_content.trim(),
|
|
|
|
|
|
scene: form.scene.trim() || undefined,
|
|
|
|
|
|
default_tone: form.default_tone.trim() || undefined,
|
|
|
|
|
|
default_word_count: form.default_word_count,
|
2026-04-05 17:14:13 +08:00
|
|
|
|
knowledge_group_ids: form.knowledge_group_ids,
|
2026-04-02 00:31:28 +08:00
|
|
|
|
}),
|
|
|
|
|
|
onSuccess: async (data) => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
message.success(t('custom.messages.ruleCreated'))
|
|
|
|
|
|
emit('saved', data.id)
|
|
|
|
|
|
emit('update:open', false)
|
|
|
|
|
|
await queryClient.invalidateQueries({ queryKey: ['promptRules'] })
|
2026-04-02 00:31:28 +08:00
|
|
|
|
},
|
|
|
|
|
|
onError: (error) => message.error(formatError(error)),
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-04-02 00:31:28 +08:00
|
|
|
|
|
|
|
|
|
|
const updateMutation = useMutation({
|
|
|
|
|
|
mutationFn: () =>
|
|
|
|
|
|
promptRulesApi.update(props.rule!.id, {
|
|
|
|
|
|
group_id: form.group_id ?? null,
|
|
|
|
|
|
name: form.name.trim(),
|
|
|
|
|
|
prompt_content: form.prompt_content.trim(),
|
|
|
|
|
|
scene: form.scene.trim() || undefined,
|
|
|
|
|
|
default_tone: form.default_tone.trim() || undefined,
|
|
|
|
|
|
default_word_count: form.default_word_count,
|
2026-04-05 17:14:13 +08:00
|
|
|
|
knowledge_group_ids: form.knowledge_group_ids,
|
2026-04-02 00:31:28 +08:00
|
|
|
|
}),
|
|
|
|
|
|
onSuccess: async () => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
message.success(t('custom.messages.ruleUpdated'))
|
2026-04-02 00:31:28 +08:00
|
|
|
|
if (props.rule?.id) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
emit('saved', props.rule.id)
|
2026-04-02 00:31:28 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
emit('update:open', false)
|
|
|
|
|
|
await queryClient.invalidateQueries({ queryKey: ['promptRules'] })
|
2026-04-02 00:31:28 +08:00
|
|
|
|
},
|
|
|
|
|
|
onError: (error) => message.error(formatError(error)),
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-04-02 00:31:28 +08:00
|
|
|
|
|
|
|
|
|
|
const submitLoading = computed(
|
|
|
|
|
|
() => createMutation.isPending.value || updateMutation.isPending.value,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
)
|
2026-04-02 00:31:28 +08:00
|
|
|
|
|
|
|
|
|
|
const drawerTitle = computed(() =>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
props.rule?.id ? t('custom.promptRule.editTitle') : t('custom.promptRule.createTitle'),
|
|
|
|
|
|
)
|
2026-04-02 00:31:28 +08:00
|
|
|
|
|
|
|
|
|
|
async function handleSubmit(): Promise<void> {
|
|
|
|
|
|
if (!form.name.trim()) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
message.warning(t('custom.messages.missingPromptName'))
|
|
|
|
|
|
return
|
2026-04-02 00:31:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!form.prompt_content.trim()) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
message.warning(t('custom.messages.missingPromptContent'))
|
|
|
|
|
|
return
|
2026-04-02 00:31:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (props.rule?.id) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
await updateMutation.mutateAsync()
|
|
|
|
|
|
return
|
2026-04-02 00:31:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
await createMutation.mutateAsync()
|
2026-04-02 00:31:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<a-drawer
|
|
|
|
|
|
:open="open"
|
|
|
|
|
|
:title="drawerTitle"
|
|
|
|
|
|
placement="right"
|
|
|
|
|
|
width="860"
|
|
|
|
|
|
:mask-closable="false"
|
|
|
|
|
|
class="prompt-rule-drawer"
|
|
|
|
|
|
@close="emit('update:open', false)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="prompt-rule-drawer__body">
|
|
|
|
|
|
<div class="prompt-rule-drawer__field">
|
|
|
|
|
|
<label class="prompt-rule-drawer__label prompt-rule-drawer__label--required">
|
2026-05-01 20:39:09 +08:00
|
|
|
|
{{ t('custom.promptRule.name') }}:
|
2026-04-02 00:31:28 +08:00
|
|
|
|
</label>
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="form.name"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
:maxlength="100"
|
|
|
|
|
|
:placeholder="t('custom.promptRule.namePlaceholder')"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="prompt-rule-drawer__field">
|
|
|
|
|
|
<label class="prompt-rule-drawer__label prompt-rule-drawer__label--required">
|
2026-05-01 20:39:09 +08:00
|
|
|
|
{{ t('custom.promptRule.content') }}:
|
2026-04-02 00:31:28 +08:00
|
|
|
|
</label>
|
|
|
|
|
|
<a-textarea
|
|
|
|
|
|
v-model:value="form.prompt_content"
|
|
|
|
|
|
:rows="12"
|
|
|
|
|
|
:placeholder="t('custom.promptRule.contentPlaceholder')"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="prompt-rule-drawer__field">
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<label class="prompt-rule-drawer__label">{{ t('custom.group.title') }}:</label>
|
2026-04-02 00:31:28 +08:00
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="form.group_id"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
:placeholder="t('custom.promptRule.groupPlaceholder')"
|
2026-05-01 20:39:09 +08:00
|
|
|
|
:options="
|
|
|
|
|
|
(groupsQuery.data.value ?? []).map((group: PromptRuleGroup) => ({
|
|
|
|
|
|
label: group.name,
|
|
|
|
|
|
value: group.id,
|
|
|
|
|
|
}))
|
|
|
|
|
|
"
|
2026-04-02 00:31:28 +08:00
|
|
|
|
allow-clear
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="prompt-rule-drawer__grid">
|
|
|
|
|
|
<div class="prompt-rule-drawer__field">
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<label class="prompt-rule-drawer__label">{{ t('custom.promptRule.scene') }}:</label>
|
2026-04-02 00:31:28 +08:00
|
|
|
|
<a-input v-model:value="form.scene" size="large" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="prompt-rule-drawer__field">
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<label class="prompt-rule-drawer__label">{{ t('custom.promptRule.tone') }}:</label>
|
2026-04-02 00:31:28 +08:00
|
|
|
|
<a-input v-model:value="form.default_tone" size="large" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="prompt-rule-drawer__field">
|
|
|
|
|
|
<label class="prompt-rule-drawer__label">
|
2026-05-01 20:39:09 +08:00
|
|
|
|
{{ t('custom.promptRule.wordCount') }}:
|
2026-04-02 00:31:28 +08:00
|
|
|
|
<a-tooltip :title="t('custom.promptRule.wordCountHint')">
|
|
|
|
|
|
<InfoCircleOutlined style="margin-left: 6px; color: #8c8c8c; cursor: help" />
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
v-model:value="form.default_word_count"
|
|
|
|
|
|
:min="100"
|
|
|
|
|
|
:max="6000"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-05 17:14:13 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="prompt-rule-drawer__field">
|
|
|
|
|
|
<label class="prompt-rule-drawer__label">知识库引用:</label>
|
|
|
|
|
|
<KnowledgeGroupSelect
|
|
|
|
|
|
v-model="form.knowledge_group_ids"
|
|
|
|
|
|
placeholder="可选,用于补充生成内容的背景信息"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-04-02 00:31:28 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<div class="prompt-rule-drawer__footer">
|
|
|
|
|
|
<a-button size="large" @click="emit('update:open', false)">
|
2026-05-01 20:39:09 +08:00
|
|
|
|
{{ t('common.cancel') }}
|
2026-04-02 00:31:28 +08:00
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button type="primary" size="large" :loading="submitLoading" @click="handleSubmit">
|
2026-05-01 20:39:09 +08:00
|
|
|
|
{{ t('common.confirm') }}
|
2026-04-02 00:31:28 +08:00
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-drawer>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.prompt-rule-drawer :deep(.ant-drawer-header) {
|
|
|
|
|
|
padding: 30px 28px;
|
|
|
|
|
|
border-bottom: 1px solid #edf2f7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.prompt-rule-drawer :deep(.ant-drawer-title) {
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.prompt-rule-drawer :deep(.ant-drawer-body) {
|
|
|
|
|
|
padding: 30px 32px 32px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.prompt-rule-drawer :deep(.ant-drawer-footer) {
|
|
|
|
|
|
padding: 18px 28px;
|
|
|
|
|
|
border-top: 1px solid #edf2f7;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.prompt-rule-drawer__body {
|
|
|
|
|
|
padding: 0 20px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.prompt-rule-drawer__field {
|
|
|
|
|
|
margin-top: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.prompt-rule-drawer__field:first-child {
|
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.prompt-rule-drawer__grid .prompt-rule-drawer__field {
|
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.prompt-rule-drawer__field :deep(.ant-input),
|
|
|
|
|
|
.prompt-rule-drawer__field :deep(.ant-input-number),
|
|
|
|
|
|
.prompt-rule-drawer__field :deep(.ant-select-selector),
|
|
|
|
|
|
.prompt-rule-drawer__field :deep(textarea.ant-input) {
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.prompt-rule-drawer__label {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.prompt-rule-drawer__label--required::before {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
content: '*';
|
2026-04-02 00:31:28 +08:00
|
|
|
|
margin-right: 4px;
|
|
|
|
|
|
color: #ff4d4f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.prompt-rule-drawer__grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
gap: 24px 18px;
|
|
|
|
|
|
margin-top: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.prompt-rule-drawer__footer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 820px) {
|
|
|
|
|
|
.prompt-rule-drawer__grid {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|