From f040a680d611e962c3c92df70b7c8268a551b344 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 13 Aug 2026 18:58:24 +0800 Subject: [PATCH] fix(pay): render paid reading as fragments --- .../frontend/single-post/hidden-content.css | 114 ++++++++++++++++ assets/css/gutenberg-blocks.css | 31 +++++ assets/js/gutenberg-blocks.js | 57 ++++++++ inc/core-hooks/part-3.php | 2 +- inc/helpers/aksrt-pay.php | 123 ++++++++++-------- inc/helpers/aksrt-single.php | 5 +- src/Core/ResourceMetaBox.php | 7 + src/Core/Setup.php | 5 + src/Core/Shortcodes.php | 37 ++++-- 9 files changed, 316 insertions(+), 65 deletions(-) diff --git a/assets/css/frontend/single-post/hidden-content.css b/assets/css/frontend/single-post/hidden-content.css index fb2c3a17..f96e8e74 100644 --- a/assets/css/frontend/single-post/hidden-content.css +++ b/assets/css/frontend/single-post/hidden-content.css @@ -119,3 +119,117 @@ opacity: .85; } +/* ---- Paid fragment purchase card ---- */ +.aksrt-paywall { + position: relative; + margin: 24px 0; +} + +.aksrt-paywall__card { + position: relative; + z-index: 0; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 18px; + padding: 22px 24px; + background: linear-gradient(135deg, var(--surface-1, #fff) 0%, var(--surface-2, #f8fafc) 100%); + border: 1px solid var(--color-line, #e5e7eb); + border-radius: var(--radius-lg, 14px); + box-shadow: 0 10px 28px rgba(15, 23, 42, .08); +} + +.aksrt-paywall--fragment .aksrt-paywall__card { + border-style: dashed; + border-color: color-mix(in srgb, var(--color-primary, #f04494) 42%, var(--color-line, #e5e7eb)); +} + +.aksrt-paywall__icon { + display: grid; + place-items: center; + width: 44px; + height: 44px; + flex: 0 0 auto; + border-radius: 50%; + color: var(--color-primary, #f04494); + background: color-mix(in srgb, var(--color-primary, #f04494) 12%, transparent); + font-size: 20px; +} + +.aksrt-paywall__body { min-width: 0; } + +.aksrt-paywall__title { + margin: 0 0 4px; + color: var(--color-ink, #15171a); + font-size: 17px; + font-weight: 700; +} + +.aksrt-paywall__desc { + margin: 0; + color: var(--color-ink-3, #525866); + font-size: 14px; + line-height: 1.6; +} + +.aksrt-paywall__price { margin-top: 8px; } +.aksrt-paywall__price-amount { color: var(--color-error, #ef4444); font-size: 20px; font-weight: 700; } +.aksrt-paywall__price-member { color: var(--color-primary, #f04494); font-size: 16px; font-weight: 700; } +.aksrt-paywall__price-free { color: var(--color-success, #10b981); font-size: 16px; font-weight: 700; } +.aksrt-paywall__price-original { margin-left: 8px; color: var(--color-ink-4, #8a8f98); font-size: 13px; text-decoration: line-through; } + +.aksrt-paywall__actions { + position: relative; + z-index: 1; + display: flex; + flex: 0 0 auto; + flex-direction: column; + align-items: center; + gap: 8px; + text-align: center; +} + +.aksrt-paywall__btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 40px; + white-space: nowrap; + cursor: pointer; +} + +.aksrt-paywall__btn:focus-visible, +.aksrt-paywall__login-link:focus-visible { + outline: 3px solid color-mix(in srgb, var(--color-primary, #f04494) 35%, transparent); + outline-offset: 3px; +} + +.aksrt-paywall__hint, +.aksrt-paywall__login-link { + margin: 0; + color: var(--color-ink-4, #8a8f98); + font-size: 12px; + line-height: 1.5; +} + +.aksrt-paywall__login-link { text-decoration: underline; } +.aksrt-gallery-locked { filter: blur(8px); pointer-events: none; user-select: none; } +.aksrt-paywall-preview { margin: 0 0 16px; padding: 14px 16px; border-left: 3px solid var(--color-primary, #f04494); background: var(--surface-2, #f8fafc); border-radius: 0 10px 10px 0; color: var(--color-ink-2, #374151); line-height: 1.7; } +.aksrt-paywall-preview p:last-child { margin-bottom: 0; } +.aksrt-paid-gallery__overlay .but, .aksrt-paid-video__placeholder .but { margin-top: 10px; } + +@media (max-width: 680px) { + .aksrt-paywall__card { + grid-template-columns: auto minmax(0, 1fr); + padding: 18px; + } + + .aksrt-paywall__actions { + grid-column: 1 / -1; + align-items: stretch; + width: 100%; + } + + .aksrt-paywall__btn { width: 100%; } +} diff --git a/assets/css/gutenberg-blocks.css b/assets/css/gutenberg-blocks.css index 2b6f4d77..b9658679 100644 --- a/assets/css/gutenberg-blocks.css +++ b/assets/css/gutenberg-blocks.css @@ -363,6 +363,37 @@ background: var(--surface-2, #f8fafc); } +/* ---- Paid Fragment ---- */ + +.aksrt-paid-fragment-block { + margin: 16px 0; + border: 1px dashed #ec4899; + border-radius: 12px; + overflow: hidden; + background: #fff7fb; +} + +.aksrt-paid-fragment-block .components-notice { + margin: 12px; +} + +.aksrt-paid-fragment-block__label { + display: flex; + align-items: center; + gap: 8px; + padding: 9px 14px; + color: #9d174d; + background: #fce7f3; + font-size: 13px; + font-weight: 700; +} + +.aksrt-paid-fragment-block__content { + min-height: 72px; + padding: 16px; + background: #fff; +} + /* ── Responsive ── */ @media (max-width: 767px) { diff --git a/assets/js/gutenberg-blocks.js b/assets/js/gutenberg-blocks.js index 1c5293be..15619999 100644 --- a/assets/js/gutenberg-blocks.js +++ b/assets/js/gutenberg-blocks.js @@ -16,6 +16,7 @@ var el = wp.element.createElement; var Fragment = wp.element.Fragment; + var RawHTML = wp.element.RawHTML; var registerBlockTypeRaw = wp.blocks.registerBlockType; var blockEditor = wp.blockEditor || wp.editor || {}; var InspectorControls = blockEditor.InspectorControls || null; @@ -28,6 +29,8 @@ var TextControl = wp.components.TextControl; var ToggleControl = wp.components.ToggleControl; var RangeControl = wp.components.RangeControl; + var Notice = wp.components.Notice; + var useSelect = window.wp.data && window.wp.data.useSelect; if (!registerBlockTypeRaw || !el) return; @@ -750,4 +753,58 @@ }); } + // ================================================================ + // 9. Paid Fragment Block + // ================================================================ + + function usePaidReadingConfigured() { + if (!useSelect) return true; + return useSelect(function (select) { + var editor = select('core/editor'); + if (!editor || !editor.getEditedPostAttribute) return true; + + var meta = editor.getEditedPostAttribute('meta') || {}; + var pay = meta._aksrt_pay || {}; + if (pay.pay_type !== 'reading') return false; + return pay.pay_modo === 'points' + ? Number(pay.points_price || 0) > 0 + : Number(pay.price || 0) > 0; + }, []); + } + + registerBlockType('aksrt/paid-fragment', { + title: '付费可见', + icon: 'lock', + category: CATEGORY, + description: '仅购买文章付费阅读后可见的片段。', + attributes: { + content: { type: 'string', default: '' }, + }, + edit: function (props) { + var configured = usePaidReadingConfigured(); + return h('div', { className: 'aksrt-paid-fragment-block' }, + !configured && Notice ? h(Notice, { status: 'warning', isDismissible: false }, + '请在“付费内容设置”中选择“付费阅读”并设置有效价格;未配置时此片段会公开显示。' + ) : null, + h('div', { className: 'aksrt-paid-fragment-block__label' }, + h('span', { className: 'dashicons dashicons-lock', 'aria-hidden': true }), + '付费可见片段' + ), + h(RichText, { + tagName: 'div', + className: 'aksrt-paid-fragment-block__content', + value: props.attributes.content, + onChange: function (value) { props.setAttributes({ content: value }); }, + placeholder: '输入购买后可见的内容…', + }) + ); + }, + save: function (props) { + // Save a single canonical shortcode so hand-written and block-authored + // content use the identical server-side rendering path. + var shortcode = '[hidecontent type="payshow"]' + props.attributes.content + '[/hidecontent]'; + return RawHTML ? h(RawHTML, null, shortcode) : shortcode; + }, + }); + })(); diff --git a/inc/core-hooks/part-3.php b/inc/core-hooks/part-3.php index 37b02c46..245c6310 100644 --- a/inc/core-hooks/part-3.php +++ b/inc/core-hooks/part-3.php @@ -139,7 +139,7 @@ add_action('enqueue_block_editor_assets', function () { 'aksrt-gutenberg-blocks', $base_uri . '/assets/js/gutenberg-blocks.js', // wp-media-utils: MediaUpload for carousel block - ['wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-hooks', 'wp-media-utils'], + ['wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-hooks', 'wp-media-utils', 'wp-data'], (string) filemtime($blocks_js), true ); diff --git a/inc/helpers/aksrt-pay.php b/inc/helpers/aksrt-pay.php index 1001bb35..c29a77d7 100644 --- a/inc/helpers/aksrt-pay.php +++ b/inc/helpers/aksrt-pay.php @@ -675,6 +675,53 @@ if (!function_exists('aksrt_is_paid_reading')) { } } +/** + * Detect the canonical paid-fragment shortcode in a post's raw content. + * + * The editor writes this exact shortcode format, so this is the single + * source of truth for whether a reading price has any protected fragment to + * apply to. It deliberately does not inspect rendered content. + */ +if (!function_exists('aksrt_post_has_payshow_shortcode')) { + function aksrt_post_has_payshow_shortcode($post = null): bool { + $p = get_post($post); + if (!($p instanceof WP_Post)) { + return false; + } + + return preg_match( + '/\[hidecontent\b[^\]]*\btype\s*=\s*(?:"payshow"|\'payshow\'|payshow)[^\]]*\]/i', + (string) $p->post_content + ) === 1; + } +} + +/** + * A paid fragment is active only for an explicitly priced paid-reading post. + * + * A zero-price reading configuration is intentionally public: authors can + * save content before setting a price without accidentally hiding it. + */ +if (!function_exists('aksrt_is_payshow_purchase_enabled')) { + function aksrt_is_payshow_purchase_enabled($post = null): bool { + if (!aksrt_post_has_payshow_shortcode($post)) { + return false; + } + + $meta = aksrt_get_pay_meta($post); + if (($meta['pay_type'] ?? AKSRT_PAY_TYPE_NO) !== AKSRT_PAY_TYPE_READING) { + return false; + } + + $pay_modo = ($meta['pay_modo'] ?? '0') === 'points' ? 'points' : '0'; + $price = $pay_modo === 'points' + ? (float) ($meta['points_price'] ?? 0) + : (float) ($meta['price'] ?? 0); + + return $price > 0; + } +} + if (!function_exists('aksrt_is_paid_download')) { function aksrt_is_paid_download($post = null): bool { return aksrt_get_pay_meta($post)['pay_type'] === AKSRT_PAY_TYPE_DOWNLOAD; @@ -1093,7 +1140,7 @@ if (!function_exists('aksrt_get_pay_attributes_html')) { // ============================================================ if (!function_exists('aksrt_get_paywall_html')) { - function aksrt_get_paywall_html(WP_Post $post): string { + function aksrt_get_paywall_html(WP_Post $post, bool $is_fragment = false): string { $meta = aksrt_get_pay_meta($post); $pay_type = $meta['pay_type']; $priced = function_exists('aksrt_get_user_pay_price') ? aksrt_get_user_pay_price($post) : null; @@ -1117,6 +1164,9 @@ if (!function_exists('aksrt_get_paywall_html')) { ]; $type_info = $type_labels[$pay_type] ?? $type_labels[AKSRT_PAY_TYPE_READING]; + if ($is_fragment && $pay_type === AKSRT_PAY_TYPE_READING) { + $type_info['desc'] = __('付费后即可查看此片段内容', 'aksrt'); + } $login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(get_permalink($post)); $is_logged_in = is_user_logged_in(); $original_price = (float) ($meta['original_price'] ?? 0); @@ -1129,13 +1179,12 @@ if (!function_exists('aksrt_get_paywall_html')) { ob_start(); ?> -
-
-
-
-
- -
+
+
+
+ +
+

@@ -1150,18 +1199,18 @@ if (!function_exists('aksrt_get_paywall_html')) {
+
+
0 && $pay_modo !== 'points' && function_exists('aksrt_allow_guest_content_pay') && aksrt_allow_guest_content_pay($post)) : ?> -

- -

+ - - - + + + @@ -1173,7 +1222,7 @@ if (!function_exists('aksrt_get_paywall_html')) {

- +

@@ -1183,38 +1232,6 @@ if (!function_exists('aksrt_get_paywall_html')) { } } -// ============================================================ -// 付费墙样式注入 -// ============================================================ - -add_action('wp_head', function (): void { - if (is_admin() || !is_singular()) return; - global $post; - if (!($post instanceof WP_Post)) return; - if (!function_exists('aksrt_can_access_content') || aksrt_can_access_content($post)) return; - ?> - - 'no']; $pay_type = (string) ($pay_meta['pay_type'] ?? 'no'); $is_paid_post = function_exists('aksrt_is_paid_post') && aksrt_is_paid_post($post_id); - // Pay module: download box OR non-download paid types (reading/gallery/video/vip). - $show_pay_box = $has_download || ($is_paid_post && $pay_type !== 'no' && $pay_type !== 'download'); + // Paid-reading purchase is rendered inline by each payshow shortcode. + // Keep the article-level module for download, gallery, video and VIP only. + $show_pay_box = $has_download || ($is_paid_post && !in_array($pay_type, ['no', 'download', 'reading'], true)); $breadcrumbs_html = ''; if (!empty($settings['breadcrumbs']) && function_exists('aksrt_breadcrumbs')) { diff --git a/src/Core/ResourceMetaBox.php b/src/Core/ResourceMetaBox.php index ab776da8..7a8dad1a 100644 --- a/src/Core/ResourceMetaBox.php +++ b/src/Core/ResourceMetaBox.php @@ -89,6 +89,10 @@ class ResourceMetaBox {
+ +
@@ -378,6 +382,9 @@ class ResourceMetaBox { var isPaid = t !== 'no'; fields.setAttribute('data-disabled', isPaid ? '0' : '1'); + var readingHint = document.getElementById('aksrt-pmb-reading-hint'); + if (readingHint) readingHint.hidden = t !== 'reading'; + // 根据付费类型显示/隐藏相关字段 if (dlFields) dlFields.style.display = (t === 'download') ? '' : 'none'; if (galleryFields) galleryFields.style.display = (t === 'gallery') ? '' : 'none'; diff --git a/src/Core/Setup.php b/src/Core/Setup.php index 729d9542..fdcf898b 100644 --- a/src/Core/Setup.php +++ b/src/Core/Setup.php @@ -163,10 +163,15 @@ class Setup { 'type' => 'string', 'enum' => array_keys(DomainModel::pay_type_labels()), ], + 'pay_modo' => ['type' => 'string', 'enum' => ['0', 'points']], 'price' => ['type' => 'number'], + 'points_price' => ['type' => 'integer'], 'original_price' => ['type' => 'number'], 'vip1_price' => ['type' => 'number'], 'vip2_price' => ['type' => 'number'], + 'vip1_points' => ['type' => 'integer'], + 'vip2_points' => ['type' => 'integer'], + 'pay_limit' => ['type' => 'integer', 'enum' => [0, 1, 2]], 'version' => ['type' => 'string'], 'size' => ['type' => 'string'], 'license' => ['type' => 'string'], diff --git a/src/Core/Shortcodes.php b/src/Core/Shortcodes.php index a92bd57b..1af9de7a 100644 --- a/src/Core/Shortcodes.php +++ b/src/Core/Shortcodes.php @@ -101,6 +101,17 @@ class Shortcodes { $atts = shortcode_atts(['message' => ''], $atts, 'payshow'); $pid = get_the_ID(); + $post = $pid > 0 ? get_post($pid) : null; + // Payshow is a paid-reading fragment, not a generic hide mechanism. + // Until the post has an active reading price, keep its content public + // so authors can compose and publish before completing price setup. + if (!($post instanceof \WP_Post) + || !function_exists('aksrt_is_payshow_purchase_enabled') + || !aksrt_is_payshow_purchase_enabled($post)) { + return '
' + . do_shortcode($content) . '
'; + } + $can_view = $this->viewer_can_see_paid_block($pid); if ($can_view) { @@ -109,18 +120,22 @@ class Shortcodes { } $message = $atts['message'] ?: __('此内容需付费后查看', 'aksrt'); - $uid = get_current_user_id(); - $html = '
'; - $html .= '
'; - $html .= ' '; - $html .= '' . esc_html($message) . ''; - $html .= '
'; - if ($uid <= 0) { - $html .= '
'; + if (function_exists('aksrt_get_paywall_html')) { + $paywall = aksrt_get_paywall_html($post, true); + if ($message !== '') { + $paywall = str_replace( + '

', + '

' . esc_html($message) . ' ', + $paywall + ); + } + return $paywall; } - $html .= '

'; - return $html; + + return '
' + . '
' + . esc_html($message) + . '
'; } /**