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(); ?> -