diff --git a/assets/admin/src/schema/sections/post-list/page.js b/assets/admin/src/schema/sections/post-list/page.js index 84341277..bfcabd5e 100644 --- a/assets/admin/src/schema/sections/post-list/page.js +++ b/assets/admin/src/schema/sections/post-list/page.js @@ -49,10 +49,11 @@ export const pageSchema = { post_article_video_cover_scale: 56, item_heading_bold: false, post_button_toptext: '喜欢就支持一下吧', - post_rewards_enabled: false, - post_rewards_text: '', - post_rewards_image: '', - post_rewards_btn_text: '赞赏', + post_rewards_s: false, + post_rewards_text: '赞赏', + post_author_card_limit: 6, + post_author_card_orderby: 'views', + post_author_card_style: 'card', article_maxheight_kg: false, article_maxheight: 1000, }, @@ -71,6 +72,17 @@ export const pageSchema = { { key: 'breadcrumbs_single_text', label: '末项显示「正文」', type: 'switch', default: true, dependsOn: { key: 'breadcrumbs_single_s', value: true }, help: '关闭后末项显示文章标题' }, { key: 'post_prevnext_s', label: '上一篇/下一篇', type: 'switch', default: true }, { key: 'post_authordesc_s', label: '作者简介', type: 'switch', default: true }, + { key: 'post_author_card_limit', label: '作者卡文章数', type: 'number', default: 6, min: 0, max: 12, dependsOn: { key: 'post_authordesc_s', value: true } }, + { key: 'post_author_card_orderby', label: '作者卡文章排序', type: 'select', default: 'views', options: [ + { label: '浏览量', value: 'views' }, + { label: '发布时间', value: 'date' }, + { label: '更新时间', value: 'modified' }, + { label: '评论数', value: 'comment_count' }, + ], dependsOn: { key: 'post_authordesc_s', value: true } }, + { key: 'post_author_card_style', label: '作者卡文章样式', type: 'radio', default: 'card', options: [ + { label: '卡片', value: 'card' }, + { label: '列表', value: 'list' }, + ], dependsOn: { key: 'post_authordesc_s', value: true } }, { key: 'post_single_hide_time_s', label: '隐藏文章时间', type: 'switch', default: false }, ], }, @@ -137,10 +149,8 @@ export const pageSchema = { { key: 'post_copyright_enabled', label: '版权信息', type: 'switch', default: true }, { key: 'post_copyright_text', label: '版权默认文案', type: 'textarea', default: '', help: '留空则自动生成"本文由 XX 原创发布"' }, { key: 'post_button_toptext', label: '按钮上方文案', type: 'input', default: '喜欢就支持一下吧', help: '显示在点赞/打赏/分享按钮上方' }, - { key: 'post_rewards_enabled', label: '打赏', type: 'switch', default: false }, - { key: 'post_rewards_btn_text', label: '打赏按钮文案', type: 'input', default: '赞赏', dependsOn: { key: 'post_rewards_enabled', value: true } }, - { key: 'post_rewards_text', label: '打赏说明', type: 'input', default: '', dependsOn: { key: 'post_rewards_enabled', value: true } }, - { key: 'post_rewards_image', label: '打赏二维码 URL', type: 'input', default: '', dependsOn: { key: 'post_rewards_enabled', value: true } }, + { key: 'post_rewards_s', label: '用户打赏', type: 'switch', default: false, help: '开启后用户可配置微信/支付宝收款码;无码则不显示按钮。' }, + { key: 'post_rewards_text', label: '打赏按钮文案', type: 'input', default: '赞赏', dependsOn: { key: 'post_rewards_s', value: true } }, { key: 'article_maxheight_kg', label: '正文限高', type: 'switch', default: false, help: '开启后长文默认折叠,可展开全文' }, { key: 'article_maxheight', label: '限高像素', type: 'number', default: 1000, min: 200, max: 5000, dependsOn: { key: 'article_maxheight_kg', value: true } }, ], diff --git a/assets/js/reward.js b/assets/js/reward.js index d6455f7c..113a45d0 100644 --- a/assets/js/reward.js +++ b/assets/js/reward.js @@ -1,8 +1,5 @@ /** - * Reward (打赏) — dedicated module. - * - * Handles the reward modal: amount selection, custom amount input, - * QR code display, and open/close interaction. + * Reward (打赏) — per-user QR modal openers. * * @package AKSRT */ @@ -10,95 +7,52 @@ 'use strict'; function initReward() { - var btn = document.getElementById('action-reward'); - if (!btn) return; + function closeAll() { + document.querySelectorAll('.reward-modal.is-open').forEach(function (m) { + m.classList.remove('is-open'); + m.setAttribute('aria-hidden', 'true'); + }); + document.body.style.overflow = ''; + } - var modal = document.getElementById('reward-modal'); - if (!modal) return; - - var closeBtn = modal.querySelector('.reward-modal__close'); - var overlay = modal.querySelector('.reward-modal__overlay'); - var amountBtns = modal.querySelectorAll('.reward-amounts__btn'); - var customWrap = modal.querySelector('.reward-custom'); - var customInput = modal ? modal.querySelector('.reward-custom__input') : null; - var customConfirm = modal ? modal.querySelector('.reward-custom__confirm') : null; - var selectedAmount = 0; - - /* ---- open / close ---- */ - function openReward() { + function openForUser(userId) { + var id = String(userId || ''); + var modal = id + ? document.getElementById('reward-modal-' + id) + : document.querySelector('.reward-modal'); + if (!modal) return; + closeAll(); modal.classList.add('is-open'); + modal.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; } - function closeReward() { - modal.classList.remove('is-open'); - document.body.style.overflow = ''; - // reset selection - selectedAmount = 0; - amountBtns.forEach(function (b) { b.classList.remove('is-active'); }); - if (customInput) customInput.value = ''; - } - - btn.addEventListener('click', openReward); document.querySelectorAll('[data-aksrt-reward-open]').forEach(function (el) { - if (el === btn) return; el.addEventListener('click', function (e) { e.preventDefault(); - openReward(); + openForUser(el.getAttribute('data-user-id') || ''); + }); + }); + + document.querySelectorAll('[data-aksrt-reward-close]').forEach(function (el) { + el.addEventListener('click', function (e) { + e.preventDefault(); + closeAll(); }); }); - if (closeBtn) closeBtn.addEventListener('click', closeReward); - if (overlay) overlay.addEventListener('click', closeReward); document.addEventListener('keydown', function (e) { - if (e.key === 'Escape' && modal.classList.contains('is-open')) closeReward(); + if (e.key === 'Escape') closeAll(); }); - - /* ---- amount selection ---- */ - amountBtns.forEach(function (b) { - b.addEventListener('click', function () { - amountBtns.forEach(function (x) { x.classList.remove('is-active'); }); - b.classList.add('is-active'); - selectedAmount = parseInt(b.dataset.amount || '0', 10); - // clear custom input when preset is chosen - if (customInput) customInput.value = ''; - }); - }); - - /* ---- custom amount ---- */ - if (customInput) { - customInput.addEventListener('focus', function () { - amountBtns.forEach(function (x) { x.classList.remove('is-active'); }); - selectedAmount = 0; - }); - customInput.addEventListener('input', function () { - // strip non-digits - customInput.value = customInput.value.replace(/[^\d]/g, ''); - }); - } - - if (customConfirm) { - customConfirm.addEventListener('click', function () { - var val = customInput ? parseInt(customInput.value, 10) : 0; - if (val > 0) { - selectedAmount = val; - amountBtns.forEach(function (x) { x.classList.remove('is-active'); }); - } - }); - } } - /* ---- register with AksrtFrontend lifecycle ---- */ if (window.AksrtFrontend && typeof window.AksrtFrontend.register === 'function') { window.AksrtFrontend.register('aksrt-reward', function () { initReward(); }); + } else if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initReward); } else { - // fallback: run on DOMContentLoaded - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initReward); - } else { - initReward(); - } + initReward(); } })(); diff --git a/inc/bootstrap.php b/inc/bootstrap.php index 7e9bc3b7..31c7d2c6 100644 --- a/inc/bootstrap.php +++ b/inc/bootstrap.php @@ -45,6 +45,7 @@ require_once AKSRT_DIR . '/inc/helpers/aksrt-coupon-order.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-credits.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-null.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-identity.php'; +require_once AKSRT_DIR . '/inc/helpers/aksrt-rewards.php'; require_once AKSRT_DIR . '/inc/helpers/menu-decoration.php'; require_once AKSRT_DIR . '/inc/helpers/class-aksrt-nav-walker.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-transactions.php'; diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-post-list.php b/inc/classes/admin/schema/class-aksrt-admin-schema-post-list.php index 57febfbf..919a9eda 100644 --- a/inc/classes/admin/schema/class-aksrt-admin-schema-post-list.php +++ b/inc/classes/admin/schema/class-aksrt-admin-schema-post-list.php @@ -75,6 +75,17 @@ class AKSRT_Admin_Schema_Post_List { ['key' => 'breadcrumbs_single_text', 'label' => '末项显示「正文」', 'type' => 'switch', 'default' => true, 'depends_on' => 'breadcrumbs_single_s', 'description' => '关闭后末项显示文章标题'], ['key' => 'post_prevnext_s', 'label' => '上一篇/下一篇', 'type' => 'switch', 'default' => true], ['key' => 'post_authordesc_s', 'label' => '作者简介', 'type' => 'switch', 'default' => true], + ['key' => 'post_author_card_limit', 'label' => '作者卡文章数', 'type' => 'number', 'default' => 6, 'min' => 0, 'max' => 12], + ['key' => 'post_author_card_orderby', 'label' => '作者卡文章排序', 'type' => 'select', 'default' => 'views', 'options' => [ + ['label' => '浏览量', 'value' => 'views'], + ['label' => '发布时间', 'value' => 'date'], + ['label' => '更新时间', 'value' => 'modified'], + ['label' => '评论数', 'value' => 'comment_count'], + ]], + ['key' => 'post_author_card_style', 'label' => '作者卡文章样式', 'type' => 'radio', 'default' => 'card', 'options' => [ + ['label' => '卡片', 'value' => 'card'], + ['label' => '列表', 'value' => 'list'], + ]], ['key' => 'post_single_hide_time_s', 'label' => '隐藏文章时间', 'type' => 'switch', 'default' => false], ]], ['key' => 'cover', 'title' => '封面展示', 'fields' => [ @@ -127,8 +138,8 @@ class AKSRT_Admin_Schema_Post_List { ['key' => 'post_copyright_enabled', 'label' => '版权信息', 'type' => 'switch', 'default' => true], ['key' => 'post_copyright_text', 'label' => '版权默认文案', 'type' => 'textarea', 'default' => '', 'description' => '留空则自动生成"本文由 XX 原创发布"'], ['key' => 'post_wechat_share', 'label' => '微信分享', 'type' => 'switch', 'default' => false], - ['key' => 'post_rewards_enabled', 'label' => '打赏功能', 'type' => 'switch', 'default' => false], - ['key' => 'post_rewards_text', 'label' => '打赏文案', 'type' => 'input', 'default' => ''], + ['key' => 'post_rewards_s', 'label' => '用户打赏', 'type' => 'switch', 'default' => false], + ['key' => 'post_rewards_text', 'label' => '打赏按钮文案', 'type' => 'input', 'default' => '赞赏'], ['key' => 'post_button_toptext', 'label' => '按钮上方文案', 'type' => 'input', 'default' => '喜欢就支持一下吧'], ]], ['key' => 'content', 'title' => '内容处理', 'fields' => [ diff --git a/inc/classes/class-aksrt-points-free.php b/inc/classes/class-aksrt-points-free.php index 6bdf767b..b20984eb 100644 --- a/inc/classes/class-aksrt-points-free.php +++ b/inc/classes/class-aksrt-points-free.php @@ -180,9 +180,12 @@ class AKSRT_Points_Free { * @return list */ public static function task_list_for_user(int $user_id = 0): array { - unset($user_id); // reserved for per-user progress later $cfg = self::settings(); $list = []; + $login_done = false; + if ($user_id > 0) { + $login_done = (string) get_user_meta($user_id, self::META_LOGIN, true) === self::today_key(); + } foreach (self::catalog() as $key => $meta) { $pts = (int) ($cfg['tasks'][$key] ?? 0); // Hide sign_up from daily task list (one-time); still grant on register. @@ -192,12 +195,21 @@ class AKSRT_Points_Free { if ($pts <= 0) { continue; } + $status = 'todo'; + if (in_array($key, ['login', 'sign_in'], true) && $login_done) { + $status = 'done_today'; + } elseif (in_array($key, ['post_like', 'post_favorite', 'followed', 'comment_like', 'bbs_essence', 'bbs_adopt', 'bbs_posts_hot', 'bbs_comment_hot', 'bbs_score_extra'], true)) { + $status = 'passive'; + } $list[] = [ - 'key' => $key, - 'title' => $meta[0], - 'desc' => $meta[1], - 'points' => $pts, - 'enabled' => true, + 'key' => $key, + 'title' => $meta[0], + 'desc' => $meta[1], + 'points' => $pts, + 'enabled' => true, + 'status' => $status, + 'progress' => null, + 'target' => null, ]; } return $list; diff --git a/inc/classes/settings/class-aksrt-settings-post-display.php b/inc/classes/settings/class-aksrt-settings-post-display.php index e33c6254..13950e4c 100644 --- a/inc/classes/settings/class-aksrt-settings-post-display.php +++ b/inc/classes/settings/class-aksrt-settings-post-display.php @@ -237,10 +237,20 @@ class AKSRT_Settings_Post_Display { $authordesc = !isset($settings['post_authordesc_s']) || !empty($settings['post_authordesc_s']); $hide_time = !empty($settings['post_single_hide_time_s']); $share_enabled = !isset($settings['share_s']) || !empty($settings['share_s']); - $rewards_enabled = !empty($settings['post_rewards_enabled']) || !empty($settings['post_rewards_s']); - $rewards_image = (string) ($settings['post_rewards_image'] ?? $settings['post_rewards_qrcode'] ?? $settings['post_rewards_img'] ?? ''); + $rewards_enabled = !empty($settings['post_rewards_s']) || !empty($settings['post_rewards_enabled']); $rewards_text = sanitize_text_field((string) ($settings['post_rewards_text'] ?? '')); - $rewards_btn_text = sanitize_text_field((string) ($settings['post_rewards_btn_text'] ?? '赞赏')); + if ($rewards_text === '') { + $rewards_text = '赞赏'; + } + $author_card_limit = max(0, min(12, (int) ($settings['post_author_card_limit'] ?? 6))); + $author_card_orderby = sanitize_key((string) ($settings['post_author_card_orderby'] ?? 'views')); + if (!in_array($author_card_orderby, ['views', 'date', 'modified', 'comment_count'], true)) { + $author_card_orderby = 'views'; + } + $author_card_style = sanitize_key((string) ($settings['post_author_card_style'] ?? 'card')); + if (!in_array($author_card_style, ['card', 'list'], true)) { + $author_card_style = 'card'; + } $maxheight_enabled = !empty($settings['article_maxheight_kg']) || !empty($settings['post_article_maxheight_kg']) || !empty($settings['post_maxheight_kg']); @@ -318,11 +328,15 @@ class AKSRT_Settings_Post_Display { // Canonical + legacy share. 'share_enabled' => $share_enabled, 'share_s' => $share_enabled, - // Rewards (Zibll post_rewards_s). + // Author card posts (Zibll user_card_box defaults). + 'author_card_limit' => $author_card_limit, + 'author_card_orderby' => $author_card_orderby, + 'author_card_style' => $author_card_style, + // Rewards (Zibll post_rewards_s) — per-user QR only. 'rewards_enabled' => $rewards_enabled, - 'rewards_image' => $rewards_image, + 'post_rewards_s' => $rewards_enabled, 'rewards_text' => $rewards_text, - 'rewards_btn_text' => $rewards_btn_text !== '' ? $rewards_btn_text : '赞赏', + 'rewards_btn_text' => $rewards_text, // Content max height (Zibll article_maxheight_kg / article_maxheight). 'maxheight_enabled' => $maxheight_enabled, 'maxheight' => $maxheight, diff --git a/inc/helpers/aksrt-frontend-tags.php b/inc/helpers/aksrt-frontend-tags.php index 065211d3..330a736f 100644 --- a/inc/helpers/aksrt-frontend-tags.php +++ b/inc/helpers/aksrt-frontend-tags.php @@ -835,28 +835,59 @@ if (!function_exists('aksrt_comment_action_permissions')) : * @return array{can_edit:bool,can_delete:bool,can_moderate:bool,can_pin:bool} */ function aksrt_comment_action_permissions($comment): array { - $comment = $comment instanceof WP_Comment ? $comment : get_comment($comment); - $none = ['can_edit' => false, 'can_delete' => false, 'can_moderate' => false, 'can_pin' => false]; - if (!($comment instanceof WP_Comment) || !is_user_logged_in()) { - return $none; + $comment = $comment instanceof WP_Comment ? $comment : get_comment($comment); + $none = ['can_edit' => false, 'can_delete' => false, 'can_moderate' => false, 'can_pin' => false]; + if (!($comment instanceof WP_Comment) || !is_user_logged_in()) { + return $none; + } + $uid = get_current_user_id(); + $is_wp_mod = current_user_can('moderate_comments') || current_user_can('manage_options'); + $is_owner = (int) $comment->user_id > 0 && (int) $comment->user_id === $uid; + $post_id = (int) $comment->comment_post_ID; + $post = $post_id > 0 ? get_post($post_id) : null; + $is_post_author = $post instanceof WP_Post && (int) $post->post_author === $uid; + $ctx = $post_id > 0 ? $post_id : null; + + $can = static function (string $cap) use ($uid, $ctx): bool { + if (function_exists('aksrt_user_can')) { + return aksrt_user_can($cap, $uid, $ctx); + } + if (class_exists('AKSRT_Settings_Capabilities')) { + return AKSRT_Settings_Capabilities::user_can($cap, $uid, $ctx); + } + return false; + }; + + $edit_window = 10; + if (class_exists('AKSRT_Settings')) { + $display = AKSRT_Settings::get_comment_display_settings(); + $edit_window = (int) ($display['edit_window'] ?? 10); + } + $within_window = $edit_window > 0 + && (time() - strtotime((string) $comment->comment_date_gmt)) < ($edit_window * MINUTE_IN_SECONDS); + + $can_edit_own = $is_owner && $within_window && $can('comment_edit'); + $can_edit_others = (!$is_owner) && ( + ($is_post_author && $can('comment_edit_my_post')) + || $can('comment_edit_other') + ); + $can_delete_own = $is_owner && $can('comment_delete'); + $can_delete_others = (!$is_owner) && ( + ($is_post_author && $can('comment_delete_my_post')) + || $can('comment_delete_other') + ); + $can_pin = (int) $comment->comment_parent === 0 && ( + ($is_post_author && $can('comment_set_topping_my_post')) + || $can('comment_set_topping_other') + ); + + return [ + 'can_edit' => $is_wp_mod || $can_edit_own || $can_edit_others, + 'can_delete' => $is_wp_mod || $can_delete_own || $can_delete_others, + 'can_moderate' => $is_wp_mod || $can('comment_audit_other') || ($is_post_author && $can('comment_audit_my_post')), + 'can_pin' => $is_wp_mod || $can_pin, + ]; } - $is_moderator = current_user_can('moderate_comments'); - $is_owner = (int) $comment->user_id > 0 && (int) $comment->user_id === get_current_user_id(); - // Owner self-edit window (minutes); moderators bypass the window. - $edit_window = 10; - if (class_exists('AKSRT_Settings')) { - $display = AKSRT_Settings::get_comment_display_settings(); - $edit_window = (int) ($display['edit_window'] ?? 10); - } - $within_window = $edit_window > 0 - && (time() - strtotime((string) $comment->comment_date_gmt)) < ($edit_window * MINUTE_IN_SECONDS); - return [ - 'can_edit' => $is_moderator || ($is_owner && $within_window), - 'can_delete' => $is_moderator || $is_owner, - 'can_moderate' => $is_moderator, - 'can_pin' => $is_moderator && (int) $comment->comment_parent === 0, - ]; - } endif; if (!function_exists('aksrt_is_comment_pinned')) : /** diff --git a/inc/helpers/aksrt-rewards.php b/inc/helpers/aksrt-rewards.php new file mode 100644 index 00000000..f7819d19 --- /dev/null +++ b/inc/helpers/aksrt-rewards.php @@ -0,0 +1,113 @@ + '', 'alipay' => '']; + if ($user_id <= 0) { + return $out; + } + foreach (['weixin' => 'aksrt_rewards_weixin', 'alipay' => 'aksrt_rewards_alipay'] as $key => $meta) { + $raw = get_user_meta($user_id, $meta, true); + if (is_numeric($raw) && (int) $raw > 0) { + $url = wp_get_attachment_image_url((int) $raw, 'full'); + $out[$key] = is_string($url) ? $url : ''; + } else { + $out[$key] = esc_url_raw((string) $raw); + } + } + return $out; + } +} + +if (!function_exists('aksrt_user_has_rewards_qr')) { + function aksrt_user_has_rewards_qr(int $user_id): bool { + $urls = aksrt_get_user_rewards_img_urls($user_id); + return $urls['weixin'] !== '' || $urls['alipay'] !== ''; + } +} + +if (!function_exists('aksrt_get_rewards_button')) { + function aksrt_get_rewards_button(int $user_id, string $class = 'but jb-blue ml10 pw-1em'): string { + if ($user_id <= 0 || !aksrt_rewards_enabled() || !aksrt_user_has_rewards_qr($user_id)) { + return ''; + } + $label = '赞赏'; + if (class_exists('AKSRT_Settings') && method_exists('AKSRT_Settings', 'get_post_page_settings')) { + $s = AKSRT_Settings::get_post_page_settings(); + $label = (string) ($s['rewards_btn_text'] ?? $s['rewards_text'] ?? $label); + if ($label === '') { + $label = '赞赏'; + } + } + return ''; + } +} + +if (!function_exists('aksrt_render_rewards_modal_for_user')) { + function aksrt_render_rewards_modal_for_user(int $user_id): void { + static $done = []; + if ($user_id <= 0 || isset($done[$user_id])) { + return; + } + if (!aksrt_rewards_enabled() || !aksrt_user_has_rewards_qr($user_id)) { + return; + } + $done[$user_id] = true; + $urls = aksrt_get_user_rewards_img_urls($user_id); + $label = '赞赏'; + if (class_exists('AKSRT_Settings') && method_exists('AKSRT_Settings', 'get_post_page_settings')) { + $s = AKSRT_Settings::get_post_page_settings(); + $label = (string) ($s['rewards_text'] ?? $label); + if ($label === '') { + $label = __('赞赏', 'aksrt'); + } + } + ?> + + - + 0 && function_exists('aksrt_get_rewards_button')) + ? aksrt_get_rewards_button($reward_author, 'action action-rewards') + : ''; + if ($reward_btn === '' && !empty($settings['rewards_enabled']) && function_exists('aksrt_user_has_rewards_qr') && $reward_author > 0 && aksrt_user_has_rewards_qr($reward_author)) { + $label = esc_html((string) ($settings['rewards_btn_text'] ?? __('赞赏', 'aksrt'))); + $reward_btn = ''; + } + if ($reward_btn !== '') : + echo $reward_btn; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + if (function_exists('aksrt_render_rewards_modal_for_user')) { + aksrt_render_rewards_modal_for_user($reward_author); + } + endif; + // legacy placeholder to keep structure; actual button echoed above + if (false) : ?> - -
- + + + + +
- - + -
+