diff --git a/apps/admin-web/src/i18n/messages/en-US.ts b/apps/admin-web/src/i18n/messages/en-US.ts index b902bc7..4fb5611 100644 --- a/apps/admin-web/src/i18n/messages/en-US.ts +++ b/apps/admin-web/src/i18n/messages/en-US.ts @@ -882,12 +882,14 @@ const enUS = { }, actions: { analyze: 'Analyze', + analyzeCompetitors: 'AI fill competitors', addCompetitor: 'Add competitor', addSection: 'Add section', addOutlineNode: 'Add node', addOutlineChild: 'Add child', favorite: 'Save', saved: 'Saved', + unfavorite: 'Unsave', submit: 'Submit and generate', }, review: { @@ -937,8 +939,8 @@ const enUS = { 'AI will use the brand question to organize the topic context and suggest reference targets.', }, assist: { - analyzeTitle: 'AI is analyzing brand questions and competitors. Please wait…', - analyzeTitleNoCompetitors: 'AI is analyzing brand questions and context. Please wait…', + analyzeTitle: 'AI is filling competitor information. Please wait…', + analyzeTitleNoCompetitors: 'AI is filling competitor information. Please wait…', titleTitle: 'AI is generating titles. Please wait…', outlineTitle: 'AI is generating the article outline. Please wait…', analyzeStages: { @@ -946,7 +948,8 @@ const enUS = { keywords: 'Extracting keyword opportunities…', questions: 'Organizing brand questions…', competitors: 'Mapping competitors and websites…', - context: 'Organizing the content context…', + context: 'Organizing brand and website context…', + refine: 'Merging and filtering competitor suggestions…', }, titleStages: { context: 'Preparing the current brief…', @@ -970,9 +973,11 @@ const enUS = { missingOutline: 'Please select at least one outline section.', missingGeneratedOutline: 'Please generate and confirm the article outline first.', customOutlineTooLong: 'Custom sections cannot exceed {count} characters.', + selectBrandBeforeCompetitors: 'Choose a brand from the library before managing competitors.', selectBrandBeforeFavorite: 'Choose a brand from the library before saving competitors.', missingCompetitorName: 'Enter a competitor name before saving it to the library.', savedCompetitor: 'Competitor saved to the brand library.', + unsavedCompetitor: 'Competitor removed from the brand library.', assistReady: 'AI analysis is ready. You can keep refining the draft.', assistFailed: 'AI analysis failed. Please try again.', assistTimeout: 'AI analysis timed out. Please try again.', diff --git a/apps/admin-web/src/i18n/messages/zh-CN.ts b/apps/admin-web/src/i18n/messages/zh-CN.ts index b5ef5f2..6982fbb 100644 --- a/apps/admin-web/src/i18n/messages/zh-CN.ts +++ b/apps/admin-web/src/i18n/messages/zh-CN.ts @@ -839,12 +839,14 @@ const zhCN = { }, actions: { analyze: "分析", + analyzeCompetitors: "AI 补齐竞品", addCompetitor: "添加竞品", addSection: "添加结构", addOutlineNode: "添加节点", addOutlineChild: "添加子节点", favorite: "收藏", saved: "已收藏", + unfavorite: "取消收藏", submit: "提交并生成", }, review: { @@ -891,8 +893,8 @@ const zhCN = { researchReportDescription: "AI 将基于品牌问题梳理主题背景,并生成参照对象参考", }, assist: { - analyzeTitle: "AI 正在分析品牌问题和竞品,请稍后…", - analyzeTitleNoCompetitors: "AI 正在分析品牌问题和评测上下文,请稍后…", + analyzeTitle: "AI 正在补齐竞品信息,请稍后…", + analyzeTitleNoCompetitors: "AI 正在补齐竞品信息,请稍后…", titleTitle: "AI 正在生成标题,请稍后…", outlineTitle: "AI 正在生成文章大纲,请稍后…", analyzeStages: { @@ -900,7 +902,8 @@ const zhCN = { keywords: "关键词提炼中…", questions: "品牌问题整理中…", competitors: "竞品与官网信息补充中…", - context: "评测上下文整理中…", + context: "正在整理品牌与官网上下文…", + refine: "正在合并并筛选竞品列表…", }, titleStages: { context: "正在整理当前内容上下文…", @@ -925,9 +928,11 @@ const zhCN = { missingOutline: "请至少选择一个文章结构段落", missingGeneratedOutline: "请先生成并确认文章大纲", customOutlineTooLong: "自定义结构不能超过 {count} 个字", + selectBrandBeforeCompetitors: "请先从品牌库选择品牌后再管理竞品", selectBrandBeforeFavorite: "请先从品牌库选择品牌后再收藏竞品", missingCompetitorName: "至少需要填写竞品名称后才能收藏", savedCompetitor: "竞品已收藏到品牌词库", + unsavedCompetitor: "已取消收藏该竞品", assistReady: "AI 分析已完成,可以继续调整内容。", assistFailed: "AI 分析失败,请稍后重试。", assistTimeout: "AI 分析超时,请稍后重试。", diff --git a/apps/admin-web/src/views/TemplateWizardView.vue b/apps/admin-web/src/views/TemplateWizardView.vue index c49d63b..5960cd9 100644 --- a/apps/admin-web/src/views/TemplateWizardView.vue +++ b/apps/admin-web/src/views/TemplateWizardView.vue @@ -328,6 +328,7 @@ const showCompetitorsCard = computed( wizardConfig.value?.basic?.cards?.competitors?.visible !== false, ) const canSaveCompetitorsToLibrary = computed(() => Boolean(selectedBrandId.value)) +const canManageCompetitors = computed(() => Boolean(selectedBrandId.value)) const customOutlineMaxLength = computed( () => wizardConfig.value?.outline?.custom_max_length ?? CUSTOM_OUTLINE_MAX_LENGTH, ) @@ -434,27 +435,9 @@ const previewCardCopy = computed( () => wizardConfig.value?.structure?.cards?.preview ?? {}, ) const analyzeButtonLabel = computed( - () => wizardConfig.value?.basic?.analyze_button_label || t('templates.wizard.actions.analyze'), + () => t('templates.wizard.actions.analyzeCompetitors'), ) -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 showBrandSummaryBox = computed(() => Boolean(selectedBrandId.value)) const reviewAlertMessage = computed( () => wizardConfig.value?.review?.alert_message || t('templates.wizard.hints.async'), ) @@ -483,11 +466,9 @@ const hasWizardDraftContent = computed( const assistStages = computed(() => assistTaskKind.value === 'analyze' ? [ - t('templates.wizard.assist.analyzeStages.brand'), - t('templates.wizard.assist.analyzeStages.questions'), - showCompetitorsCard.value - ? t('templates.wizard.assist.analyzeStages.competitors') - : t('templates.wizard.assist.analyzeStages.context'), + t('templates.wizard.assist.analyzeStages.context'), + t('templates.wizard.assist.analyzeStages.competitors'), + t('templates.wizard.assist.analyzeStages.refine'), ] : assistTaskKind.value === 'title' ? [ @@ -507,9 +488,7 @@ const assistStages = computed(() => ) const assistHeading = computed(() => assistTaskKind.value === 'analyze' - ? showCompetitorsCard.value - ? t('templates.wizard.assist.analyzeTitle') - : t('templates.wizard.assist.analyzeTitleNoCompetitors') + ? t('templates.wizard.assist.analyzeTitle') : assistTaskKind.value === 'title' ? t('templates.wizard.assist.titleTitle') : t('templates.wizard.assist.outlineTitle'), @@ -827,6 +806,14 @@ function validateBasicInfo(): boolean { return true } +function validateAnalyzeInfo(): boolean { + if (!canManageCompetitors.value) { + message.warning(t('templates.wizard.messages.selectBrandBeforeCompetitors')) + return false + } + return true +} + function validateStructure(): boolean { if (!finalTitle.value) { message.warning(t('templates.wizard.messages.missingTitle')) @@ -885,7 +872,7 @@ function handlePrev(): void { } async function handleAnalyze(): Promise { - if (!validateBasicInfo()) { + if (!validateAnalyzeInfo()) { return } await runAnalyzeTask(true) @@ -1092,6 +1079,10 @@ function applyAnalyzeResult(result: TemplateAnalyzeResult): void { } function addCompetitorRow(): void { + if (!canManageCompetitors.value) { + message.warning(t('templates.wizard.messages.selectBrandBeforeCompetitors')) + return + } competitorDrafts.value = [ ...competitorDrafts.value, { @@ -1114,14 +1105,27 @@ async function handleFavoriteCompetitor(item: DraftCompetitor): Promise { message.warning(t('templates.wizard.messages.selectBrandBeforeFavorite')) return } - if (!item.name.trim()) { - message.warning(t('templates.wizard.messages.missingCompetitorName')) - return - } competitorSavingKey.value = item.key try { + if (item.saved && item.libraryId) { + await brandsApi.removeCompetitor(brandId, item.libraryId) + item.libraryId = undefined + item.saved = false + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ['brands'] }), + queryClient.invalidateQueries({ queryKey: ['brands', brandId, 'competitors'] }), + ]) + message.success(t('templates.wizard.messages.unsavedCompetitor')) + return + } + + if (!item.name.trim()) { + message.warning(t('templates.wizard.messages.missingCompetitorName')) + return + } + const payload = { name: item.name.trim(), website: item.website.trim() || null, @@ -2423,26 +2427,7 @@ function onStructureDragEnd(): void { -
-
- -
-

{{ aiBannerCopy.title }}

-

{{ aiBannerCopy.description }}

-
-
- - {{ analyzeButtonLabel }} - -
- -
+
{{ t('templates.wizard.sections.brandSummary') }}
@@ -2523,10 +2508,26 @@ function onStructureDragEnd(): void {

{{ competitorsCardCopy.hint || t('templates.wizard.hints.competitors') }}

- - - {{ t('templates.wizard.actions.addCompetitor') }} - +
+ + + {{ analyzeButtonLabel }} + + + + {{ t('templates.wizard.actions.addCompetitor') }} + +
@@ -2570,7 +2571,7 @@ function onStructureDragEnd(): void { {{ canSaveCompetitorsToLibrary && item.saved - ? t('templates.wizard.actions.saved') + ? t('templates.wizard.actions.unfavorite') : t('templates.wizard.actions.favorite') }} @@ -3195,60 +3196,6 @@ function onStructureDragEnd(): void { gap: 18px; } -.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; @@ -3272,6 +3219,34 @@ function onStructureDragEnd(): void { line-height: 1.7; } +.competitor-header-actions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 10px; +} + +.competitor-ai-button { + border: none; + border-radius: 8px; + background: #1677ff; + box-shadow: 0 4px 12px rgba(22, 119, 255, 0.16); + font-weight: 700; +} + +.competitor-ai-button:hover:not([disabled]) { + background: #0958d9 !important; + box-shadow: 0 6px 16px rgba(22, 119, 255, 0.22); +} + +.competitor-ai-button[disabled], +.competitor-ai-button.ant-btn-loading { + background: #f1f5f9 !important; + border: 1px solid #e2e8f0 !important; + color: #94a3b8 !important; + box-shadow: none !important; +} + .competitor-table { border: 1px solid rgba(20, 44, 88, 0.08); border-radius: 20px; @@ -3788,6 +3763,10 @@ function onStructureDragEnd(): void { justify-content: flex-start; } + .competitor-header-actions { + justify-content: flex-start; + } + .outline-grid { grid-template-columns: 1fr; }