feat(creator-studio): switch writing editor to TinyMCE

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-06 20:51:18 +08:00
1 parent 4868de2cda
commit 384d2b7aa0
25 files changed
+1255 -7076

No files matched your search

+2 -9
View File
@@ -214,7 +214,7 @@ final class CreatorRest {
$data = [
'posts_id' => $post_id,
'post_title' => $payload['title'],
'post_content' => $this->block_content($payload['content']),
'post_content' => $payload['content'],
'excerpt' => $payload['excerpt'],
'category' => $payload['categories'],
'tags' => implode(',', array_map('strval', $payload['tags'])),
@@ -298,7 +298,7 @@ final class CreatorRest {
$data = [
'posts_id' => $post_id,
'post_title' => $payload['title'],
'post_content' => $this->block_content($payload['content']),
'post_content' => $payload['content'],
'excerpt' => $payload['excerpt'],
'category' => $payload['categories'],
'tags' => implode(',', array_map('strval', $payload['tags'])),
@@ -507,13 +507,6 @@ final class CreatorRest {
PrivateAssets::claim_many($keys, $post_id, $user_id);
}
/**
* 客户端提交的是 JSON 区块,落到 post_content 前统一交给 wp_kses。
*/
private function block_content(string $content): string {
return $content;
}
/**
* @param array<string, mixed> $settings
* @return array<string, mixed>
+126 -43
View File
@@ -2,8 +2,7 @@
/**
* Jio Creator Studio 前台创作工作台。
*
* 与默认 WordPress 后台编辑器隔离的独立入口:只承载前台写作体验所需的静态资源、
* 初始上下文与挂载点。写作界面是 Gutenberg 内核 + 自定义 React 工作台。
* 与 WordPress 后台编辑器隔离的前台写作工作台;正文由 TinyMCE 编辑,React 只负责操作栏、发布配置与保存编排。
*
* @package Jio
*/
@@ -53,25 +52,57 @@ final class CreatorStudio {
$uri = get_template_directory_uri() . '/' . self::BUILD_DIR;
$meta = include $dir . '/index.asset.php';
$deps = is_array($meta['dependencies'] ?? null) ? $meta['dependencies'] : [];
// core/image 等块的行内 RichTextToolbarButton 由 WordPress 的格式库注册;
// BlockTools 负责浮层插槽,但必须显式入队前台 wp-format-library。
$deps[] = 'wp-format-library';
// Creator Studio uses the classic WordPress TinyMCE runtime, not Gutenberg.
wp_enqueue_editor();
$deps[] = 'editor';
$deps = array_values(array_unique($deps));
$ver = (string) ($meta['version'] ?? AKSRT_VERSION);
// 区块样式是前台编辑体验的一部分;编辑器专属样式只在已注册时追加,
// 避免在没有注册该 handle 的前台请求上产生悬空依赖。
$style_deps = ['jio-public'];
foreach (['wp-block-library', 'wp-block-editor', 'wp-components', 'wp-format-library'] as $handle) {
if (wp_style_is($handle, 'registered')) {
$style_deps[] = $handle;
}
$plugin_path = get_template_directory() . '/assets/js/front-submit-editor.js';
$plugin_url = '';
if (file_exists($plugin_path)) {
$plugin_url = add_query_arg(
'ver',
(string) filemtime($plugin_path),
get_template_directory_uri() . '/assets/js/front-submit-editor.js'
);
}
$content_css_path = get_template_directory() . '/assets/css/frontend/pages/submit-editor.css';
$content_css_url = get_template_directory_uri() . '/assets/css/frontend/pages/submit-editor.css';
if (file_exists($content_css_path)) {
$content_css_url = add_query_arg('ver', (string) filemtime($content_css_path), $content_css_url);
}
$payload = $this->bootstrap();
$payload['creator']['contentCss'] = $content_css_url;
$tinymce_data = $this->tinymce_payload($payload);
if (file_exists($plugin_path)) {
wp_enqueue_script('jio-creator-tinymce-tools', $plugin_url, ['editor'], (string) filemtime($plugin_path), true);
wp_add_inline_script(
'jio-creator-tinymce-tools',
'window.jioSubmitData = ' . wp_json_encode($tinymce_data) . ';',
'before'
);
$deps[] = 'jio-creator-tinymce-tools';
}
$deps = array_values(array_unique($deps));
$media_css = get_template_directory() . '/assets/css/frontend/pages/submit.css';
if (file_exists($media_css)) {
wp_enqueue_style(
'jio-creator-media',
get_template_directory_uri() . '/assets/css/frontend/pages/submit.css',
['jio-public'],
(string) filemtime($media_css)
);
}
wp_enqueue_style(
'jio-creator-studio',
$uri . '/index.css',
$style_deps,
['jio-public', 'jio-creator-media'],
$ver
);
@@ -83,38 +114,93 @@ final class CreatorStudio {
true
);
// Jio 自有区块:编辑器里已注册的同一份定义,前台工作台复用,
// 避免维护第二套 Jio Blocks。前台没有注册 wp-media-utils,
// 该脚本只依赖 block-editor 的 MediaUpload,不需要它。
$blocks_js = get_template_directory() . '/assets/js/gutenberg-blocks.js';
if (file_exists($blocks_js) && !wp_script_is('jio-gutenberg-blocks', 'enqueued')) {
wp_enqueue_script(
'jio-gutenberg-blocks',
get_template_directory_uri() . '/assets/js/gutenberg-blocks.js',
['wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-hooks', 'wp-data'],
(string) filemtime($blocks_js),
true
);
}
// 区块前台样式(jio/carousel 等自定义区块的输出样式)。
$blocks_css = get_template_directory() . '/assets/css/gutenberg-blocks.css';
if (file_exists($blocks_css)) {
wp_enqueue_style(
'jio-gutenberg-blocks-front',
get_template_directory_uri() . '/assets/css/gutenberg-blocks.css',
[],
(string) filemtime($blocks_css)
);
}
wp_add_inline_script(
'jio-creator-studio',
'window.jioCreatorStudio = ' . wp_json_encode($this->bootstrap()) . ';',
'window.jioCreatorStudio = ' . wp_json_encode($payload) . ';',
'before'
);
}
private function tinymce_payload(array $payload): array {
$capabilities = is_array($payload['capabilities'] ?? null) ? $payload['capabilities'] : [];
$base = rest_url(CreatorRest::NS);
$system_tools = class_exists('Jio\\Settings\\ExtensionRuntime')
? \Jio\Settings\ExtensionRuntime::get_system_tools_settings()
: [];
return [
'restUrl' => esc_url_raw($base),
'restNonce' => wp_create_nonce('wp_rest'),
'postId' => (int) ($payload['sourceId'] ?? 0),
'media' => [
'image' => ['url' => esc_url_raw(rest_url(CreatorRest::NS . '/front-submit/media/image'))],
'video' => ['url' => esc_url_raw(rest_url(CreatorRest::NS . '/front-submit/media/video'))],
'file' => ['url' => esc_url_raw(rest_url(CreatorRest::NS . '/front-submit/media/file'))],
],
'uploads' => [
'image' => !empty($capabilities['canUploadImage']) ? [
'url' => esc_url_raw(rest_url(CreatorRest::NS . '/front-submit/upload/image')),
'maxSize' => FrontSubmit::upload_limit_bytes('image'),
] : null,
'video' => !empty($capabilities['canUploadVideo']) ? [
'url' => esc_url_raw(rest_url(CreatorRest::NS . '/front-submit/upload/video')),
'maxSize' => FrontSubmit::upload_limit_bytes('video'),
] : null,
'resource' => !empty($capabilities['canUploadFile']) ? [
'url' => esc_url_raw(rest_url(CreatorRest::NS . '/front-submit/upload/resource')),
'maxSize' => FrontSubmit::upload_limit_bytes('resource'),
] : null,
],
'editor' => [
'canUploadImage' => !empty($capabilities['canUploadImage']),
'canUploadVideo' => !empty($capabilities['canUploadVideo']),
'canEmbedVideo' => !empty($capabilities['canEmbed']),
'canUploadFile' => !empty($capabilities['canUploadFile']),
'canHide' => !empty($capabilities['canHide']),
'canPay' => !empty($capabilities['canPay']),
'floatToolbar' => false,
],
'imageUploadMultiple' => class_exists('Jio\\Attachment\\UploadLimits')
? \Jio\Attachment\UploadLimits::image_batch_limit()
: 6,
'splitUpload' => [
'enabled' => !empty($system_tools['split_upload_s']),
'minSize' => (int) ($system_tools['split_minimum_size'] ?? 20),
],
'i18n' => [
'insertImage' => __('插入图片', 'jio'),
'insertVideo' => __('插入视频', 'jio'),
'uploadUnavailable' => __('当前账号不能上传此类文件', 'jio'),
'fileTooLarge' => __('文件大小超过限制', 'jio'),
'invalidFile' => __('请选择有效文件', 'jio'),
'error' => __('操作失败,请重试', 'jio'),
'close' => __('关闭', 'jio'),
'mineImage' => __('我的图片', 'jio'),
'mineVideo' => __('我的视频', 'jio'),
'uploadImage' => __('上传图片', 'jio'),
'uploadVideo' => __('上传视频', 'jio'),
'externalImage' => __('外链图片', 'jio'),
'externalVideo' => __('外链视频', 'jio'),
'selectImages' => __('选择或拖入图片', 'jio'),
'selectVideo' => __('选择或拖入视频', 'jio'),
'confirmUpload' => __('确认上传', 'jio'),
'confirmInsert' => __('确认插入', 'jio'),
'loading' => __('加载中…', 'jio'),
'emptyMedia' => __('暂无媒体文件', 'jio'),
'searchMedia' => __('搜索媒体(至少 2 个字符)', 'jio'),
'searchMin' => __('请输入至少 2 个字符进行搜索', 'jio'),
'uploading' => __('正在上传…', 'jio'),
'uploaded' => __('上传成功', 'jio'),
'invalidMediaUrl' => __('请填写有效地址', 'jio'),
'externalImageHint' => __('每行填写一个图片地址:', 'jio'),
'externalVideoHint' => __('请填写视频文件地址:', 'jio'),
'embedVideo' => __('嵌入视频', 'jio'),
'embedVideoHint' => __('请输入嵌入地址或者直接粘贴 iframe 嵌入代码:', 'jio'),
'confirmEmbed' => __('确认嵌入', 'jio'),
],
];
}
/**
* 渲染给前端的启动上下文。
*
@@ -139,10 +225,7 @@ final class CreatorStudio {
'privateUrl'=> esc_url_raw(rest_url(CreatorRest::NS . '/creator/private-upload')),
'bootstrapUrl' => esc_url_raw(rest_url(CreatorRest::NS . '/creator/bootstrap')),
],
'blocks' => [
'mediaUploadUrl' => esc_url_raw(rest_url(CreatorRest::NS . '/front-submit/upload/image')),
'mediaListUrl' => esc_url_raw(rest_url(CreatorRest::NS . '/front-submit/media/image')),
],
]);
}
}