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:
2026-05-12 01:12:01 +08:00
parent f06c8dfc01
commit 635782041a
6 changed files with 137 additions and 22 deletions
+13
View File
@@ -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 正在分析关键词和评测上下文,请稍后…",
+17 -6
View File
@@ -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;
}
+2 -1
View File
@@ -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