diff --git a/apps/admin-web/src/i18n/messages/en-US.ts b/apps/admin-web/src/i18n/messages/en-US.ts index bd1fc59..f54f97d 100644 --- a/apps/admin-web/src/i18n/messages/en-US.ts +++ b/apps/admin-web/src/i18n/messages/en-US.ts @@ -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…', }, diff --git a/apps/admin-web/src/i18n/messages/zh-CN.ts b/apps/admin-web/src/i18n/messages/zh-CN.ts index 6c669d3..a31e05e 100644 --- a/apps/admin-web/src/i18n/messages/zh-CN.ts +++ b/apps/admin-web/src/i18n/messages/zh-CN.ts @@ -858,6 +858,12 @@ const zhCN = { }, promptVisible: "Prompt 可见", promptSealed: "Prompt 已封装", + aiBanner: { + title: "一键智能分析", + defaultDescription: "AI 将自动补充品牌简介、发掘核心关键词并智能提取竞品信息", + productReviewDescription: "AI 将自动梳理产品背景、提炼评测关键词并生成同类替代方案参考", + researchReportDescription: "AI 将自动梳理主题背景、提炼研究关键词并生成参照对象参考", + }, assist: { analyzeTitle: "AI 正在分析关键词和竞品,请稍后…", analyzeTitleNoCompetitors: "AI 正在分析关键词和评测上下文,请稍后…", diff --git a/apps/admin-web/src/layouts/AppShell.vue b/apps/admin-web/src/layouts/AppShell.vue index cced722..9728dbb 100644 --- a/apps/admin-web/src/layouts/AppShell.vue +++ b/apps/admin-web/src/layouts/AppShell.vue @@ -67,12 +67,23 @@ const membershipExpiryText = computed(() => { day: '2-digit', }).format(date) }) -const selectedKeys = computed(() => { - if (route.meta.navKey === null) { - return [] - } +const navKeyOverrides: Record = { + workspace: '/workspace', +} - return [String(route.meta.navKey ?? route.path)] +const effectiveNavKey = computed(() => { + 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(() => { const headerBreadcrumbs = computed(() => { 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 }] diff --git a/apps/admin-web/src/views/TemplateWizardView.vue b/apps/admin-web/src/views/TemplateWizardView.vue index 394c4d9..7638ebc 100644 --- a/apps/admin-web/src/views/TemplateWizardView.vue +++ b/apps/admin-web/src/views/TemplateWizardView.vue @@ -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( 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 { } } +function resolveExitPath(): string { + return route.query.from === 'workspace' ? '/workspace' : '/articles/templates' +} + async function handleSaveDraftAndExit(): Promise { - await router.replace('/articles/templates') + await router.replace(resolveExitPath()) } function dedupeStrings(values: string[]): string[] { @@ -2223,9 +2247,6 @@ function onStructureDragEnd(): void {

{{ brandCardCopy.title || t('templates.wizard.sections.brandInfo') }}

{{ brandCardCopy.hint || t('templates.wizard.hints.brandFlow') }}

- - {{ analyzeButtonLabel }} -
@@ -2277,6 +2298,25 @@ function onStructureDragEnd(): void {
+
+
+ +
+

{{ aiBannerCopy.title }}

+

{{ aiBannerCopy.description }}

+
+
+ + {{ analyzeButtonLabel }} + +
+
{{ 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; diff --git a/apps/admin-web/src/views/TemplatesView.vue b/apps/admin-web/src/views/TemplatesView.vue index 7f29239..07c6e80 100644 --- a/apps/admin-web/src/views/TemplatesView.vue +++ b/apps/admin-web/src/views/TemplatesView.vue @@ -2,7 +2,6 @@ import { AppstoreOutlined, ArrowRightOutlined, - BlockOutlined, ExperimentOutlined, FileTextOutlined, NodeIndexOutlined, @@ -458,10 +457,6 @@ function refreshRecords(): void {

{{ t('route.templates.description') }}

- - - {{ t('templates.actions.batchGenerate') }} - {{ 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; } diff --git a/apps/admin-web/src/views/WorkspaceView.vue b/apps/admin-web/src/views/WorkspaceView.vue index fb01a24..1bd93f0 100644 --- a/apps/admin-web/src/views/WorkspaceView.vue +++ b/apps/admin-web/src/views/WorkspaceView.vue @@ -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 { query: { template_id: String(detail.template_id), article_id: String(detail.id), + from: 'workspace', }, }) return