diff --git a/apps/admin-web/src/App.vue b/apps/admin-web/src/App.vue index b6d015d..6d76dc9 100644 --- a/apps/admin-web/src/App.vue +++ b/apps/admin-web/src/App.vue @@ -12,6 +12,7 @@ import ImageUploadProgressOverlay from '@/components/ImageUploadProgressOverlay. colorPrimary: '#1677ff', borderRadius: 6, colorBgLayout: '#f0f2f5', + zIndexPopupBase: 3000, }, }" > diff --git a/apps/admin-web/src/components/ArticlePublishStatus.vue b/apps/admin-web/src/components/ArticlePublishStatus.vue index b29b78a..b3ba628 100644 --- a/apps/admin-web/src/components/ArticlePublishStatus.vue +++ b/apps/admin-web/src/components/ArticlePublishStatus.vue @@ -251,6 +251,13 @@ function enterprisePlatformFallback(platformId: string) { accent: '#0f766e', } } + if (platformId === 'wordpress') { + return { + name: 'WordPress', + shortName: 'WP', + accent: '#21759b', + } + } return getPublishPlatformMeta(platformId) } diff --git a/apps/admin-web/src/components/GenerateTaskDrawer.vue b/apps/admin-web/src/components/GenerateTaskDrawer.vue index 5ba158d..49e3cdd 100644 --- a/apps/admin-web/src/components/GenerateTaskDrawer.vue +++ b/apps/admin-web/src/components/GenerateTaskDrawer.vue @@ -4,12 +4,16 @@ import { MinusCircleFilled, PlusOutlined, ScheduleOutlined, + SyncOutlined, } from '@ant-design/icons-vue' import type { + EnterpriseSiteCategory, + EnterpriseSiteConnection, JsonValue, KolSubscriptionPromptCard, KolVariableDefinition, PromptRuleSimple, + ScheduleEnterpriseSiteTarget, ScheduleKind, ScheduleTargetType, ScheduleTask, @@ -25,6 +29,7 @@ import KolDynamicForm from '@/components/kol/KolDynamicForm.vue' import KolPlatformTags from '@/components/kol/KolPlatformTags.vue' import PromptRuleModal from '@/components/PromptRuleModal.vue' import { + enterpriseSitesApi, generateApi, imagesApi, kolGenerateApi, @@ -67,6 +72,21 @@ const companyStore = useCompanyStore() const promptDrawerOpen = ref(false) const coverPickerOpen = ref(false) +type SchedulePublishTargetTab = 'media_accounts' | 'enterprise_sites' +type SchedulePublishType = 'publish' | 'draft' + +interface ScheduleEnterpriseSiteCard { + id: number + name: string + siteUrl: string + cmsType: string + status: string + statusText: string + selectable: boolean + disabledReason: string + categoryCount: number + defaultCategoryId: string +} const scheduleDayOptions = [ { labelKey: 'custom.schedule.weekdays.mon', value: 'mon' }, @@ -87,7 +107,11 @@ const form = reactive({ kolEnableWebSearch: false, kolKnowledgeGroupIds: [] as number[], autoPublish: false, + publishTargetTab: 'media_accounts' as SchedulePublishTargetTab, publishAccountIds: [] as string[], + publishEnterpriseSiteIds: [] as number[], + enterpriseSiteCategoryIds: {} as Record, + enterprisePublishType: 'publish' as SchedulePublishType, coverEnabled: false, coverAssetUrl: '', coverFileName: '', @@ -143,6 +167,12 @@ const accountsQuery = useQuery({ queryFn: () => tenantAccountsApi.list(), }) +const enterpriseSitesQuery = useQuery({ + queryKey: ['tenant', 'enterprise-sites', 'schedule-task-drawer'], + enabled: schedulePublishQueryEnabled, + queryFn: () => enterpriseSitesApi.list(), +}) + const promptOptions = computed(() => (rulesQuery.data.value ?? []).map((rule: PromptRuleSimple) => ({ label: rule.name, @@ -179,8 +209,37 @@ const schedulePublishAccounts = computed(() => const schedulePublishLoading = computed( () => platformsQuery.isPending.value || accountsQuery.isPending.value, ) +const scheduleEnterpriseSitesLoading = computed(() => enterpriseSitesQuery.isPending.value) + +const scheduleEnterpriseSites = computed(() => enterpriseSitesQuery.data.value ?? []) + +const scheduleEnterpriseSiteCards = computed(() => + scheduleEnterpriseSites.value.map((site) => { + const siteName = + site.name?.trim() || site.site_name?.trim() || site.site_url?.trim() || '企业站点' + const status = enterpriseSiteConnectionStatus(site) + const disabledReason = resolveScheduleEnterpriseSiteDisabledReason(site, status) + return { + id: site.id, + name: siteName, + siteUrl: site.site_url, + cmsType: cmsTypeLabel(site.cms_type), + status, + statusText: enterpriseSiteStatusLabel(status), + selectable: !disabledReason, + disabledReason, + categoryCount: site.category_count, + defaultCategoryId: site.default_scode?.trim() || '', + } + }), +) + +const enterpriseCategoriesBySiteId = ref>({}) const selectedSchedulePublishPlatformIds = computed(() => { + if (!form.autoPublish || form.publishTargetTab !== 'media_accounts') { + return [] + } const selected = new Set(form.publishAccountIds) return Array.from( new Set( @@ -241,6 +300,23 @@ watch( }, ) +watch( + scheduleEnterpriseSiteCards, + (cards) => { + if (enterpriseSitesQuery.isPending.value || enterpriseSitesQuery.isFetching.value) { + return + } + if (cards.length === 0) { + return + } + const selectableIds = new Set(cards.filter((card) => card.selectable).map((card) => card.id)) + form.publishEnterpriseSiteIds = form.publishEnterpriseSiteIds.filter((id) => + selectableIds.has(id), + ) + }, + { immediate: true }, +) + watch(currentBrandId, () => { form.promptRuleId = undefined }) @@ -331,6 +407,16 @@ const updateScheduleMutation = useMutation({ onError: (error) => message.error(formatError(error)), }) +const syncEnterpriseCategoriesMutation = useMutation({ + mutationFn: (siteId: number) => enterpriseSitesApi.syncCategories(siteId), + onSuccess: (categories, siteId) => { + setEnterpriseCategories(siteId, categories) + void queryClient.invalidateQueries({ queryKey: ['tenant', 'enterprise-sites'] }) + message.success(t('custom.messages.enterpriseCategoriesSynced')) + }, + onError: (error) => message.error(formatError(error)), +}) + const instantGenerateMutation = useMutation({ mutationFn: () => generateApi.fromRule(buildInstantPayload()), onSuccess: async () => { @@ -362,7 +448,27 @@ function hydrateForm(): void { form.kolEnableWebSearch = Boolean(task?.generation_input?.enable_web_search) form.kolKnowledgeGroupIds = normalizeNumberList(task?.generation_input?.knowledge_group_ids) form.autoPublish = isSchedule.value ? Boolean(task?.auto_publish) : false + form.publishTargetTab = + task?.publish_enterprise_site_targets?.length && !task?.publish_account_ids?.length + ? 'enterprise_sites' + : 'media_accounts' form.publishAccountIds = isSchedule.value ? [...(task?.publish_account_ids ?? [])] : [] + const enterpriseTargets = isSchedule.value ? (task?.publish_enterprise_site_targets ?? []) : [] + form.publishEnterpriseSiteIds = enterpriseTargets.map((target) => target.site_id) + form.enterpriseSiteCategoryIds = enterpriseTargets.reduce>( + (acc, target) => { + acc[target.site_id] = target.category_id + return acc + }, + {}, + ) + form.enterprisePublishType = + enterpriseTargets.find((target) => target.publish_type === 'draft')?.publish_type === 'draft' + ? 'draft' + : 'publish' + enterpriseTargets.forEach((target) => { + void loadEnterpriseCategories(target.site_id) + }) form.coverAssetUrl = isSchedule.value ? resolveApiURL(task?.cover_asset_url) : '' form.coverFileName = deriveCoverFileName(form.coverAssetUrl) form.coverImageAssetId = isSchedule.value ? (task?.cover_image_asset_id ?? null) : null @@ -400,6 +506,12 @@ function selectDefaultSubscriptionPrompt(): void { function buildSchedulePayload() { const coverEnabled = form.autoPublish && effectiveScheduleCoverEnabled.value const coverAssetUrl = coverEnabled ? form.coverAssetUrl.trim() : '' + const publishAccountIds = + form.autoPublish && form.publishTargetTab === 'media_accounts' ? form.publishAccountIds : [] + const publishEnterpriseSiteTargets = + form.autoPublish && form.publishTargetTab === 'enterprise_sites' + ? buildScheduleEnterpriseSiteTargets() + : [] const basePayload = { name: form.name.trim(), target_type: form.targetType, @@ -414,7 +526,8 @@ function buildSchedulePayload() { random_window_start: '01:00:00', random_window_end: '05:00:00', auto_publish: form.autoPublish, - publish_account_ids: form.autoPublish ? form.publishAccountIds : [], + publish_account_ids: publishAccountIds, + publish_enterprise_site_targets: publishEnterpriseSiteTargets, cover_asset_url: coverAssetUrl || null, cover_image_asset_id: coverAssetUrl ? form.coverImageAssetId : null, enable_web_search: form.targetType === 'prompt_rule' ? form.enableWebSearch : false, @@ -476,6 +589,88 @@ function isSchedulePublishAccountSelected(accountId: string): boolean { return form.publishAccountIds.includes(accountId) } +function buildScheduleEnterpriseSiteTargets(): ScheduleEnterpriseSiteTarget[] { + return form.publishEnterpriseSiteIds + .map((siteId) => ({ + site_id: siteId, + category_id: form.enterpriseSiteCategoryIds[siteId]?.trim() ?? '', + publish_type: form.enterprisePublishType, + })) + .filter((target) => target.site_id > 0 && target.category_id) +} + +function toggleScheduleEnterpriseSite(site: ScheduleEnterpriseSiteCard): void { + if (!site.selectable) { + return + } + if (form.publishEnterpriseSiteIds.includes(site.id)) { + form.publishEnterpriseSiteIds = form.publishEnterpriseSiteIds.filter((id) => id !== site.id) + return + } + form.publishEnterpriseSiteIds = [...form.publishEnterpriseSiteIds, site.id] + if (!form.enterpriseSiteCategoryIds[site.id]) { + form.enterpriseSiteCategoryIds = { + ...form.enterpriseSiteCategoryIds, + [site.id]: site.defaultCategoryId, + } + } + void loadEnterpriseCategories(site.id) +} + +function isScheduleEnterpriseSiteSelected(siteId: number): boolean { + return form.publishEnterpriseSiteIds.includes(siteId) +} + +function enterpriseCategoryOptions(siteId: number) { + return (enterpriseCategoriesBySiteId.value[siteId] ?? []).map((category) => ({ + label: enterpriseCategoryLabel(category), + value: category.remote_id, + })) +} + +function enterpriseCategoryLabel(category: EnterpriseSiteCategory): string { + const slug = category.slug?.trim() + return slug ? `${category.name} / ${slug}` : category.name +} + +function setEnterpriseCategories(siteId: number, categories: EnterpriseSiteCategory[]): void { + enterpriseCategoriesBySiteId.value = { + ...enterpriseCategoriesBySiteId.value, + [siteId]: categories, + } + const current = form.enterpriseSiteCategoryIds[siteId]?.trim() + const currentExists = categories.some((category) => category.remote_id === current) + if ((!current || !currentExists) && categories.length > 0) { + form.enterpriseSiteCategoryIds = { + ...form.enterpriseSiteCategoryIds, + [siteId]: categories[0].remote_id, + } + } +} + +async function loadEnterpriseCategories(siteId: number): Promise { + if (enterpriseCategoriesBySiteId.value[siteId]) { + return + } + try { + const categories = await enterpriseSitesApi.categories(siteId) + setEnterpriseCategories(siteId, categories) + } catch { + enterpriseCategoriesBySiteId.value = { + ...enterpriseCategoriesBySiteId.value, + [siteId]: [], + } + } +} + +function syncEnterpriseCategories(siteId: number): void { + syncEnterpriseCategoriesMutation.mutate(siteId) +} + +function enterpriseSelectPopupContainer(triggerNode?: HTMLElement | null): HTMLElement { + return (triggerNode?.closest('.ant-drawer-content') as HTMLElement | null) ?? document.body +} + async function uploadScheduleCover( file: File, ): Promise<{ url: string; fileName: string; assetId?: number | null }> { @@ -568,11 +763,28 @@ function validateForm(): boolean { } if (isSchedule.value && form.autoPublish) { - if (form.publishAccountIds.length === 0) { - message.warning(t('custom.messages.missingPublishAccounts')) + if (form.publishTargetTab === 'media_accounts' && form.publishAccountIds.length === 0) { + message.warning(t('custom.messages.missingPublishTargets')) return false } - if (scheduleCoverRequired.value && !form.coverAssetUrl.trim()) { + if (form.publishTargetTab === 'enterprise_sites') { + if (form.publishEnterpriseSiteIds.length === 0) { + message.warning(t('custom.messages.missingPublishTargets')) + return false + } + const missingCategorySite = form.publishEnterpriseSiteIds.find( + (siteId) => !form.enterpriseSiteCategoryIds[siteId]?.trim(), + ) + if (missingCategorySite) { + message.warning(t('custom.messages.missingEnterpriseSiteCategory')) + return false + } + } + if ( + form.publishTargetTab === 'media_accounts' && + scheduleCoverRequired.value && + !form.coverAssetUrl.trim() + ) { message.warning(t('custom.messages.missingScheduleCover')) return false } @@ -637,6 +849,47 @@ function normalizeScheduleDays(days?: string[] | null): string[] { .filter((day) => allowed.has(day) && input.includes(day)) } +function cmsTypeLabel(cmsType: string): string { + switch (String(cmsType ?? '').trim()) { + case 'pbootcms': + return 'PBootCMS' + case 'wordpress': + return 'WordPress' + default: + return cmsType || 'CMS' + } +} + +function enterpriseSiteConnectionStatus(site: EnterpriseSiteConnection): string { + return String(site.status ?? '').trim() +} + +function enterpriseSiteStatusLabel(status: string): string { + switch (String(status ?? '').trim()) { + case 'connected': + return t('custom.task.enterpriseSiteConnected') + case 'error': + return t('custom.task.enterpriseSiteError') + case 'disabled': + return t('custom.task.enterpriseSiteDisabled') + default: + return t('custom.task.enterpriseSiteDraft') + } +} + +function resolveScheduleEnterpriseSiteDisabledReason( + site: EnterpriseSiteConnection, + status: string, +): string { + if (status === 'disabled') { + return t('custom.task.enterpriseSiteDisabledHint') + } + if (status === 'error') { + return site.last_error?.trim() || t('custom.task.enterpriseSiteErrorHint') + } + return '' +} + function fieldKey(key?: string | null, id?: string): string { return key?.trim() || id || '' } @@ -754,7 +1007,7 @@ function normalizeJsonRecord(value: Record): Record @@ -1001,18 +1254,29 @@ function normalizeJsonRecord(value: Record): Record
-

{{ t('custom.task.publishAccountsHint') }}

+

{{ t('custom.task.publishTargetsHint') }}

+ + -
+
{{ t('custom.task.publishAccountEmptyTitle') }}

{{ t('custom.task.publishAccountEmptyHint') }}

+ + +
@@ -1419,10 +1787,14 @@ function normalizeJsonRecord(value: Record): Record): Record): Record(() => const enterpriseSites = computed(() => enterpriseSitesQuery.data.value ?? []) const enterpriseSiteCards = computed(() => enterpriseSites.value.map((site) => { + const siteName = + site.name?.trim() || site.site_name?.trim() || site.site_url?.trim() || '企业站点' + const cmsType = String(site.cms_type ?? '').trim() const alreadyPublished = successfulEnterpriseSiteIds.value.has(site.id) const activeStatus = activeEnterpriseRecordStatusBySiteId.value.get(site.id) const connectionStatus = enterpriseSiteConnectionStatus(site) @@ -277,9 +281,10 @@ const enterpriseSiteCards = computed(() => ) return { id: site.id, - name: site.name, + name: siteName, siteUrl: site.site_url, - cmsType: cmsTypeLabel(site.cms_type), + cmsTypeId: cmsType, + cmsType: cmsTypeLabel(cmsType), status: connectionStatus, statusText: alreadyPublished ? '已发布' @@ -364,14 +369,18 @@ const selectedEnterpriseSiteCards = computed(() => { }) const modalTitle = computed(() => detailQuery.data.value?.title || t('article.untitled')) -const enterpriseTargetPlatformId = computed(() => - selectedEnterpriseSiteCards.value.length > 0 ? 'pbootcms' : '', +const enterpriseTargetPlatformIds = computed(() => + Array.from( + new Set( + selectedEnterpriseSiteCards.value + .map((site) => normalizeEnterprisePlatformId(site.cmsTypeId)) + .filter((platformId): platformId is string => Boolean(platformId)), + ), + ), ) const selectedPlatformIds = computed(() => activeTargetTab.value === 'enterprise_sites' - ? enterpriseTargetPlatformId.value - ? [enterpriseTargetPlatformId.value] - : [] + ? enterpriseTargetPlatformIds.value : Array.from(new Set(selectedCards.value.map((card) => card.platformId))), ) watch([selectedPlatformIds, activeTargetTab], () => { @@ -441,8 +450,8 @@ const publishAdmissionPassed = computed( () => complianceDisplayDecision(publishAdmissionResult.value) === 'pass', ) -function enterpriseSelectPopupContainer(triggerNode: HTMLElement): HTMLElement { - return (triggerNode.closest('.ant-modal-content') as HTMLElement | null) ?? document.body +function enterpriseSelectPopupContainer(triggerNode?: HTMLElement | null): HTMLElement { + return (triggerNode?.closest('.ant-modal-content') as HTMLElement | null) ?? document.body } const mandatoryAdmissionBlocking = computed( () => publishComplianceMandatory.value && !publishAdmissionPassed.value, @@ -878,7 +887,7 @@ async function handleEnterprisePublishSuccess( message: '企业站发布失败', description: formatStoredErrorMessage(failed[0]?.error_message) || - '请检查插件、栏目和 PBootCMS API 配置。', + '请检查站点凭证、栏目和 CMS 接口配置。', placement: 'topRight', duration: 6, }) @@ -887,7 +896,7 @@ async function handleEnterprisePublishSuccess( message: enterprisePublishType.value === 'draft' ? '企业站草稿已保存' : '企业站发布成功', description: succeeded.length === 1 - ? succeeded[0]?.external_article_url || 'PBootCMS 已返回成功。' + ? succeeded[0]?.external_article_url || 'CMS 已返回成功。' : `共 ${succeeded.length} 个企业站点已完成。`, placement: 'topRight', duration: 5, @@ -1129,15 +1138,23 @@ function cmsTypeLabel(cmsType: string): string { return 'PBootCMS' case 'wordpress': return 'WordPress' - case 'dedecms': - return 'DedeCMS' - case 'phpcms': - return 'PHPCMS' default: return cmsType || 'CMS' } } +function normalizeEnterprisePlatformId(cmsType: string): string { + const normalized = String(cmsType ?? '').trim().toLowerCase() + switch (normalized) { + case 'pbootcms': + return 'pbootcms' + case 'wordpress': + return 'wordpress' + default: + return normalized + } +} + function enterpriseSiteStatusLabel(status: string): string { switch (String(status ?? '').trim()) { case 'connected': @@ -1167,6 +1184,8 @@ function enterpriseSiteStatusColor(status: string): string { function enterpriseCategoryOptions(siteId: number) { return (enterpriseCategoriesBySiteId.value[siteId] ?? []).map((category) => ({ label: enterpriseCategoryLabel(category), + name: category.name, + slug: category.slug?.trim() ?? '', value: category.remote_id, })) } @@ -1182,7 +1201,8 @@ function setEnterpriseCategories(siteId: number, categories: EnterpriseSiteCateg [siteId]: categories, } const current = enterpriseSiteCategoryIds.value[siteId]?.trim() - if (!current && categories.length > 0) { + const currentExists = categories.some((category) => category.remote_id === current) + if ((!current || !currentExists) && categories.length > 0) { enterpriseSiteCategoryIds.value = { ...enterpriseSiteCategoryIds.value, [siteId]: categories[0].remote_id, @@ -1618,7 +1638,10 @@ function decisionAlertType(result: ComplianceCheckResult | null | undefined) { />
-
+
+ > + +
- + @@ -2157,6 +2187,12 @@ function decisionAlertType(result: ComplianceCheckResult | null | undefined) { padding-bottom: 4px; } +.publish-modal__list--enterprise { + grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); + align-items: start; + max-height: 520px; +} + /* Custom scrollbar for list */ .publish-modal__list::-webkit-scrollbar { width: 6px; @@ -2213,6 +2249,7 @@ function decisionAlertType(result: ComplianceCheckResult | null | undefined) { padding: 0; gap: 0; overflow: visible; + min-width: 0; } .publish-modal__card--enterprise:hover:not(.publish-modal__card--disabled):not( @@ -2283,17 +2320,19 @@ function decisionAlertType(result: ComplianceCheckResult | null | undefined) { .publish-modal__config-body { display: flex; - align-items: center; + flex-direction: column; + align-items: stretch; gap: 8px; width: 100%; -} - -.publish-modal__enterprise-select { - flex: 1; min-width: 0; } +.publish-modal__enterprise-select { + width: 100%; +} + .publish-modal__sync-btn { + width: 100%; flex-shrink: 0; display: inline-flex; align-items: center; @@ -2314,6 +2353,24 @@ function decisionAlertType(result: ComplianceCheckResult | null | undefined) { z-index: 3200; } +:global(.publish-modal__enterprise-select-dropdown .ant-select-item-option) { + padding: 6px 12px; +} + +:global(.publish-modal__enterprise-select-dropdown .ant-select-item-option-content) { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; +} + +.publish-modal__select-option-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; +} + .publish-modal__site-warning { margin: -4px 16px 14px; color: #b45309; @@ -2454,6 +2511,37 @@ function decisionAlertType(result: ComplianceCheckResult | null | undefined) { border-top-color: #bfdbfe; } +.publish-modal__card--enterprise .publish-modal__card-identity { + align-items: flex-start; + flex: 1; +} + +.publish-modal__card--enterprise .publish-modal__identity-copy { + gap: 4px; +} + +.publish-modal__card--enterprise .publish-modal__identity-copy strong { + white-space: normal; + overflow-wrap: anywhere; +} + +.publish-modal__card--enterprise .publish-modal__identity-copy span { + white-space: normal; + overflow: visible; + text-overflow: clip; +} + +.publish-modal__card--enterprise .publish-modal__platform-line { + align-items: flex-start; + width: 100%; +} + +.publish-modal__card--enterprise .publish-modal__platform-text { + overflow: visible; + text-overflow: clip; + overflow-wrap: anywhere; +} + .publish-modal__check { width: 18px; height: 18px; diff --git a/apps/admin-web/src/components/ScheduleTaskTab.vue b/apps/admin-web/src/components/ScheduleTaskTab.vue index be300da..89296b8 100644 --- a/apps/admin-web/src/components/ScheduleTaskTab.vue +++ b/apps/admin-web/src/components/ScheduleTaskTab.vue @@ -353,6 +353,19 @@ function dispatchStatusMeta(status?: string | null): { label: string; color: str } return { label: t('custom.schedule.notDispatched'), color: 'default' } } + +function schedulePublishTargetsText(record: ScheduleTask): string { + const parts: string[] = [] + const platformText = formatPublishPlatformList(record.auto_publish_platforms) + if (platformText !== '--') { + parts.push(platformText) + } + const enterpriseSiteCount = record.publish_enterprise_site_targets?.length ?? 0 + if (enterpriseSiteCount > 0) { + parts.push(t('custom.schedule.enterpriseSiteTargetCount', { count: enterpriseSiteCount })) + } + return parts.length ? parts.join('、') : '--' +}