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.
This commit is contained in:
2026-04-02 00:31:28 +08:00
parent de30497f59
commit b31d8d0096
101 changed files with 16671 additions and 721 deletions
@@ -0,0 +1,274 @@
<script setup lang="ts">
import {
BoldOutlined,
ItalicOutlined,
OrderedListOutlined,
PicCenterOutlined,
PictureOutlined,
RedoOutlined,
UndoOutlined,
UnorderedListOutlined,
} from "@ant-design/icons-vue";
import { redoCommand, undoCommand } from "@milkdown/kit/plugin/history";
import {
insertHrCommand,
insertImageCommand,
toggleEmphasisCommand,
toggleStrongCommand,
wrapInBlockquoteCommand,
wrapInBulletListCommand,
wrapInHeadingCommand,
wrapInOrderedListCommand,
} from "@milkdown/kit/preset/commonmark";
import { callCommand } from "@milkdown/kit/utils";
import { Crepe, CrepeFeature } from "@milkdown/crepe";
import "@milkdown/crepe/theme/common/style.css";
import "@milkdown/crepe/theme/frame.css";
import { Milkdown, useEditor, useInstance } from "@milkdown/vue";
import { computed, onBeforeUnmount, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{
title: string;
modelValue: string;
disabled?: boolean;
}>();
const emit = defineEmits<{
"update:title": [value: string];
"update:modelValue": [value: string];
}>();
const { t } = useI18n();
const crepeRef = ref<Crepe | null>(null);
const { loading } = useEditor((root) => {
const crepe = new Crepe({
root,
defaultValue: props.modelValue || "",
features: {
[CrepeFeature.TopBar]: false,
[CrepeFeature.Toolbar]: true,
[CrepeFeature.Latex]: false,
[CrepeFeature.BlockEdit]: false,
},
});
crepe.setReadonly(!!props.disabled);
crepe.on((listener) => {
listener.markdownUpdated((_ctx, markdown) => {
emit("update:modelValue", markdown);
});
});
crepeRef.value = crepe;
return crepe;
});
const [instanceLoading, getEditor] = useInstance();
const editorDisabled = computed(() => props.disabled || instanceLoading.value || loading.value);
watch(
() => props.disabled,
(value) => {
crepeRef.value?.setReadonly(!!value);
},
{ immediate: true },
);
onBeforeUnmount(() => {
crepeRef.value = null;
});
function runCommand(command: { key: unknown }, payload?: unknown): void {
if (editorDisabled.value) {
return;
}
const editor = getEditor();
if (!editor) {
return;
}
editor.action(callCommand(command.key as never, payload as never));
}
function insertImage(): void {
if (editorDisabled.value) {
return;
}
const src = window.prompt(t("article.editor.imagePrompt"));
if (!src?.trim()) {
return;
}
runCommand(insertImageCommand, { src: src.trim() });
}
</script>
<template>
<div class="article-editor-canvas" :class="{ 'article-editor-canvas--disabled': disabled }">
<div class="article-editor-canvas__toolbar">
<a-button size="small" type="text" @click="runCommand(undoCommand)">
<template #icon><UndoOutlined /></template>
</a-button>
<a-button size="small" type="text" @click="runCommand(redoCommand)">
<template #icon><RedoOutlined /></template>
</a-button>
<span class="article-editor-canvas__divider"></span>
<a-button size="small" type="text" @click="runCommand(wrapInHeadingCommand, 1)">H1</a-button>
<a-button size="small" type="text" @click="runCommand(wrapInHeadingCommand, 2)">H2</a-button>
<a-button size="small" type="text" @click="runCommand(wrapInHeadingCommand, 3)">H3</a-button>
<span class="article-editor-canvas__divider"></span>
<a-button size="small" type="text" @click="runCommand(toggleStrongCommand)">
<template #icon><BoldOutlined /></template>
</a-button>
<a-button size="small" type="text" @click="runCommand(toggleEmphasisCommand)">
<template #icon><ItalicOutlined /></template>
</a-button>
<a-button size="small" type="text" @click="runCommand(wrapInBlockquoteCommand)">
</a-button>
<a-button size="small" type="text" @click="runCommand(wrapInBulletListCommand)">
<template #icon><UnorderedListOutlined /></template>
</a-button>
<a-button size="small" type="text" @click="runCommand(wrapInOrderedListCommand)">
<template #icon><OrderedListOutlined /></template>
</a-button>
<a-button size="small" type="text" @click="runCommand(insertHrCommand)">
<template #icon><PicCenterOutlined /></template>
</a-button>
<a-button size="small" type="text" @click="insertImage">
<template #icon><PictureOutlined /></template>
</a-button>
</div>
<div class="article-editor-canvas__surface">
<div class="article-editor-canvas__title-row">
<a-input
:value="title"
size="large"
:disabled="disabled"
:bordered="false"
class="article-editor-canvas__title-input"
:placeholder="t('article.editor.titlePlaceholder')"
@update:value="emit('update:title', String($event ?? ''))"
/>
</div>
<Milkdown />
<div v-if="loading" class="article-editor-canvas__loading">
<a-skeleton active :paragraph="{ rows: 10 }" />
</div>
<div v-if="disabled" class="article-editor-canvas__mask">
{{ t("article.editor.messages.locked") }}
</div>
</div>
</div>
</template>
<style scoped>
.article-editor-canvas {
position: relative;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
border: 1px solid #edf2fa;
border-radius: 24px;
overflow: hidden;
background:
radial-gradient(circle at top right, rgba(64, 110, 255, 0.08), transparent 20%),
linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
}
.article-editor-canvas__toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
padding: 14px 18px;
border-bottom: 1px solid #eef2f8;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(12px);
}
.article-editor-canvas__divider {
width: 1px;
height: 20px;
margin: 0 2px;
background: #e5ebf5;
}
.article-editor-canvas__surface {
position: relative;
flex: 1;
min-height: 0;
overflow-y: auto;
}
.article-editor-canvas__title-row {
position: relative;
z-index: 1;
padding: 18px 28px 0;
}
.article-editor-canvas__title-row :deep(.ant-input) {
height: 68px;
padding: 0;
border: 0;
background: transparent;
box-shadow: none;
color: #101828;
font-size: 30px !important;
font-weight: 800;
line-height: 1.15;
}
.article-editor-canvas__title-row :deep(.ant-input::placeholder) {
color: #98a2b3;
}
.article-editor-canvas__loading {
position: absolute;
inset: 0;
z-index: 2;
padding: 24px;
background: rgba(255, 255, 255, 0.92);
}
/* Removed height 100% blocks to allow natural vertical content growth and scrolling */
.article-editor-canvas__surface :deep(.ProseMirror) {
min-height: 640px;
padding: 14px 36px 80px;
background: transparent;
}
.article-editor-canvas__surface :deep(.ProseMirror:focus) {
outline: none;
}
.article-editor-canvas__mask {
position: absolute;
inset: 0;
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 120px;
color: #475467;
font-size: 14px;
background: rgba(255, 255, 255, 0.58);
backdrop-filter: blur(3px);
}
.article-editor-canvas--disabled .article-editor-canvas__toolbar {
opacity: 0.56;
}
.article-editor-canvas--disabled .article-editor-canvas__title-row {
opacity: 0.72;
}
</style>