feat(creator-studio): centered paper layout and scheduled publishing
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
384d2b7aa0
commit
7bdf7cf018
20 files changed
+549
-308
No files matched your search
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* 封面预览条(公众号式)。
|
||||
*
|
||||
* 位于标题上方一行:有封面时显示缩略图,点击回到发布抽屉的封面区;
|
||||
* 无封面时给一个低调的入口。封面能力由服务端 bootstrap 决定,前端不放大权限。
|
||||
*
|
||||
* @package Jio
|
||||
*/
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
type Featured = { type: string; data: Record<string, any>; mediaId?: number };
|
||||
|
||||
type Props = {
|
||||
featured: Featured;
|
||||
canEdit: boolean;
|
||||
onOpen: () => void;
|
||||
onClear: () => void;
|
||||
};
|
||||
|
||||
export function CoverStrip({ featured, canEdit, onOpen, onClear }: Props) {
|
||||
const url = typeof featured?.data?.url === 'string' ? featured.data.url : '';
|
||||
|
||||
if (!canEdit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
return (
|
||||
<button type="button" className="jio-creator__cover is-empty" onClick={onOpen}>
|
||||
<i className="fa far fa-image" aria-hidden="true" />
|
||||
<span>{__('添加封面', 'jio')}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="jio-creator__cover">
|
||||
<button type="button" className="jio-creator__cover-thumb" onClick={onOpen}>
|
||||
<img src={url} alt="" />
|
||||
<span className="jio-creator__cover-edit">
|
||||
<i className="fa fas fa-pen" aria-hidden="true" />
|
||||
{__('更换封面', 'jio')}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="jio-creator__cover-clear"
|
||||
onClick={onClear}
|
||||
aria-label={__('移除封面', 'jio')}
|
||||
>
|
||||
<i className="fa fas fa-times" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 发布设置卡片(位于正文纸张下方)。
|
||||
*
|
||||
* 只承载与"这篇会被怎样发布"有关的低频选项:封面与定时发布。
|
||||
* 分类、标签、付费等配置仍在发布抽屉里;服务端 bootstrap 决定每项是否可用。
|
||||
*
|
||||
* @package Jio
|
||||
*/
|
||||
|
||||
import { useMemo, useRef, useState } from '@wordpress/element';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
import { apiUpload } from '../services/api';
|
||||
|
||||
type Featured = { type: string; data: Record<string, any>; mediaId?: number };
|
||||
|
||||
type Props = {
|
||||
featured: Featured;
|
||||
canEditCover: boolean;
|
||||
canSchedule: boolean;
|
||||
scheduleEnabled: boolean;
|
||||
scheduleAt: string;
|
||||
onFeaturedChange: (featured: Featured) => void;
|
||||
onScheduleChange: (enabled: boolean, at: string) => void;
|
||||
};
|
||||
|
||||
const bootstrap: Record<string, any> = window['jioCreatorStudio'] || {};
|
||||
const nonce: string = bootstrap.nonce || '';
|
||||
|
||||
const pad = (value: number): string => String(value).padStart(2, '0');
|
||||
|
||||
function toLocalInput(date: Date): string {
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
}
|
||||
|
||||
export function defaultScheduleTime(): string {
|
||||
// 默认给一小时后,避免用户点开开关就看到一个"已经过去"的时间。
|
||||
return toLocalInput(new Date(Date.now() + 60 * 60 * 1000));
|
||||
}
|
||||
|
||||
export function PublishSettings({
|
||||
featured,
|
||||
canEditCover,
|
||||
canSchedule,
|
||||
scheduleEnabled,
|
||||
scheduleAt,
|
||||
onFeaturedChange,
|
||||
onScheduleChange,
|
||||
}: Props) {
|
||||
const coverInput = useRef<HTMLInputElement | null>(null);
|
||||
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 uploadCover = async () => {
|
||||
const file = coverInput.current?.files?.[0];
|
||||
const uploadUrl = bootstrap.media?.uploadUrl?.image;
|
||||
if (!file || !uploadUrl) {
|
||||
return;
|
||||
}
|
||||
setError('');
|
||||
setUploading(true);
|
||||
try {
|
||||
const response = await apiUpload(uploadUrl, file, nonce);
|
||||
const id = Number(response.attachment_id || response.id || 0);
|
||||
const url = String(response.url || '');
|
||||
if (!id || !url) {
|
||||
throw new Error(__('上传后无法读取图片。', 'jio'));
|
||||
}
|
||||
onFeaturedChange({ type: 'image', mediaId: id, data: { id, url } });
|
||||
} catch (uploadError: any) {
|
||||
setError(String(uploadError?.message || __('上传失败', 'jio')));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
if (coverInput.current) {
|
||||
coverInput.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="jio-creator__settings" aria-label={__('发布设置', 'jio')}>
|
||||
<h2 className="jio-creator__settings-title">{__('发布设置', 'jio')}</h2>
|
||||
|
||||
{canEditCover && (
|
||||
<div className="jio-creator__setting-row">
|
||||
<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__cover-control">
|
||||
{coverUrl ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="jio-creator__cover-preview"
|
||||
onClick={() => coverInput.current?.click()}
|
||||
aria-label={__('更换封面', 'jio')}
|
||||
>
|
||||
<img src={coverUrl} alt="" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="jio-creator__cover-clear"
|
||||
onClick={() => onFeaturedChange({ type: '', data: {} })}
|
||||
aria-label={__('移除封面', 'jio')}
|
||||
>
|
||||
<i className="fa fas fa-times" aria-hidden="true" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="jio-creator__cover-add"
|
||||
onClick={() => coverInput.current?.click()}
|
||||
disabled={uploading}
|
||||
>
|
||||
<i className="fa fas fa-plus" aria-hidden="true" />
|
||||
<span>{uploading ? __('上传中…', 'jio') : __('添加封面', 'jio')}</span>
|
||||
</button>
|
||||
)}
|
||||
<input
|
||||
ref={coverInput}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="jio-creator__cover-input"
|
||||
onChange={() => void uploadCover()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <p className="jio-creator-error">{error}</p>}
|
||||
|
||||
<div className="jio-creator__setting-row">
|
||||
<div className="jio-creator__setting-copy">
|
||||
<span className="jio-creator__setting-label">{__('定时发布', 'jio')}</span>
|
||||
<p className="jio-creator__setting-help">
|
||||
{canSchedule
|
||||
? __('开启后到点自动发布,发布前不会出现在文章列表。', 'jio')
|
||||
: __('当前账号提交后需要审核,审核通过后才会发布。', 'jio')}
|
||||
</p>
|
||||
</div>
|
||||
<label className="jio-creator-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={scheduleEnabled}
|
||||
disabled={!canSchedule}
|
||||
onChange={(event) => onScheduleChange(event.target.checked, scheduleAt || defaultScheduleTime())}
|
||||
/>
|
||||
<span aria-hidden="true" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{canSchedule && scheduleEnabled && (
|
||||
<div className="jio-creator__schedule">
|
||||
<label>
|
||||
<span>{__('发布时间', 'jio')}</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={scheduleAt}
|
||||
min={minSchedule}
|
||||
onChange={(event) => onScheduleChange(true, event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<p className="jio-creator-muted">
|
||||
{sprintf(
|
||||
/* translators: %s: site timezone */
|
||||
__('按站点时区(%s)发布,至少需要晚于当前时间 1 分钟。', 'jio'),
|
||||
String(bootstrap.timezone || 'UTC')
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -8,13 +8,13 @@
|
||||
*/
|
||||
|
||||
import { createRoot, useCallback, useEffect, useMemo, useRef, useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
import { apiPost } from './services/api';
|
||||
import { PublishDrawer } from './panels/publish-drawer';
|
||||
import { CoverStrip } from './components/cover-strip';
|
||||
import { AuthorRow } from './components/author-row';
|
||||
import { EditorToolbar } from './components/editor-toolbar';
|
||||
import { PublishSettings } from './components/publish-settings';
|
||||
import { AuthorRow } from './components/author-row';
|
||||
import { SaveState, type SaveStateValue } from './components/save-state';
|
||||
import { useAutosave } from './hooks/use-autosave';
|
||||
import { useLocalRecovery } from './hooks/use-local-recovery';
|
||||
@@ -48,13 +48,15 @@ type Limits = {
|
||||
pointsMax?: number;
|
||||
};
|
||||
|
||||
type Schedule = { enabled: boolean; at: string };
|
||||
|
||||
type Draft = {
|
||||
postId: number;
|
||||
versionId: number;
|
||||
rev: number;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
content: string;
|
||||
schedule: Schedule;
|
||||
categories: number[];
|
||||
tags: string[];
|
||||
custom: Record<string, string[]>;
|
||||
@@ -71,8 +73,8 @@ function initialDraft(): Draft {
|
||||
versionId: Number(bootstrap.versionId || 0),
|
||||
rev: Number(bootstrap.versionRev || 0),
|
||||
title: String(bootstrap.title || ''),
|
||||
excerpt: String(bootstrap.excerpt || ''),
|
||||
content: String(bootstrap.content || ''),
|
||||
schedule: { enabled: false, at: '' },
|
||||
categories: selected.map(Number),
|
||||
tags: tags.slice(),
|
||||
custom: bootstrap.custom || {},
|
||||
@@ -191,12 +193,15 @@ function Studio(): JSX.Element {
|
||||
version_id: current.versionId,
|
||||
rev: current.rev,
|
||||
title: current.title,
|
||||
excerpt: current.excerpt,
|
||||
content: current.content,
|
||||
categories: current.categories,
|
||||
tags: current.tags,
|
||||
custom: current.custom,
|
||||
featured: current.featured,
|
||||
schedule: {
|
||||
enabled: !!current.schedule.enabled,
|
||||
at: current.schedule.enabled ? current.schedule.at : '',
|
||||
},
|
||||
pay: normalizePayForRequest(current.pay),
|
||||
...extra,
|
||||
};
|
||||
@@ -613,12 +618,7 @@ function Studio(): JSX.Element {
|
||||
)}
|
||||
|
||||
<main className="jio-creator__canvas">
|
||||
<CoverStrip
|
||||
featured={draft.featured}
|
||||
canEdit={!!capabilities.canUploadImage}
|
||||
onOpen={() => setDrawerOpen(true)}
|
||||
onClear={() => patchDraft({ featured: { type: '', data: {} } })}
|
||||
/>
|
||||
<AuthorRow author={bootstrap.author || {}} />
|
||||
|
||||
<label className="jio-creator__title">
|
||||
<span className="screen-reader-text">{__('文章标题', 'jio')}</span>
|
||||
@@ -626,33 +626,36 @@ function Studio(): JSX.Element {
|
||||
value={draft.title}
|
||||
onChange={(event) => patchDraft({ title: event.target.value })}
|
||||
rows={1}
|
||||
placeholder={limits.titlePlaceholder || __('请输入标题', 'jio')}
|
||||
maxLength={(Number(limits.titleMax) || 30) * 4}
|
||||
placeholder={sprintf(
|
||||
/* translators: %d: max title length */
|
||||
__('请输入标题(最多 %d 个字)', 'jio'),
|
||||
Number(limits.titleMax) || 100
|
||||
)}
|
||||
spellCheck={false}
|
||||
maxLength={Number(limits.titleMax) || 100}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="jio-creator__excerpt">
|
||||
<span>{__('摘要(可选)', 'jio')}</span>
|
||||
<textarea
|
||||
value={draft.excerpt}
|
||||
onChange={(event) => patchDraft({ excerpt: event.target.value })}
|
||||
rows={2}
|
||||
placeholder={__('一句话概括这篇文章', 'jio')}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<AuthorRow author={bootstrap.author || {}} />
|
||||
|
||||
<div className="jio-creator__editor" data-editor-ready={editorReady ? 'true' : 'false'}>
|
||||
<textarea id={EDITOR_ID} defaultValue={draft.content} onChange={(event) => updateContent(event.target.value)} aria-label={__('文章正文', 'jio')} />
|
||||
</div>
|
||||
|
||||
<footer className="jio-creator__footer">
|
||||
<span>{__('字数', 'jio')}:{charCount}</span>
|
||||
<span>{__('约', 'jio')} {Math.max(1, Math.round(charCount / 400))} {__('分钟阅读', 'jio')}</span>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<PublishSettings
|
||||
featured={draft.featured}
|
||||
canEditCover={!!capabilities.canUploadImage}
|
||||
canSchedule={!!capabilities.canSchedule}
|
||||
scheduleEnabled={!!draft.schedule.enabled}
|
||||
scheduleAt={draft.schedule.at}
|
||||
onFeaturedChange={(featured) => patchDraft({ featured })}
|
||||
onScheduleChange={(enabled, at) => patchDraft({ schedule: { enabled, at } })}
|
||||
/>
|
||||
|
||||
<footer className="jio-creator__footer">
|
||||
<span>{__('字数', 'jio')}:{charCount}</span>
|
||||
<span>{__('约', 'jio')} {Math.max(1, Math.round(charCount / 400))} {__('分钟阅读', 'jio')}</span>
|
||||
</footer>
|
||||
|
||||
{drawerOpen && (
|
||||
<PublishDrawer
|
||||
draft={draft}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* 发布配置抽屉。
|
||||
*
|
||||
* 封面、分类、摘要、付费与资料附件都只在这里出现,正文画布保持干净;
|
||||
* 分类、标签、付费与资料附件都在这里配置;封面与定时发布在正文下方的
|
||||
* 「发布设置」卡片里,避免把封面入口和正文挤在同一张纸上。
|
||||
* 手机端以全屏面板呈现。所有字段与权限来自服务端 bootstrap,前端不自行放大权限。
|
||||
*
|
||||
* @package Jio
|
||||
@@ -12,7 +13,6 @@ import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
import { apiUpload } from '../services/api';
|
||||
|
||||
type Featured = { type: string; data: Record<string, any>; mediaId?: number };
|
||||
type PayState = {
|
||||
enabled: boolean;
|
||||
modo: 'cash' | 'points';
|
||||
@@ -27,11 +27,9 @@ type PayState = {
|
||||
type Draft = {
|
||||
postId: number;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
categories: number[];
|
||||
tags: string[];
|
||||
custom: Record<string, string[]>;
|
||||
featured: Featured;
|
||||
pay: PayState;
|
||||
};
|
||||
|
||||
@@ -89,7 +87,6 @@ export function PublishDrawer(props: Props) {
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
const [uploadError, setUploadError] = useState('');
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const coverInput = useRef<HTMLInputElement | null>(null);
|
||||
const fileInput = useRef<HTMLInputElement | null>(null);
|
||||
const captchaSlot = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -135,28 +132,6 @@ export function PublishDrawer(props: Props) {
|
||||
setTagInput('');
|
||||
};
|
||||
|
||||
const uploadCover = async () => {
|
||||
const file = coverInput.current?.files?.[0];
|
||||
if (!file || !bootstrap.media?.uploadUrl?.image) {
|
||||
return;
|
||||
}
|
||||
setUploadError('');
|
||||
setUploading(true);
|
||||
try {
|
||||
const response = await apiUpload(bootstrap.media.uploadUrl.image, file, nonce);
|
||||
const id = Number(response.attachment_id || response.id || 0);
|
||||
const url = String(response.url || '');
|
||||
if (!id || !url) {
|
||||
throw new Error(__('上传后无法读取图片。', 'jio'));
|
||||
}
|
||||
setDraft({ featured: { type: 'image', mediaId: id, data: { id, url } } });
|
||||
} catch (error: any) {
|
||||
setUploadError(String(error?.message || __('上传失败', 'jio')));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadAttachment = async () => {
|
||||
const file = fileInput.current?.files?.[0];
|
||||
if (!file) {
|
||||
@@ -213,36 +188,6 @@ export function PublishDrawer(props: Props) {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<section className="jio-creator-field">
|
||||
<h3>{__('封面', 'jio')}</h3>
|
||||
{draft.featured?.data?.url ? (
|
||||
<div className="jio-creator-cover">
|
||||
<img src={String(draft.featured.data.url)} alt="" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDraft({ featured: { type: '', data: {} } })}
|
||||
aria-label={__('移除封面', 'jio')}
|
||||
>
|
||||
<i className="fa fas fa-times" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
) : capabilities.canUploadImage ? (
|
||||
<label className="jio-creator-cover-empty">
|
||||
<input
|
||||
ref={coverInput}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={() => void uploadCover()}
|
||||
disabled={uploading}
|
||||
/>
|
||||
<i className="fa fas fa-plus" aria-hidden="true" />
|
||||
<span>{uploading ? __('上传中…', 'jio') : __('上传封面', 'jio')}</span>
|
||||
</label>
|
||||
) : (
|
||||
<p className="jio-creator-muted">{__('当前账号不能设置封面。', 'jio')}</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="jio-creator-field">
|
||||
<h3>{__('分类', 'jio')}</h3>
|
||||
<div className="jio-creator-chips">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
$ink: #243330;
|
||||
$ink-soft: #5c6b66;
|
||||
$paper: #fafbfa;
|
||||
$line: #e2e9e5;
|
||||
$ink: #2b2b2b;
|
||||
$ink-soft: #7d8683;
|
||||
$paper: #f6f6f6;
|
||||
$line: #e6e9e8;
|
||||
$accent: #267665;
|
||||
$accent-strong: #1d5f52;
|
||||
$warn: #b4573f;
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Jio Creator Studio — 沉浸式写作工作台。
|
||||
*
|
||||
* 视觉方向:纸面写作而不是后台配置。单一深青绿用于“保存/发布”这类推进动作,
|
||||
* 其余保持低对比;发布设置全部收进抽屉,正文区域只有标题、摘要与画布。
|
||||
* 视觉方向:浅灰页面 + 居中白纸。写作卡片居中并承担主要视觉重量,
|
||||
* 高频操作固定在顶栏,封面与定时发布收在正文下方的「发布设置」卡片里。
|
||||
*/
|
||||
|
||||
@use './tokens' as *;
|
||||
@@ -14,7 +14,7 @@
|
||||
* 只在投稿模板的 body class 下生效,不影响其他页面。
|
||||
*/
|
||||
.jio-page-submit {
|
||||
background: $paper;
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
/* 主题固定导航会给 body.nav-fixed 预留 88px;工作台隐藏了导航,不应保留空位。 */
|
||||
@@ -43,7 +43,7 @@ body.jio-page-submit.has-static-nav {
|
||||
.jio-creator {
|
||||
--jc-ink: #{$ink};
|
||||
--jc-ink-soft: #{$ink-soft};
|
||||
--jc-paper: #{$paper};
|
||||
--jc-paper: #f6f6f6;
|
||||
--jc-line: #{$line};
|
||||
--jc-accent: #{$accent};
|
||||
--jc-accent-strong: #{$accent-strong};
|
||||
@@ -373,78 +373,57 @@ body.jio-page-submit.has-static-nav {
|
||||
}
|
||||
|
||||
.jio-creator__canvas {
|
||||
/* 公众号式阅读宽度:正文行宽控制在约 40 个汉字,留白优先。 */
|
||||
width: min(720px, 100%);
|
||||
margin: 0 auto;
|
||||
padding: clamp(20px, 4vw, 44px) clamp(20px, 5vw, 52px) 140px;
|
||||
/* 居中白纸:正文行宽固定,页面底色是浅灰,卡片本身承担视觉重量。 */
|
||||
width: min(900px, calc(100% - clamp(24px, 6vw, 72px)));
|
||||
margin: 40px auto 0;
|
||||
padding: clamp(30px, 4vw, 50px) clamp(28px, 5vw, 56px) clamp(36px, 5vw, 56px);
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(24, 39, 34, 0.04), 0 8px 28px rgba(24, 39, 34, 0.05);
|
||||
}
|
||||
|
||||
.jio-creator__title {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 0;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 50px;
|
||||
padding: 0;
|
||||
min-height: 46px;
|
||||
padding: 0 0 16px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #eef0ef;
|
||||
background: transparent;
|
||||
color: var(--jc-ink);
|
||||
font-family: "Songti SC", "Noto Serif SC", "Source Han Serif SC", serif;
|
||||
font-size: clamp(23px, 2.6vw, 27px);
|
||||
font-weight: 500;
|
||||
color: #2b2b2b;
|
||||
font-family: inherit;
|
||||
font-size: clamp(26px, 3vw, 32px);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1.4;
|
||||
line-height: 1.35;
|
||||
resize: none;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
border-bottom-color: #dfe3e1;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #b7c2bd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__excerpt {
|
||||
display: block;
|
||||
margin-bottom: 22px;
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: #9aa8a2;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 6px 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--jc-ink-soft);
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
resize: none;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
color: #c3c7c5;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__editor {
|
||||
position: relative;
|
||||
min-height: 50vh;
|
||||
margin: 12px 0 30px;
|
||||
min-height: 52vh;
|
||||
margin: 22px 0 0;
|
||||
|
||||
.wp-editor-tabs,
|
||||
.wp-media-buttons,
|
||||
.quicktags-toolbar,
|
||||
.mce-top-part {
|
||||
.mce-top-part,
|
||||
.tox .tox-editor-header,
|
||||
.tox .tox-statusbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -455,12 +434,6 @@ body.jio-page-submit.has-static-nav {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.mce-top-part,
|
||||
.tox .tox-editor-header,
|
||||
.tox .tox-statusbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mce-tinymce,
|
||||
.tox-tinymce {
|
||||
border: 0 !important;
|
||||
@@ -477,76 +450,134 @@ body.jio-page-submit.has-static-nav {
|
||||
|
||||
.mce-edit-area iframe,
|
||||
.tox .tox-edit-area__iframe {
|
||||
min-height: 55vh;
|
||||
min-height: 52vh;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 55vh;
|
||||
min-height: 52vh;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--jc-ink);
|
||||
color: #2b2b2b;
|
||||
font: inherit;
|
||||
font-size: 16px;
|
||||
line-height: 1.85;
|
||||
line-height: 1.8;
|
||||
resize: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--jc-line);
|
||||
color: var(--jc-ink-soft);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ---------- 封面预览条 ---------- */
|
||||
|
||||
.jio-creator__cover {
|
||||
position: relative;
|
||||
margin-bottom: 18px;
|
||||
|
||||
&.is-empty {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 1px dashed #d3ded8;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #8c9a94;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.16s ease, color 0.16s ease;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
border-color: var(--jc-accent);
|
||||
color: var(--jc-accent-strong);
|
||||
outline: 0;
|
||||
&::placeholder {
|
||||
color: #b9bfbc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__cover-thumb {
|
||||
position: relative;
|
||||
/* ---------- 发布设置卡片 ---------- */
|
||||
|
||||
.jio-creator__settings {
|
||||
width: min(900px, calc(100% - clamp(24px, 6vw, 72px)));
|
||||
margin: 20px auto 0;
|
||||
padding: clamp(20px, 3vw, 28px) clamp(24px, 4vw, 40px);
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(24, 39, 34, 0.04), 0 8px 28px rgba(24, 39, 34, 0.05);
|
||||
}
|
||||
|
||||
.jio-creator__settings-title {
|
||||
margin: 0 0 4px;
|
||||
color: #2b2b2b;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.jio-creator__setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #f2f4f3;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__setting-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.jio-creator__setting-label {
|
||||
display: block;
|
||||
width: 100%;
|
||||
color: #2b2b2b;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.jio-creator__setting-help {
|
||||
margin: 4px 0 0;
|
||||
color: #9aa2a0;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.jio-creator__cover-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.jio-creator__cover-input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.jio-creator__cover-add {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-height: 34px;
|
||||
padding: 0 14px;
|
||||
border: 1px dashed #d8dedb;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #7c8683;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled),
|
||||
&:focus-visible:not(:disabled) {
|
||||
border-color: var(--jc-accent);
|
||||
color: var(--jc-accent-strong);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__cover-preview {
|
||||
display: block;
|
||||
width: 112px;
|
||||
height: 63px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: #eef3f0;
|
||||
border-radius: 6px;
|
||||
background: #eef1f0;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 2.35 / 1;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
@@ -556,45 +587,67 @@ body.jio-page-submit.has-static-nav {
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__cover-edit {
|
||||
position: absolute;
|
||||
inset: auto 0 0 0;
|
||||
display: flex;
|
||||
.jio-creator__cover-clear {
|
||||
display: inline-flex;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
background: linear-gradient(to top, rgba(15, 25, 22, 0.62), rgba(15, 25, 22, 0));
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.16s ease;
|
||||
}
|
||||
|
||||
.jio-creator__cover-thumb:hover .jio-creator__cover-edit,
|
||||
.jio-creator__cover-thumb:focus-visible .jio-creator__cover-edit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.jio-creator__cover-clear {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(15, 25, 22, 0.55);
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
color: #98a19e;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: rgba(15, 25, 22, 0.78);
|
||||
background: #f3f5f4;
|
||||
color: var(--jc-warn);
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__schedule {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 4px 0 18px;
|
||||
|
||||
label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #6f7875;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
input[type="datetime-local"] {
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #e3e7e5;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: #2b2b2b;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.jio-creator-muted {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: min(900px, calc(100% - clamp(24px, 6vw, 72px)));
|
||||
margin: 16px auto 0;
|
||||
padding: 0 4px 140px;
|
||||
color: #98a19e;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ---------- 作者行 ---------- */
|
||||
|
||||
.jio-creator__author {
|
||||
@@ -728,13 +781,30 @@ body.jio-page-submit.has-static-nav {
|
||||
}
|
||||
|
||||
.jio-creator__canvas {
|
||||
padding: 20px 16px calc(120px + env(safe-area-inset-bottom));
|
||||
width: calc(100% - 20px);
|
||||
margin: 14px auto 0;
|
||||
padding: 22px 18px 28px;
|
||||
}
|
||||
|
||||
.jio-creator__settings {
|
||||
width: calc(100% - 20px);
|
||||
margin-top: 12px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.jio-creator__setting-row {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.jio-creator__title textarea {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.jio-creator__footer {
|
||||
width: calc(100% - 20px);
|
||||
padding-bottom: calc(120px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.jio-creator__editor .mce-edit-area iframe,
|
||||
.jio-creator__editor .tox .tox-edit-area__iframe {
|
||||
min-height: 48vh;
|
||||
|
||||
Reference in new issue
Block a user