feat(imitation): add article imitation create flow

Introduce 仿写创作: new list and create views, backend imitation service
and prompt templates, worker task routing for imitation jobs, and one-click
rewrite triggers from question citation sources. Surface source article
URL/title on article list/detail, and restrict failed articles to delete-only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 21:36:44 +08:00
parent 46d0a7aea0
commit 40d9a6cc63
29 changed files with 2678 additions and 120 deletions
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { CopyOutlined } from "@ant-design/icons-vue";
import { CopyOutlined, LinkOutlined } from "@ant-design/icons-vue";
import { useQuery, useQueryClient } from "@tanstack/vue-query";
import { message } from "ant-design-vue";
import type { TableColumnsType } from "ant-design-vue";
@@ -122,6 +122,8 @@ const generateMeta = computed(() =>
getGenerateStatusMeta(streamStatus.value || detail.value?.generate_status),
);
const publishMeta = computed(() => getPublishStatusMeta(detail.value?.publish_status));
const sourceArticleTitle = computed(() => detail.value?.source_title?.trim() || "");
const sourceArticleURL = computed(() => detail.value?.source_url?.trim() || "");
function handleClose(): void {
activeTab.value = "content";
@@ -284,6 +286,27 @@ async function copyPublishLink(record: PublishRecord): Promise<void> {
<a-descriptions-item :label="t('article.meta.wordCount')">
{{ detail.word_count || "--" }}
</a-descriptions-item>
<a-descriptions-item
v-if="sourceArticleURL"
:label="t('article.meta.sourceArticle')"
:span="2"
>
<a
class="article-drawer__source-link"
:href="sourceArticleURL"
target="_blank"
rel="noopener noreferrer"
:title="sourceArticleURL"
>
<LinkOutlined />
<span class="article-drawer__source-copy">
<span v-if="sourceArticleTitle" class="article-drawer__source-title">
{{ sourceArticleTitle }}
</span>
<span class="article-drawer__source-url">{{ sourceArticleURL }}</span>
</span>
</a>
</a-descriptions-item>
</a-descriptions>
</section>
@@ -423,6 +446,37 @@ async function copyPublishLink(record: PublishRecord): Promise<void> {
margin-top: 24px;
}
.article-drawer__source-link {
display: inline-flex;
max-width: 100%;
align-items: center;
gap: 6px;
color: #1677ff;
}
.article-drawer__source-copy {
display: flex;
min-width: 0;
flex-direction: column;
gap: 4px;
}
.article-drawer__source-title,
.article-drawer__source-url {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.article-drawer__source-title {
color: #1f2937;
font-weight: 500;
}
.article-drawer__source-url {
color: #1677ff;
}
.article-drawer__alert {
margin-top: 24px;
}
@@ -3,6 +3,7 @@ import {
AppstoreOutlined,
CalendarOutlined,
ClockCircleOutlined,
CopyOutlined,
EditOutlined,
TeamOutlined,
ThunderboltOutlined,
@@ -24,7 +25,7 @@ const sourceLabel = computed(() =>
const createdAtLabel = computed(() => formatDateTime(props.createdAt));
const sourceVariant = computed<
"template" | "custom" | "instant" | "schedule" | "free-create" | "kol" | "default"
"template" | "custom" | "instant" | "schedule" | "free-create" | "imitation" | "kol" | "default"
>(() => {
if (props.sourceType === "template") {
return "template";
@@ -32,6 +33,9 @@ const sourceVariant = computed<
if (props.sourceType === "free_create") {
return "free-create";
}
if (props.sourceType === "imitation") {
return "imitation";
}
if (props.sourceType === "kol") {
return "kol";
}
@@ -59,6 +63,8 @@ const badgeIcon = computed<Component>(() => {
return ThunderboltOutlined;
case "free-create":
return EditOutlined;
case "imitation":
return CopyOutlined;
case "kol":
return TeamOutlined;
default:
@@ -133,6 +139,12 @@ const badgeIcon = computed<Component>(() => {
color: #18794e;
}
.article-source-meta__badge--imitation {
border: 1px solid #bfdbfe;
background: linear-gradient(180deg, #f8fbff 0%, #e6f0ff 100%);
color: #1d4ed8;
}
.article-source-meta__badge--kol {
border: 1px solid #d6e4ff;
background: linear-gradient(180deg, #f5f8ff 0%, #eaf1ff 100%);
@@ -508,13 +508,6 @@ function normalizePlatformUid(value?: string | null): string {
</div>
</section>
<a-alert
type="info"
show-icon
message="发布调度规则"
description="客户端在线时会立即开始消费;客户端离线时也允许提交,任务会先写入 SaaS 队列,客户端恢复在线后继续执行。"
/>
<section class="publish-modal__section">
<div class="publish-modal__section-header">
<h3><span class="required-star">*</span> 目标账号</h3>