Files
wordpress-theme-aksrt/src/Content/VipServiceModal.php
T
kate522andClaude Opus 5 427968a721 chore(branding): remove leftover upstream theme identifiers
The theme was renamed Zibll/子比 → AKSRT → Jio, but upstream branding
survived the sweeps in CSS classes, the SVG sprite prefix, a persisted
widget field ID, JS events, admin Vue class names, test infrastructure
and docblocks.

Renames:
- zib-widget / zib-widget-wrap / zib-slider → jio-widget-box /
  jio-widget-box-wrap / jio-slider-box (jio-widget is taken by the modern
  widget shell, jio-box-* by the card primitives)
- zib-partners-scroll + zps-* → jio-partners-scroll + jps-*
- zib-combo-info-card + zci-* → jio-combo-info-card + jci-*
- zibsvg- → jiosvg-, including the exported picker helpers and the
  fa-zibsvg- family-prefix guard
- zibpay_type → pay_type; the dual-read fallback in PostQuery collapses
  to a single key
- [zibajax="submit"] dropped (no PHP emitter); zib_ajax.success →
  jio_ajax.success
- aks-* admin Vue classes → jio-*
- zib_palette() → palette()

User-facing vendor traces are gone: the floating "more" button default no
longer ships a zibll.com link or 子比主题 marketing copy, the notice
widget ships no preset promo entries, and the four 官网教程 links are
removed from the post-list admin schema.

Test infrastructure renamed to neutral names (docker-compose.reference.yml,
Caddyfile, article-baseline.json, modern-widgets-contract.test.cjs) with
their DB/volume/env/slug identifiers following.

Kept intentionally, documented in tests/fixtures/README.md: the golden
fixture reference fingerprint (a SHA-256 record of an external directory,
read by validate-widget-golden / generate-widget-golden-fixture /
audit-widget-system), the doesNotMatch guards that prove the old tokens are
gone, and the reference theme's own meta key in the visual fixtures.

No backward-compat or data migration: saved widget configs holding old
icon keys or the old field ID silently lose them, per the project's
rewrite-over-compatibility rule.

Verified: 132/132 node contract tests, php tests/widget-system/run.php
(146 assertions), audit-admin-contracts, audit-psr4-paths and
validate-widget-golden all green; tracked bundles regenerated and in sync.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-02 00:06:54 +08:00

360 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* VIP modal view model + card HTML (legacy pay-vip modal / vip-card parity).
*
* @package Jio
*/
namespace Jio\Content;
defined('ABSPATH') || exit;
trait VipServiceModal {
/**
* Build the minimal JSON view model consumed by pay-vip.js.
*
* The catalog, member state, package prices, payment methods, and allowed
* actions are all calculated server-side. No payment gateway response or
* server-rendered modal HTML is included in this payload.
*
* @return array<string,mixed>
*/
public function get_modal_view_model(int $user_id, int $prefer_level = 1): array {
$catalog = $this->get_catalog($user_id);
$vip = $this->get_vip_settings_context();
$member = $this->get_vip_member_context($user_id);
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
$prefer_level = max(1, min(2, $prefer_level));
$level_view = [];
foreach ([1, 2] as $level) {
if (empty($levels[$level]['enabled'])) {
continue;
}
$equity = (string) ($levels[$level]['equity'] ?? '');
$equity = function_exists('jio_format_vip_equity_html')
? (string) jio_format_vip_equity_html($equity)
: wp_kses_post($equity);
$level_view[] = [
'level' => $level,
'name' => wp_kses_post((string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level))),
'name_text' => (string) ($levels[$level]['name'] ?? ('VIP' . $level)),
'desc' => wp_kses_post((string) ($levels[$level]['desc'] ?? '')),
'equity' => $equity,
'img_icon' => esc_url_raw((string) ($levels[$level]['img_icon'] ?? '')),
];
}
$package_ids = [];
$has_renew = false;
$has_upgrade = false;
$groups = is_array($catalog['level_packages'] ?? null) ? $catalog['level_packages'] : [];
foreach ($groups as $group) {
if (!is_array($group)) {
continue;
}
$action = sanitize_key((string) ($group['action'] ?? 'pay'));
$has_renew = $has_renew || $action === 'renew';
$has_upgrade = $has_upgrade || $action === 'upgrade';
foreach ((array) ($group['items'] ?? []) as $item) {
if (is_array($item) && (int) ($item['package_id'] ?? 0) > 0) {
$package_ids[] = (int) $item['package_id'];
}
}
}
$points_exchange = is_array($catalog['points_exchange'] ?? null) ? $catalog['points_exchange'] : [];
$methods = is_array($catalog['payment_methods'] ?? null) ? $catalog['payment_methods'] : [];
// 支付方式 logo(与充值/积分弹窗的 payment-method-radio 组件同源)。
$methods = array_map(function ($m) {
$id = sanitize_key((string) ($m['id'] ?? ''));
return [
'id' => $id,
'name' => (string) ($m['name'] ?? $id),
'logo' => $this->payment_method_logo_url($id),
];
}, $methods);
$card_pass = is_array($catalog['card_pass'] ?? null)
? array_merge(['enabled' => false, 'desc' => '', 'only_password' => false], $catalog['card_pass'])
: ['enabled' => false, 'desc' => '', 'only_password' => false];
$no_payment_method = $methods === [];
$action_reasons = [
'purchase' => $no_payment_method
? 'no_payment_method'
: ($package_ids === [] ? 'no_package' : ''),
'renew' => $no_payment_method
? 'no_payment_method'
: ($has_renew ? '' : 'not_available'),
'upgrade' => $no_payment_method
? 'no_payment_method'
: ($has_upgrade ? '' : 'not_available'),
'points_exchange' => !empty($points_exchange['enabled']) && !empty($points_exchange['packages'])
? ''
: 'not_available',
];
$member_state = (string) ($member['state'] ?? 'unopened');
$member_version = substr(hash('sha256', (string) wp_json_encode([
'level' => (int) ($member['level'] ?? 0),
'expires' => (int) ($member['expires'] ?? 0),
'state' => $member_state,
])), 0, 16);
$catalog_version = substr(hash('sha256', (string) wp_json_encode([
'packages' => $groups,
'points' => $points_exchange,
'methods' => $methods,
'levels' => $level_view,
])), 0, 16);
$user = get_userdata($user_id);
$avatar_url = function_exists('get_avatar_url')
? (string) get_avatar_url($user_id, ['size' => 42])
: '';
return [
'enabled' => !empty($catalog['enabled']),
'prefer_level' => $prefer_level,
'catalog' => [
'desc' => (string) ($catalog['desc'] ?? ''),
'more' => (string) ($vip['more'] ?? ''),
'currency_name' => (string) ($catalog['currency_name'] ?? ''),
'level_packages' => $groups,
'points_exchange' => $points_exchange,
'wallet_balance' => (string) ($catalog['wallet_balance'] ?? '0.00'),
],
'member' => [
'id' => $user_id,
'name' => $user ? (string) $user->display_name : '',
'avatar_url' => $avatar_url,
'level' => (int) ($member['level'] ?? 0),
'expires' => (int) ($member['expires'] ?? 0),
'expires_label' => (string) ($member['expires_label'] ?? ''),
'state' => (string) ($member['state'] ?? 'unopened'),
'is_permanent' => !empty($member['is_permanent']),
'logged_in' => $user_id > 0,
],
'levels' => $level_view,
'payment_methods' => $methods,
'card_pass' => $card_pass,
'currency_mark' => (string) (\Jio\Settings\Settings::get_payment_gateway_settings()['currency']['mark'] ?? '¥'),
'allowed_actions' => [
'purchase' => $package_ids !== [] && $methods !== [],
'renew' => $has_renew && $methods !== [],
'upgrade' => $has_upgrade && $methods !== [],
'points_exchange' => !empty($points_exchange['enabled']) && !empty($points_exchange['packages']),
],
'action_reasons' => $action_reasons,
'version' => [
'schema' => 1,
'member' => $member_version,
'catalog' => $catalog_version,
// Stable revision token; do not use request time and bust short cache.
'updated_at' => $member_version . ':' . $catalog_version,
'tags' => ['vip', 'orders'],
],
];
}
/**
* 积分兑换会员弹窗。
*
* 服务端静态输出刷新式弹窗外壳:
* .modal.flex.jc.fade > .modal-dialog.modal-mini.full-sm
* > .modal-content > .modal-body
* 与余额充值/购买积分弹窗同构(静止态由 vip-modal.css 显式隐藏,打开态由
* pay-vip.js 的自包含开关以内联 display:flex 驱动)。
*
* 入口有两处(均带 data-jio-vip-points-open,pay-vip.js 文档级委托开合):
* 会员购买弹窗内的「积分兑换会员」按钮,以及 /user/vip/ 档位购买卡下方的
* 入口横条(jio_get_vip_points_exchange_entry_html)。.pay-vip 触发点
* 全站可见,因此由 jio_render_vip_points_exchange_modal() 在 wp_footer
* 全局输出一次。
*
* @return string 空串表示积分兑换不可用(不输出弹窗壳)。
*/
public function get_points_exchange_modal_html(int $user_id = 0): string {
$user_id = max(0, $user_id);
if ($user_id <= 0 || !class_exists('\Jio\Settings\Settings')) {
return '';
}
// 先做零成本门控,避免每次页面渲染都去查积分余额。
$vip = $this->get_vip_settings_context();
if (empty($vip['enabled']) || empty($vip['points_exchange']['enabled']) || !\Jio\Settings\Settings::credits_enabled()) {
return '';
}
$catalog = $this->get_catalog($user_id);
$points_ex = is_array($catalog['points_exchange'] ?? null) ? $catalog['points_exchange'] : [];
$packages = is_array($points_ex['packages'] ?? null) ? $points_ex['packages'] : [];
if ($packages === []) {
return '';
}
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
$user_points = max(0, (int) ($catalog['user_points'] ?? 0));
$currency = (string) ($catalog['currency_name'] ?? __('积分', 'jio'));
$points_center_url = function_exists('jio_get_user_center_tab_url')
? jio_get_user_center_tab_url('credits')
: home_url('/');
// 会员卡片(参考主题 data-for/data-value 选中模型;data-value 为本主题的套餐 ID)。
$cards = '';
$min_points = 0;
$first_package_id = 0;
foreach ($packages as $index => $package) {
if (!is_array($package)) {
continue;
}
$package_id = max(0, (int) ($package['package_id'] ?? 0));
if ($package_id <= 0) {
continue;
}
if ($first_package_id === 0) {
$first_package_id = $package_id;
}
$level = max(1, min(2, (int) ($package['level'] ?? 1)));
$points = max(0, (int) ($package['points'] ?? 0));
// 参考主题时长行:30<span class="px12">天</span> / 永久;time 为 Permanent 时按永久渲染。
$is_permanent = !empty($package['is_permanent']);
$pkg_time = max(0, (int) ($package['time'] ?? 0));
$pkg_unit = ((string) ($package['unit'] ?? 'day')) === 'month' ? 'month' : 'day';
$duration = ($is_permanent || $pkg_time <= 0)
? esc_html__('永久', 'jio')
: (string) $pkg_time . '<span class="px12">' . esc_html($pkg_unit === 'month' ? __('个月', 'jio') : __('天', 'jio')) . '</span>';
$level_name = (string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level));
$cards .= '<div class="vip-card vip-theme' . $level . ($index === 0 ? ' active' : '') . ' " data-for="product_id" data-value="' . $package_id . '">'
. '<span class=" ">' . $this->points_exchange_card_icon($levels[$level] ?? []) . wp_kses_post($level_name) . '</span>'
. '<div class="flex jsb ac hh em16 mt10">'
. '<span class="shrink0 mr10">' . $duration . '</span>'
. '<span class="shrink0">' . $points . '<span class="px12">' . esc_html($currency) . '</span></span>'
. '</div></div>';
if ($min_points === 0 || $points < $min_points) {
$min_points = $points;
}
}
if ($cards === '') {
return '';
}
$desc_text = trim((string) ($vip['points_exchange']['desc'] ?? ''));
$desc = $desc_text !== ''
// 管理员编辑的富文本说明:保存端已 wp_kses_post,这里同级别输出
// (原样拼接)。
? '<div class="muted-box muted-2-color padding-10 em09 mt20 jio-vip-points-desc">' . wp_kses_post($desc_text) . '</div>'
: '';
// 最低价套餐仍超出用户积分 → badg c-red btn-block 提示。
if ($min_points > $user_points) {
$form = '<div class="mt20"><div class="badg c-red btn-block">'
. esc_html(sprintf(__('%s不足,暂时无法兑换', 'jio'), $currency))
. '</div></div>';
} else {
$form = '<div class="mt20">'
. '<input type="hidden" name="product_id" value="' . $first_package_id . '">'
. '<input type="hidden" name="order_type" value="4">'
. '<input type="hidden" name="action" value="points_initiate_pay">'
. '<button type="submit" class="mt6 but jb-yellow btn-block radius padding-lg" data-jio-vip-points-submit>'
. '<i class="fa fas fa-check" aria-hidden="true"></i>' . esc_html__('立即兑换', 'jio')
. '</button></div>';
}
$header = '<div class="mb10 touch">'
. '<button type="button" class="close" data-dismiss="modal" aria-label="' . esc_attr__('关闭', 'jio') . '">'
. '<svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg></button>'
. '<b class="modal-title flex ac"><span class="mr6 em14">'
. '<svg class="icon" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg></span>'
. esc_html(sprintf(__('%s兑换会员', 'jio'), $currency)) . '</b></div>';
$user_box = '<div class="muted-box padding-h10 mb20">'
. '<a rel="nofollow" class="flex jsb ac" href="' . esc_url($points_center_url) . '">'
. '<span class="muted-2-color">'
. '<svg class="icon" aria-hidden="true"><use xlink:href="#icon-points"></use></svg> '
. esc_html(sprintf(__('我的%s', 'jio'), $currency)) . '</span>'
. '<span class="font-bold ml6 em14"><span class="c-green mr10">' . $user_points . '</span>'
. '<i class="fa fas fa-angle-right" aria-hidden="true"></i></span></a></div>';
$lists = '<div class="muted-2-color mb6 em09">' . esc_html__('请选择需兑换的会员', 'jio') . '</div>'
. '<div class="flex hh exchange-card-box">' . $cards . '</div>';
return '<div class="modal flex jc fade" id="jio-vip-points-modal" tabindex="-1" role="dialog" aria-hidden="false" data-jio-vip-points-modal>'
. '<div class="modal-dialog modal-mini full-sm" role="document">'
. '<div class="modal-content"><div class="modal-body">'
. $header
. '<form class="" data-jio-vip-points-form>' . $user_box . $lists . $desc . $form . '</form>'
. '</div></div></div></div>';
}
/**
* 积分兑换卡片等级图标。
*
* @param array<string,mixed> $level_settings 等级设置(含 name / img_icon)。
*/
private function points_exchange_card_icon(array $level_settings): string {
$name_text = (string) ($level_settings['name'] ?? '');
$icon_url = (string) ($level_settings['img_icon'] ?? '');
if ($icon_url === '') {
return '<i class="fa fas fa-certificate mr3" aria-hidden="true"></i>';
}
return '<img' . jio_get_lazy_attr('lazy_other', $icon_url, 'img-icon mr3')
. ' title="' . esc_attr($name_text) . '" alt="' . esc_attr($name_text) . '">';
}
/**
* 支付方式 logo 映射(assets/img/pay-*-logo.svg,与充值/积分弹窗一致)。
*/
private function payment_method_logo_url(string $id): string {
$logos = [
'balance' => 'pay-balance-logo.svg',
'alipay' => 'pay-alipay-logo.svg',
'epay_alipay' => 'pay-alipay-logo.svg',
'wechatpay' => 'pay-wechat-logo.svg',
'epay_wxpay' => 'pay-wechat-logo.svg',
'paypal' => 'pay-paypal-logo.svg',
'stripe' => 'pay-stripe-logo.svg',
'card_pass' => 'pay-card-pass-logo.svg',
];
$file = $logos[$id] ?? '';
return $file === '' ? '' : esc_url_raw(get_template_directory_uri() . '/assets/img/' . $file);
}
/**
* Mini hover card for header (legacy jiopay_get_vip_card).
*/
public function render_vip_card(int $level, int $user_id = 0): string {
$level = max(1, min(2, $level));
$vip = $this->get_vip_settings_context();
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
if (empty($levels[$level]['enabled'])) {
return '';
}
$name = wp_kses_post((string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level)));
$name_text = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
$equity = (string) ($levels[$level]['equity'] ?? '');
$cls = $user_id > 0 ? ' pay-vip' : ' signin-loader';
$equity_html = $equity !== '' && function_exists('jio_format_vip_equity_html')
? jio_format_vip_equity_html($equity, 'mb10 relative')
: ($equity !== '' ? '<div class="v-equity mb10 relative">' . wp_kses_post($equity) . '</div>' : '');
$icon_url = esc_url((string) ($levels[$level]['img_icon'] ?? ''));
$icon = $icon_url !== ''
? '<img' . jio_get_lazy_attr('lazy_other', $icon_url, 'img-icon') . ' title="' . esc_attr($name_text) . '" alt="' . esc_attr($name_text) . '" decoding="async">'
: '<i class="fa fas fa-gem" aria-hidden="true"></i>';
$html = '<div class="vip-card pointer level-' . $level . ' vip-theme' . $level . $cls . '" vip-level="' . $level . '">';
$html .= '<div class="abs-center vip-baicon" aria-hidden="true">' . $icon . '</div>';
$html .= '<div class="vip-img abs-right">' . $icon . '</div>';
$html .= '<div class="relative">';
$html .= '<div class="vip-name mb10"><span class="mr6">' . $icon . '</span>' . wp_kses_post($name) . '</div>';
$html .= $equity_html;
$html .= '<a class="but jb-blue radius payvip-button" href="javascript:;">'
. wp_kses_post(sprintf(__('开通%s', 'jio'), $name))
. '<span class="ml6"><i class="fa fas fa-chevron-circle-right" aria-hidden="true"></i></span></a>';
$html .= '</div></div>';
return $html;
}
}