diff --git a/assets/css/frontend/vip-modal.css b/assets/css/frontend/vip-modal.css index ac24c40b..db9f04d4 100644 --- a/assets/css/frontend/vip-modal.css +++ b/assets/css/frontend/vip-modal.css @@ -49,8 +49,8 @@ opacity:0 形成隐形遮罩,拦截整页点击; - 打开态由 pay-vip.js 自包含开关的内联 display:flex 驱动(block 会破坏 .flex.jc 双轴居中),与余额充值/购买积分弹窗同构; - - 入口在本页 runtime 会员弹窗内,壳需高于 runtime 宿主(100050),以复现 - 参考主题在会员弹窗之上再叠一层兑换弹窗的行为。 */ + - 入口在会员购买弹窗与 /user/vip/ 页面横条两处,壳需高于 runtime 宿主 + (100050),以复现参考主题在会员弹窗之上再叠一层兑换弹窗的行为。 */ .modal[data-aksrt-vip-points-modal] { display: none; z-index: 100060; diff --git a/assets/css/routes/user-center/credits.css b/assets/css/routes/user-center/credits.css index da9d08c9..47f15e64 100644 --- a/assets/css/routes/user-center/credits.css +++ b/assets/css/routes/user-center/credits.css @@ -1325,6 +1325,28 @@ body.uc-modal-open { margin: 0 !important; line-height: 1; } + +/* 积分兑换会员入口(参考主题用户中心 VIP 页 1:1:两张档位购买卡下方的全宽横条, + 左侧 points-color 图标 + 「积分兑换会员」,右侧「立即兑换」badg + 右箭头; + 结构与配色由 zibll 工具类(flex/ac/jsb/flex1/em12/c-yellow/badg)承担, + 此处只提供与 .uc-vip-level-card 同一变量体系的表面与间距)。 */ +.uc-vip-points-entry { + margin-top: 12px; + padding: 12px 16px; + border-radius: var(--radius, 10px); + background: var(--main-bg-color, var(--surface-1, #fff)); + box-shadow: var(--shadow-sm); + color: inherit; + text-decoration: none; +} +.uc-vip-points-entry:hover, +.uc-vip-points-entry:focus { + text-decoration: none; +} +.uc-vip-points-entry .icon { + flex: 0 0 auto; +} + .uc-vip-assets-entry { gap: 12px; } @@ -1361,6 +1383,9 @@ html:not(.dark):not(.theme-dark) .uc-vip-level-card.level-2 .v-name { .uc-vip-level-card { flex: 1 1 auto; } + .uc-vip-points-entry { + padding: 12px 14px; + } .uc-vip-tab .user-content-myvip { padding: 14px 16px; gap: 12px; diff --git a/assets/js/pay-vip.js b/assets/js/pay-vip.js index 03a04288..968897c6 100644 --- a/assets/js/pay-vip.js +++ b/assets/js/pay-vip.js @@ -695,12 +695,6 @@ }); return; } - var pointsLink = event.target.closest("[data-aksrt-vip-points-open]"); - if (pointsLink) { - event.preventDefault(); - openPointsModal(pointsLink); - return; - } var submit = event.target.closest("[data-vip-submit]"); if (submit) { event.preventDefault(); @@ -763,11 +757,24 @@ } }); + // 积分兑换入口(data-aksrt-vip-points-open)文档级委托:一处绑定同时覆盖 + // /user/vip/ 页面静态输出的入口横条与会员购买弹窗内运行时注入的同款按钮 + // (事件冒泡至 document,弹窗内不再单独挂监听)。 + function bindPointsEntry() { + document.addEventListener("click", function (event) { + var pointsLink = event.target.closest ? event.target.closest("[data-aksrt-vip-points-open]") : null; + if (!pointsLink) return; + event.preventDefault(); + openPointsModal(pointsLink); + }); + } + function bindOnce() { if (window.__aksrtPayVipBound) return; window.__aksrtPayVipBound = true; document.addEventListener("click", onDocClick, true); bindPointsModal(); + bindPointsEntry(); } jQuery(document).off("auto_fun.aksrt-pay-vip").on("auto_fun.aksrt-pay-vip", bindOnce); diff --git a/inc/helpers/aksrt-vip.php b/inc/helpers/aksrt-vip.php index b8b0c45e..1d466246 100644 --- a/inc/helpers/aksrt-vip.php +++ b/inc/helpers/aksrt-vip.php @@ -427,6 +427,55 @@ if (!function_exists('aksrt_get_vip_purchase_url')) { } } +/** + * 用户中心 /user/vip/ 页面的积分兑换会员入口(参考主题 zibpay_user_page_tab_content_vip 1:1)。 + * + * 参考主题在「我的会员」摘要与两张档位购买卡之后输出全宽入口横条: + * .c-yellow.zib-widget.flex.ac.jsb(zib-widget → 本主题 aksrt-panel 表面) + * ├─ .flex1.em12:points-color 图标 +「积分兑换会员」 + * └─ .badg.radius.c-yellow:「立即兑换」+ fa-chevron-circle-right + * 门控对齐参考主题 zibpay_get_vip_points_exchange_link:已是会员不展示 + * ($vip_level ? '' : link),且要求积分兑换会员开启、至少一个会员等级 + * 开启并存在可兑换套餐(catalog.points_exchange 已内含 credits 门控)。 + * + * 入口点击经 data-aksrt-vip-points-open 由 pay-vip.js 打开 wp_footer 输出的 + * 静态兑换弹窗(aksrt_render_vip_points_exchange_modal),与会员购买弹窗内 + * 的入口共用同一弹窗与开合开关。 + * + * @param int $user_id 当前用户 ID(<=0 返回空串)。 + * @param int $vip_level 用户当前会员等级(>0 时返回空串;缺省 -1 表示自行解析)。 + * @return string 空串表示不满足展示条件。 + */ +if (!function_exists('aksrt_get_vip_points_exchange_entry_html')) : + function aksrt_get_vip_points_exchange_entry_html(int $user_id, int $vip_level = -1): string { + if ($user_id <= 0 || !class_exists('\AKSRT\Content\VipService')) { + return ''; + } + if ($vip_level < 0) { + $vip_level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0; + } + // 参考主题契约:已经是会员就不再展示积分兑换入口。 + if ($vip_level > 0) { + return ''; + } + + $catalog = \AKSRT\Content\VipService::get_instance()->get_catalog($user_id); + $points_exchange = is_array($catalog['points_exchange'] ?? null) ? $catalog['points_exchange'] : []; + $packages = is_array($points_exchange['packages'] ?? null) ? $points_exchange['packages'] : []; + if (empty($points_exchange['enabled']) || $packages === []) { + return ''; + } + + return '' + . '' + . '' + . esc_html__('积分兑换会员', 'aksrt') + . '' + . '' . esc_html__('立即兑换', 'aksrt') + . ''; + } +endif; + /** * Format VIP equity HTML for front-end display. * @@ -507,10 +556,13 @@ if (!function_exists('aksrt_format_vip_equity_html')) { /** * 全局积分兑换会员弹窗(参考主题 refresh_modal 同构静态壳)。 * - * 入口位于会员购买弹窗(.pay-vip → assets/js/pay-vip.js)内,而 .pay-vip - * 触发点全站可见(页头、浮动按钮、用户中心、付费卡),因此弹窗壳在满足 - * 条件时随 wp_footer 全局输出一次;显隐由 pay-vip.js 自包含开关驱动, - * 静止态由 vip-modal.css 的 .modal[data-aksrt-vip-points-modal] 显式隐藏。 + * 入口有两处:会员购买弹窗(.pay-vip → assets/js/pay-vip.js)内的「积分兑换 + * 会员」按钮,以及 /user/vip/ 档位购买卡下方的入口横条 + * (aksrt_get_vip_points_exchange_entry_html);二者均带 + * data-aksrt-vip-points-open,由 pay-vip.js 文档级委托打开本弹窗。.pay-vip + * 触发点全站可见,因此弹窗壳在满足条件时随 wp_footer 全局输出一次;显隐由 + * pay-vip.js 自包含开关驱动,静止态由 vip-modal.css 的 + * .modal[data-aksrt-vip-points-modal] 显式隐藏。 */ if (!function_exists('aksrt_render_vip_points_exchange_modal')) : function aksrt_render_vip_points_exchange_modal(): void { diff --git a/inc/helpers/user-center-renderers/vip-tab.php b/inc/helpers/user-center-renderers/vip-tab.php index 0ed3b42b..90b86559 100644 --- a/inc/helpers/user-center-renderers/vip-tab.php +++ b/inc/helpers/user-center-renderers/vip-tab.php @@ -164,6 +164,12 @@ foreach ([1, 2] as $lv) { + +