refactor(creator-studio): move taxonomy to the settings card, slim the drawer to pay

Classification belongs next to everything else the author decides before
publishing, and the drawer should only configure paid resources.

- PublishSettings now renders categories, tags and custom taxonomy
  dimensions first, then cover, paid content and scheduling. Chips use a
  stacked row layout so long category lists wrap naturally.
- PublishDrawer keeps only PayPanel: taxonomy, save/publish buttons and the
  now-unused props are gone, leaving a single 完成 button.
- New PublishDialog takes over the publish action from the bottom bar, and
  with it the captcha mount point, so the gate still renders somewhere the
  author can see it.
- Promotes .jio-creator-primary / .jio-creator-ghost out of the dialog-only
  rules (drawer and pay panel buttons had no styling), and drops the dead
  .jio-creator-field / .jio-creator-cover blocks.

Contracts updated; 886 widget-system tests pass.
This commit is contained in:
kate522 committed 2026-11-09 15:47:02 +08:00
1 parent ac84e991d1
commit f97fd72bd0
11 files changed
+448 -322

No files matched your search

@@ -0,0 +1,125 @@
/**
* 发布确认弹窗。
*
* 发布 / 提交审核是底部状态栏的动作,但人机验证需要一个真实挂载点,
* 因此把验证控件与最终按钮放进这个轻量弹窗:分类、标签与付费配置在发布前
* 就已经在「发布设置」卡片和付费抽屉里配置完毕。
*
* @package Jio
*/
import { useEffect, useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
type Props = {
canPublishNow: boolean;
isPublishedEdit: boolean;
captcha: boolean;
saving: boolean;
canSubmit: boolean;
onClose: () => void;
onSaveDraft: () => void;
onSubmit: (publishNow: boolean) => void;
};
export function PublishDialog({
canPublishNow,
isPublishedEdit,
captcha,
saving,
canSubmit,
onClose,
onSaveDraft,
onSubmit,
}: Props) {
const captchaSlot = useRef<HTMLDivElement | null>(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', '');
}
};
}, []);
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && !saving) {
event.preventDefault();
onClose();
}
};
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [onClose, saving]);
return (
<div className="jio-creator-dialog" role="dialog" aria-modal="true" aria-labelledby="jio-creator-publish-title">
<button type="button" className="jio-creator-dialog__backdrop" aria-label={__('关闭', 'jio')} onClick={onClose} />
<div className="jio-creator-dialog__panel">
<header className="jio-creator-dialog__head">
<h2 id="jio-creator-publish-title">
{isPublishedEdit ? __('提交改稿', 'jio') : __('发布文章', 'jio')}
</h2>
<button type="button" className="jio-creator-dialog__close" onClick={onClose} aria-label={__('关闭', 'jio')}>
<i className="fa fas fa-times" aria-hidden="true" />
</button>
</header>
<div className="jio-creator-dialog__body">
{isPublishedEdit && (
<p className="jio-creator-dialog__note">
{__('审核通过前,读者看到的仍然是当前线上版本。', 'jio')}
</p>
)}
{captcha ? (
<div className="jio-creator-dialog__captcha" ref={captchaSlot} />
) : (
<p className="jio-creator-muted">
{isPublishedEdit
? __('确认后将提交这份改稿等待审核。', 'jio')
: canPublishNow
? __('确认后立即发布这篇文章。', 'jio')
: __('确认后将提交审核,通过后才会发布。', 'jio')}
</p>
)}
</div>
<footer className="jio-creator-dialog__foot">
<button type="button" className="jio-creator-dialog__ghost" onClick={onSaveDraft} disabled={saving}>
{__('保存草稿', 'jio')}
</button>
<button
type="button"
className="jio-creator-dialog__primary"
onClick={() => onSubmit(false)}
disabled={saving || !canSubmit}
>
{isPublishedEdit ? __('提交审核', 'jio') : canPublishNow ? __('发布', 'jio') : __('提交审核', 'jio')}
</button>
{canPublishNow && (
<button
type="button"
className="jio-creator-link"
onClick={() => onSubmit(true)}
disabled={saving || !canSubmit}
>
{__('立即发布', 'jio')}
</button>
)}
</footer>
</div>
</div>
);
}
@@ -1,8 +1,9 @@
/**
* 发布设置卡片(位于正文纸张下方)。
*
* 承载与"这篇会被怎样发布"有关的低频选项:封面、付费资源与定时发布。
* 分类、标签等配置仍在发布抽屉里;服务端 bootstrap 决定每项是否可用。
* 承载与"这篇会被怎样发布"有关的全部选项:分类、标签、自定义分类法、封面、
* 付费资源与定时发布。发布抽屉只负责付费资源的细节配置。
* 服务端 bootstrap 决定每项是否可用。
*
* @package Jio
*/
@@ -15,6 +16,12 @@ import type { PayState } from '../draft';
type Featured = { type: string; data: Record<string, any>; mediaId?: number };
export type TaxonomyDef = {
categories: Array<{ id: number; name: string }>;
tags: boolean;
customDefs: Array<{ key: string; name: string; values: Array<{ key: string; name: string }> }>;
};
type Props = {
featured: Featured;
canEditCover: boolean;
@@ -23,9 +30,16 @@ type Props = {
scheduleAt: string;
pay: PayState;
canPay: boolean;
taxonomy: TaxonomyDef;
categories: number[];
tags: string[];
custom: Record<string, string[]>;
onFeaturedChange: (featured: Featured) => void;
onScheduleChange: (enabled: boolean, at: string) => void;
onPayEnabledChange: (enabled: boolean) => void;
onCategoriesChange: (categories: number[]) => void;
onTagsChange: (tags: string[]) => void;
onCustomChange: (custom: Record<string, string[]>) => void;
/** 打开发布抽屉并直接定位到付费配置。 */
onOpenPaySettings: () => void;
};
@@ -52,17 +66,52 @@ export function PublishSettings({
scheduleAt,
pay,
canPay,
taxonomy,
categories,
tags,
custom,
onFeaturedChange,
onScheduleChange,
onPayEnabledChange,
onCategoriesChange,
onTagsChange,
onCustomChange,
onOpenPaySettings,
}: Props) {
const coverInput = useRef<HTMLInputElement | null>(null);
const [tagInput, setTagInput] = useState('');
const [uploading, setUploading] = useState(false);
const [error, setError] = useState('');
const minSchedule = useMemo(() => toLocalInput(new Date(Date.now() + 5 * 60 * 1000)), []);
const coverUrl = typeof featured?.data?.url === 'string' ? featured.data.url : '';
const toggleCategory = (id: number) => {
onCategoriesChange(
categories.includes(id) ? categories.filter((value) => value !== id) : [...categories, id]
);
};
const addTag = () => {
const value = tagInput.trim();
if (!value) {
return;
}
const next = value
.split(/[,,\n]+/u)
.map((item) => item.trim())
.filter(Boolean);
onTagsChange(Array.from(new Set([...tags, ...next])));
setTagInput('');
};
const toggleCustom = (key: string, value: string) => {
const current = custom[key] || [];
onCustomChange({
...custom,
[key]: current.includes(value) ? current.filter((item) => item !== value) : [...current, value],
});
};
const uploadCover = async () => {
const file = coverInput.current?.files?.[0];
const uploadUrl = bootstrap.media?.uploadUrl?.image;
@@ -93,6 +142,97 @@ export function PublishSettings({
<section className="jio-creator__settings" aria-label={__('发布设置', 'jio')}>
<h2 className="jio-creator__settings-title">{__('发布设置', 'jio')}</h2>
{/* 分类:卡片里的第一项,作者提交前一定会看到。 */}
<div className="jio-creator__setting-row jio-creator__setting-row--stack">
<div className="jio-creator__setting-copy">
<span className="jio-creator__setting-label">{__('分类', 'jio')}</span>
<p className="jio-creator__setting-help">{__('必选,决定文章出现在哪些栏目。', 'jio')}</p>
</div>
<div className="jio-creator-chips">
{taxonomy.categories.length === 0 && (
<span className="jio-creator-muted">{__('暂无可选分类', 'jio')}</span>
)}
{taxonomy.categories.map((category) => (
<label
key={category.id}
className={`jio-creator-chip${categories.includes(category.id) ? ' is-active' : ''}`}
>
<input
type="checkbox"
checked={categories.includes(category.id)}
onChange={() => toggleCategory(category.id)}
/>
<span>{category.name}</span>
</label>
))}
</div>
</div>
{taxonomy.tags && (
<div className="jio-creator__setting-row jio-creator__setting-row--stack">
<div className="jio-creator__setting-copy">
<span className="jio-creator__setting-label">{__('标签', 'jio')}</span>
<p className="jio-creator__setting-help">{__('多个标签用逗号分隔,回车即可添加。', 'jio')}</p>
</div>
<div>
{tags.length > 0 && (
<div className="jio-creator-chips">
{tags.map((tag) => (
<span key={tag} className="jio-creator-chip is-active">
<span>{tag}</span>
<button
type="button"
onClick={() => onTagsChange(tags.filter((value) => value !== tag))}
aria-label={sprintf(__('移除标签 %s', 'jio'), tag)}
>
<i className="fa fas fa-times" aria-hidden="true" />
</button>
</span>
))}
</div>
)}
<div className="jio-creator-inline">
<input
type="text"
value={tagInput}
placeholder={__('输入标签,逗号分隔', 'jio')}
onChange={(event) => setTagInput(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
addTag();
}
}}
/>
<button type="button" onClick={addTag}>{__('添加', 'jio')}</button>
</div>
</div>
</div>
)}
{taxonomy.customDefs.map((dimension) => (
<div className="jio-creator__setting-row jio-creator__setting-row--stack" key={dimension.key}>
<div className="jio-creator__setting-copy">
<span className="jio-creator__setting-label">{dimension.name}</span>
</div>
<div className="jio-creator-chips">
{dimension.values.map((value) => {
const active = (custom[dimension.key] || []).includes(value.key);
return (
<label key={value.key} className={`jio-creator-chip${active ? ' is-active' : ''}`}>
<input
type="checkbox"
checked={active}
onChange={() => toggleCustom(dimension.key, value.key)}
/>
<span>{value.name}</span>
</label>
);
})}
</div>
</div>
))}
{canEditCover && (
<div className="jio-creator__setting-row">
<div className="jio-creator__setting-copy">
+28 -22
View File
@@ -12,6 +12,7 @@ import { __, sprintf } from '@wordpress/i18n';
import { apiPost } from './services/api';
import { PublishDrawer } from './panels/publish-drawer';
import { PublishDialog } from './components/publish-dialog';
import { EditorToolbar } from './components/editor-toolbar';
import { PublishSettings } from './components/publish-settings';
import { StudioMasthead } from './components/studio-masthead';
@@ -90,7 +91,7 @@ function Studio(): JSX.Element {
at: '',
}));
const [drawerOpen, setDrawerOpen] = useState(false);
const [drawerFocusPay, setDrawerFocusPay] = useState(false);
const [publishOpen, setPublishOpen] = useState(false);
const [blockDialog, setBlockDialog] = useState<BlockDialogKind | null>(null);
const [notice, setNotice] = useState<{ tone: 'ok' | 'error'; text: string } | null>(null);
const [recovery, setRecovery] = useState<null | { savedAt: string }>(null);
@@ -274,6 +275,7 @@ function Studio(): JSX.Element {
dirtyRef.current = false;
setSaveState({ state: 'submitted', at: '' });
setNotice({ tone: 'ok', text: String(response.message || __('已提交', 'jio')) });
setPublishOpen(false);
setDrawerOpen(false);
const target = response.url || response.goto || bootstrap.urls?.center;
if (target) {
@@ -768,13 +770,21 @@ function Studio(): JSX.Element {
scheduleAt={draft.schedule.at}
pay={draft.pay}
canPay={!!capabilities.canPay}
taxonomy={{
categories: (bootstrap.categories && bootstrap.categories.items) || [],
tags: !!bootstrap.tags?.enabled,
customDefs: bootstrap.customDefs || [],
}}
categories={draft.categories}
tags={draft.tags}
custom={draft.custom}
onFeaturedChange={(featured) => patchDraft({ featured })}
onScheduleChange={(enabled, at) => patchDraft({ schedule: { enabled, at } })}
onPayEnabledChange={(enabled) => patchDraft({ pay: { ...draftRef.current.pay, enabled } })}
onOpenPaySettings={() => {
setDrawerFocusPay(true);
setDrawerOpen(true);
}}
onCategoriesChange={(categories) => patchDraft({ categories })}
onTagsChange={(tags) => patchDraft({ tags })}
onCustomChange={(custom) => patchDraft({ custom })}
onOpenPaySettings={() => setDrawerOpen(true)}
/>
{blockDialog && (
@@ -792,10 +802,7 @@ function Studio(): JSX.Element {
saving={!editorReady || saveState.state === 'saving' || saveState.state === 'submitting'}
onSaveDraft={() => void save(false)}
onPreview={() => void preview()}
onPublish={() => {
setDrawerFocusPay(false);
setDrawerOpen(true);
}}
onPublish={() => setPublishOpen(true)}
/>
{drawerOpen && (
@@ -803,24 +810,23 @@ function Studio(): JSX.Element {
draft={draft}
setDraft={patchDraft}
capabilities={capabilities}
limits={limits}
taxonomy={{
categories: (bootstrap.categories && bootstrap.categories.items) || [],
tags: !!bootstrap.tags?.enabled,
customDefs: bootstrap.customDefs || [],
}}
isPublishedEdit={isPublishedEdit}
focusPay={drawerFocusPay}
onClose={() => {
setDrawerOpen(false);
setDrawerFocusPay(false);
}}
onSaveDraft={() => void save(false)}
onSubmit={(publishNow: boolean) => void submit(publishNow)}
onClose={() => setDrawerOpen(false)}
privateStorage={!!bootstrap.privateStorage}
privateUploads={bootstrap.privateUploads || []}
/>
)}
{publishOpen && (
<PublishDialog
canPublishNow={!!capabilities.canPublishNow}
isPublishedEdit={isPublishedEdit}
captcha={!!bootstrap.captcha?.enabled}
saving={saveState.state === 'saving' || saveState.state === 'submitting'}
canSubmit={(draft.title || '').trim().length >= Number(limits.titleMin || 1)}
onClose={() => setPublishOpen(false)}
onSaveDraft={() => void save(false)}
onSubmit={(publishNow: boolean) => void submit(publishNow)}
/>
)}
</div>
@@ -1,15 +1,17 @@
/**
* 发布配置抽屉。
* 付费配置抽屉。
*
* 只承载付费相关内容:价格 / 会员价 / 付费下载资源。分类、标签、封面、
* 定时发布与提交动作都在正文下方的「发布设置」卡片和底部状态栏里,
* 抽屉不再重复这些入口。
*
* 分类、标签、付费与资料附件都在这里配置;封面与定时发布在正文下方的
* 「发布设置」卡片里,避免把封面入口和正文挤在同一张纸上。
* 手机端以全屏面板呈现。所有字段与权限来自服务端 bootstrap,前端不自行放大权限。
*
* @package Jio
*/
import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { useRef, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { apiUpload } from '../services/api';
import type { DownloadRow, Draft } from '../draft';
@@ -17,63 +19,31 @@ import { PayPanel, type PayBootstrap } from './pay-panel';
type Capabilities = Record<string, boolean>;
type Limits = {
titleMin?: number;
titleMax?: number;
priceMin?: number;
priceMax?: number;
pointsMin?: number;
pointsMax?: number;
};
type Props = {
draft: Draft;
setDraft: (patch: Partial<Draft>) => 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;
/** 打开时直接定位到付费配置(从正文下方的发布设置卡片进入)。 */
focusPay?: boolean;
onClose: () => void;
onSaveDraft: () => void;
onSubmit: (publishNow: boolean) => void;
privateStorage: boolean;
privateUploads: Array<Record<string, any>>;
saving: boolean;
};
const bootstrap: Record<string, any> = window['jioCreatorStudio'] || {};
const nonce: string = bootstrap.nonce || '';
export function PublishDrawer(props: Props) {
const {
draft,
setDraft,
capabilities,
limits,
taxonomy,
isPublishedEdit,
focusPay,
onClose,
onSaveDraft,
onSubmit,
privateStorage,
privateUploads,
saving,
captcha,
} = props;
const [tagInput, setTagInput] = useState('');
export function PublishDrawer({
draft,
setDraft,
capabilities,
isPublishedEdit,
onClose,
privateStorage,
privateUploads,
}: Props) {
const [uploadError, setUploadError] = useState('');
const [uploading, setUploading] = useState(false);
const captchaSlot = useRef<HTMLDivElement | null>(null);
const paySection = useRef<HTMLElement | null>(null);
const body = useRef<HTMLDivElement | null>(null);
const payBootstrap: PayBootstrap = {
freeAllowed: !!bootstrap.pay?.freeAllowed,
@@ -91,55 +61,6 @@ export function PublishDrawer(props: Props) {
pointsMax: Number(bootstrap.pay?.pointsMax || 0),
};
// 人机验证由 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', '');
}
};
}, []);
// 从发布设置卡片进入时,抽屉打开后直接滚到付费配置。
useEffect(() => {
if (focusPay) {
paySection.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
}
}, [focusPay]);
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('');
};
/** 上传一个受保护附件到私有存储,只保留 asset_key。 */
const uploadPrivate = async (file: File): Promise<DownloadRow | null> => {
setUploadError('');
@@ -189,152 +110,51 @@ export function PublishDrawer(props: Props) {
};
return (
<div className="jio-creator-drawer" role="dialog" aria-modal="true" aria-labelledby="jio-creator-publish-title">
<div className="jio-creator-drawer" role="dialog" aria-modal="true" aria-labelledby="jio-creator-pay-title">
<button type="button" className="jio-creator-drawer__backdrop" aria-label={__('关闭', 'jio')} onClick={onClose} />
<div className="jio-creator-drawer__panel">
<header className="jio-creator-drawer__head">
<h2 id="jio-creator-publish-title">{isPublishedEdit ? __('提交改稿', 'jio') : __('发布文章', 'jio')}</h2>
<div>
<h2 id="jio-creator-pay-title">{__('付费资源', 'jio')}</h2>
<p className="jio-creator-drawer__subtitle">
{__('价格、会员价与付费下载资源', 'jio')}
</p>
</div>
<button type="button" className="jio-creator-drawer__close" onClick={onClose} aria-label={__('关闭', 'jio')}>
<i className="fa fas fa-times" aria-hidden="true" />
</button>
</header>
<div className="jio-creator-drawer__body">
<div className="jio-creator-drawer__body" ref={body}>
{isPublishedEdit && (
<p className="jio-creator-drawer__note">
{__('审核通过前,读者看到的仍然是当前线上版本。', 'jio')}
</p>
)}
<section className="jio-creator-field">
<h3>{__('分类', 'jio')}</h3>
<div className="jio-creator-chips">
{taxonomy.categories.length === 0 && <span className="jio-creator-muted">{__('暂无可选分类', 'jio')}</span>}
{taxonomy.categories.map((category) => (
<label
key={category.id}
className={`jio-creator-chip${draft.categories.includes(category.id) ? ' is-active' : ''}`}
>
<input
type="checkbox"
checked={draft.categories.includes(category.id)}
onChange={() => toggleCategory(category.id)}
/>
<span>{category.name}</span>
</label>
))}
</div>
</section>
{taxonomy.tags && (
<section className="jio-creator-field">
<h3>{__('标签', 'jio')}</h3>
<div className="jio-creator-chips">
{draft.tags.map((tag) => (
<span key={tag} className="jio-creator-chip is-active">
<span>{tag}</span>
<button
type="button"
onClick={() => setDraft({ tags: draft.tags.filter((value) => value !== tag) })}
aria-label={sprintf(__('移除标签 %s', 'jio'), tag)}
>
<i className="fa fas fa-times" aria-hidden="true" />
</button>
</span>
))}
</div>
<div className="jio-creator-inline">
<input
type="text"
value={tagInput}
placeholder={__('输入标签,逗号分隔', 'jio')}
onChange={(event) => setTagInput(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
addTag();
}
}}
/>
<button type="button" onClick={addTag}>{__('添加', 'jio')}</button>
</div>
</section>
)}
{taxonomy.customDefs.map((dimension) => (
<section className="jio-creator-field" key={dimension.key}>
<h3>{dimension.name}</h3>
<div className="jio-creator-chips">
{dimension.values.map((value) => {
const current = draft.custom[dimension.key] || [];
const active = current.includes(value.key);
return (
<label key={value.key} className={`jio-creator-chip${active ? ' is-active' : ''}`}>
<input
type="checkbox"
checked={active}
onChange={() =>
setDraft({
custom: {
...draft.custom,
[dimension.key]: active
? current.filter((item) => item !== value.key)
: [...current, value.key],
},
})
}
/>
<span>{value.name}</span>
</label>
);
})}
</div>
</section>
))}
{capabilities.canPay && (
<section className="jio-creator-field" ref={paySection}>
<PayPanel
pay={draft.pay}
onChange={(pay) => setDraft({ pay })}
canDownload={!!capabilities.canPayDownload}
privateStorage={privateStorage}
privateUploads={privateUploads}
bootstrap={payBootstrap}
onUploadPrivate={uploadPrivate}
uploading={uploading}
uploadError={uploadError}
onPickAttachment={pickAttachment}
/>
</section>
)}
{!capabilities.canPay && capabilities.canSaveDraft && (
{capabilities.canPay ? (
<PayPanel
pay={draft.pay}
onChange={(pay) => setDraft({ pay })}
canDownload={!!capabilities.canPayDownload}
privateStorage={privateStorage}
privateUploads={privateUploads}
bootstrap={payBootstrap}
onUploadPrivate={uploadPrivate}
uploading={uploading}
uploadError={uploadError}
onPickAttachment={pickAttachment}
/>
) : (
<p className="jio-creator-muted">{__('当前账号不能设置付费内容。', 'jio')}</p>
)}
</div>
<footer className="jio-creator-drawer__foot">
{captcha && (
<div className="jio-creator-drawer__captcha" ref={captchaSlot} />
)}
<div className="jio-creator-drawer__actions">
<button type="button" className="jio-creator-ghost" onClick={onSaveDraft} disabled={saving}>
{__('保存草稿', 'jio')}
<button type="button" className="jio-creator-primary" onClick={onClose}>
{__('完成', 'jio')}
</button>
<button
type="button"
className="jio-creator-primary"
onClick={() => onSubmit(false)}
disabled={saving || !canSubmit}
>
{isPublishedEdit ? __('提交审核', 'jio') : capabilities.canPublishNow ? __('发布', 'jio') : __('提交审核', 'jio')}
</button>
{capabilities.canPublishNow && (
<button type="button" className="jio-creator-link" onClick={() => onSubmit(true)} disabled={saving || !canSubmit}>
{__('立即发布', 'jio')}
</button>
)}
</div>
</footer>
</div>
@@ -161,6 +161,21 @@
line-height: 1.5;
}
/* 发布确认弹窗里的人机验证挂载点:给一个最小高度,避免验证控件撑开时跳动。 */
.jio-creator-dialog__captcha {
min-height: 44px;
}
.jio-creator-dialog__note {
margin: 0;
padding: 10px 12px;
border-left: 3px solid var(--jc-accent);
background: #f2f7f4;
color: #7d8683;
font-size: 12px;
line-height: 1.6;
}
.jio-creator-dialog__url {
display: flex;
gap: 8px;
@@ -72,10 +72,6 @@
padding: 16px 24px calc(16px + env(safe-area-inset-bottom));
border-top: 1px solid $line;
background: #fff;
.jio-creator-primary {
flex: 1;
}
}
.jio-creator-drawer__actions {
@@ -84,21 +80,69 @@
gap: 12px;
}
.jio-creator-drawer__captcha {
min-height: 40px;
/* 抽屉只做付费配置,「完成」靠右,不承担发布动作。 */
.jio-creator-drawer__actions .jio-creator-primary {
flex: 0 0 auto;
min-width: 96px;
margin-left: auto;
}
.jio-creator-field {
margin-bottom: 26px;
/* 抽屉 / 付费面板共用的按钮基础样式(此前只在弹窗里定义,抽屉按钮没有外观)。 */
.jio-creator-ghost,
.jio-creator-primary {
display: inline-flex;
min-height: 34px;
align-items: center;
justify-content: center;
gap: 6px;
padding: 0 14px;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
white-space: nowrap;
}
> h3 {
margin: 0 0 10px;
font-size: 13px;
font-weight: 600;
color: $ink;
.jio-creator-ghost {
border: 1px solid $line;
background: #fff;
color: #5f6664;
&:hover:not(:disabled),
&:focus-visible:not(:disabled) {
border-color: #b7cdc3;
color: $accent-strong;
outline: 0;
}
}
.jio-creator-primary {
border: 1px solid transparent;
background: $accent;
color: #fff;
&:hover:not(:disabled),
&:focus-visible:not(:disabled) {
background: $accent-strong;
outline: 0;
}
&:disabled {
opacity: 0.55;
cursor: default;
}
}
.jio-creator-drawer__head > div {
min-width: 0;
}
.jio-creator-drawer__subtitle {
margin: 4px 0 0;
color: $ink-soft;
font-size: 12px;
line-height: 1.5;
}
.jio-creator-muted {
margin: 0;
color: $ink-soft;
@@ -315,54 +359,6 @@
border-top: 1px dashed $line;
}
.jio-creator-cover {
position: relative;
overflow: hidden;
border-radius: 10px;
img {
display: block;
width: 100%;
aspect-ratio: 2 / 1;
object-fit: cover;
}
button {
position: absolute;
top: 8px;
right: 8px;
width: 30px;
height: 30px;
border: 0;
border-radius: 50%;
background: rgba(0, 0, 0, 0.55);
color: #fff;
cursor: pointer;
}
}
.jio-creator-cover-empty {
display: flex;
min-height: 120px;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 8px;
border: 1px dashed $line;
border-radius: 10px;
background: #fafcfb;
color: $ink-soft;
font-size: 13px;
cursor: pointer;
input {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
}
}
.jio-creator-attachments {
margin-top: 18px;
padding-top: 16px;
@@ -641,6 +641,13 @@ body.jio-page-submit.has-static-nav {
}
}
/* 分类 / 标签这类需要整行展开的选择器:说明在上,选项在下。 */
.jio-creator__setting-row--stack {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.jio-creator__setting-copy {
min-width: 0;
}