feat(template-wizard): enhance competitor management and update UI messages
Frontend CI / Frontend (push) Successful in 3m27s

This commit is contained in:
2026-05-13 17:10:31 +08:00
parent 1eae6fb6d4
commit 7aa786fbf4
3 changed files with 107 additions and 118 deletions
+91 -112
View File
@@ -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<TemplateCardCopy>(
() => 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<void> {
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<void> {
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 {
</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 v-if="showBrandSummaryBox" class="summary-box">
<div class="summary-box__label">
{{ t('templates.wizard.sections.brandSummary') }}
</div>
@@ -2523,10 +2508,26 @@ function onStructureDragEnd(): void {
</h3>
<p>{{ competitorsCardCopy.hint || t('templates.wizard.hints.competitors') }}</p>
</div>
<a-button type="default" @click="addCompetitorRow">
<template #icon><PlusOutlined /></template>
{{ t('templates.wizard.actions.addCompetitor') }}
</a-button>
<div class="competitor-header-actions">
<a-button
class="competitor-ai-button"
type="primary"
:loading="assistBusy"
:disabled="!canManageCompetitors"
@click="handleAnalyze"
>
<template #icon><RobotOutlined /></template>
{{ analyzeButtonLabel }}
</a-button>
<a-button
type="default"
:disabled="!canManageCompetitors"
@click="addCompetitorRow"
>
<template #icon><PlusOutlined /></template>
{{ t('templates.wizard.actions.addCompetitor') }}
</a-button>
</div>
</div>
<div v-if="competitorDrafts.length > 0" class="competitor-table">
@@ -2570,7 +2571,7 @@ function onStructureDragEnd(): void {
</template>
{{
canSaveCompetitorsToLibrary && item.saved
? t('templates.wizard.actions.saved')
? t('templates.wizard.actions.unfavorite')
: t('templates.wizard.actions.favorite')
}}
</a-button>
@@ -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;
}