fix(vip): 在 /user/vip/ 档位购买卡下方补充积分兑换会员入口

参考主题 zibpay_user_page_tab_content_vip:非会员 + 积分兑换会员开启 +
有可兑换套餐时,在两张档位购买卡下方输出全宽入口横条(points-color 图标
+ 标题 + 立即兑换 badg),点击经 data-aksrt-vip-points-open 打开 wp_footer
静态兑换弹窗,与会员购买弹窗内入口共用同一弹窗。

- aksrt-vip.php 新增 aksrt_get_vip_points_exchange_entry_html(),门控与
  弹窗壳同源(取服务端 catalog),已是会员不展示(对齐参考主题契约)
- vip-tab.php 在卡片区之后输出入口;credits.css 补 .uc-vip-points-entry
  表面样式(与档位卡同一变量体系,暗色自适应)
- pay-vip.js 改文档级委托承接 data-aksrt-vip-points-open,一处绑定覆盖
  静态页面横条与弹窗内按钮;移除 bindModalUi 内的重复分支
- 契约测试更新为双入口(11 项),node --test 420 项通过,php -l 通过

未写数据迁移;未做真实视觉效果验收。
This commit is contained in:
2026-09-23 14:30:54 +08:00
parent b798c7a45d
commit 2801a89c1f
7 changed files with 164 additions and 22 deletions
@@ -1,15 +1,22 @@
/**
* 积分兑换会员弹窗契约(参考主题 zibpay_get_vip_points_exchange_modal 1:1 对齐)。
* 积分兑换会员弹窗与双入口契约(参考主题 1:1 对齐)。
*
* 三条硬约束:
* 硬约束:
* 1) 服务端静态输出参考主题 refresh_modal 同构壳(`modal flex jc fade` >
* `modal-dialog modal-mini full-sm` > `modal-content` > `modal-body`),
* 入口只保留在会员购买弹窗内——/user/vip/ 页底部的独立入口已移除。
* `modal-dialog modal-mini full-sm` > `modal-content` > `modal-body`)。
* 入口有两处,分别对应参考主题的两个变体:
* - 会员购买弹窗内(zibpay_get_pay_uservip_modal 的
* `btn-block but mb10 padding-lg c-green mini-radius` 按钮);
* - /user/vip/ 档位购买卡下方(zibpay_user_page_tab_content_vip 的
* `c-yellow zib-widget flex ac jsb` 全宽横条,仅非会员展示)。
* 两处入口均带 data-aksrt-vip-points-open,由 pay-vip.js 文档级委托打开
* 同一静态弹窗。
* 2) 静止态必须被 vip-modal.css 显式隐藏(.flex 覆盖回归护栏),打开态为
* flex 而非 block,且层级高于 runtime 宿主(100050),以复现参考主题在
* 会员弹窗之上再叠一层兑换弹窗的行为。
* 3) 旧的客户端自实现弹窗(renderPointsModal / aksrt-vip-points-option /
* catalog.vip_url)必须已彻底移除。
* catalog.vip_url)与旧版 /user/vip/ 自定义入口(uc-vip-points-link)
* 必须已彻底移除。
*/
const test = require('node:test');
const assert = require('node:assert');
@@ -102,11 +109,54 @@ test('会员弹窗内的入口为参考主题同款按钮并开本地弹窗', ()
assert.match(vipJs, /openPointsModal\(pointsLink\)/);
});
test('旧的自实现兑换弹窗与独立入口已彻底移除', () => {
test('/user/vip/ 档位购买卡下方的入口为参考主题用户中心同款横条', () => {
// 参考主题 zibpay_user_page_tab_content_vip:my_box → vip_card → points link,
// 横条为 `c-yellow zib-widget flex ac jsb`(zib-widget → 本主题aksrt-panel表面)。
assert.match(footer, /function aksrt_get_vip_points_exchange_entry_html/);
assert.match(vipTab, /aksrt_get_vip_points_exchange_entry_html\(\$user_id, \$level\)/);
// 结构 1:1:左 points-color 图标 + 标题(flex1 em12 c-yellow),
// 右「立即兑换」badg + 右箭头。
assert.match(footer, /uc-vip-points-entry flex ac jsb/);
assert.match(footer, /flex1 em12 c-yellow/);
assert.match(footer, /#icon-points-color/);
assert.match(footer, /badg radius c-yellow/);
assert.match(footer, /fa fas fa-chevron-circle-right ml6/);
assert.match(footer, /data-aksrt-vip-points-open/);
});
test('/user/vip/ 入口门控与弹窗壳同源:非会员 + 兑换开启 + 有可兑换套餐', () => {
// 参考主题 zibpay_get_vip_points_exchange_link 门控:已是会员不展示。
assert.match(footer, /if \(\$vip_level > 0\) \{\s*return '';/);
// 可用性直接取服务端 catalog(内含 credits 门控与等级启用过滤),
// 与 wp_footer 弹窗壳的渲染条件一致,避免出现无弹窗可开的死入口。
assert.match(footer, /get_catalog\(\$user_id\)/);
assert.match(footer, /empty\(\$points_exchange\['enabled'\]\) \|\| \$packages === \[\]/);
// 模板传入当前会员等级(>0 时不输出)。
assert.match(footer, /int \$vip_level = -1/);
});
test('页面入口样式落在 credits.css 且与档位卡同一表面体系', () => {
assert.match(creditsCss, /\.uc-vip-points-entry \{/);
assert.match(creditsCss, /\.uc-vip-points-entry\s*\{[^}]*var\(--main-bg-color, var\(--surface-1, #fff\)\)/s);
assert.match(creditsCss, /\.uc-vip-points-entry\s*\{[^}]*box-shadow:\s*var\(--shadow-sm\)/s);
});
test('入口开合由文档级委托驱动,弹窗内不再单独挂监听', () => {
// /user/vip/ 静态横条与弹窗内按钮共用一处 document 委托(冒泡)。
assert.match(vipJs, /function bindPointsEntry\(\)/);
assert.match(vipJs, /bindPointsEntry\(\);/);
assert.match(vipJs, /document\.addEventListener\("click", function \(event\) \{[\s\S]*?data-aksrt-vip-points-open/);
// bindModalUi 内的重复分支已移除:openPointsModal(pointsLink) 只保留一处调用。
const openCalls = vipJs.match(/openPointsModal\(pointsLink\)/g) || [];
assert.strictEqual(openCalls.length, 1);
});
test('旧的客户端自实现弹窗与旧版自定义入口已彻底移除', () => {
assert.doesNotMatch(vipJs, /renderPointsModal|exchangeFromModal|vipPointsPay/);
assert.doesNotMatch(vipJs, /aksrt-vip-points-option|aksrt-vip-points-modal__/);
assert.doesNotMatch(vipCss, /\.aksrt-vip-points-option|\.aksrt-vip-points-submit/);
// /user/vip/ 页底部不再有独立积分兑换入口。
// 旧版 /user/vip/ 自定义入口(uc-vip-points-link)不得回归;现行入口为
// 参考主题 1:1 的 uc-vip-points-entry 横条。
assert.doesNotMatch(vipTab, /uc-vip-points-link/);
assert.doesNotMatch(creditsCss, /\.uc-vip-points-link/);
// 用户中心侧的旧兑换控制器([data-aksrt-vip-points],全站无对应 DOM)。