c7bad83496
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>
336 lines
8.7 KiB
Vue
336 lines
8.7 KiB
Vue
<script setup lang="ts">
|
|
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'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import CoverPickerModal from '@/components/CoverPickerModal.vue'
|
|
import { imagesApi, resolveApiURL } from '@/lib/api'
|
|
import { deriveCoverFileName } from '@/lib/cover-requirements'
|
|
|
|
const props = defineProps<{
|
|
open: boolean
|
|
initialValue?: Partial<CreateKolPackageRequest>
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:open', value: boolean): void
|
|
(e: 'submit', value: CreateKolPackageRequest): void
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
const coverPickerOpen = ref(false)
|
|
|
|
const formState = reactive<CreateKolPackageRequest>({
|
|
name: '',
|
|
description: '',
|
|
industry: '',
|
|
tags: [],
|
|
price_note: '',
|
|
cover_url: '',
|
|
})
|
|
|
|
watch(
|
|
() => props.open,
|
|
(isOpen) => {
|
|
if (isOpen) {
|
|
Object.assign(formState, {
|
|
name: props.initialValue?.name || '',
|
|
description: props.initialValue?.description || '',
|
|
industry: props.initialValue?.industry || '',
|
|
tags: [...(props.initialValue?.tags || [])],
|
|
price_note: props.initialValue?.price_note || '',
|
|
cover_url: props.initialValue?.cover_url || '',
|
|
})
|
|
return
|
|
}
|
|
|
|
coverPickerOpen.value = false
|
|
},
|
|
)
|
|
|
|
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()) {
|
|
message.warning(t('kol.manage.packageForm.nameRequired'))
|
|
return
|
|
}
|
|
emit('submit', { ...formState, tags: [...formState.tags] })
|
|
emit('update:open', false)
|
|
}
|
|
|
|
function handleCancel() {
|
|
emit('update:open', false)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
async function uploadCover(file: File) {
|
|
const uploaded = await imagesApi.upload(file)
|
|
return {
|
|
url: uploaded.url,
|
|
fileName: uploaded.name,
|
|
assetId: uploaded.id,
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<a-modal
|
|
:open="open"
|
|
:title="initialValue ? t('kol.manage.editPackage') : t('kol.manage.createPackage')"
|
|
:ok-text="t('common.confirm')"
|
|
:cancel-text="t('common.cancel')"
|
|
@ok="handleOk"
|
|
@cancel="handleCancel"
|
|
>
|
|
<a-form layout="vertical">
|
|
<a-form-item :label="t('common.title')" required>
|
|
<a-input v-model:value="formState.name" :placeholder="t('common.inputPlease')" />
|
|
</a-form-item>
|
|
|
|
<a-form-item :label="t('common.description')">
|
|
<a-textarea v-model:value="formState.description" :rows="3" />
|
|
</a-form-item>
|
|
|
|
<a-form-item :label="t('kol.marketplace.filter.industry')">
|
|
<a-input v-model:value="formState.industry" />
|
|
</a-form-item>
|
|
|
|
<a-form-item :label="t('kol.manage.packageForm.tags')">
|
|
<a-select
|
|
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')">
|
|
<a-input
|
|
v-model:value="formState.price_note"
|
|
:placeholder="t('kol.manage.packageForm.priceNotePlaceholder')"
|
|
/>
|
|
</a-form-item>
|
|
|
|
<a-form-item :label="t('kol.manage.packageForm.cover')">
|
|
<div class="package-cover-field">
|
|
<button type="button" class="package-cover-picker" @click="coverPickerOpen = true">
|
|
<template v-if="coverPreviewUrl">
|
|
<img
|
|
:src="coverPreviewUrl"
|
|
:alt="t('kol.manage.packageForm.cover')"
|
|
class="package-cover-picker__image"
|
|
/>
|
|
</template>
|
|
<template v-else>
|
|
<div class="package-cover-picker__empty">
|
|
<PictureOutlined class="package-cover-picker__icon" />
|
|
<strong>{{ t('kol.manage.packageForm.selectCover') }}</strong>
|
|
<span>{{ t('kol.manage.packageForm.coverHint') }}</span>
|
|
</div>
|
|
</template>
|
|
</button>
|
|
|
|
<div class="package-cover-actions">
|
|
<a-button @click="coverPickerOpen = true">
|
|
{{
|
|
coverPreviewUrl
|
|
? t('kol.manage.packageForm.changeCover')
|
|
: t('kol.manage.packageForm.selectCover')
|
|
}}
|
|
</a-button>
|
|
<a-button v-if="coverPreviewUrl" danger @click="handleRemoveCover">
|
|
<template #icon><DeleteOutlined /></template>
|
|
{{ t('kol.manage.packageForm.removeCover') }}
|
|
</a-button>
|
|
</div>
|
|
</div>
|
|
</a-form-item>
|
|
</a-form>
|
|
</a-modal>
|
|
|
|
<CoverPickerModal
|
|
v-model:open="coverPickerOpen"
|
|
:article-id="null"
|
|
:platform-ids="[]"
|
|
:current-url="coverPreviewUrl"
|
|
:current-file-name="coverFileName"
|
|
:title="t('kol.manage.packageForm.coverPicker.title')"
|
|
:local-tab-label="t('kol.manage.packageForm.coverPicker.localTab')"
|
|
:empty-upload-title="t('kol.manage.packageForm.coverPicker.emptyTitle')"
|
|
:empty-upload-hint="t('kol.manage.packageForm.coverPicker.emptyHint')"
|
|
:upload-handler="uploadCover"
|
|
@confirmed="handleCoverConfirmed"
|
|
/>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.package-cover-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.package-cover-picker {
|
|
width: 100%;
|
|
min-height: 180px;
|
|
border: 1px dashed #d0d5dd;
|
|
border-radius: 12px;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
background: #f8fafc;
|
|
cursor: pointer;
|
|
transition:
|
|
border-color 0.2s ease,
|
|
box-shadow 0.2s ease;
|
|
}
|
|
|
|
.package-cover-picker:hover {
|
|
border-color: #1677ff;
|
|
box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.08);
|
|
}
|
|
|
|
.package-cover-picker__image {
|
|
display: block;
|
|
width: 100%;
|
|
height: 180px;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.package-cover-picker__empty {
|
|
display: flex;
|
|
min-height: 180px;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
padding: 20px;
|
|
color: #475467;
|
|
text-align: center;
|
|
}
|
|
|
|
.package-cover-picker__empty strong {
|
|
color: #111827;
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.package-cover-picker__empty span {
|
|
max-width: 320px;
|
|
font-size: 12px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.package-cover-picker__icon {
|
|
font-size: 28px;
|
|
color: #1677ff;
|
|
}
|
|
|
|
.package-cover-actions {
|
|
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>
|