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:
@@ -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>
|
||||
Reference in New Issue
Block a user