Files
geo/apps/admin-web/src/components/PromptRuleModal.vue
T
root 162abdc97c
Backend CI / Backend (push) Has been cancelled
Frontend CI / Frontend (push) Failing after 1m39s
chore(frontend): introduce prettier + eslint and prune unused code
- Add Prettier 3 with prettier-plugin-organize-imports (sorts/removes unused imports)
- Add ESLint 10 flat config with typescript-eslint + eslint-plugin-vue + eslint-config-prettier
- Add root scripts: format, format:check, lint, lint:fix
- Reformat 257 files across admin-web, ops-web, desktop-client, packages
- Remove unused locals/exports flagged by --noUnusedLocals/--noUnusedParameters
- Fix duplicate localTabLabel key, surrogate-pair regex u-flag, NBSP literal in regex
- Skip server/ (Go) and apps/browser-extension/ (deprecated per ADR)

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

312 lines
8.4 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 { 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'
import KnowledgeGroupSelect from '@/components/KnowledgeGroupSelect.vue'
import { promptRulesApi } from '@/lib/api'
import { formatError } from '@/lib/errors'
const props = defineProps<{
open: boolean
rule?: PromptRule | null
}>()
const emit = defineEmits<{
'update:open': [value: boolean]
saved: [ruleId: number]
}>()
const queryClient = useQueryClient()
const { t } = useI18n()
const form = reactive({
name: '',
prompt_content: '',
scene: '',
default_tone: '',
default_word_count: undefined as number | undefined,
group_id: undefined as number | undefined,
knowledge_group_ids: [] as number[],
})
const groupsQuery = useQuery({
queryKey: ['promptRules', 'groups'],
queryFn: () => promptRulesApi.listGroups(),
})
watch(
() => props.open,
(open) => {
if (!open) {
return
}
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 ?? []
},
)
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,
knowledge_group_ids: form.knowledge_group_ids,
}),
onSuccess: async (data) => {
message.success(t('custom.messages.ruleCreated'))
emit('saved', data.id)
emit('update:open', false)
await queryClient.invalidateQueries({ queryKey: ['promptRules'] })
},
onError: (error) => message.error(formatError(error)),
})
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,
knowledge_group_ids: form.knowledge_group_ids,
}),
onSuccess: async () => {
message.success(t('custom.messages.ruleUpdated'))
if (props.rule?.id) {
emit('saved', props.rule.id)
}
emit('update:open', false)
await queryClient.invalidateQueries({ queryKey: ['promptRules'] })
},
onError: (error) => message.error(formatError(error)),
})
const submitLoading = computed(
() => createMutation.isPending.value || updateMutation.isPending.value,
)
const drawerTitle = computed(() =>
props.rule?.id ? t('custom.promptRule.editTitle') : t('custom.promptRule.createTitle'),
)
async function handleSubmit(): Promise<void> {
if (!form.name.trim()) {
message.warning(t('custom.messages.missingPromptName'))
return
}
if (!form.prompt_content.trim()) {
message.warning(t('custom.messages.missingPromptContent'))
return
}
if (props.rule?.id) {
await updateMutation.mutateAsync()
return
}
await createMutation.mutateAsync()
}
</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">
{{ t('custom.promptRule.name') }}
</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">
{{ t('custom.promptRule.content') }}
</label>
<a-textarea
v-model:value="form.prompt_content"
:rows="12"
:placeholder="t('custom.promptRule.contentPlaceholder')"
/>
</div>
<div class="prompt-rule-drawer__field">
<label class="prompt-rule-drawer__label">{{ t('custom.group.title') }}</label>
<a-select
v-model:value="form.group_id"
size="large"
style="width: 100%"
:placeholder="t('custom.promptRule.groupPlaceholder')"
:options="
(groupsQuery.data.value ?? []).map((group: PromptRuleGroup) => ({
label: group.name,
value: group.id,
}))
"
allow-clear
/>
</div>
<div class="prompt-rule-drawer__grid">
<div class="prompt-rule-drawer__field">
<label class="prompt-rule-drawer__label">{{ t('custom.promptRule.scene') }}</label>
<a-input v-model:value="form.scene" size="large" />
</div>
<div class="prompt-rule-drawer__field">
<label class="prompt-rule-drawer__label">{{ t('custom.promptRule.tone') }}</label>
<a-input v-model:value="form.default_tone" size="large" />
</div>
<div class="prompt-rule-drawer__field">
<label class="prompt-rule-drawer__label">
{{ t('custom.promptRule.wordCount') }}
<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>
<div class="prompt-rule-drawer__field">
<label class="prompt-rule-drawer__label">知识库引用</label>
<KnowledgeGroupSelect
v-model="form.knowledge_group_ids"
placeholder="可选,用于补充生成内容的背景信息"
/>
</div>
</div>
<template #footer>
<div class="prompt-rule-drawer__footer">
<a-button size="large" @click="emit('update:open', false)">
{{ t('common.cancel') }}
</a-button>
<a-button type="primary" size="large" :loading="submitLoading" @click="handleSubmit">
{{ t('common.confirm') }}
</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 {
content: '*';
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>