feat(creator-studio): 公众号式插入面板与封面条

- 新增常驻「添加内容」按钮与插入面板:分组网格列出正文/标题/列表/引用、
  图片/视频/嵌入、代码/分隔线/表格/付费内容;条目不满足能力时不出现在面板里
- 面板支持方向键在网格内移动焦点、Esc 关闭;桌面居中浮层,移动端贴底抽屉并
  留出安全区
- 空文档插入区块时替换初始空白段落,不留空行
- 新增封面条:无封面时给低调入口,有封面时显示 2.35:1 缩略图,悬停出现
  「更换封面」,右上角可移除
- 补充契约测试:头部/页脚/挂件区开关、插入面板真实区块名与能力过滤、
  中文字数口径

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-06 15:57:44 +08:00
1 parent d07cffe99e
commit 79909f449e
10 files changed
+678 -7

No files matched your search

+52 -4
View File
@@ -9,9 +9,9 @@
*/
import { createRoot, useCallback, useEffect, useMemo, useRef, useState } from '@wordpress/element';
import { parse, serialize } from '@wordpress/blocks';
import { createBlock, parse, serialize } from '@wordpress/blocks';
import { registerCoreBlocks } from '@wordpress/block-library';
import { useSelect } from '@wordpress/data';
import { useDispatch, useSelect } from '@wordpress/data';
import { BlockEditorProvider, BlockList, BlockTools } from '@wordpress/block-editor';
import { count as countWords } from '@wordpress/wordcount';
import { __ } from '@wordpress/i18n';
@@ -20,6 +20,9 @@ import { apiPost } from './services/api';
import { installApiFetchNonce } from './services/api-fetch';
import { installMediaUploadBridge } from './services/media';
import { PublishDrawer } from './panels/publish-drawer';
import { CoverStrip } from './components/cover-strip';
import { InsertPanel, type InsertItem } from './panels/insert-panel';
import { buildInsertGroups } from './utils/insert-items';
import { SaveState, type SaveStateValue } from './components/save-state';
import { useAutosave } from './hooks/use-autosave';
import { useLocalRecovery } from './hooks/use-local-recovery';
@@ -106,6 +109,7 @@ function Studio(): JSX.Element {
const [blocks, setBlocks] = useState(() => parse(draft.content || ''));
const [saveState, setSaveState] = useState<SaveStateValue>({ state: 'ready', at: '' });
const [drawerOpen, setDrawerOpen] = useState(false);
const [insertOpen, setInsertOpen] = useState(false);
const [notice, setNotice] = useState<{ tone: 'ok' | 'error'; text: string } | null>(null);
const [recovery, setRecovery] = useState<null | { savedAt: string }>(null);
const dirtyRef = useRef(false);
@@ -328,9 +332,26 @@ function Studio(): JSX.Element {
[]
);
const insertGroups = useMemo(() => buildInsertGroups(capabilities), [capabilities]);
const insertBlock = useCallback(
(item: InsertItem) => {
setInsertOpen(false);
dirtyRef.current = true;
setBlocks((current: any[]) => {
// 空文档时直接替换初始的空白段落,避免留下一个空行。
const next = [...current];
if (next.length === 1 && next[0].name === 'core/paragraph' && !next[0].attributes?.content) {
return [createBlock(item.name, item.attributes || {})];
}
return [...next, createBlock(item.name, item.attributes || {})];
});
},
[]
);
return (
<div className="jio-creator" data-status={bootstrap.status || ''} data-blocks={selectedBlockCount}>
<header className="jio-creator__bar">
<div className="jio-creator" data-status={bootstrap.status || ''} data-blocks={selectedBlockCount}> <header className="jio-creator__bar">
<a className="jio-creator__back" href={bootstrap.urls?.center || '/'}>
<i className="fa fas fa-angle-left" aria-hidden="true" />
<span>{__('返回', 'jio')}</span>
@@ -371,6 +392,13 @@ function Studio(): JSX.Element {
)}
<main className="jio-creator__canvas">
<CoverStrip
featured={draft.featured}
canEdit={!!capabilities.canUploadImage}
onOpen={() => setDrawerOpen(true)}
onClear={() => patchDraft({ featured: { type: '', data: {} } })}
/>
<label className="jio-creator__title">
<span className="screen-reader-text">{__('文章标题', 'jio')}</span>
<textarea
@@ -407,6 +435,18 @@ function Studio(): JSX.Element {
<BlockList />
</BlockTools>
</BlockEditorProvider>
{/* 公众号式常驻插入按钮:始终停在正文末尾,点击打开内容面板。 */}
<button
type="button"
className="jio-creator__insert"
onClick={() => setInsertOpen(true)}
aria-haspopup="dialog"
aria-expanded={insertOpen}
>
<i className="fa fas fa-plus" aria-hidden="true" />
<span>{__('添加内容', 'jio')}</span>
</button>
</div>
<footer className="jio-creator__footer">
@@ -417,6 +457,14 @@ function Studio(): JSX.Element {
</footer>
</main>
{insertOpen && (
<InsertPanel
groups={insertGroups}
onInsert={insertBlock}
onClose={() => setInsertOpen(false)}
/>
)}
{drawerOpen && (
<PublishDrawer
draft={draft}