feat(creator-studio): dialog-driven table/math/footnote, theme quote, code language picker

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-06 23:23:59 +08:00
1 parent 7c33482be7
commit 7885f42cff
14 files changed
+1005 -54

No files matched your search

+129 -17
View File
@@ -18,7 +18,19 @@ import { SaveState, type SaveStateValue } from './components/save-state';
import { useAutosave } from './hooks/use-autosave';
import { useLocalRecovery } from './hooks/use-local-recovery';
import { normalizePayForRequest } from './utils/pay';
import { codeBlockMarkup, dividerMarkup, footnoteMarkup, listMarkup, mathMarkup, tableMarkup, tocMarkup } from './utils/block-markup';
import { EDITOR_CONTENT_STYLE } from './editor-content-style';
import {
codeBlockMarkup,
dividerMarkup,
footnoteMarkup,
listMarkup,
mathMarkup,
quoteMarkup,
tableMarkup,
tocMarkup,
type QuoteColor,
} from './utils/block-markup';
import { BlockDialog, type BlockDialogKind, type BlockDialogResult } from './components/block-dialog';
import './styles/studio.scss';
const bootstrap: Record<string, any> = window['jioCreatorStudio'] || {};
@@ -132,6 +144,9 @@ function Studio(): JSX.Element {
at: '',
}));
const [drawerOpen, setDrawerOpen] = useState(false);
const [blockDialog, setBlockDialog] = useState<BlockDialogKind | null>(null);
const [codeDialog, setCodeDialog] = useState<null | { language: string; code: string }>(null);
const codeTargetRef = useRef<any>(null);
const [notice, setNotice] = useState<{ tone: 'ok' | 'error'; text: string } | null>(null);
const [recovery, setRecovery] = useState<null | { savedAt: string }>(null);
const dirtyRef = useRef(false);
@@ -384,7 +399,7 @@ function Studio(): JSX.Element {
wpautop: true,
body_class: 'jio-creator__tinymce',
content_css: bootstrap.creator?.contentCss || '',
content_style: 'body{margin:0;color:#243330;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans SC","PingFang SC","Microsoft YaHei",sans-serif;font-size:16px;line-height:1.85;letter-spacing:.01em}p{margin:0 0 1.1em}h1,h2,h3,h4,h5,h6{margin:1.5em 0 .65em;line-height:1.4}img,iframe{max-width:100%}img{height:auto}',
content_style: EDITOR_CONTENT_STYLE,
height: 650,
placeholder: String(bootstrap.limits?.contentPlaceholder || ''),
setup: (editor: any) => {
@@ -400,6 +415,24 @@ function Studio(): JSX.Element {
updateFormatValue(editor);
});
editor.on('NodeChange', () => updateFormatValue(editor));
// 代码块右上角的语言标签:点击整块即可换语言,等价于公众号的「高亮」入口。
editor.on('click', (event: any) => {
const pre = editor.dom.getParent(event.target, 'pre.enlighter-pre');
if (!pre) {
return;
}
// 只响应右上角语言标签那一片,光标点进代码正文时保持正常编辑。
const rect = pre.getBoundingClientRect();
const relativeY = Number(event.clientY) - rect.top;
const relativeX = Number(event.clientX) - rect.left;
if (relativeY > 40 || relativeX < rect.width - 120) {
return;
}
event.preventDefault();
const language = String(pre.getAttribute('data-enlighter-language') || 'generic');
codeTargetRef.current = pre;
setCodeDialog({ language, code: String((pre.textContent || '').trim()) });
});
editor.on('input change Undo Redo AddUndo', () => {
updateContent(editor.getContent());
updateHistoryAvailability(editor);
@@ -508,22 +541,9 @@ function Studio(): JSX.Element {
}
}, [focusEditor]);
const insertBlock = useCallback((kind: string) => {
const insertHtml = useCallback((html: string) => {
const editor = focusEditor();
if (!editor) {
return;
}
const markup: Record<string, string> = {
list: listMarkup(false),
toc: tocMarkup(),
divider: dividerMarkup(),
code: codeBlockMarkup(),
footnote: footnoteMarkup(),
math: mathMarkup(),
table: tableMarkup(),
};
const html = markup[kind];
if (!html) {
if (!editor || !html) {
return;
}
editor.insertContent(html);
@@ -532,6 +552,60 @@ function Studio(): JSX.Element {
updateHistoryAvailability(editor);
}, [focusEditor]);
// 直接插入、无需参数的小块;需要参数的块打开通用弹窗。
const insertBlock = useCallback((kind: string) => {
rememberSelection();
// code 走「语言选择」弹窗,其余三类走通用插入弹窗。
if (kind === 'table' || kind === 'math' || kind === 'footnote') {
setBlockDialog(kind as BlockDialogKind);
return;
}
if (kind === 'code') {
setCodeDialog({ language: 'generic', code: '' });
return;
}
if (kind === 'list') {
insertHtml(listMarkup(false));
return;
}
if (kind === 'toc') {
insertHtml(tocMarkup());
return;
}
if (kind === 'divider') {
insertHtml(dividerMarkup());
}
}, [insertHtml, rememberSelection]);
const insertQuote = useCallback((colorClass: QuoteColor) => {
const editor = focusEditor();
if (!editor) {
return;
}
const selected = String(editor.selection.getContent() || '').trim();
const existing = editor.dom.getParent(editor.selection.getNode(), '.quote_q');
if (existing) {
existing.className = `quote_q quote-mce${colorClass ? ` ${colorClass}` : ''}`;
editor.nodeChanged();
return;
}
insertHtml(quoteMarkup(selected, colorClass));
}, [focusEditor, insertHtml]);
const applyBlockDialog = useCallback((result: BlockDialogResult) => {
setBlockDialog(null);
// 弹窗抢走焦点后,插入前把光标放回正文的原选区。
if (result.kind === 'table') {
insertHtml(tableMarkup(result));
} else if (result.kind === 'math') {
insertHtml(mathMarkup(result));
} else if (result.kind === 'footnote') {
insertHtml(footnoteMarkup(result.note));
} else {
insertHtml(codeBlockMarkup(result.language, result.code));
}
}, [insertHtml]);
const openMedia = useCallback((type: 'image' | 'video' | 'file') => {
const editor = editorRef.current;
const media = (window as any).JioSubmitMedia;
@@ -608,6 +682,7 @@ function Studio(): JSX.Element {
onOpenLink={openLink}
onOpenMedia={openMedia}
onInsertBlock={insertBlock}
onInsertQuote={insertQuote}
/>
<div className="jio-creator__bar-actions jio-creator__toolbar-extension" data-toolbar-extension aria-label={__('工具扩展区', 'jio')}>
<SaveState value={saveState} />
@@ -676,6 +751,43 @@ function Studio(): JSX.Element {
onScheduleChange={(enabled, at) => patchDraft({ schedule: { enabled, at } })}
/>
{blockDialog && (
<BlockDialog
kind={blockDialog}
onSubmit={applyBlockDialog}
onClose={() => setBlockDialog(null)}
/>
)}
{codeDialog && (
<BlockDialog
kind="code"
initialLanguage={codeDialog.language}
initialCode={codeDialog.code}
onSubmit={(result) => {
setCodeDialog(null);
if (result.kind !== 'code') {
return;
}
const previous = codeTargetRef.current;
codeTargetRef.current = null;
insertHtml(codeBlockMarkup(result.language, result.code));
if (previous) {
// 换语言时移除旧块,避免留下两份代码。
const editor = editorRef.current;
if (editor && !editor.removed) {
editor.dom.remove(previous);
updateContent(editor.getContent());
}
}
}}
onClose={() => {
setCodeDialog(null);
codeTargetRef.current = null;
}}
/>
)}
{drawerOpen && (
<PublishDrawer
draft={draft}