feat: scope articles by brand
This commit is contained in:
@@ -26,6 +26,7 @@ import KnowledgeGroupSelect from '@/components/KnowledgeGroupSelect.vue'
|
||||
import { articlesApi, brandsApi, normalizeInputParams, templatesApi } from '@/lib/api'
|
||||
import { getTemplateMeta } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
interface DraftCompetitor {
|
||||
key: string
|
||||
@@ -151,6 +152,7 @@ const queryClient = useQueryClient()
|
||||
const { locale: uiLocale, t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
let competitorKeySeed = 0
|
||||
let outlineNodeSeed = 0
|
||||
@@ -167,8 +169,6 @@ const savingDraft = ref(false)
|
||||
const leaveConfirmOpen = ref(false)
|
||||
const leaveConfirmBusy = ref(false)
|
||||
|
||||
const selectedBrandId = ref<number | null>(null)
|
||||
const brandName = ref('')
|
||||
const officialWebsite = ref('')
|
||||
const brandSummary = ref('')
|
||||
|
||||
@@ -209,11 +209,9 @@ const templateDetailQuery = useQuery({
|
||||
queryFn: () => templatesApi.detail(templateId.value),
|
||||
})
|
||||
|
||||
const brandsQuery = useQuery({
|
||||
queryKey: ['brands', 'wizard-context'],
|
||||
enabled,
|
||||
queryFn: () => brandsApi.list(),
|
||||
})
|
||||
const selectedBrand = computed(() => companyStore.currentBrand)
|
||||
const selectedBrandId = computed(() => selectedBrand.value?.id ?? null)
|
||||
const normalizedBrandName = computed(() => selectedBrand.value?.name?.trim() || '')
|
||||
|
||||
const draftArticleQuery = useQuery({
|
||||
queryKey: computed(() => ['articles', 'detail', draftArticleId.value]),
|
||||
@@ -283,15 +281,6 @@ const templateMeta = computed(() => {
|
||||
helper: templateCardConfig.value.hero?.helper || fallback.helper,
|
||||
}
|
||||
})
|
||||
const selectedBrand = computed(
|
||||
() => brandsQuery.data.value?.find((brand) => brand.id === selectedBrandId.value) ?? null,
|
||||
)
|
||||
const brandNameOptions = computed(() =>
|
||||
(brandsQuery.data.value ?? []).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
})),
|
||||
)
|
||||
|
||||
function findQuestionById(id: number | null): Question | null {
|
||||
if (!id) {
|
||||
@@ -323,9 +312,6 @@ const selectedQuestionTexts = computed(() =>
|
||||
dedupeStrings([primaryQuestionText.value, ...supplementalQuestionTexts.value]),
|
||||
)
|
||||
|
||||
const normalizedBrandName = computed(
|
||||
() => brandName.value.trim() || selectedBrand.value?.name || '',
|
||||
)
|
||||
const finalTitle = computed(() => customTitle.value.trim() || selectedTitle.value.trim())
|
||||
const primaryKeyword = computed(() => primaryQuestionText.value)
|
||||
const showCompetitorsCard = computed(
|
||||
@@ -455,7 +441,6 @@ const isPageLoading = computed(
|
||||
const hasWizardDraftContent = computed(
|
||||
() =>
|
||||
Boolean(selectedBrandId.value) ||
|
||||
Boolean(brandName.value.trim()) ||
|
||||
Boolean(officialWebsite.value.trim()) ||
|
||||
Boolean(brandSummary.value.trim()) ||
|
||||
Boolean(primaryQuestionId.value) ||
|
||||
@@ -504,10 +489,7 @@ function resetWizardState(detail: NonNullable<typeof templateDetail.value>): voi
|
||||
currentArticleId.value = draftArticleId.value
|
||||
currentStep.value = 0
|
||||
articleLocale.value = 'zh-CN'
|
||||
selectedBrandId.value = null
|
||||
brandName.value = ''
|
||||
officialWebsite.value = ''
|
||||
brandSummary.value = ''
|
||||
hydrateBrandContextFromCurrentBrand()
|
||||
primaryQuestionId.value = null
|
||||
supplementalQuestionIds.value = []
|
||||
competitorDrafts.value = []
|
||||
@@ -526,9 +508,9 @@ function resetWizardState(detail: NonNullable<typeof templateDetail.value>): voi
|
||||
detail.template_key,
|
||||
buildTemplateRenderContext({
|
||||
locale: 'zh-CN',
|
||||
brandName: '',
|
||||
officialWebsite: '',
|
||||
brandSummary: '',
|
||||
brandName: normalizedBrandName.value,
|
||||
officialWebsite: officialWebsite.value,
|
||||
brandSummary: brandSummary.value,
|
||||
primaryQuestion: '',
|
||||
supplementalQuestions: [],
|
||||
inputParams: {},
|
||||
@@ -542,9 +524,9 @@ function resetWizardState(detail: NonNullable<typeof templateDetail.value>): voi
|
||||
detail.template_key,
|
||||
buildTemplateRenderContext({
|
||||
locale: 'zh-CN',
|
||||
brandName: '',
|
||||
officialWebsite: '',
|
||||
brandSummary: '',
|
||||
brandName: normalizedBrandName.value,
|
||||
officialWebsite: officialWebsite.value,
|
||||
brandSummary: brandSummary.value,
|
||||
primaryQuestion: '',
|
||||
supplementalQuestions: [],
|
||||
inputParams: {},
|
||||
@@ -565,10 +547,9 @@ function applyDraftArticleState(detail: ArticleDetail): void {
|
||||
|
||||
currentStep.value = clampStep(numberFromUnknown(draftState.current_step))
|
||||
articleLocale.value = stringFromUnknown(draftState.locale) || articleLocale.value
|
||||
selectedBrandId.value = numberFromUnknown(draftState.selected_brand_id) ?? null
|
||||
brandName.value = stringFromUnknown(draftState.brand_name) || ''
|
||||
officialWebsite.value = stringFromUnknown(draftState.official_website) || ''
|
||||
brandSummary.value = stringFromUnknown(draftState.brand_summary) || ''
|
||||
hydrateBrandContextFromCurrentBrand()
|
||||
officialWebsite.value = stringFromUnknown(draftState.official_website) || officialWebsite.value
|
||||
brandSummary.value = stringFromUnknown(draftState.brand_summary) || brandSummary.value
|
||||
primaryQuestionId.value = numberFromUnknown(draftState.primary_question_id) ?? null
|
||||
supplementalQuestionIds.value =
|
||||
numberListFromUnknown(draftState.supplemental_question_ids)?.slice(0, 3) ?? []
|
||||
@@ -604,6 +585,14 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => selectedBrand.value,
|
||||
() => {
|
||||
hydrateBrandContextFromCurrentBrand()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
baseOutlineOptions,
|
||||
(options) => {
|
||||
@@ -660,23 +649,6 @@ watch(supplementalQuestionIds, (ids) => {
|
||||
}
|
||||
})
|
||||
|
||||
function handleBrandNameInput(value: string): void {
|
||||
brandName.value = value
|
||||
const matchedBrand = brandsQuery.data.value?.find((item) => item.name === value)
|
||||
selectedBrandId.value = matchedBrand?.id ?? null
|
||||
}
|
||||
|
||||
function handleBrandNameSelect(value: string): void {
|
||||
const brand = brandsQuery.data.value?.find((item) => item.name === value)
|
||||
if (!brand) {
|
||||
selectedBrandId.value = null
|
||||
brandName.value = value
|
||||
return
|
||||
}
|
||||
selectedBrandId.value = brand.id
|
||||
brandName.value = brand.name
|
||||
}
|
||||
|
||||
watch(selectedBrandId, async (brandId) => {
|
||||
if (restoringDraft.value) {
|
||||
return
|
||||
@@ -690,10 +662,7 @@ watch(selectedBrandId, async (brandId) => {
|
||||
return
|
||||
}
|
||||
|
||||
const brand = brandsQuery.data.value?.find((item) => item.id === brandId)
|
||||
if (brand && !brandName.value.trim()) {
|
||||
brandName.value = brand.name
|
||||
}
|
||||
const brand = selectedBrand.value
|
||||
if (brand?.website && !officialWebsite.value.trim()) {
|
||||
officialWebsite.value = brand.website
|
||||
}
|
||||
@@ -719,7 +688,13 @@ watch(selectedBrandId, async (brandId) => {
|
||||
description: item.description ?? '',
|
||||
saved: true,
|
||||
}))
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
function hydrateBrandContextFromCurrentBrand(): void {
|
||||
const brand = selectedBrand.value
|
||||
officialWebsite.value = brand?.website ?? ''
|
||||
brandSummary.value = brand?.description ?? ''
|
||||
}
|
||||
|
||||
onBeforeRouteLeave(async () => {
|
||||
if (!shouldConfirmBeforeLeavingWizard()) {
|
||||
@@ -1310,7 +1285,7 @@ function buildDraftState(): Record<string, JsonValue> {
|
||||
current_step: currentStep.value,
|
||||
locale: articleLocale.value,
|
||||
selected_brand_id: selectedBrandId.value,
|
||||
brand_name: brandName.value.trim(),
|
||||
brand_name: normalizedBrandName.value,
|
||||
official_website: officialWebsite.value.trim(),
|
||||
brand_summary: brandSummary.value.trim(),
|
||||
primary_question_id: primaryQuestionId.value,
|
||||
@@ -2420,14 +2395,10 @@ function onStructureDragEnd(): void {
|
||||
<label class="required-asterisk">
|
||||
{{ t('templates.wizard.sections.brandName') }}
|
||||
</label>
|
||||
<a-auto-complete
|
||||
v-model:value="brandName"
|
||||
allow-clear
|
||||
:options="brandNameOptions"
|
||||
:placeholder="t('templates.wizard.placeholders.brandName')"
|
||||
@change="handleBrandNameInput"
|
||||
@select="handleBrandNameSelect"
|
||||
/>
|
||||
<div class="current-brand-field">
|
||||
<strong>{{ normalizedBrandName || t('shell.currentCompanyPlaceholder') }}</strong>
|
||||
<span v-if="selectedBrand?.website">{{ selectedBrand.website }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-item">
|
||||
@@ -3201,6 +3172,37 @@ function onStructureDragEnd(): void {
|
||||
content: ':';
|
||||
}
|
||||
|
||||
.current-brand-field {
|
||||
display: flex;
|
||||
min-height: 32px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
padding: 5px 11px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background: #fafcff;
|
||||
}
|
||||
|
||||
.current-brand-field strong {
|
||||
overflow: hidden;
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.current-brand-field span {
|
||||
overflow: hidden;
|
||||
color: #667085;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.optional-field-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user