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>
This commit is contained in:
1 parent
7a8279f8e7
commit
fac4a714e0
45 files changed
+28495
-56
No files matched your search
@@ -32,6 +32,38 @@ if (!empty($_GET['edit']) && empty($ctx['edit_post'])) {
|
||||
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();
|
||||
|
||||
|
||||
@@ -336,10 +336,22 @@ if ($is_need_purchase && $user_amount > 0) {
|
||||
<?php foreach ($downloads as $idx => $row) :
|
||||
$label = isset($row['label']) ? trim((string) $row['label']) : '';
|
||||
$type = isset($row['type']) ? (string) $row['type'] : 'direct';
|
||||
$asset_key = class_exists('\Jio\Content\PrivateAssets')
|
||||
? \Jio\Content\PrivateAssets::sanitize_asset_key((string) ($row['asset_key'] ?? ''))
|
||||
: '';
|
||||
$is_protected = $asset_key !== '';
|
||||
$url = isset($row['url']) ? (string) $row['url'] : '';
|
||||
if ($url === '') {
|
||||
// 直连模式没有可用的原始地址,空源不渲染。
|
||||
continue;
|
||||
if (!$is_protected) {
|
||||
if ($url === '') {
|
||||
// 直连模式没有可用的原始地址,空源不渲染。
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// 受保护附件按设计没有公开 URL,只带 asset_key。
|
||||
$url = '';
|
||||
}
|
||||
if ($is_protected && $label === '') {
|
||||
$label = trim((string) ($row['file_name'] ?? ''));
|
||||
}
|
||||
$extract_code = isset($row['extract_code']) ? trim((string) $row['extract_code']) : '';
|
||||
$has_extract = $extract_code !== '';
|
||||
@@ -349,7 +361,9 @@ if ($is_need_purchase && $user_amount > 0) {
|
||||
$button_class = isset($row['class']) ? trim((string) $row['class']) : '';
|
||||
$has_copy = $copy_val !== '';
|
||||
|
||||
$service = function_exists('jio_detect_cloud_service') ? jio_detect_cloud_service($url) : ['key' => 'other', 'name' => $label, 'color' => '#6b7280'];
|
||||
$service = $is_protected
|
||||
? ['key' => 'protected', 'name' => __('受保护下载', 'jio'), 'color' => '#267665']
|
||||
: (function_exists('jio_detect_cloud_service') ? jio_detect_cloud_service($url) : ['key' => 'other', 'name' => $label, 'color' => '#6b7280']);
|
||||
if ($label === '') {
|
||||
$label = strpos($service['name'], '本地') === 0 ? sprintf(__('下载源 %d', 'jio'), $idx + 1) : (string) $service['name'];
|
||||
}
|
||||
@@ -376,6 +390,21 @@ if ($is_need_purchase && $user_amount > 0) {
|
||||
?>
|
||||
<li class="resource-download-box__item">
|
||||
<div class="resource-download-box__row">
|
||||
<?php if ($is_protected && !$is_need_login) : ?>
|
||||
<button
|
||||
type="button"
|
||||
class="resource-download-box__btn btn-primary resource-download-box__btn--service-protected<?php echo esc_attr($btn_modifier); ?><?php echo esc_attr($btn_extra_class); ?>"
|
||||
data-jio-protected-asset="<?php echo esc_attr($asset_key); ?>"
|
||||
data-jio-protected-post="<?php echo esc_attr((string) $post_id); ?>"
|
||||
style="--service-color: <?php echo esc_attr($service['color']); ?>"
|
||||
>
|
||||
<?php echo $btn_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<span><?php echo esc_html($btn_text); ?></span>
|
||||
<span class="resource-download-box__service-badge" style="background: <?php echo esc_attr($service['color']); ?>">
|
||||
<?php echo esc_html($service['name']); ?>
|
||||
</span>
|
||||
</button>
|
||||
<?php else : ?>
|
||||
<a
|
||||
class="resource-download-box__btn btn-primary resource-download-box__btn--service-<?php echo esc_attr($service['key']); ?><?php echo esc_attr($btn_modifier); ?><?php echo esc_attr($btn_extra_class); ?>"
|
||||
href="<?php echo esc_url($btn_href); ?>"<?php echo $btn_target; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><?php echo $btn_rel; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
@@ -389,6 +418,7 @@ if ($is_need_purchase && $user_amount > 0) {
|
||||
<?php echo esc_html($service['name']); ?>
|
||||
</span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($has_extract || $has_copy) : ?>
|
||||
<button type="button" class="resource-download-box__btn resource-download-box__btn--extract"
|
||||
<?php if ($has_copy && $copy_key !== '') : ?>data-toggle="tooltip" title="<?php echo esc_attr(sprintf(__('点击复制%s', 'jio'), $copy_key)); ?>"<?php endif; ?>
|
||||
|
||||
@@ -15,7 +15,15 @@ $status_labels = ['publish' => __('已发布', 'jio'), 'draft' => __('草稿', '
|
||||
<?php while ($q->have_posts()) : $q->the_post();
|
||||
$post_id = get_the_ID();
|
||||
$post_status = get_post_status();
|
||||
$edit_url = $front_submit && $front_submit->can_edit_post($post_id, (int) $user_id) ? jio_get_new_post_url($post_id) : '';
|
||||
$review_note = '';
|
||||
if ($front_submit && class_exists('\\Jio\\Core\\SubmissionVersion') && \Jio\Core\SubmissionVersion::requires_version((int) $post_id)) {
|
||||
$working = \Jio\Core\SubmissionVersion::get_working((int) $post_id, (int) $user_id);
|
||||
if ($working instanceof WP_Post) {
|
||||
$review_note = \Jio\Core\SubmissionVersion::status_label(\Jio\Core\SubmissionVersion::status((int) $working->ID));
|
||||
}
|
||||
}
|
||||
// 编辑入口始终指向源文章;创作工作台会自行解析作者当前的工作版本。
|
||||
$edit_url = $front_submit && $front_submit->can_edit_post($post_id, (int) $user_id) ? jio_get_new_post_url((int) $post_id) : '';
|
||||
$can_delete = $front_submit && $front_submit->can_delete_post($post_id, (int) $user_id);
|
||||
$price = (float) get_post_meta($post_id, '_jio_pay_price', true);
|
||||
?>
|
||||
@@ -23,7 +31,7 @@ $status_labels = ['publish' => __('已发布', 'jio'), 'draft' => __('草稿', '
|
||||
<a class="creator-post-row__thumb" href="<?php echo esc_url(get_permalink($post_id)); ?>" aria-label="<?php echo esc_attr(get_the_title()); ?>">
|
||||
<?php if (has_post_thumbnail($post_id)) : the_post_thumbnail('medium'); else : ?><span><i class="fa fas fa-pen" aria-hidden="true"></i></span><?php endif; ?>
|
||||
</a>
|
||||
<div class="creator-post-row__body"><div class="creator-post-row__heading"><h4><a href="<?php echo esc_url(get_permalink($post_id)); ?>"><?php echo esc_html(get_the_title()); ?></a></h4><span class="creator-post-row__status creator-post-row__status--<?php echo esc_attr($post_status); ?>"><?php echo esc_html($status_labels[$post_status] ?? $post_status); ?></span></div><p><?php echo esc_html(wp_trim_words(get_the_excerpt(), 20, '…')); ?></p><div class="creator-post-row__meta"><time datetime="<?php echo esc_attr(get_the_date('c')); ?>"><?php echo esc_html(get_the_date()); ?></time><span><?php echo $price > 0 ? esc_html__('付费内容', 'jio') : esc_html__('免费内容', 'jio'); ?></span></div></div>
|
||||
<div class="creator-post-row__body"><div class="creator-post-row__heading"><h4><a href="<?php echo esc_url(get_permalink($post_id)); ?>"><?php echo esc_html(get_the_title($post_id)); ?></a></h4><span class="creator-post-row__status creator-post-row__status--<?php echo esc_attr($post_status); ?>"><?php echo esc_html($status_labels[$post_status] ?? $post_status); ?></span><?php if ($review_note !== '') : ?><span class="creator-post-row__status creator-post-row__status--review"><?php echo esc_html(sprintf(__('改稿:%s', 'jio'), $review_note)); ?></span><?php endif; ?></div><p><?php echo esc_html(wp_trim_words(get_the_excerpt(), 20, '…')); ?></p><div class="creator-post-row__meta"><time datetime="<?php echo esc_attr(get_the_date('c')); ?>"><?php echo esc_html(get_the_date()); ?></time><span><?php echo $price > 0 ? esc_html__('付费内容', 'jio') : esc_html__('免费内容', 'jio'); ?></span></div></div>
|
||||
<div class="creator-post-row__actions"><?php if ($edit_url) : ?><a href="<?php echo esc_url($edit_url); ?>" class="creator-post-row__action"><?php esc_html_e('编辑', 'jio'); ?></a><?php endif; ?><a href="<?php echo esc_url(get_permalink($post_id)); ?>" class="creator-post-row__action"><?php esc_html_e('查看', 'jio'); ?></a><?php if ($can_delete) : ?><button type="button" class="creator-post-row__action creator-post-row__action--danger" data-action="delete-post" data-post-id="<?php echo esc_attr((string) $post_id); ?>" data-nonce="<?php echo esc_attr(wp_create_nonce('jio_submit')); ?>"><?php esc_html_e('删除', 'jio'); ?></button><?php endif; ?></div>
|
||||
</article>
|
||||
<?php endwhile; ?>
|
||||
|
||||
Reference in new issue
Block a user