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:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeftOutlined } from "@ant-design/icons-vue";
|
||||
import { ArrowLeftOutlined, CopyOutlined } from "@ant-design/icons-vue";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import dayjs from "dayjs";
|
||||
import type {
|
||||
@@ -194,6 +194,29 @@ function goBack(): void {
|
||||
});
|
||||
}
|
||||
|
||||
function openExternalURL(url: string | null | undefined): void {
|
||||
const normalized = String(url ?? "").trim();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
window.open(normalized, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
|
||||
function openImitationCreate(sourceURL: string | null | undefined, sourceTitle?: string | null): void {
|
||||
const normalizedURL = String(sourceURL ?? "").trim();
|
||||
if (!normalizedURL) {
|
||||
return;
|
||||
}
|
||||
|
||||
void router.push({
|
||||
name: "article-imitation-create",
|
||||
query: {
|
||||
source_url: normalizedURL,
|
||||
source_title: String(sourceTitle ?? "").trim() || undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function formatPercent(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined) {
|
||||
return "--";
|
||||
@@ -1025,10 +1048,11 @@ const citationAnalysisColumns = computed(() => [
|
||||
]);
|
||||
|
||||
const contentCitationColumns = computed(() => [
|
||||
{ title: t("tracking.columns.contentName"), key: "contentName", dataIndex: "contentName", width: "40%" },
|
||||
{ title: t("tracking.columns.contentName"), key: "contentName", dataIndex: "contentName", width: "38%" },
|
||||
{ title: t("tracking.columns.channelName"), key: "channelName", dataIndex: "channelName", width: "20%", align: "center" as const },
|
||||
{ title: t("tracking.columns.citations"), key: "citationCount", dataIndex: "citationCount", align: "center" as const, width: "20%" },
|
||||
{ title: t("tracking.columns.share"), key: "citationRate", dataIndex: "citationRate", align: "center" as const, width: "20%" },
|
||||
{ title: t("tracking.columns.citations"), key: "citationCount", dataIndex: "citationCount", align: "center" as const, width: "16%" },
|
||||
{ title: t("tracking.columns.share"), key: "citationRate", dataIndex: "citationRate", align: "center" as const, width: "16%" },
|
||||
{ title: t("common.actions"), key: "actions", align: "center" as const, width: "10%" },
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -1138,15 +1162,16 @@ const contentCitationColumns = computed(() => [
|
||||
|
||||
<div ref="citationSourcesScrollRef" class="tracking-question-card__scroll-area custom-scrollbar">
|
||||
<div v-if="activePlatform?.citations?.length" class="tracking-question-source-list">
|
||||
<a
|
||||
<div
|
||||
v-for="(citation, index) in activePlatform.citations"
|
||||
:key="citation.cited_url"
|
||||
:href="citation.cited_url"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
class="tracking-question-source-card"
|
||||
:class="{ 'is-linked': highlightedCitationIndex === index + 1 }"
|
||||
:data-citation-index="index + 1"
|
||||
@click="openExternalURL(citation.cited_url)"
|
||||
@keydown.enter="openExternalURL(citation.cited_url)"
|
||||
>
|
||||
<div class="tracking-question-source-card__headline">
|
||||
<div class="tracking-question-source-card__indexes">
|
||||
@@ -1168,6 +1193,15 @@ const contentCitationColumns = computed(() => [
|
||||
<strong>{{ citation.site_name }}</strong>
|
||||
<span class="tracking-question-source-card__divider" v-if="resolveCitationTitle(citation)">·</span>
|
||||
<span class="tracking-question-source-card__title" :title="resolveCitationTitle(citation)">{{ resolveCitationTitle(citation) }}</span>
|
||||
<a-tooltip :title="t('tracking.imitationAction')">
|
||||
<button
|
||||
type="button"
|
||||
class="tracking-question-imitation-trigger tracking-question-imitation-trigger--source"
|
||||
@click.stop="openImitationCreate(citation.cited_url, resolveCitationTitle(citation))"
|
||||
>
|
||||
<CopyOutlined />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
<a-tag
|
||||
v-if="citation.article_id && supportsContentCitationDisplay(activePlatform?.ai_platform_id)"
|
||||
color="blue"
|
||||
@@ -1179,7 +1213,7 @@ const contentCitationColumns = computed(() => [
|
||||
<div class="tracking-question-source-card__link">
|
||||
{{ citation.cited_url }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<a-empty v-else :description="t('tracking.noCitations')" />
|
||||
</div>
|
||||
@@ -1279,6 +1313,18 @@ const contentCitationColumns = computed(() => [
|
||||
<template v-else-if="column.key === 'citationRate'">
|
||||
<strong class="metric-highlight">{{ formatPercent(record.citationRate) }}</strong>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-tooltip v-if="record.citedURL" :title="t('tracking.imitationAction')">
|
||||
<button
|
||||
type="button"
|
||||
class="tracking-question-imitation-trigger tracking-question-imitation-trigger--table-action"
|
||||
@click.stop="openImitationCreate(record.citedURL, record.contentName)"
|
||||
>
|
||||
<CopyOutlined />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
<span v-else class="tracking-question-table__empty-action">--</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
@@ -1546,6 +1592,7 @@ const contentCitationColumns = computed(() => [
|
||||
|
||||
.tracking-question-source-card,
|
||||
.tracking-question-content-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
@@ -1553,6 +1600,7 @@ const contentCitationColumns = computed(() => [
|
||||
border: 1px solid #edf2f7;
|
||||
border-radius: 10px;
|
||||
background: #f8fbff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -1563,6 +1611,10 @@ const contentCitationColumns = computed(() => [
|
||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.05);
|
||||
}
|
||||
|
||||
.tracking-question-source-card {
|
||||
padding-right: 58px;
|
||||
}
|
||||
|
||||
.tracking-question-source-card.is-linked {
|
||||
border-color: #93c5fd;
|
||||
background: #eef6ff;
|
||||
@@ -1649,6 +1701,62 @@ const contentCitationColumns = computed(() => [
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tracking-question-imitation-trigger {
|
||||
display: inline-flex;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
color: #2563eb;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transform: translateY(1px);
|
||||
transition:
|
||||
opacity 0.18s ease,
|
||||
transform 0.18s ease,
|
||||
border-color 0.18s ease,
|
||||
background-color 0.18s ease;
|
||||
}
|
||||
|
||||
.tracking-question-imitation-trigger:hover {
|
||||
border-color: #93c5fd;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.tracking-question-imitation-trigger--source {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 16px;
|
||||
transform: translateY(calc(-50% + 1px));
|
||||
}
|
||||
|
||||
.tracking-question-source-card:hover .tracking-question-imitation-trigger,
|
||||
.tracking-question-source-card:focus-within .tracking-question-imitation-trigger {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.tracking-question-source-card:hover .tracking-question-imitation-trigger--source,
|
||||
.tracking-question-source-card:focus-within .tracking-question-imitation-trigger--source {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.tracking-question-imitation-trigger--table-action {
|
||||
margin: 0 auto;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.tracking-question-table__empty-action {
|
||||
color: #cbd5e1;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tracking-question-source-card__link {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
|
||||
Reference in New Issue
Block a user