/** * 发布配置抽屉。 * * 分类、标签、付费与资料附件都在这里配置;封面与定时发布在正文下方的 * 「发布设置」卡片里,避免把封面入口和正文挤在同一张纸上。 * 手机端以全屏面板呈现。所有字段与权限来自服务端 bootstrap,前端不自行放大权限。 * * @package Jio */ import { useEffect, useMemo, useRef, useState } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { apiUpload } from '../services/api'; type PayState = { enabled: boolean; modo: 'cash' | 'points'; price: string; points: number; vip1Price: string; vip2Price: string; payLimit: number; downloads: Array>; }; type Draft = { postId: number; title: string; categories: number[]; tags: string[]; custom: Record; pay: PayState; }; type Capabilities = Record; type Limits = { titleMin?: number; titleMax?: number; priceMin?: number; priceMax?: number; pointsMin?: number; pointsMax?: number; }; type Props = { draft: Draft; setDraft: (patch: Partial) => void; capabilities: Capabilities; limits: Limits; captcha: boolean; taxonomy: { categories: Array<{ id: number; name: string }>; tags: boolean; customDefs: Array<{ key: string; name: string; values: Array<{ key: string; name: string }> }>; }; isPublishedEdit: boolean; onClose: () => void; onSaveDraft: () => void; onSubmit: (publishNow: boolean) => void; privateStorage: boolean; privateUploads: Array>; saving: boolean; }; const bootstrap: Record = window['jioCreatorStudio'] || {}; const nonce: string = bootstrap.nonce || ''; export function PublishDrawer(props: Props) { const { draft, setDraft, capabilities, limits, taxonomy, isPublishedEdit, onClose, onSaveDraft, onSubmit, privateStorage, privateUploads, saving, captcha, } = props; const [tagInput, setTagInput] = useState(''); const [uploadError, setUploadError] = useState(''); const [uploading, setUploading] = useState(false); const fileInput = useRef(null); const captchaSlot = useRef(null); // 人机验证由 PHP 渲染(脚本在页面加载时按 uid 注册实例),这里只把挂载点 // 移进抽屉,保证验证控件在用户提交时可见、可交互。 useEffect(() => { const slot = captchaSlot.current; const mount = document.getElementById('jio-creator-captcha'); if (!slot || !mount) { return undefined; } const previous = mount.parentElement; slot.appendChild(mount); mount.removeAttribute('hidden'); return () => { if (previous) { previous.appendChild(mount); mount.setAttribute('hidden', ''); } }; }, []); const titleLength = useMemo(() => (draft.title || '').trim().length, [draft.title]); const canSubmit = titleLength >= Number(limits.titleMin || 1); const toggleCategory = (id: number) => { const has = draft.categories.includes(id); setDraft({ categories: has ? draft.categories.filter((value) => value !== id) : [...draft.categories, id], }); }; const addTag = () => { const value = tagInput.trim(); if (!value) { return; } const next = value .split(/[,,\n]+/u) .map((item) => item.trim()) .filter(Boolean); setDraft({ tags: Array.from(new Set([...draft.tags, ...next])) }); setTagInput(''); }; const uploadAttachment = async () => { const file = fileInput.current?.files?.[0]; if (!file) { return; } setUploadError(''); setUploading(true); try { const response = await apiUpload(bootstrap.creator.privateUrl, file, nonce, { post_id: String(draft.postId || 0), }); const next = { type: 'protected', assetKey: String(response.asset_key || ''), fileName: String(response.name || file.name), sizeBytes: Number(response.size || file.size), sizeLabel: String(response.size_label || ''), label: '', }; setDraft({ pay: { ...draft.pay, enabled: true, downloads: [...draft.pay.downloads, next] }, }); if (fileInput.current) { fileInput.current.value = ''; } } catch (error: any) { setUploadError(String(error?.message || __('资料上传失败', 'jio'))); } finally { setUploading(false); } }; const removeDownload = (index: number) => { setDraft({ pay: { ...draft.pay, downloads: draft.pay.downloads.filter((_, i) => i !== index) }, }); }; return (