b31d8d0096
- 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.
297 lines
7.8 KiB
Vue
297 lines
7.8 KiB
Vue
<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>
|