From 7a59c652522285a71ba20330c5ecfd070329ba9f Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 11 Nov 2026 16:37:31 +0800 Subject: [PATCH] feat(forum): add quick publish, captcha and hidden content Port the reference inline quick-publish composer to the forum home: title/body inputs, image upload, plate/topic/access/hidden-content dropdowns and save/publish actions. Wire the site captcha into forum publishing through a new `forum_post` scene (admin + REST gate), and turn the hidden-content box into the theme's own visibility shortcodes (login/comment/vip/payshow) so the existing gate renderer unlocks them. Co-Authored-By: Claude Fable 5 --- .../src/schema/sections/permissions/index.js | 1 + assets/css/routes/forum.css | 16 +++ assets/js/forum-runtime.js | 15 +++ inc/modules/captcha.php | 1 + src/Admin/Schema/SchemaPermissions.php | 1 + src/Forum/EditView.php | 18 ++++ src/Forum/QuickPost.php | 100 ++++++++++++++---- src/Forum/Rest.php | 20 ++++ src/Forum/RestPosts.php | 10 ++ tests/forum/pages-contract.test.cjs | 40 +++++++ tools/admin-contract-report.json | 2 +- 11 files changed, 204 insertions(+), 20 deletions(-) diff --git a/assets/admin/src/schema/sections/permissions/index.js b/assets/admin/src/schema/sections/permissions/index.js index 93d1be58..72245602 100644 --- a/assets/admin/src/schema/sections/permissions/index.js +++ b/assets/admin/src/schema/sections/permissions/index.js @@ -84,6 +84,7 @@ export const permissionsSchemas = { { label: '资源下载', value: 'download' }, { label: '每日签到', value: 'signin' }, { label: '前台投稿', value: 'front_submit' }, + { label: '论坛发帖', value: 'forum_post' }, { label: '打赏用户', value: 'tip_user' }, { label: '提交工单', value: 'ticket_create' }, { label: '回复工单', value: 'ticket_reply' }, diff --git a/assets/css/routes/forum.css b/assets/css/routes/forum.css index d5d155fd..3b9a9c12 100644 --- a/assets/css/routes/forum.css +++ b/assets/css/routes/forum.css @@ -51,6 +51,22 @@ } .quick-posts-box{border-radius:var(--main-radius);padding:15px} .quick-posts-box .title-theme{margin-bottom:10px} +.quick-input-group{padding:15px;border:1px solid var(--main-border-color);border-radius:var(--main-radius)} +.quick-posts-box .edit-posts-title{position:relative} +.quick-posts-box .edit-posts-title .line-form-line{display:none} +.quick-posts-box .edit-posts-title textarea{width:100%;color:var(--main-color);padding:0;margin-bottom:10px;font-weight:500;font-size:18px;border:0;border-bottom:1px solid var(--main-border-color);background:transparent;resize:none;outline:0} +.quick-posts-box .edit-posts-content textarea{outline:0!important;border:none;display:block;width:100%;padding:0;background:transparent;color:var(--main-color);resize:vertical;min-height:88px} +.quick-posts-box .preview .add,.quick-posts-box .preview .preview-item{width:90px;height:90px;border:1px dashed var(--main-border-color);background:none;margin:2px;border-radius:var(--main-radius);position:relative} +.quick-posts-box .preview .add:before{content:"+";position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:28px;color:var(--muted-3-color)} +.quick-posts-box .drop-select .dropdown-menu{min-width:320px} +.quick-more-btns{color:var(--muted-color);position:relative;flex-wrap:wrap;gap:6px} +.quick-more-btns .drop-btn{padding:6px 4px;cursor:pointer} +.quick-more-btns .drop-select{position:relative} +.quick-posts-submit .but{padding:6px 5px;min-width:85px} +.quick-posts-submit .quick-captcha{margin-bottom:8px} +.hide-type-drop .select-drop-box .badg.active{--this-bg:var(--focus-color);--this-color:#fff} +.hide-type-drop textarea{margin-top:8px;width:100%} +.jio-forum__captcha{margin-bottom:15px} .forum-home .jio-widget-box{clear:both;background-color:var(--main-bg-color);padding:15px;box-shadow:0 0 10px var(--main-shadow);border-radius:var(--main-radius);margin-bottom:20px} .forum-home .jio-widget-box .btn-block{display:block;width:100%;text-align:center} .forum-home .jio-widget-box .title-theme{font-size:15px;font-weight:600} diff --git a/assets/js/forum-runtime.js b/assets/js/forum-runtime.js index 72d06722..dbf5885b 100644 --- a/assets/js/forum-runtime.js +++ b/assets/js/forum-runtime.js @@ -134,6 +134,21 @@ syncHidden('post_status', status.dataset.forumStatus) return } + const hideType = event.target.closest('[data-forum-hide-type]') + if (hideType) { + root.querySelectorAll('[data-forum-hide-type]').forEach((item) => item.classList.toggle('active', item === hideType)) + syncHidden('hide_type', hideType.dataset.forumHideType) + return + } + const topic = event.target.closest('[data-forum-topic],[data-forum-topic-none]') + if (topic) { + const name = topic.dataset.forumTopic || '' + root.querySelectorAll('[data-forum-topic],[data-forum-topic-none]').forEach((item) => item.classList.toggle('active', item === topic)) + syncHidden('topics', name) + const label = root.querySelector('[name="topics"]') + if (label) label.lastChild.textContent = name || '话题' + return + } const pick = event.target.closest('[data-forum-cover-pick]') if (pick) { root.querySelector('[data-forum-cover-file]')?.click() diff --git a/inc/modules/captcha.php b/inc/modules/captcha.php index 62c46ef6..f4d441b2 100644 --- a/inc/modules/captcha.php +++ b/inc/modules/captcha.php @@ -374,6 +374,7 @@ function jio_captcha_scenes(): array { 'download' => __('资源下载', 'jio'), 'signin' => __('每日签到', 'jio'), 'front_submit' => __('前台投稿', 'jio'), + 'forum_post' => __('论坛发帖', 'jio'), 'tip_user' => __('打赏用户', 'jio'), 'ticket_create' => __('提交工单', 'jio'), 'ticket_reply' => __('回复工单', 'jio'), diff --git a/src/Admin/Schema/SchemaPermissions.php b/src/Admin/Schema/SchemaPermissions.php index dba626e5..d15a5a76 100644 --- a/src/Admin/Schema/SchemaPermissions.php +++ b/src/Admin/Schema/SchemaPermissions.php @@ -80,6 +80,7 @@ class SchemaPermissions { ['label' => '资源下载', 'value' => 'download'], ['label' => '每日签到', 'value' => 'signin'], ['label' => '前台投稿', 'value' => 'front_submit'], + ['label' => '论坛发帖', 'value' => 'forum_post'], ['label' => '打赏用户', 'value' => 'tip_user'], ['label' => '提交工单', 'value' => 'ticket_create'], ['label' => '回复工单', 'value' => 'ticket_reply'], diff --git a/src/Forum/EditView.php b/src/Forum/EditView.php index c64709b5..b4979d4b 100644 --- a/src/Forum/EditView.php +++ b/src/Forum/EditView.php @@ -71,6 +71,12 @@ final class EditView { echo self::term_box('tags', '标签', Core::TAG, $tags, 'fa-tag'); echo self::type_box($type); echo self::vote_box($vote_options, $post); + echo self::hide_box($post); + if (!$post && function_exists('jio_render_captcha') && function_exists('jio_is_captcha_enabled_for') && jio_is_captcha_enabled_for('forum_post')) { + echo '
'; + jio_render_captcha('forum_post'); + echo '
'; + } \Jio\WidgetSystem\WidgetAreas::render('forum_edit_sidebar'); echo ''; self::close(); @@ -150,6 +156,18 @@ final class EditView { return '
发布类型
' . $list . '
'; } + /** Hidden-content composer: the block becomes a site visibility shortcode on save. */ + private static function hide_box(?\WP_Post $post): string { + $types = ''; + foreach (['logged' => '登录可见', 'reply' => '评论可见', 'vip1' => '会员可见', 'payshow' => '付费可见'] as $key => $label) { + $types .= '' . esc_html($label) . ''; + } + return ''; + } + /** @param string[] $options */ private static function vote_box(array $options, ?\WP_Post $post): string { $type = $post ? (string) get_post_meta($post->ID, '_jio_forum_type', true) : 'normal'; diff --git a/src/Forum/QuickPost.php b/src/Forum/QuickPost.php index 9a5cdbfc..0b1ef547 100644 --- a/src/Forum/QuickPost.php +++ b/src/Forum/QuickPost.php @@ -5,34 +5,96 @@ namespace Jio\Forum; defined('ABSPATH') || exit; final class QuickPost { - /** Inline "publish now" box shown on the configured home tab. */ + /** Inline "publish now" box shown on the configured home tab (reference layout). */ public static function render(): string { $user_id = get_current_user_id(); - if (!$user_id) { - return '

登录后即可快速发布' . esc_html(self::post_name()) . '

登录
'; + $post_name = (string) jio_get_setting('jio_forum_post_name', '帖子') ?: '帖子'; + $plate_name = (string) jio_get_setting('jio_forum_plate_name', '版块') ?: '版块'; + $topic_name = (string) jio_get_setting('jio_forum_topic_name', '话题') ?: '话题'; + $plates = get_posts(['post_type' => Core::PLATE, 'post_status' => 'publish', 'posts_per_page' => 200, 'orderby' => 'title', 'order' => 'ASC']); + if (!$plates) return ''; + + $html = '
'; + $html .= '
'; + $html .= '
'; + if (jio_get_setting('jio_forum_upload_image', true)) { + $html .= '
'; } - $plates = get_posts(['post_type' => Core::PLATE, 'post_status' => 'publish', 'posts_per_page' => 100, 'orderby' => 'title', 'order' => 'ASC']); + $html .= '
'; + + // More options: plate / topic / access / hidden content + $html .= '
'; + $html .= self::plate_dropdown($plates, $plate_name, $user_id); + $html .= self::topic_dropdown($topic_name, $user_id); + $html .= self::access_dropdown($user_id); + if ($user_id) { + $html .= self::hide_dropdown(); + } + $html .= '
'; + + // Submit row + captcha + $html .= '
'; + if (function_exists('jio_render_captcha') && function_exists('jio_is_captcha_enabled_for') && jio_is_captcha_enabled_for('forum_post')) { + $html .= '
'; + ob_start(); + jio_render_captcha('forum_post'); + $html .= (string) ob_get_clean(); + $html .= '
'; + } + $html .= '
'; + if ($user_id) { + $html .= ''; + $html .= ''; + } else { + $html .= ''; + } + $html .= '
'; + $html .= ''; + $html .= '

'; + return $html . '
'; + } + + /** @param \WP_Post[] $plates */ + private static function plate_dropdown(array $plates, string $plate_name, int $user_id): string { $options = ''; foreach ($plates as $plate) { - $options .= ''; + $options .= '
' . esc_html($plate->post_title) . '
'; } - if ($options === '') return ''; - - return '
' - . '

快速发布' . esc_html(self::post_name()) . '

' - . '' - . '' - . '
' - . '' - . '' - . '

'; + if ($user_id <= 0) { + return ''; + } + return ''; } - private static function post_name(): string { - return (string) jio_get_setting('jio_forum_post_name', '帖子') ?: '帖子'; + private static function topic_dropdown(string $topic_name, int $user_id): string { + if ($user_id <= 0) { + return ''; + } + $terms = get_terms(['taxonomy' => Core::TOPIC, 'hide_empty' => false, 'number' => 100, 'orderby' => 'count', 'order' => 'DESC']); + $options = '
不添加' . esc_html($topic_name) . '
'; + foreach (is_array($terms) ? $terms : [] as $term) { + $options .= '
' . esc_html($term->name) . '
'; + } + return ''; } - private static function plate_name(): string { - return (string) jio_get_setting('jio_forum_plate_name', '版块') ?: '版块'; + private static function access_dropdown(int $user_id): string { + $labels = ['all' => '公开', 'login' => '登录后可查看', 'follow' => '关注版块后可查看', 'points' => '支付积分查看', 'pay' => '付费查看']; + if ($user_id <= 0) { + return ''; + } + $options = ''; + foreach ($labels as $key => $label) { + $options .= '
' . esc_html($label) . '
'; + } + return ''; + } + + private static function hide_dropdown(): string { + $items = ''; + foreach (['logged' => '登录可见', 'reply' => '评论可见', 'vip1' => '会员可见', 'payshow' => '付费可见'] as $key => $label) { + $items .= '' . esc_html($label) . ''; + } + return ''; } } diff --git a/src/Forum/Rest.php b/src/Forum/Rest.php index ab950fc7..e566338a 100644 --- a/src/Forum/Rest.php +++ b/src/Forum/Rest.php @@ -94,6 +94,26 @@ final class Rest { return is_array($json) ? $json : $request->get_params(); } + /** + * Quick-publish "hidden content" box becomes the site's own visibility + * shortcode, so the existing gate renderer owns unlock logic. + */ + private static function append_hidden_content(string $content, array $input): string { + $type = sanitize_key((string) ($input['hide_type'] ?? '')); + $body = trim((string) ($input['hide_content'] ?? '')); + if ($type === '' || $body === '') { + return $content; + } + $mode = ['logged' => 'login', 'reply' => 'comment', 'vip1' => 'vip', 'payshow' => 'payshow'][$type] ?? ''; + if ($mode === '') { + return $content; + } + $safe = wp_kses_post($body); + $tag = $mode === 'payshow' ? 'payshow' : 'hidecontent type="' . $mode . '"'; + $close = $mode === 'payshow' ? 'payshow' : 'hidecontent'; + return rtrim($content) . "\n\n[" . $tag . ']' . $safe . '[/' . $close . ']'; + } + private static function post(int $id): ?\WP_Post { $post = get_post($id); return $post instanceof \WP_Post && $post->post_type === Core::POST ? $post : null; diff --git a/src/Forum/RestPosts.php b/src/Forum/RestPosts.php index 104a9bb8..0db40aef 100644 --- a/src/Forum/RestPosts.php +++ b/src/Forum/RestPosts.php @@ -20,6 +20,15 @@ trait RestPosts { return self::fail('无权编辑此帖子', 403); } $input = self::input($request); + if ($id === 0 && function_exists('jio_require_captcha')) { + // Same gate as the article submit flow; disabled scenes pass through. + $captcha = jio_require_captcha('forum_post', [ + 'jio_captcha_proof' => sanitize_text_field((string) ($input['jio_captcha_proof'] ?? '')), + ]); + if (is_wp_error($captcha)) { + return self::fail($captcha->get_error_message(), 403); + } + } $plate_id = absint($input['plate_id'] ?? ($current ? get_post_meta($id, '_jio_forum_plate_id', true) : 0)); if (!self::plate($plate_id) || !Access::can_publish($user_id, $plate_id)) { return self::fail('没有在此版块发布的权限', 403); @@ -35,6 +44,7 @@ trait RestPosts { } $title = sanitize_text_field((string) ($input['title'] ?? ($current->post_title ?? ''))); $content = wp_kses_post((string) ($input['content'] ?? ($current->post_content ?? ''))); + $content = self::append_hidden_content($content, $input); if (mb_strlen($title) < 2 || mb_strlen($title) > 120 || trim(wp_strip_all_tags($content)) === '') { return self::fail('请填写标题和正文'); } diff --git a/tests/forum/pages-contract.test.cjs b/tests/forum/pages-contract.test.cjs index 29fc651c..546423a3 100644 --- a/tests/forum/pages-contract.test.cjs +++ b/tests/forum/pages-contract.test.cjs @@ -61,3 +61,43 @@ test('compose page saves draft state and the cover through the REST contract', ( assert.ok(runtime.includes(token), `compose runtime missing ${token}`) } }) + +test('quick publish mirrors the reference inline composer', () => { + const quick = read('src/Forum/QuickPost.php') + for (const token of ['quick-posts-box', 'quick-input-group', 'edit-posts-title', 'line-form-input', 'edit-posts-content', 'quick-upload', 'quick-more-btns', 'plate-drop', 'topic-drop', 'allow-view-drop', 'hide-type-drop', 'quick-posts-submit', 'data-forum-draft']) { + assert.ok(quick.includes(token), `quick publish missing ${token}`) + } + // Guests get the login trigger instead of the editor actions. + assert.match(quick, /signin-loader/) + assert.match(quick, /登录后发布/) +}) + +test('forum publishing is gated by the site captcha scene', () => { + const rest = read('src/Forum/RestPosts.php') + const captcha = read('inc/modules/captcha.php') + const schema = read('src/Admin/Schema/SchemaPermissions.php') + const vue = read('assets/admin/src/schema/sections/permissions/index.js') + const quick = read('src/Forum/QuickPost.php') + const edit = read('src/Forum/EditView.php') + assert.match(captcha, /'forum_post'\s*=>\s*__\('论坛发帖'/) + assert.match(schema, /\['label' => '论坛发帖', 'value' => 'forum_post'\]/) + assert.match(vue, /\{ label: '论坛发帖', value: 'forum_post' \}/) + assert.match(rest, /jio_require_captcha\('forum_post'/) + assert.match(rest, /jio_captcha_proof/) + assert.match(quick, /jio_render_captcha\('forum_post'\)/) + assert.match(edit, /jio_render_captcha\('forum_post'\)/) +}) + +test('hidden content becomes the site visibility shortcode on save', () => { + const rest = read('src/Forum/Rest.php') + const posts = read('src/Forum/RestPosts.php') + const quick = read('src/Forum/QuickPost.php') + const edit = read('src/Forum/EditView.php') + assert.match(posts, /append_hidden_content\(\$content, \$input\)/) + assert.match(rest, /'logged' => 'login', 'reply' => 'comment', 'vip1' => 'vip', 'payshow' => 'payshow'/) + assert.match(rest, /hidecontent type="/) + assert.match(rest, /payshow/) + for (const token of ['hide_type', 'hide_content', '登录可见', '评论可见', '会员可见', '付费可见']) { + assert.ok(quick.includes(token) && edit.includes(token), `hidden content missing ${token}`) + } +}) diff --git a/tools/admin-contract-report.json b/tools/admin-contract-report.json index 0a1fc009..4b3f37d9 100644 --- a/tools/admin-contract-report.json +++ b/tools/admin-contract-report.json @@ -1,5 +1,5 @@ { - "generated_at": "2026-11-11T08:16:48+00:00", + "generated_at": "2026-11-11T08:37:24+00:00", "errors": [], "warnings": [ "customPage tools-update section 'tools/update' not in PHP schema (ok if pure operational page)",