Files
geo/apps/admin-web/src/components/PromptRuleModal.vue
T
root b31d8d0096 feat(migrations): Harden task audit tracking and optimize article templates
- Added migration to harden task audit tracking by modifying audit_logs and related tables.
- Introduced operator_id to several tables for better tracking of actions.
- Updated article_templates with new prompt templates for various article types, enhancing content generation.
- Created prompt_rules and schedule_tasks tables to manage content generation rules and scheduling.
- Added foreign key constraints to articles for better data integrity.
2026-04-02 00:31:28 +08:00

297 lines
7.8 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 { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
import { message } from "ant-design-vue";
import type { PromptRule, PromptRuleGroup } from "@geo/shared-types";
import { computed, reactive, watch } from "vue";
import { useI18n } from "vue-i18n";
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,
});
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;
},
);
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,
}),
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,
}),
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>
<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>