Files
wordpress-theme-aksrt/templates/pages/page-submit.php
T
kate522andClaude Opus 5 fac4a714e0 feat(creator-studio): 前台 Gutenberg 创作工作台与受保护付费附件
前台投稿页改为沉浸式创作工作台:Gutenberg 负责区块解析与序列化,
React + TypeScript 负责工作台外壳与发布抽屉,Jio 服务端继续作为权限、
审核与付费的唯一权威。

- 新增 assets/creator-studio 独立构建(wp-scripts,锁定 WP 6.4 时代包),
  仅在投稿模板加载,构建缺失或游客访问时回退原有 TinyMCE 表单
- 自动保存(4s 防抖 + rev 冲突 409)、本地快照恢复、发布配置抽屉、
  移动端全屏单列与安全区适配
- 图片插入走现有 front-submit 上传管线,保留能力校验与待认领生命周期
- 已发布文章的改稿写入私有版本,审核通过前线上内容/链接/付费条款不变;
  原生后台「改稿审核」一次性应用或退回,并校验源文章并发指纹
- 私有附件新表 jio_private_asset + jio_dl_token.asset_id,schema 升到 3.4.0;
  文件存放 ABSPATH 之外,未配置私有存储时 fail closed
- 下载令牌绑定文章/附件/用户/IP/UA/次数,每次交付复核 Visibility 的
  订单、VIP 与配额判定;中断传输归还次数
- 付费字段改为写入前校验,非法价格不再留下半保存文章
- 新增 creator-studio 契约测试;更新受影响的 schema/下载/投稿契约

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-06 14:53:23 +08:00

307 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Template Name: Jio - 写文章/投稿
* Template Post Type: page
*
* Front-end article submission page matching the reference newposts layout.
* 注意:page-newposts.php 是「最新文章」列表,不是投稿。
*
* @package Jio
*/
defined('ABSPATH') || exit;
$ctx = include get_template_directory() . '/templates/parts/submit/context.php';
if (!is_array($ctx)) {
$ctx = [];
}
// Feature disabled → soft 404.
if (empty($ctx['enabled'])) {
global $wp_query;
$wp_query->set_404();
status_header(404);
nocache_headers();
include get_query_template('404');
return;
}
// Invalid edit id → strip.
if (!empty($_GET['edit']) && empty($ctx['edit_post'])) {
wp_safe_redirect(remove_query_arg('edit'));
exit;
}
// Jio Creator Studio:登录用户 + 构建产物存在时使用沉浸式 Gutenberg 工作台。
// 否则保留原有 TinyMCE 表单作为降级路径(游客投稿、未构建、无权限)。
$studio = class_exists('\Jio\Core\CreatorStudio') ? \Jio\Core\CreatorStudio::get_instance() : null;
$use_studio = $studio
&& $studio::is_built()
&& get_current_user_id() > 0
&& !empty($ctx['is_can_new']);
if ($use_studio) {
get_header();
?>
<main id="main-content" role="main" tabindex="-1">
<div id="jio-creator-studio">
<div class="jio-studio-skeleton" aria-live="polite">
<?php esc_html_e('正在加载创作工作台…', 'jio'); ?>
</div>
</div>
<?php
// 人机验证挂载点:由发布抽屉在提交时移入并展示,脚本扫描只执行一次。
if (function_exists('jio_render_captcha') && function_exists('jio_is_captcha_enabled_for') && jio_is_captcha_enabled_for('front_submit')) :
?>
<div id="jio-creator-captcha" hidden>
<?php jio_render_captcha('front_submit'); ?>
</div>
<?php
endif;
?>
</main>
<?php
get_footer();
return;
}
// wp_editor() is rendered after get_header(); queue its runtime before wp_head.
wp_enqueue_editor();
// Assets.
$css = get_template_directory() . '/assets/css/frontend/pages/submit.css';
if (file_exists($css)) {
wp_enqueue_style(
'jio-submit',
get_template_directory_uri() . '/assets/css/frontend/pages/submit.css',
['jio-public'],
(string) filemtime($css)
);
}
$js = get_template_directory() . '/assets/js/front-submit.js';
$editor_js = get_template_directory() . '/assets/js/front-submit-editor.js';
if (file_exists($editor_js)) {
wp_enqueue_script(
'jio-front-submit-editor',
get_template_directory_uri() . '/assets/js/front-submit-editor.js',
['editor'],
(string) filemtime($editor_js),
true
);
}
$pay_download_js = get_template_directory() . '/assets/js/front-submit-pay-download.js';
if (file_exists($pay_download_js)) {
wp_enqueue_script(
'jio-front-submit-pay-download',
get_template_directory_uri() . '/assets/js/front-submit-pay-download.js',
['jio-front-submit-editor'],
(string) filemtime($pay_download_js),
true
);
}
if (file_exists($js)) {
wp_enqueue_script(
'jio-front-submit',
get_template_directory_uri() . '/assets/js/front-submit.js',
['loader_js', 'jio-front-submit-editor', 'jio-front-submit-pay-download'],
(string) filemtime($js),
true
);
$upload_image_max_bytes = \Jio\Core\FrontSubmit::upload_limit_bytes('image');
$upload_video_max_bytes = \Jio\Core\FrontSubmit::upload_limit_bytes('video');
$upload_resource_max_bytes = \Jio\Core\FrontSubmit::upload_limit_bytes('resource');
$image_upload_multiple = class_exists('\Jio\Attachment\UploadLimits')
? \Jio\Attachment\UploadLimits::image_batch_limit()
: 6;
$system_tools = class_exists('\Jio\Settings\ExtensionRuntime')
? \Jio\Settings\ExtensionRuntime::get_system_tools_settings()
: [];
$float_scenes = is_array($system_tools['mce_float_toolbar'] ?? null) ? $system_tools['mce_float_toolbar'] : ['pc_s', 'm_s'];
$float_toolbar = wp_is_mobile() ? in_array('m_s', $float_scenes, true) : in_array('pc_s', $float_scenes, true);
wp_localize_script('jio-front-submit-editor', 'jioSubmitData', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('jio_submit'),
'restUrl' => esc_url_raw(rest_url('jio/v1')),
'restNonce' => wp_create_nonce('wp_rest'),
'postId' => (int) ($ctx['edit_id'] ?? 0),
'mediaUrl' => esc_url_raw(rest_url('jio/v1/front-submit/media')),
'media' => [
'image' => ['url' => esc_url_raw(rest_url('jio/v1/front-submit/media/image'))],
'video' => ['url' => esc_url_raw(rest_url('jio/v1/front-submit/media/video'))],
'file' => ['url' => esc_url_raw(rest_url('jio/v1/front-submit/media/file'))],
],
'splitUpload' => [
'enabled' => !empty($system_tools['split_upload_s']),
'minSize' => (int) ($system_tools['split_minimum_size'] ?? 20),
],
'imageUploadMultiple' => $image_upload_multiple,
'editor' => [
'canUploadImage' => !empty($ctx['can_upload_img']),
'canUploadVideo' => !empty($ctx['can_upload_video']),
'canEmbedVideo' => !empty($ctx['can_iframe']),
'canUploadFile' => !empty($ctx['can_upload_file']),
'canHide' => !empty($ctx['can_hide']),
'canPay' => !empty($ctx['can_pay']),
'floatToolbar' => $float_toolbar,
],
'uploads' => [
'image' => !empty($ctx['can_upload_img']) ? [
'url' => esc_url_raw(rest_url('jio/v1/front-submit/upload/image')),
'maxSize' => $upload_image_max_bytes,
] : null,
'video' => !empty($ctx['can_upload_video']) ? [
'url' => esc_url_raw(rest_url('jio/v1/front-submit/upload/video')),
'maxSize' => $upload_video_max_bytes,
] : null,
'resource' => !empty($ctx['can_upload_file']) ? [
'url' => esc_url_raw(rest_url('jio/v1/front-submit/upload/resource')),
'maxSize' => $upload_resource_max_bytes,
] : null,
],
'i18n' => [
'confirmLeave' => __('你有未保存的更改,确定要离开吗?', 'jio'),
'saving' => __('保存中...', 'jio'),
'saved' => __('草稿已保存', 'jio'),
'submitting' => __('提交中...', 'jio'),
'submitted' => __('投稿已提交', 'jio'),
'error' => __('操作失败,请重试', 'jio'),
'confirmDelete' => __('确定要删除这个草稿吗?', 'jio'),
'deleting' => __('正在删除草稿…', 'jio'),
'deleted' => __('草稿已删除', 'jio'),
'uploading' => __('正在上传…', 'jio'),
'uploaded' => __('上传成功', 'jio'),
'fileTooLarge' => __('文件大小超过限制', 'jio'),
'lastSaved' => __('最后保存:', 'jio'),
'preview' => __('预览文章', 'jio'),
'setVideoCoverTitle' => __('为视频设置封面', 'jio'),
'close' => __('关闭', 'jio'),
'insertImage' => __('插入图片', 'jio'),
'uploadImage' => __('上传图片', 'jio'),
'externalImage' => __('外链图片', 'jio'),
'insertVideo' => __('插入视频', 'jio'),
'uploadVideo' => __('上传视频', 'jio'),
'externalVideo' => __('外链视频', 'jio'),
'embedVideo' => __('嵌入视频', 'jio'),
'selectVideo' => __('选择或拖入视频', 'jio'),
'videoUploadHint' => __('上传完成后自动插入正文', 'jio'),
'externalVideoHint' => __('请填写视频文件地址:', 'jio'),
'embedVideoHint' => __('请输入嵌入地址或者直接粘贴 iframe 嵌入代码:', 'jio'),
'confirmEmbed' => __('确认嵌入', 'jio'),
'insertFile' => __('上传附件', 'jio'),
'uploadFile' => __('上传附件', 'jio'),
'externalFile' => __('输入地址', 'jio'),
'selectFile' => __('选择或拖入附件', 'jio'),
'fileUploadHint' => __('上传完成后自动插入正文', 'jio'),
'externalFileHint' => __('请填写附件地址:', 'jio'),
'selectImages' => __('选择或拖入图片', 'jio'),
'imageUploadHint' => __('可批量选择,上传完成后自动插入正文', 'jio'),
'selectedFiles' => __('已选择 %d 个文件', 'jio'),
'confirmUpload' => __('确认上传', 'jio'),
'externalImageHint' => __('每行填写一个图片地址:', 'jio'),
'confirmInsert' => __('确认插入', 'jio'),
'invalidMediaUrl' => __('请填写有效地址', 'jio'),
'invalidVideoUrl' => __('请填写有效的视频嵌入地址', 'jio'),
'invalidFile' => __('请选择有效文件', 'jio'),
'uploadUnavailable' => __('当前账号不能上传此类文件', 'jio'),
'mine' => __('我的媒体', 'jio'),
'mineImage' => __('我的图片', 'jio'),
'mineVideo' => __('我的视频', 'jio'),
'mineFile' => __('我的文件', 'jio'),
'search' => __('搜索', 'jio'),
'searchMedia' => __('搜索媒体(至少 2 个字符)', 'jio'),
'searchMin' => __('请输入至少 2 个字符进行搜索', 'jio'),
'clear' => __('清除', 'jio'),
'loadMore' => __('加载更多', 'jio'),
'loading' => __('加载中…', 'jio'),
'empty' => __('暂无媒体文件', 'jio'),
'emptyMedia' => __('暂无媒体文件', 'jio'),
'mediaError' => __('媒体加载失败', 'jio'),
'mediaUnavailable' => __('媒体库暂不可用', 'jio'),
'confirmSelected' => __('确认选择(%d)', 'jio'),
'maxItems' => __('最多选择 %d 个文件', 'jio'),
'maxMediaItems' => __('最多选择 %d 个文件', 'jio'),
'uploadingClose' => __('上传中,请稍候再关闭', 'jio'),
'mediaPickerUnavailable' => __('媒体选择器加载失败,请刷新页面后重试', 'jio'),
'download' => __('下载', 'jio'),
'quote' => __('引言', 'jio'),
'quoteGray' => __('灰色引言', 'jio'),
'quoteRed' => __('红色引言', 'jio'),
'quoteBlue' => __('蓝色引言', 'jio'),
'quoteGreen' => __('绿色引言', 'jio'),
'edit' => __('编辑', 'jio'),
'insert' => __('插入', 'jio'),
'highlightCode' => __('高亮代码', 'jio'),
'selectLanguage' => __('选择语言', 'jio'),
'codeGeneric' => __('通用高亮', 'jio'),
'codeTheme' => __('主题', 'jio'),
'codeFollowGlobal' => __('跟随全局设置', 'jio'),
'codeLabel' => __('代码:', 'jio'),
'hiddenContent' => __('隐藏内容', 'jio'),
'hiddenPlaceholder' => __('隐藏内容', 'jio'),
'replyVisible' => __('回复可见', 'jio'),
'loginVisible' => __('登录可见', 'jio'),
'vipVisible' => __('VIP 可见', 'jio'),
'payVisible' => __('付费可见', 'jio'),
'addResource' => __('添加资源', 'jio'),
'editResource' => __('编辑资源', 'jio'),
'downloadUrl' => __('下载地址', 'jio'),
'enterDownloadUrl' => __('请输入资源下载地址', 'jio'),
'pasteLinkHint' => __('支持直接粘贴网盘分享链接,并自动识别提取码', 'jio'),
'resourceNote' => __('资源备注', 'jio'),
'enterResourceNote' => __('请输入资源备注', 'jio'),
'resourceNoteHint' => __('例如提取码、解压密码等', 'jio'),
'clickCopy' => __('点击复制', 'jio'),
'copyName' => __('复制名称', 'jio'),
'copyContent' => __('复制内容', 'jio'),
'copyHint' => __('填写后,用户可点击资源备注复制内容', 'jio'),
'customButton' => __('自定义按钮', 'jio'),
'customButtonPlaceholder' => __('自定义下载按钮名称', 'jio'),
'selectButtonStyle' => __('选择按钮样式', 'jio'),
'extractCode' => __('提取码', 'jio'),
'extractCodeColon' => __('提取码:', 'jio'),
'confirm' => __('确认', 'jio'),
],
]);
}
get_header();
?>
<main id="main-content" role="main" class="container" tabindex="-1">
<form id="jio-submit-form" data-jio-submit-form data-dirty="false">
<div class="content-wrap newposts-wrap">
<div class="content-layout">
<?php
$main = get_template_directory() . '/templates/parts/submit/editor-main.php';
if (file_exists($main)) {
include $main;
}
?>
</div>
</div>
<div class="sidebar show-sidebar">
<?php \Jio\WidgetSystem\WidgetAreas::render('newposts_sidebar_top'); ?>
<?php
$terms = get_template_directory() . '/templates/parts/submit/sidebar-terms.php';
if (file_exists($terms)) {
include $terms;
}
$pay = get_template_directory() . '/templates/parts/submit/sidebar-pay.php';
if (file_exists($pay)) {
include $pay;
}
$actions = get_template_directory() . '/templates/parts/submit/sidebar-submit.php';
if (file_exists($actions)) {
include $actions;
}
?>
<?php \Jio\WidgetSystem\WidgetAreas::render('newposts_sidebar_bottom'); ?>
</div>
</form>
</main>
<?php
get_footer();