feat(enterprise-site): add WordPress support and scheduled auto-publish to sites
Frontend CI / Frontend (push) Successful in 4m17s
Backend CI / Backend (push) Failing after 6m42s

Add WordPress as an enterprise-site CMS type alongside pbootcms, and let
schedule tasks auto-publish generated articles to enterprise sites.

- WordPress connection/publisher service and tests; register wordpress
  media platform and relax cms_type CHECK constraint
- New publish_enterprise_site_targets JSONB column on schedule_tasks with
  array type constraint; thread targets through scheduler dispatch,
  prompt/KOL generation, and worker
- Admin UI: enterprise-site targets in generate/schedule/publish flows,
  KOL package form, MediaView, and i18n strings

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 19:44:24 +08:00
parent 41f5623791
commit c7bad83496
30 changed files with 2946 additions and 696 deletions
@@ -54,6 +54,7 @@ interface EnterpriseSiteCard {
id: number
name: string
siteUrl: string
cmsTypeId: string
cmsType: string
status: string
statusText: string
@@ -265,6 +266,9 @@ const accountCards = computed<PublishAccountCard[]>(() =>
const enterpriseSites = computed(() => enterpriseSitesQuery.data.value ?? [])
const enterpriseSiteCards = computed<EnterpriseSiteCard[]>(() =>
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<EnterpriseSiteCard[]>(() =>
)
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) {
/>
</div>
<div v-if="enterpriseSiteCards.length" class="publish-modal__list">
<div
v-if="enterpriseSiteCards.length"
class="publish-modal__list publish-modal__list--enterprise"
>
<article
v-for="site in enterpriseSiteCards"
:key="site.id"
@@ -1758,12 +1781,22 @@ function decisionAlertType(result: ComplianceCheckResult | null | undefined) {
:options="enterpriseCategoryOptions(site.id)"
:loading="syncEnterpriseCategoriesMutation.isPending.value"
:get-popup-container="enterpriseSelectPopupContainer"
:dropdown-match-select-width="true"
:dropdown-style="{ maxWidth: 'calc(100vw - 96px)' }"
popup-class-name="publish-modal__enterprise-select-dropdown"
class="publish-modal__enterprise-select"
placeholder="选择栏目"
@focus="loadEnterpriseCategories(site.id)"
@click.stop
/>
>
<template #option="item">
<a-tooltip :title="item.label" placement="left">
<div class="publish-modal__select-option-text">
{{ item.label }}
</div>
</a-tooltip>
</template>
</a-select>
<a-button
class="publish-modal__sync-btn"
:loading="syncEnterpriseCategoriesMutation.isPending.value"
@@ -1781,10 +1814,7 @@ function decisionAlertType(result: ComplianceCheckResult | null | undefined) {
</article>
</div>
<a-empty
v-else
description="当前还没有企业自建站点,请先到媒体管理添加 PBootCMS 站点。"
/>
<a-empty v-else description="当前还没有企业自建站点,请先到媒体管理添加站点连接。" />
</template>
</a-tab-pane>
</a-tabs>
@@ -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;