feat(admin-web): preserve template wizard entry source and refresh AI banner
- Carry `from=workspace` query from the workbench into the wizard so cancel and post-generate redirects return to the workbench instead of always landing on /articles/templates. - AppShell now resolves an effective navKey from `route.query.from`, so the sidebar highlight and breadcrumb track the entry source while inside the wizard. - Redesign the brand-info AI analyze CTA into a standalone banner with template-aware copy (default / product_review / research_report) and add the matching i18n keys. - Drop the unused 批量生成文章 button and its styling from the templates page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -897,22 +897,35 @@ const enUS = {
|
||||
},
|
||||
promptVisible: 'Prompt visible',
|
||||
promptSealed: 'Prompt sealed',
|
||||
aiBanner: {
|
||||
title: 'One-click AI analysis',
|
||||
defaultDescription:
|
||||
'AI will complete the brand summary, find core keywords, and extract competitor context.',
|
||||
productReviewDescription:
|
||||
'AI will organize the product context, extract review keywords, and suggest comparable alternatives.',
|
||||
researchReportDescription:
|
||||
'AI will organize the topic context, extract research keywords, and suggest reference targets.',
|
||||
},
|
||||
assist: {
|
||||
analyzeTitle: 'AI is analyzing keywords and competitors. Please wait…',
|
||||
analyzeTitleNoCompetitors: 'AI is analyzing keywords and context. Please wait…',
|
||||
titleTitle: 'AI is generating titles. Please wait…',
|
||||
outlineTitle: 'AI is generating the article outline. Please wait…',
|
||||
analyzeStages: {
|
||||
brand: 'Analyzing the brand context…',
|
||||
keywords: 'Extracting keyword opportunities…',
|
||||
competitors: 'Mapping competitors and websites…',
|
||||
context: 'Organizing the content context…',
|
||||
},
|
||||
titleStages: {
|
||||
context: 'Preparing the current brief…',
|
||||
strategy: 'Generating title directions from keywords and competitors…',
|
||||
strategyNoCompetitors: 'Generating title directions from the keywords and brief…',
|
||||
finalize: 'Returning 5 title options…',
|
||||
},
|
||||
outlineStages: {
|
||||
context: 'Preparing the title, keywords, and competitor context…',
|
||||
contextNoCompetitors: 'Preparing the title, keywords, and content context…',
|
||||
structure: 'Generating the article outline from the selected structure…',
|
||||
finalize: 'Returning the editable outline result…',
|
||||
},
|
||||
|
||||
@@ -858,6 +858,12 @@ const zhCN = {
|
||||
},
|
||||
promptVisible: "Prompt 可见",
|
||||
promptSealed: "Prompt 已封装",
|
||||
aiBanner: {
|
||||
title: "一键智能分析",
|
||||
defaultDescription: "AI 将自动补充品牌简介、发掘核心关键词并智能提取竞品信息",
|
||||
productReviewDescription: "AI 将自动梳理产品背景、提炼评测关键词并生成同类替代方案参考",
|
||||
researchReportDescription: "AI 将自动梳理主题背景、提炼研究关键词并生成参照对象参考",
|
||||
},
|
||||
assist: {
|
||||
analyzeTitle: "AI 正在分析关键词和竞品,请稍后…",
|
||||
analyzeTitleNoCompetitors: "AI 正在分析关键词和评测上下文,请稍后…",
|
||||
|
||||
@@ -67,12 +67,23 @@ const membershipExpiryText = computed(() => {
|
||||
day: '2-digit',
|
||||
}).format(date)
|
||||
})
|
||||
const selectedKeys = computed(() => {
|
||||
if (route.meta.navKey === null) {
|
||||
return []
|
||||
}
|
||||
const navKeyOverrides: Record<string, string> = {
|
||||
workspace: '/workspace',
|
||||
}
|
||||
|
||||
return [String(route.meta.navKey ?? route.path)]
|
||||
const effectiveNavKey = computed<string | null>(() => {
|
||||
if (route.meta.navKey === null) {
|
||||
return null
|
||||
}
|
||||
const from = route.query.from
|
||||
if (typeof from === 'string' && navKeyOverrides[from]) {
|
||||
return navKeyOverrides[from]
|
||||
}
|
||||
return String(route.meta.navKey ?? route.path)
|
||||
})
|
||||
|
||||
const selectedKeys = computed(() => {
|
||||
return effectiveNavKey.value ? [effectiveNavKey.value] : []
|
||||
})
|
||||
const pageTitle = computed(() => t(String(route.meta.titleKey ?? 'route.workspace.title')))
|
||||
|
||||
@@ -165,7 +176,7 @@ const navSections = computed<NavSection[]>(() => {
|
||||
|
||||
const headerBreadcrumbs = computed<HeaderBreadcrumb[]>(() => {
|
||||
const currentTitle = pageTitle.value
|
||||
const navKey = route.meta.navKey === null ? null : String(route.meta.navKey ?? route.path)
|
||||
const navKey = effectiveNavKey.value
|
||||
|
||||
if (!navKey) {
|
||||
return [{ label: currentTitle }]
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
HolderOutlined,
|
||||
LeftOutlined,
|
||||
PlusOutlined,
|
||||
RobotOutlined,
|
||||
StarFilled,
|
||||
} from '@ant-design/icons-vue'
|
||||
import type {
|
||||
@@ -254,7 +255,7 @@ const mutation = useMutation({
|
||||
])
|
||||
message.info(t('templates.wizard.messages.queued'))
|
||||
allowWizardLeave = true
|
||||
await router.replace('/articles/templates')
|
||||
await router.replace(resolveExitPath())
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error(formatError(error))
|
||||
@@ -408,6 +409,25 @@ const previewCardCopy = computed<TemplateCardCopy>(
|
||||
const analyzeButtonLabel = computed(
|
||||
() => wizardConfig.value?.basic?.analyze_button_label || t('templates.wizard.actions.analyze'),
|
||||
)
|
||||
const aiBannerCopy = computed(() => {
|
||||
switch (templateDetail.value?.template_key) {
|
||||
case 'product_review':
|
||||
return {
|
||||
title: t('templates.wizard.aiBanner.title'),
|
||||
description: t('templates.wizard.aiBanner.productReviewDescription'),
|
||||
}
|
||||
case 'research_report':
|
||||
return {
|
||||
title: t('templates.wizard.aiBanner.title'),
|
||||
description: t('templates.wizard.aiBanner.researchReportDescription'),
|
||||
}
|
||||
default:
|
||||
return {
|
||||
title: t('templates.wizard.aiBanner.title'),
|
||||
description: t('templates.wizard.aiBanner.defaultDescription'),
|
||||
}
|
||||
}
|
||||
})
|
||||
const reviewAlertMessage = computed(
|
||||
() => wizardConfig.value?.review?.alert_message || t('templates.wizard.hints.async'),
|
||||
)
|
||||
@@ -1216,8 +1236,12 @@ async function saveWizardDraft(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveExitPath(): string {
|
||||
return route.query.from === 'workspace' ? '/workspace' : '/articles/templates'
|
||||
}
|
||||
|
||||
async function handleSaveDraftAndExit(): Promise<void> {
|
||||
await router.replace('/articles/templates')
|
||||
await router.replace(resolveExitPath())
|
||||
}
|
||||
|
||||
function dedupeStrings(values: string[]): string[] {
|
||||
@@ -2223,9 +2247,6 @@ function onStructureDragEnd(): void {
|
||||
<h3>{{ brandCardCopy.title || t('templates.wizard.sections.brandInfo') }}</h3>
|
||||
<p>{{ brandCardCopy.hint || t('templates.wizard.hints.brandFlow') }}</p>
|
||||
</div>
|
||||
<a-button type="primary" ghost :loading="assistBusy" @click="handleAnalyze">
|
||||
{{ analyzeButtonLabel }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="field-grid">
|
||||
@@ -2277,6 +2298,25 @@ function onStructureDragEnd(): void {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wizard-ai-banner">
|
||||
<div class="wizard-ai-banner__info">
|
||||
<RobotOutlined class="wizard-ai-banner__icon" />
|
||||
<div class="wizard-ai-banner__text">
|
||||
<h4>{{ aiBannerCopy.title }}</h4>
|
||||
<p>{{ aiBannerCopy.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a-button
|
||||
type="primary"
|
||||
class="wizard-ai-banner__btn"
|
||||
size="large"
|
||||
:loading="assistBusy"
|
||||
@click="handleAnalyze"
|
||||
>
|
||||
{{ analyzeButtonLabel }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="summary-box">
|
||||
<div class="summary-box__label">
|
||||
{{ t('templates.wizard.sections.brandSummary') }}
|
||||
@@ -2959,6 +2999,60 @@ function onStructureDragEnd(): void {
|
||||
content: '*';
|
||||
}
|
||||
|
||||
.wizard-ai-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 24px;
|
||||
padding: 16px 20px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, rgba(22, 119, 255, 0.04), rgba(22, 119, 255, 0.08));
|
||||
border: 1px solid rgba(22, 119, 255, 0.15);
|
||||
box-shadow: 0 4px 16px rgba(22, 119, 255, 0.05);
|
||||
}
|
||||
|
||||
.wizard-ai-banner__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.wizard-ai-banner__icon {
|
||||
font-size: 28px;
|
||||
background: linear-gradient(135deg, #1677ff, #36cfc9);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.wizard-ai-banner__text h4 {
|
||||
margin: 0 0 4px;
|
||||
color: #1f2f4d;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wizard-ai-banner__text p {
|
||||
margin: 0;
|
||||
color: #6b7a99;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.wizard-ai-banner__btn {
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
padding: 0 24px;
|
||||
background: linear-gradient(135deg, #1677ff, #0958d9);
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.2);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.wizard-ai-banner__btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(22, 119, 255, 0.3);
|
||||
}
|
||||
|
||||
.summary-box {
|
||||
margin-top: 18px;
|
||||
padding: 16px 18px;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
ArrowRightOutlined,
|
||||
BlockOutlined,
|
||||
ExperimentOutlined,
|
||||
FileTextOutlined,
|
||||
NodeIndexOutlined,
|
||||
@@ -458,10 +457,6 @@ function refreshRecords(): void {
|
||||
<p>{{ t('route.templates.description') }}</p>
|
||||
</div>
|
||||
<div class="templates-view__header-actions">
|
||||
<a-button class="batch-generate-btn">
|
||||
<template #icon><BlockOutlined /></template>
|
||||
{{ t('templates.actions.batchGenerate') }}
|
||||
</a-button>
|
||||
<a-button type="primary" class="templates-view__create-btn" @click="openTemplatePicker">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
{{ t('templates.actions.chooseTemplate') }}
|
||||
@@ -796,11 +791,6 @@ function refreshRecords(): void {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.batch-generate-btn {
|
||||
color: #1677ff;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
.templates-view__filters {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ function resolveTemplateIcon(template: TemplateCard): unknown {
|
||||
function openTemplate(template: TemplateCard): void {
|
||||
void router.push({
|
||||
path: '/articles/wizard',
|
||||
query: { template_id: String(template.id) },
|
||||
query: { template_id: String(template.id), from: 'workspace' },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -215,6 +215,7 @@ async function openEditor(article: RecentArticle): Promise<void> {
|
||||
query: {
|
||||
template_id: String(detail.template_id),
|
||||
article_id: String(detail.id),
|
||||
from: 'workspace',
|
||||
},
|
||||
})
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user