feat(enterprise-site): add WordPress support and scheduled auto-publish to sites
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:
@@ -12,6 +12,7 @@ import ImageUploadProgressOverlay from '@/components/ImageUploadProgressOverlay.
|
||||
colorPrimary: '#1677ff',
|
||||
borderRadius: 6,
|
||||
colorBgLayout: '#f0f2f5',
|
||||
zIndexPopupBase: 3000,
|
||||
},
|
||||
}"
|
||||
>
|
||||
|
||||
@@ -251,6 +251,13 @@ function enterprisePlatformFallback(platformId: string) {
|
||||
accent: '#0f766e',
|
||||
}
|
||||
}
|
||||
if (platformId === 'wordpress') {
|
||||
return {
|
||||
name: 'WordPress',
|
||||
shortName: 'WP',
|
||||
accent: '#21759b',
|
||||
}
|
||||
}
|
||||
return getPublishPlatformMeta(platformId)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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<number, string>,
|
||||
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<PublishAccountCard[]>(() =>
|
||||
const schedulePublishLoading = computed(
|
||||
() => platformsQuery.isPending.value || accountsQuery.isPending.value,
|
||||
)
|
||||
const scheduleEnterpriseSitesLoading = computed(() => enterpriseSitesQuery.isPending.value)
|
||||
|
||||
const scheduleEnterpriseSites = computed(() => enterpriseSitesQuery.data.value ?? [])
|
||||
|
||||
const scheduleEnterpriseSiteCards = computed<ScheduleEnterpriseSiteCard[]>(() =>
|
||||
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<Record<number, EnterpriseSiteCategory[]>>({})
|
||||
|
||||
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<Record<number, string>>(
|
||||
(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<void> {
|
||||
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<string, unknown>): Record<string, Jso
|
||||
:title="drawerTitle"
|
||||
placement="right"
|
||||
width="1120"
|
||||
:mask-closable="false"
|
||||
:mask-closable="true"
|
||||
class="generate-task-drawer"
|
||||
@close="emit('update:open', false)"
|
||||
>
|
||||
@@ -1001,18 +1254,29 @@ function normalizeJsonRecord(value: Record<string, unknown>): Record<string, Jso
|
||||
<template v-if="form.autoPublish">
|
||||
<div class="generate-task-drawer__field">
|
||||
<label class="generate-task-drawer__label generate-task-drawer__label--required">
|
||||
{{ t('custom.task.publishAccounts') }}:
|
||||
{{ t('custom.task.publishTargets') }}:
|
||||
</label>
|
||||
<p class="generate-task-drawer__hint">{{ t('custom.task.publishAccountsHint') }}</p>
|
||||
<p class="generate-task-drawer__hint">{{ t('custom.task.publishTargetsHint') }}</p>
|
||||
|
||||
<a-segmented
|
||||
v-model:value="form.publishTargetTab"
|
||||
:options="[
|
||||
{ label: t('custom.task.publishTargetMediaAccounts'), value: 'media_accounts' },
|
||||
{ label: t('custom.task.publishTargetEnterpriseSites'), value: 'enterprise_sites' },
|
||||
]"
|
||||
class="generate-task-drawer__target-tabs"
|
||||
/>
|
||||
|
||||
<a-skeleton
|
||||
v-if="schedulePublishLoading"
|
||||
v-if="form.publishTargetTab === 'media_accounts' && schedulePublishLoading"
|
||||
active
|
||||
:title="false"
|
||||
:paragraph="{ rows: 4 }"
|
||||
/>
|
||||
<div
|
||||
v-else-if="schedulePublishAccounts.length"
|
||||
v-else-if="
|
||||
form.publishTargetTab === 'media_accounts' && schedulePublishAccounts.length
|
||||
"
|
||||
class="generate-task-drawer__account-grid"
|
||||
>
|
||||
<button
|
||||
@@ -1057,10 +1321,114 @@ function normalizeJsonRecord(value: Record<string, unknown>): Record<string, Jso
|
||||
</a-tooltip>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="generate-task-drawer__empty">
|
||||
<div
|
||||
v-else-if="form.publishTargetTab === 'media_accounts'"
|
||||
class="generate-task-drawer__empty"
|
||||
>
|
||||
<strong>{{ t('custom.task.publishAccountEmptyTitle') }}</strong>
|
||||
<p>{{ t('custom.task.publishAccountEmptyHint') }}</p>
|
||||
</div>
|
||||
|
||||
<a-skeleton
|
||||
v-if="form.publishTargetTab === 'enterprise_sites' && scheduleEnterpriseSitesLoading"
|
||||
active
|
||||
:title="false"
|
||||
:paragraph="{ rows: 4 }"
|
||||
/>
|
||||
<template v-else-if="form.publishTargetTab === 'enterprise_sites'">
|
||||
<div class="generate-task-drawer__enterprise-toolbar">
|
||||
<span>{{ t('custom.task.enterprisePublishType') }}</span>
|
||||
<a-segmented
|
||||
v-model:value="form.enterprisePublishType"
|
||||
:options="[
|
||||
{ label: t('custom.task.enterprisePublishNow'), value: 'publish' },
|
||||
{ label: t('custom.task.enterprisePublishDraft'), value: 'draft' },
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="scheduleEnterpriseSiteCards.length"
|
||||
class="generate-task-drawer__account-grid"
|
||||
>
|
||||
<article
|
||||
v-for="site in scheduleEnterpriseSiteCards"
|
||||
:key="site.id"
|
||||
class="generate-task-drawer__site-card"
|
||||
:class="{
|
||||
'generate-task-drawer__site-card--active': isScheduleEnterpriseSiteSelected(
|
||||
site.id,
|
||||
),
|
||||
'generate-task-drawer__site-card--disabled': !site.selectable,
|
||||
}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="generate-task-drawer__site-main"
|
||||
:aria-disabled="!site.selectable"
|
||||
@click="toggleScheduleEnterpriseSite(site)"
|
||||
>
|
||||
<span class="generate-task-drawer__account-check">
|
||||
<span v-if="isScheduleEnterpriseSiteSelected(site.id)"></span>
|
||||
</span>
|
||||
<span class="generate-task-drawer__site-avatar">
|
||||
{{ site.name.slice(0, 1).toUpperCase() }}
|
||||
</span>
|
||||
<span class="generate-task-drawer__account-copy">
|
||||
<strong>{{ site.name }}</strong>
|
||||
<small>{{ site.cmsType }} · {{ site.siteUrl }}</small>
|
||||
</span>
|
||||
<a-tooltip :title="site.disabledReason || site.statusText" placement="top">
|
||||
<span
|
||||
class="generate-task-drawer__account-status"
|
||||
:class="
|
||||
site.status === 'connected'
|
||||
? 'generate-task-drawer__account-status--immediate'
|
||||
: 'generate-task-drawer__account-status--unavailable'
|
||||
"
|
||||
>
|
||||
{{ site.statusText }}
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="isScheduleEnterpriseSiteSelected(site.id)"
|
||||
class="generate-task-drawer__site-config"
|
||||
@click.stop
|
||||
>
|
||||
<a-select
|
||||
v-model:value="form.enterpriseSiteCategoryIds[site.id]"
|
||||
:options="enterpriseCategoryOptions(site.id)"
|
||||
:loading="syncEnterpriseCategoriesMutation.isPending.value"
|
||||
:placeholder="t('custom.task.enterpriseCategoryPlaceholder')"
|
||||
:get-popup-container="enterpriseSelectPopupContainer"
|
||||
:dropdown-match-select-width="true"
|
||||
popup-class-name="generate-task-drawer__enterprise-select-dropdown"
|
||||
class="generate-task-drawer__site-category-select"
|
||||
@focus="loadEnterpriseCategories(site.id)"
|
||||
@click.stop
|
||||
/>
|
||||
<a-button
|
||||
class="generate-task-drawer__site-sync"
|
||||
:loading="syncEnterpriseCategoriesMutation.isPending.value"
|
||||
@click="syncEnterpriseCategories(site.id)"
|
||||
>
|
||||
<template #icon><SyncOutlined /></template>
|
||||
{{ t('custom.task.enterpriseSyncCategories') }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<p v-if="site.disabledReason" class="generate-task-drawer__site-warning">
|
||||
{{ site.disabledReason }}
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else class="generate-task-drawer__empty">
|
||||
<strong>{{ t('custom.task.enterpriseSiteEmptyTitle') }}</strong>
|
||||
<p>{{ t('custom.task.enterpriseSiteEmptyHint') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="generate-task-drawer__field">
|
||||
@@ -1419,10 +1787,14 @@ function normalizeJsonRecord(value: Record<string, unknown>): Record<string, Jso
|
||||
|
||||
.generate-task-drawer__account-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.generate-task-drawer__target-tabs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.generate-task-drawer__account {
|
||||
display: grid;
|
||||
grid-template-columns: 20px 36px minmax(0, 1fr);
|
||||
@@ -1473,6 +1845,11 @@ function normalizeJsonRecord(value: Record<string, unknown>): Record<string, Jso
|
||||
background: #355dff;
|
||||
}
|
||||
|
||||
.generate-task-drawer__site-card--active .generate-task-drawer__account-check {
|
||||
border-color: #355dff;
|
||||
background: #355dff;
|
||||
}
|
||||
|
||||
.generate-task-drawer__account-check span {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
@@ -1553,6 +1930,117 @@ function normalizeJsonRecord(value: Record<string, unknown>): Record<string, Jso
|
||||
color: #d92d20;
|
||||
}
|
||||
|
||||
.generate-task-drawer__enterprise-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.generate-task-drawer__enterprise-toolbar span {
|
||||
color: #374151;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.generate-task-drawer__site-card {
|
||||
border: 1px solid #e1e8f2;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
background 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.generate-task-drawer__site-card:hover:not(.generate-task-drawer__site-card--disabled) {
|
||||
border-color: #b7c7ec;
|
||||
background: #fbfdff;
|
||||
}
|
||||
|
||||
.generate-task-drawer__site-card--active {
|
||||
border-color: #355dff;
|
||||
background: #f4f7ff;
|
||||
box-shadow: 0 4px 12px rgba(53, 93, 255, 0.08);
|
||||
}
|
||||
|
||||
.generate-task-drawer__site-card--disabled {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.generate-task-drawer__site-main {
|
||||
display: grid;
|
||||
grid-template-columns: 20px 36px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 78px;
|
||||
padding: 12px 14px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.generate-task-drawer__site-card--disabled .generate-task-drawer__site-main {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.generate-task-drawer__site-avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 1px solid #e4e9f2;
|
||||
border-radius: 8px;
|
||||
background: #f2f4f7;
|
||||
color: #344054;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.generate-task-drawer__site-config {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin: 0 14px 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
z-index: 2;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.generate-task-drawer__site-card--active .generate-task-drawer__site-config {
|
||||
background: #ffffff;
|
||||
border-color: #bfdbfe;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.generate-task-drawer__site-category-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.generate-task-drawer__site-sync {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.generate-task-drawer__site-warning {
|
||||
margin: -4px 14px 14px;
|
||||
color: #b42318;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.generate-task-drawer__cover-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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('、') : '--'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -490,7 +503,7 @@ function dispatchStatusMeta(status?: string | null): { label: string; color: str
|
||||
<a-tag :color="record.auto_publish ? 'blue' : 'default'">
|
||||
{{
|
||||
record.auto_publish
|
||||
? formatPublishPlatformList(record.auto_publish_platforms)
|
||||
? schedulePublishTargetsText(record)
|
||||
: t('custom.schedule.manualPublish')
|
||||
}}
|
||||
</a-tag>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { DeleteOutlined, PictureOutlined } from '@ant-design/icons-vue'
|
||||
import { CloseOutlined, DeleteOutlined, PictureOutlined } from '@ant-design/icons-vue'
|
||||
import type { CreateKolPackageRequest } from '@geo/shared-types'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
@@ -52,6 +52,13 @@ watch(
|
||||
|
||||
const coverPreviewUrl = computed(() => resolveApiURL(formState.cover_url))
|
||||
const coverFileName = computed(() => deriveCoverFileName(formState.cover_url))
|
||||
const tagOptions = computed(() =>
|
||||
Array.from(new Set(formState.tags.map((tag) => tag.trim()).filter(Boolean))).map((tag) => ({
|
||||
label: tag,
|
||||
value: tag,
|
||||
title: null,
|
||||
})),
|
||||
)
|
||||
|
||||
function handleOk() {
|
||||
if (!formState.name.trim()) {
|
||||
@@ -70,6 +77,21 @@ function handleRemoveCover() {
|
||||
formState.cover_url = ''
|
||||
}
|
||||
|
||||
function getModalPopupContainer(triggerNode: HTMLElement): HTMLElement {
|
||||
return (triggerNode.closest('.ant-modal-content') as HTMLElement | null) ?? document.body
|
||||
}
|
||||
|
||||
function preventTagMouseDown(event: MouseEvent): void {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
function handleTagClose(onClose: (event?: MouseEvent) => void, event: MouseEvent): void {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
onClose(event)
|
||||
}
|
||||
|
||||
function handleCoverConfirmed(payload: { url: string }) {
|
||||
formState.cover_url = resolveApiURL(payload.url)
|
||||
}
|
||||
@@ -111,8 +133,29 @@ async function uploadCover(file: File) {
|
||||
v-model:value="formState.tags"
|
||||
mode="tags"
|
||||
style="width: 100%"
|
||||
:options="tagOptions"
|
||||
:placeholder="t('kol.manage.packageForm.tagsPlaceholder')"
|
||||
/>
|
||||
:get-popup-container="getModalPopupContainer"
|
||||
popup-class-name="kol-package-tags-dropdown"
|
||||
>
|
||||
<template #tagRender="{ label, closable, onClose }">
|
||||
<span class="package-tag" @mousedown="preventTagMouseDown">
|
||||
<span class="package-tag__label">{{ label }}</span>
|
||||
<button
|
||||
v-if="closable"
|
||||
type="button"
|
||||
class="package-tag__remove"
|
||||
:aria-label="t('common.delete')"
|
||||
@click="handleTagClose(onClose, $event)"
|
||||
>
|
||||
<CloseOutlined />
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
<template #option="{ label }">
|
||||
<span class="package-tag-option">{{ label }}</span>
|
||||
</template>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('kol.manage.packageForm.priceNote')">
|
||||
@@ -240,4 +283,53 @@ async function uploadCover(file: File) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.package-tag {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-inline-end: 4px;
|
||||
padding: 1px 4px 1px 8px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
background: #f5f5f5;
|
||||
color: #1f2937;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.package-tag__label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.package-tag__remove {
|
||||
display: inline-flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: #667085;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.package-tag__remove:hover {
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
:global(.kol-package-tags-dropdown .ant-select-item-option) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:global(.kol-package-tags-dropdown .ant-select-item-option-content),
|
||||
.package-tag-option {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1624,13 +1624,33 @@ const enUS = {
|
||||
'No platform data is available yet. Please verify the Media Management setup first.',
|
||||
publishSection: 'Publish Settings',
|
||||
autoPublish: 'Auto publish',
|
||||
autoPublishHint: 'When enabled, scheduled articles enter the publish queue after generation.',
|
||||
autoPublishHint:
|
||||
'When enabled, scheduled articles publish to selected media accounts or enterprise sites.',
|
||||
publishTargets: 'Publish targets',
|
||||
publishTargetsHint:
|
||||
'Choose desktop media accounts or enterprise sites. At least one target is required.',
|
||||
publishTargetMediaAccounts: 'Media accounts',
|
||||
publishTargetEnterpriseSites: 'Enterprise sites',
|
||||
publishAccounts: 'Media accounts',
|
||||
publishAccountsHint:
|
||||
'Choose specific accounts. Offline clients queue first and continue after reconnecting.',
|
||||
publishAccountEmptyTitle: 'No media accounts',
|
||||
publishAccountEmptyHint:
|
||||
'Bind media accounts in the desktop client before configuring auto publish.',
|
||||
enterprisePublishType: 'Publish type',
|
||||
enterprisePublishNow: 'Publish now',
|
||||
enterprisePublishDraft: 'Save draft',
|
||||
enterpriseCategoryPlaceholder: 'Choose category',
|
||||
enterpriseSyncCategories: 'Sync categories',
|
||||
enterpriseSiteEmptyTitle: 'No enterprise sites',
|
||||
enterpriseSiteEmptyHint:
|
||||
'Add a site connection in Media Management and sync categories first.',
|
||||
enterpriseSiteConnected: 'Connected',
|
||||
enterpriseSiteError: 'Connection error',
|
||||
enterpriseSiteDisabled: 'Disabled',
|
||||
enterpriseSiteDraft: 'Pending check',
|
||||
enterpriseSiteDisabledHint: 'This enterprise site is disabled.',
|
||||
enterpriseSiteErrorHint: 'This enterprise site has a connection error. Test it first.',
|
||||
cover: 'Cover image',
|
||||
coverHint: 'Keep the cover clear, complete, and visually polished.',
|
||||
scheduleCoverHint:
|
||||
@@ -1683,7 +1703,8 @@ const enUS = {
|
||||
'New tasks default to a random time between 01:00 and 05:00 to smooth overnight load. You can edit it before saving.',
|
||||
executionTimeDefault: 'Default staggered time',
|
||||
to: 'to',
|
||||
autoPublishPlatforms: 'Auto publish platforms',
|
||||
autoPublishPlatforms: 'Auto publish targets',
|
||||
enterpriseSiteTargetCount: '{count} enterprise sites',
|
||||
autoPublish: 'Auto publish',
|
||||
manualPublish: 'Manual publish',
|
||||
nextRun: 'Next run',
|
||||
@@ -1775,6 +1796,9 @@ const enUS = {
|
||||
invalidRandomWindow: 'The schedule time configuration is invalid.',
|
||||
missingScheduleDays: 'Select at least one run day.',
|
||||
missingPublishAccounts: 'Choose at least one media account when auto publish is enabled.',
|
||||
missingPublishTargets: 'Choose at least one publish target when auto publish is enabled.',
|
||||
missingEnterpriseSiteCategory: 'Choose an enterprise site category.',
|
||||
enterpriseCategoriesSynced: 'Enterprise site categories synced.',
|
||||
missingScheduleCover: 'A selected media account platform requires a cover image.',
|
||||
missingPromptName: 'Enter a prompt name first.',
|
||||
missingPromptContent: 'Enter prompt content first.',
|
||||
|
||||
@@ -1550,11 +1550,28 @@ const zhCN = {
|
||||
platformEmptyHint: '当前还没有可用的平台数据,请先检查媒体管理配置。',
|
||||
publishSection: '发布设置',
|
||||
autoPublish: '是否自动发布',
|
||||
autoPublishHint: '开启后,定时生成完成的文章会使用指定媒体账号自动进入发布队列。',
|
||||
autoPublishHint: '开启后,定时生成完成的文章会自动发布到选中的媒体账号或企业自建站点。',
|
||||
publishTargets: '发布目标',
|
||||
publishTargetsHint: '可选择桌面媒体账号,也可选择企业自建站点;至少选择一个目标。',
|
||||
publishTargetMediaAccounts: '媒体账号',
|
||||
publishTargetEnterpriseSites: '企业自建站点',
|
||||
publishAccounts: '媒体账号',
|
||||
publishAccountsHint: '请选择具体账号。客户端离线时会先排队,上线后继续发布。',
|
||||
publishAccountEmptyTitle: '暂无可选媒体账号',
|
||||
publishAccountEmptyHint: '请先在桌面端绑定媒体账号,再回到这里配置自动发布。',
|
||||
enterprisePublishType: '发布方式',
|
||||
enterprisePublishNow: '直接发布',
|
||||
enterprisePublishDraft: '保存草稿',
|
||||
enterpriseCategoryPlaceholder: '选择栏目',
|
||||
enterpriseSyncCategories: '同步栏目',
|
||||
enterpriseSiteEmptyTitle: '暂无企业自建站点',
|
||||
enterpriseSiteEmptyHint: '请先到媒体管理添加站点连接,并同步栏目。',
|
||||
enterpriseSiteConnected: '已连通',
|
||||
enterpriseSiteError: '连接异常',
|
||||
enterpriseSiteDisabled: '已禁用',
|
||||
enterpriseSiteDraft: '待校验',
|
||||
enterpriseSiteDisabledHint: '该企业站点已禁用。',
|
||||
enterpriseSiteErrorHint: '该企业站点连接异常,请先测试连通性。',
|
||||
cover: '封面图',
|
||||
coverHint: '请保证封面清晰、美观和完整',
|
||||
scheduleCoverHint: '默认不设置封面。需要时可单独指定,生成完成后会写入文章并用于发布。',
|
||||
@@ -1604,7 +1621,8 @@ const zhCN = {
|
||||
'新建任务会默认随机落在 01:00-05:00,避免凌晨任务集中;你也可以手动修改为固定执行时间。',
|
||||
executionTimeDefault: '默认错峰生成',
|
||||
to: '至',
|
||||
autoPublishPlatforms: '自动发布平台',
|
||||
autoPublishPlatforms: '自动发布目标',
|
||||
enterpriseSiteTargetCount: '企业自建站点 {count} 个',
|
||||
autoPublish: '自动发布',
|
||||
manualPublish: '手动发布',
|
||||
nextRun: '下次执行',
|
||||
@@ -1695,6 +1713,9 @@ const zhCN = {
|
||||
invalidRandomWindow: '执行时间配置不正确',
|
||||
missingScheduleDays: '请至少选择一个执行日',
|
||||
missingPublishAccounts: '开启自动发布后,请至少选择一个媒体账号',
|
||||
missingPublishTargets: '开启自动发布后,请至少选择一个发布目标',
|
||||
missingEnterpriseSiteCategory: '请选择企业自建站点栏目',
|
||||
enterpriseCategoriesSynced: '企业站点栏目已同步',
|
||||
missingScheduleCover: '已选媒体账号的平台要求封面图,请先设置封面',
|
||||
missingPromptName: '请先填写 Prompt 名称',
|
||||
missingPromptContent: '请先填写 Prompt 内容',
|
||||
|
||||
@@ -136,6 +136,8 @@ const errorMessageMap: Record<string, string> = {
|
||||
brand_not_found: '品牌不存在或已删除',
|
||||
keyword_not_found: '关键词不存在或已删除',
|
||||
publish_cover_required: '当前选择的平台要求上传封面图,请先上传封面图',
|
||||
publish_targets_required: '开启自动发布后,请至少选择一个发布目标',
|
||||
invalid_publish_enterprise_site_targets: '企业站点发布目标格式不正确',
|
||||
desktop_account_client_missing: '所选账号还没有绑定桌面客户端,暂时无法发布',
|
||||
desktop_account_not_found: '所选桌面账号不存在,请刷新后重试',
|
||||
desktop_client_offline: '当前登录账号的桌面客户端未在线,请先打开 desktop-client',
|
||||
@@ -264,6 +266,7 @@ const LEGACY_ENTERPRISE_SITE_CATEGORY_SYNC_FAILED_PATTERN =
|
||||
/^enterprise site category sync failed: /i
|
||||
const LEGACY_PBOOTCMS_HTTP_PATTERN = /^pbootcms\s+\w+\s+returned http \d+$/i
|
||||
const LEGACY_PBOOTCMS_CODE_PATTERN = /^pbootcms\s+\w+\s+failed with code \d+$/i
|
||||
const LEGACY_WORDPRESS_HTTP_PATTERN = /^wordpress\s+.*\s+returned http \d+$/i
|
||||
const LEGACY_ENTERPRISE_SITE_FAILED_DETAIL_PATTERN = /^failed to .*enterprise site/i
|
||||
const LEGACY_ENTERPRISE_SITE_EXISTS_PATTERN =
|
||||
/^(enterprise site connection already exists|another enterprise site already uses this domain)$/i
|
||||
@@ -321,6 +324,31 @@ function translateRawErrorMessage(raw: string): string | null {
|
||||
if (lower.startsWith('pbootcms ') && lower.includes(' endpoint is unavailable')) {
|
||||
return 'CMS 接口不可用,请确认接口安装路径'
|
||||
}
|
||||
if (lower.startsWith('request wordpress ')) {
|
||||
return 'WordPress 请求失败,请检查站点地址和网络连通性'
|
||||
}
|
||||
if (lower.startsWith('parse wordpress response')) {
|
||||
return 'WordPress 响应解析失败,请确认 REST API 返回 JSON 格式'
|
||||
}
|
||||
if (
|
||||
lower.includes('rest_not_logged_in') ||
|
||||
lower.includes('incorrect_password') ||
|
||||
lower.includes('invalid_username')
|
||||
) {
|
||||
return 'WordPress 认证失败,请使用 WordPress 用户名和 Application Password'
|
||||
}
|
||||
if (lower.startsWith('invalid wordpress site url')) {
|
||||
return '站点域名格式不正确'
|
||||
}
|
||||
if (lower.includes('invalid wordpress category id')) {
|
||||
return 'WordPress 栏目 ID 不正确,请先同步栏目后再发布'
|
||||
}
|
||||
if (LEGACY_WORDPRESS_HTTP_PATTERN.test(normalized)) {
|
||||
return 'WordPress REST API 请求失败,请检查站点权限和接口状态'
|
||||
}
|
||||
if (lower.includes('unsupported cms type')) {
|
||||
return '当前版本暂不支持该 CMS 类型'
|
||||
}
|
||||
if (LEGACY_ENTERPRISE_SITE_FAILED_DETAIL_PATTERN.test(normalized)) {
|
||||
return '企业站点操作失败,请稍后重试'
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user