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
@@ -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