feat: scope articles by brand
This commit is contained in:
@@ -16,6 +16,7 @@ import ArticleEditorCanvas from '@/components/ArticleEditorCanvas.vue'
|
||||
import PublishArticleModal from '@/components/PublishArticleModal.vue'
|
||||
import { articlesApi, complianceApi } from '@/lib/api'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
type EditorCanvasPublic = InstanceType<typeof ArticleEditorCanvas> & {
|
||||
scrollToText?: (text: string) => boolean
|
||||
@@ -25,6 +26,7 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const { t } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const articleId = computed(() => Number(route.params.id))
|
||||
const title = ref('')
|
||||
@@ -38,8 +40,10 @@ const editorComplianceResult = ref<ComplianceCheckResult | null>(null)
|
||||
const editorCanvasRef = ref<EditorCanvasPublic | null>(null)
|
||||
|
||||
const detailQuery = useQuery({
|
||||
queryKey: computed(() => ['articles', 'detail', articleId.value]),
|
||||
enabled: computed(() => Number.isInteger(articleId.value) && articleId.value > 0),
|
||||
queryKey: computed(() => ['articles', 'detail', companyStore.currentBrandId, articleId.value]),
|
||||
enabled: computed(
|
||||
() => Boolean(companyStore.currentBrandId) && Number.isInteger(articleId.value) && articleId.value > 0,
|
||||
),
|
||||
queryFn: () => articlesApi.detail(articleId.value),
|
||||
})
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,14 +22,19 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { brandsApi } from '@/lib/api'
|
||||
import { formatDateTime } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const activeTab = ref<'questions' | 'competitors'>('questions')
|
||||
const selectedBrandId = ref<number | null>(null)
|
||||
const selectedBrandId = computed<number | null>({
|
||||
get: () => companyStore.currentBrandId,
|
||||
set: (brandId) => companyStore.setCurrentBrand(brandId),
|
||||
})
|
||||
|
||||
const brandModalOpen = ref(false)
|
||||
const editingBrandId = ref<number | null>(null)
|
||||
@@ -148,6 +153,7 @@ const brandMutations = {
|
||||
message.success(t('brands.messages.createBrand'))
|
||||
brandModalOpen.value = false
|
||||
selectedBrandId.value = brand.id
|
||||
companyStore.setCurrentBrand(brand.id)
|
||||
await invalidateBrandQueries()
|
||||
},
|
||||
onError: (error) => message.error(formatError(error)),
|
||||
@@ -320,6 +326,14 @@ function openBrandModal(brand?: Brand): void {
|
||||
brandModalOpen.value = true
|
||||
}
|
||||
|
||||
function selectBrand(brandId: number): void {
|
||||
if (!Number.isFinite(brandId) || brandId <= 0) {
|
||||
return
|
||||
}
|
||||
selectedBrandId.value = brandId
|
||||
void queryClient.invalidateQueries({ queryKey: ['brands'] })
|
||||
}
|
||||
|
||||
function ensureQuestionCreatable(): boolean {
|
||||
if (!selectedBrandId.value) {
|
||||
message.warning(t('brands.messages.chooseBrand'))
|
||||
@@ -414,6 +428,7 @@ async function invalidateBrandQueries(): Promise<void> {
|
||||
queryClient.invalidateQueries({ queryKey: ['brands'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['workspace'] }),
|
||||
])
|
||||
await companyStore.refreshBrands()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -440,7 +455,7 @@ async function invalidateBrandQueries(): Promise<void> {
|
||||
:key="brand.id"
|
||||
class="brand-card"
|
||||
:class="{ 'brand-card--active': brand.id === selectedBrandId }"
|
||||
@click="selectedBrandId = brand.id"
|
||||
@click="selectBrand(brand.id)"
|
||||
>
|
||||
<div class="brand-card__top">
|
||||
<div>
|
||||
|
||||
@@ -20,10 +20,12 @@ import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
|
||||
import { copyTextToClipboard } from '@/lib/clipboard'
|
||||
import { getPublishStatusMeta } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const publishModalOpen = ref(false)
|
||||
const selectedPublishArticleId = ref<number | null>(null)
|
||||
@@ -75,7 +77,8 @@ const articleParams = computed<ArticleListParams>(() => {
|
||||
})
|
||||
|
||||
const articleListQuery = useQuery({
|
||||
queryKey: computed(() => ['articles', 'list', articleParams.value]),
|
||||
queryKey: computed(() => ['articles', 'list', companyStore.currentBrandId, articleParams.value]),
|
||||
enabled: computed(() => Boolean(companyStore.currentBrandId)),
|
||||
queryFn: () => articlesApi.list(articleParams.value),
|
||||
})
|
||||
|
||||
@@ -179,6 +182,10 @@ async function handleDelete(articleId: number): Promise<void> {
|
||||
}
|
||||
|
||||
function handleCreate(): void {
|
||||
if (!companyStore.currentBrandId) {
|
||||
message.warning(t('templates.wizard.messages.selectBrandBeforeQuestion'))
|
||||
return
|
||||
}
|
||||
createMutation.mutate()
|
||||
}
|
||||
|
||||
@@ -209,7 +216,12 @@ async function copyArticle(articleId: number): Promise<void> {
|
||||
<p>{{ t('route.freeCreate.description') }}</p>
|
||||
</div>
|
||||
<div class="free-create-view__header-actions">
|
||||
<a-button type="primary" :loading="createMutation.isPending.value" @click="handleCreate">
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!companyStore.currentBrandId"
|
||||
:loading="createMutation.isPending.value"
|
||||
@click="handleCreate"
|
||||
>
|
||||
<template #icon><PlusOutlined /></template>
|
||||
{{ t('freeCreate.actions.create') }}
|
||||
</a-button>
|
||||
|
||||
@@ -9,17 +9,18 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import KnowledgeGroupSelect from '@/components/KnowledgeGroupSelect.vue'
|
||||
import { articlesApi, brandsApi } from '@/lib/api'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const { t } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const form = reactive({
|
||||
source_url: '',
|
||||
source_title: '',
|
||||
locale: 'zh-CN',
|
||||
brand_name: '',
|
||||
region: '',
|
||||
keywords: [] as string[],
|
||||
preserve_points: '',
|
||||
@@ -29,26 +30,14 @@ const form = reactive({
|
||||
knowledge_group_ids: [] as number[],
|
||||
})
|
||||
|
||||
const brandsQuery = useQuery({
|
||||
queryKey: ['brands'],
|
||||
queryFn: () => brandsApi.list(),
|
||||
})
|
||||
|
||||
const brandNameOptions = computed(() =>
|
||||
(brandsQuery.data.value ?? []).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
})),
|
||||
)
|
||||
|
||||
const selectedBrand = computed(
|
||||
() => brandsQuery.data.value?.find((item) => item.name === form.brand_name.trim()) ?? null,
|
||||
)
|
||||
const selectedBrand = computed(() => companyStore.currentBrand)
|
||||
const selectedBrandId = computed(() => selectedBrand.value?.id ?? null)
|
||||
const currentBrandName = computed(() => selectedBrand.value?.name?.trim() || '')
|
||||
|
||||
const questionsQuery = useQuery({
|
||||
queryKey: computed(() => ['brands', selectedBrand.value?.id, 'questions', 'imitation']),
|
||||
enabled: computed(() => Boolean(selectedBrand.value?.id)),
|
||||
queryFn: () => brandsApi.listQuestions(selectedBrand.value?.id as number),
|
||||
queryKey: computed(() => ['brands', selectedBrandId.value, 'questions', 'imitation']),
|
||||
enabled: computed(() => Boolean(selectedBrandId.value)),
|
||||
queryFn: () => brandsApi.listQuestions(selectedBrandId.value as number),
|
||||
})
|
||||
|
||||
const keywordOptions = computed(() =>
|
||||
@@ -61,7 +50,7 @@ const keywordOptions = computed(() =>
|
||||
const canSubmit = computed(
|
||||
() =>
|
||||
form.source_url.trim().length > 0 &&
|
||||
form.brand_name.trim().length > 0 &&
|
||||
currentBrandName.value.length > 0 &&
|
||||
!generateMutation.isPending.value,
|
||||
)
|
||||
|
||||
@@ -71,7 +60,7 @@ const generateMutation = useMutation({
|
||||
source_url: form.source_url.trim(),
|
||||
source_title: form.source_title.trim(),
|
||||
locale: form.locale,
|
||||
brand_name: form.brand_name.trim(),
|
||||
brand_name: currentBrandName.value,
|
||||
region: form.region.trim(),
|
||||
keywords: form.keywords.map((item) => item.trim()).filter(Boolean),
|
||||
preserve_points: form.preserve_points.trim(),
|
||||
@@ -117,7 +106,7 @@ function submit(): void {
|
||||
message.warning(t('imitation.create.sourceURLRequired'))
|
||||
return
|
||||
}
|
||||
if (!form.brand_name.trim()) {
|
||||
if (!currentBrandName.value) {
|
||||
message.warning(t('imitation.create.brandNameRequired'))
|
||||
return
|
||||
}
|
||||
@@ -231,12 +220,10 @@ function submit(): void {
|
||||
<div class="imitation-generate-page__field-grid">
|
||||
<div class="imitation-generate-page__field">
|
||||
<label class="required-asterisk">{{ t('imitation.create.brandName') }}</label>
|
||||
<a-auto-complete
|
||||
v-model:value="form.brand_name"
|
||||
allow-clear
|
||||
:options="brandNameOptions"
|
||||
:placeholder="t('imitation.create.brandNamePlaceholder')"
|
||||
/>
|
||||
<div class="current-brand-field">
|
||||
<strong>{{ currentBrandName || t('shell.currentCompanyPlaceholder') }}</strong>
|
||||
<span v-if="selectedBrand?.website">{{ selectedBrand.website }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="imitation-generate-page__field">
|
||||
@@ -508,6 +495,37 @@ function submit(): 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;
|
||||
}
|
||||
|
||||
.required-asterisk::before {
|
||||
margin-right: 4px;
|
||||
color: #ff4d4f;
|
||||
|
||||
@@ -20,10 +20,12 @@ import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/a
|
||||
import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
|
||||
import { getGenerateStatusMeta, getPublishStatusMeta, hasActiveGenerationStatus } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const publishModalOpen = ref(false)
|
||||
const selectedPublishArticleId = ref<number | null>(null)
|
||||
@@ -80,7 +82,8 @@ const articleParams = computed<ArticleListParams>(() => {
|
||||
})
|
||||
|
||||
const articleListQuery = useQuery({
|
||||
queryKey: computed(() => ['articles', 'list', articleParams.value]),
|
||||
queryKey: computed(() => ['articles', 'list', companyStore.currentBrandId, articleParams.value]),
|
||||
enabled: computed(() => Boolean(companyStore.currentBrandId)),
|
||||
queryFn: () => articlesApi.list(articleParams.value),
|
||||
})
|
||||
|
||||
@@ -205,6 +208,10 @@ function resetFilters(): void {
|
||||
}
|
||||
|
||||
function openCreate(): void {
|
||||
if (!companyStore.currentBrandId) {
|
||||
message.warning(t('templates.wizard.messages.selectBrandBeforeQuestion'))
|
||||
return
|
||||
}
|
||||
void router.push({ name: 'article-imitation-create' })
|
||||
}
|
||||
|
||||
@@ -278,7 +285,7 @@ async function copyArticle(articleId: number): Promise<void> {
|
||||
<p>{{ t('route.imitation.description') }}</p>
|
||||
</div>
|
||||
<div class="imitation-view__header-actions">
|
||||
<a-button type="primary" @click="openCreate">
|
||||
<a-button type="primary" :disabled="!companyStore.currentBrandId" @click="openCreate">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
{{ t('imitation.actions.create') }}
|
||||
</a-button>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
isKolVariableValueEmpty,
|
||||
normalizeKolVariableDefinition,
|
||||
} from '@/lib/kol-placeholders'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
import { LeftOutlined } from '@ant-design/icons-vue'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { message } from 'ant-design-vue'
|
||||
@@ -20,6 +21,7 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const { t } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const subscriptionPromptId = computed(() => String(route.params.subscriptionPromptId))
|
||||
const sourcePage = computed<'workspace' | 'templates' | null>(() => {
|
||||
@@ -106,6 +108,10 @@ function handleBack() {
|
||||
function handleSubmit() {
|
||||
const schemaValue = schemaQuery.data.value
|
||||
if (!schemaValue?.schema_json?.variables) return
|
||||
if (!companyStore.currentBrandId) {
|
||||
message.warning(t('templates.wizard.messages.selectBrandBeforeQuestion'))
|
||||
return
|
||||
}
|
||||
|
||||
const missingLabels: string[] = []
|
||||
schemaValue.schema_json.variables.forEach((v) => {
|
||||
|
||||
@@ -97,10 +97,12 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import AnimatedCharacters from '@/components/login-animation/AnimatedCharacters.vue'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
const REMEMBER_STORAGE_KEY = 'geo:admin:login:remember'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
@@ -131,6 +133,9 @@ onMounted(() => {
|
||||
formState.identifier = parsed.identifier
|
||||
remember.value = true
|
||||
}
|
||||
if (parsed && typeof parsed.password === 'string') {
|
||||
formState.password = parsed.password
|
||||
}
|
||||
} catch {
|
||||
window.localStorage.removeItem(REMEMBER_STORAGE_KEY)
|
||||
}
|
||||
@@ -143,7 +148,10 @@ function persistRememberedCredentials(): void {
|
||||
if (remember.value) {
|
||||
window.localStorage.setItem(
|
||||
REMEMBER_STORAGE_KEY,
|
||||
JSON.stringify({ identifier: formState.identifier }),
|
||||
JSON.stringify({
|
||||
identifier: formState.identifier,
|
||||
password: formState.password,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
window.localStorage.removeItem(REMEMBER_STORAGE_KEY)
|
||||
@@ -159,6 +167,7 @@ async function handleSubmit(): Promise<void> {
|
||||
|
||||
try {
|
||||
await authStore.login(formState)
|
||||
companyStore.reset()
|
||||
persistRememberedCredentials()
|
||||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/workspace'
|
||||
await router.replace(redirect)
|
||||
|
||||
@@ -20,6 +20,7 @@ import { formatDateTime } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { resolvePlatformLogoUrl } from '@/lib/publish-account-cards'
|
||||
import { getPublishPlatformMeta, normalizePublishPlatformId } from '@/lib/publish-platforms'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
const INLINE_ERROR_HEAD_CHARS = 52
|
||||
@@ -307,6 +308,7 @@ function buildDesktopWorkbenchUrl(record: {
|
||||
|
||||
const { t } = useI18n()
|
||||
const queryClient = useQueryClient()
|
||||
const companyStore = useCompanyStore()
|
||||
const page = ref(1)
|
||||
const searchTitle = ref('')
|
||||
const appliedTitle = ref('')
|
||||
@@ -321,8 +323,10 @@ const tasksQuery = useQuery({
|
||||
queryKey: computed(() => [
|
||||
'tenant',
|
||||
'publish-tasks',
|
||||
companyStore.currentBrandId,
|
||||
{ page: page.value, page_size: PAGE_SIZE, title: appliedTitle.value },
|
||||
]),
|
||||
enabled: computed(() => Boolean(companyStore.currentBrandId)),
|
||||
queryFn: () =>
|
||||
publishTasksApi.list({
|
||||
page: page.value,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -42,10 +42,12 @@ import {
|
||||
getTemplateMeta,
|
||||
} from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const pickerOpen = ref(false)
|
||||
const pickerMode = ref<'general' | 'refined' | null>(null)
|
||||
@@ -90,7 +92,8 @@ const kolCardsQuery = useQuery({
|
||||
})
|
||||
|
||||
const recentArticlesQuery = useQuery({
|
||||
queryKey: ['workspace', 'recent-articles', 'templates'],
|
||||
queryKey: computed(() => ['workspace', 'recent-articles', 'templates', companyStore.currentBrandId]),
|
||||
enabled: computed(() => Boolean(companyStore.currentBrandId)),
|
||||
queryFn: () => workspaceApi.recentArticles(),
|
||||
})
|
||||
|
||||
@@ -131,7 +134,8 @@ const articleParams = computed<ArticleListParams>(() => {
|
||||
})
|
||||
|
||||
const articleListQuery = useQuery({
|
||||
queryKey: computed(() => ['articles', 'list', articleParams.value]),
|
||||
queryKey: computed(() => ['articles', 'list', companyStore.currentBrandId, articleParams.value]),
|
||||
enabled: computed(() => Boolean(companyStore.currentBrandId)),
|
||||
queryFn: () => articlesApi.list(articleParams.value),
|
||||
})
|
||||
|
||||
@@ -330,6 +334,10 @@ function resetFilters(): void {
|
||||
}
|
||||
|
||||
function openTemplatePicker(): void {
|
||||
if (!companyStore.currentBrandId) {
|
||||
message.warning(t('templates.wizard.messages.selectBrandBeforeQuestion'))
|
||||
return
|
||||
}
|
||||
pickerOpen.value = true
|
||||
pickerMode.value = 'general'
|
||||
}
|
||||
@@ -339,12 +347,20 @@ function choosePickerMode(mode: 'general' | 'refined'): void {
|
||||
}
|
||||
|
||||
function startTemplate(template: TemplateListItem): void {
|
||||
if (!companyStore.currentBrandId) {
|
||||
message.warning(t('templates.wizard.messages.selectBrandBeforeQuestion'))
|
||||
return
|
||||
}
|
||||
pickerOpen.value = false
|
||||
pickerMode.value = null
|
||||
void router.push({ path: '/articles/wizard', query: { template_id: String(template.id) } })
|
||||
}
|
||||
|
||||
function openRefinedTemplate(card: KolWorkspaceCard): void {
|
||||
if (!companyStore.currentBrandId) {
|
||||
message.warning(t('templates.wizard.messages.selectBrandBeforeQuestion'))
|
||||
return
|
||||
}
|
||||
pickerOpen.value = false
|
||||
pickerMode.value = null
|
||||
void router.push({
|
||||
@@ -471,7 +487,12 @@ function refreshRecords(): void {
|
||||
<p>{{ t('route.templates.description') }}</p>
|
||||
</div>
|
||||
<div class="templates-view__header-actions">
|
||||
<a-button type="primary" class="templates-view__create-btn" @click="openTemplatePicker">
|
||||
<a-button
|
||||
type="primary"
|
||||
class="templates-view__create-btn"
|
||||
:disabled="!companyStore.currentBrandId"
|
||||
@click="openTemplatePicker"
|
||||
>
|
||||
<template #icon><PlusOutlined /></template>
|
||||
{{ t('templates.actions.chooseTemplate') }}
|
||||
</a-button>
|
||||
|
||||
@@ -17,15 +17,18 @@ import { monitoringApi } from '@/lib/api'
|
||||
import { formatDateTime } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { isMonitoringPlatformId, normalizeMonitoringPlatformId } from '@/lib/monitoring-platforms'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const trackingMaxHistoryDays = 30
|
||||
const trackingToday = dayjs().format('YYYY-MM-DD')
|
||||
|
||||
const brandId = computed(() => parsePositiveNumber(route.params.brandId))
|
||||
const routeBrandId = computed(() => parsePositiveNumber(route.params.brandId))
|
||||
const brandId = computed(() => companyStore.currentBrandId ?? routeBrandId.value)
|
||||
const questionId = computed(() => parsePositiveNumber(route.params.questionId))
|
||||
const questionHash = computed(() => normalizeQueryValue(route.query.question_hash))
|
||||
const questionTitleFallback = computed(() => normalizeQueryValue(route.query.question_text))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import type {
|
||||
Brand,
|
||||
DesktopAccountInfo,
|
||||
MonitoringHotQuestion,
|
||||
MonitoringPlatformAuthorizationStatus,
|
||||
@@ -25,10 +24,12 @@ import {
|
||||
normalizeMonitoringPlatformFilter,
|
||||
normalizeMonitoringPlatformId,
|
||||
} from '@/lib/monitoring-platforms'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const trackingMaxHistoryDays = 30
|
||||
const trackingBusinessTimeZone = 'Asia/Shanghai'
|
||||
@@ -98,7 +99,6 @@ const trackingPlatformOptions = [
|
||||
] as const
|
||||
const allQuestionOptionValue = 'all'
|
||||
|
||||
const selectedBrandId = useStorage<number | null>('tracking_selected_brand', null)
|
||||
const selectedQuestionId = useStorage<number | null>('tracking_selected_question', null)
|
||||
const selectedPlatformId = useStorage<string>('tracking_selected_ai_platform_id', 'all')
|
||||
const selectedBusinessDate = useStorage<string>('tracking_selected_business_date', trackingToday)
|
||||
@@ -117,32 +117,8 @@ const selectedQuestionSetOptionValue = computed<string | number>({
|
||||
},
|
||||
})
|
||||
|
||||
const brandsQuery = useQuery({
|
||||
queryKey: ['tracking', 'brands'],
|
||||
queryFn: () => brandsApi.list(),
|
||||
})
|
||||
|
||||
watch(
|
||||
() => brandsQuery.data.value,
|
||||
(brands) => {
|
||||
if (!brands?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const requestedBrandId = parseNumericQuery(route.query.brand_id)
|
||||
if (requestedBrandId && brands.some((item) => item.id === requestedBrandId)) {
|
||||
selectedBrandId.value = requestedBrandId
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedBrandId.value && brands.some((item) => item.id === selectedBrandId.value)) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedBrandId.value = brands[0].id
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
const selectedBrandId = computed(() => companyStore.currentBrandId)
|
||||
const selectedBrand = computed(() => companyStore.currentBrand)
|
||||
|
||||
const questionsQuery = useQuery({
|
||||
queryKey: computed(() => ['tracking', 'questions', selectedBrandId.value]),
|
||||
@@ -325,10 +301,6 @@ const collectNowMutation = useMutation({
|
||||
},
|
||||
})
|
||||
|
||||
const selectedBrand = computed<Brand | null>(() => {
|
||||
return brandsQuery.data.value?.find((item) => item.id === selectedBrandId.value) ?? null
|
||||
})
|
||||
|
||||
const currentUserClientOnline = computed(() => {
|
||||
return dashboardQuery.data.value?.runtime.current_user_client_online === true
|
||||
})
|
||||
@@ -850,17 +822,6 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
||||
:options="trackingPlatformOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="tracking-action-item">
|
||||
<span class="tracking-action-label">品牌</span>
|
||||
<a-select
|
||||
v-model:value="selectedBrandId"
|
||||
class="tracking-select"
|
||||
:placeholder="t('tracking.brandPlaceholder')"
|
||||
:options="
|
||||
(brandsQuery.data.value ?? []).map((item) => ({ label: item.name, value: item.id }))
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="tracking-action-item">
|
||||
<span class="tracking-action-label">关键词库(搜索词)</span>
|
||||
<a-select
|
||||
|
||||
@@ -32,9 +32,11 @@ import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
|
||||
import { formatDateTime, getTemplateMeta, hasActiveGenerationStatus } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { isMediaPublishAccount } from '@/lib/publish-platforms'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const router = useRouter()
|
||||
const companyStore = useCompanyStore()
|
||||
const { t } = useI18n()
|
||||
const selectedArticleId = ref<number | null>(null)
|
||||
const articleDrawerOpen = ref(false)
|
||||
@@ -43,16 +45,18 @@ const publishModalOpen = ref(false)
|
||||
let recentArticlesPollingTimer: number | null = null
|
||||
|
||||
const overviewQuery = useQuery({
|
||||
queryKey: ['workspace', 'overview'],
|
||||
queryKey: computed(() => ['workspace', 'overview', companyStore.currentBrandId]),
|
||||
queryFn: () => workspaceApi.overview(),
|
||||
enabled: computed(() => Boolean(companyStore.currentBrandId)),
|
||||
})
|
||||
const accountsQuery = useQuery({
|
||||
queryKey: ['tenant', 'desktop-accounts', 'workspace-overview'],
|
||||
queryFn: () => tenantAccountsApi.list(),
|
||||
})
|
||||
const recentArticlesQuery = useQuery({
|
||||
queryKey: ['workspace', 'recent-articles'],
|
||||
queryKey: computed(() => ['workspace', 'recent-articles', companyStore.currentBrandId]),
|
||||
queryFn: () => workspaceApi.recentArticles(),
|
||||
enabled: computed(() => Boolean(companyStore.currentBrandId)),
|
||||
})
|
||||
const templateCardsQuery = useQuery({
|
||||
queryKey: ['workspace', 'template-cards'],
|
||||
@@ -177,6 +181,10 @@ function resolveTemplateIcon(template: TemplateCard): unknown {
|
||||
}
|
||||
|
||||
function openTemplate(template: TemplateCard): void {
|
||||
if (!companyStore.currentBrandId) {
|
||||
message.warning(t('templates.wizard.messages.selectBrandBeforeQuestion'))
|
||||
return
|
||||
}
|
||||
void router.push({
|
||||
path: '/articles/wizard',
|
||||
query: { template_id: String(template.id), from: 'workspace' },
|
||||
@@ -184,6 +192,10 @@ function openTemplate(template: TemplateCard): void {
|
||||
}
|
||||
|
||||
function openRefinedTemplate(card: KolWorkspaceCard): void {
|
||||
if (!companyStore.currentBrandId) {
|
||||
message.warning(t('templates.wizard.messages.selectBrandBeforeQuestion'))
|
||||
return
|
||||
}
|
||||
void router.push({
|
||||
name: 'kol-generate',
|
||||
params: { subscriptionPromptId: String(card.subscription_prompt_id) },
|
||||
@@ -192,16 +204,28 @@ function openRefinedTemplate(card: KolWorkspaceCard): void {
|
||||
}
|
||||
|
||||
function openArticle(articleId: number): void {
|
||||
if (!companyStore.currentBrandId) {
|
||||
message.warning(t('templates.wizard.messages.selectBrandBeforeQuestion'))
|
||||
return
|
||||
}
|
||||
selectedArticleId.value = articleId
|
||||
articleDrawerOpen.value = true
|
||||
}
|
||||
|
||||
function openPublish(articleId: number): void {
|
||||
if (!companyStore.currentBrandId) {
|
||||
message.warning(t('templates.wizard.messages.selectBrandBeforeQuestion'))
|
||||
return
|
||||
}
|
||||
selectedPublishArticleId.value = articleId
|
||||
publishModalOpen.value = true
|
||||
}
|
||||
|
||||
async function openEditor(article: RecentArticle): Promise<void> {
|
||||
if (!companyStore.currentBrandId) {
|
||||
message.warning(t('templates.wizard.messages.selectBrandBeforeQuestion'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
const detail = await articlesApi.detail(article.id)
|
||||
|
||||
@@ -228,6 +252,10 @@ async function openEditor(article: RecentArticle): Promise<void> {
|
||||
}
|
||||
|
||||
async function copyArticle(articleId: number): Promise<void> {
|
||||
if (!companyStore.currentBrandId) {
|
||||
message.warning(t('templates.wizard.messages.selectBrandBeforeQuestion'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
const detail = await articlesApi.detail(articleId)
|
||||
const content = buildArticleClipboardContent(detail)
|
||||
@@ -245,17 +273,23 @@ async function copyArticle(articleId: number): Promise<void> {
|
||||
}
|
||||
|
||||
async function handleDelete(articleId: number): Promise<void> {
|
||||
if (!companyStore.currentBrandId) {
|
||||
message.warning(t('templates.wizard.messages.selectBrandBeforeQuestion'))
|
||||
return
|
||||
}
|
||||
await deleteMutation.mutateAsync(articleId)
|
||||
}
|
||||
|
||||
function refreshDashboard(): void {
|
||||
void Promise.all([
|
||||
overviewQuery.refetch(),
|
||||
const refreshes: Promise<unknown>[] = [
|
||||
accountsQuery.refetch(),
|
||||
recentArticlesQuery.refetch(),
|
||||
templateCardsQuery.refetch(),
|
||||
kolCardsQuery.refetch(),
|
||||
])
|
||||
]
|
||||
if (companyStore.currentBrandId) {
|
||||
refreshes.push(overviewQuery.refetch(), recentArticlesQuery.refetch())
|
||||
}
|
||||
void Promise.all(refreshes)
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -285,7 +319,13 @@ function startRecentArticlesPolling(): void {
|
||||
}
|
||||
|
||||
recentArticlesPollingTimer = window.setInterval(() => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['workspace', 'recent-articles'] })
|
||||
if (!companyStore.currentBrandId) {
|
||||
stopRecentArticlesPolling()
|
||||
return
|
||||
}
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ['workspace', 'recent-articles', companyStore.currentBrandId],
|
||||
})
|
||||
void queryClient.invalidateQueries({ queryKey: ['articles'] })
|
||||
void recentArticlesQuery.refetch()
|
||||
}, 3000)
|
||||
|
||||
Reference in New Issue
Block a user