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>
577 lines
22 KiB
PHP
577 lines
22 KiB
PHP
<?php
|
||
/**
|
||
* Jio VIP Helper — VIP 会员等级与展示辅助函数
|
||
*
|
||
* @package Jio
|
||
*/
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
/**
|
||
* Resolve the complete VIP membership state for a user.
|
||
*
|
||
* Explicit membership meta is authoritative, including an explicit level 0.
|
||
* Order fallback is only for users without a canonical membership level.
|
||
*
|
||
* @return array{level:int,expires:int,state:string,expired_level:int,source:string,has_explicit_state:bool,is_permanent:bool,current_level:int,current_expires:int,current_state:string,fallback_level:int,fallback_expires:int,fallback_state:string,has_fallback:bool,is_layered_upgrade:bool}
|
||
*/
|
||
function jio_get_user_vip_state(int $user_id = 0): array {
|
||
$empty = [
|
||
'level' => 0,
|
||
'expires' => 0,
|
||
'state' => 'unopened',
|
||
'expired_level' => 0,
|
||
'source' => 'none',
|
||
'has_explicit_state' => false,
|
||
'is_permanent' => false,
|
||
'current_level' => 0,
|
||
'current_expires' => 0,
|
||
'current_state' => 'unopened',
|
||
'fallback_level' => 0,
|
||
'fallback_expires' => 0,
|
||
'fallback_state' => 'none',
|
||
'has_fallback' => false,
|
||
'is_layered_upgrade' => false,
|
||
];
|
||
if ($user_id <= 0) {
|
||
$user_id = get_current_user_id();
|
||
}
|
||
if ($user_id <= 0 || !class_exists('\Jio\Settings\Settings')) {
|
||
return $empty;
|
||
}
|
||
$vip = \Jio\Settings\Settings::get_payment_vip_settings();
|
||
if (empty($vip['enabled'])) {
|
||
return $empty;
|
||
}
|
||
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
|
||
$vip1_enabled = !empty($levels[1]['enabled']);
|
||
$vip2_enabled = !empty($levels[2]['enabled']);
|
||
$has_explicit = metadata_exists('user', $user_id, 'jio_vip_level');
|
||
$level = min(2, max(0, (int) get_user_meta($user_id, 'jio_vip_level', true)));
|
||
$expires = max(0, (int) get_user_meta($user_id, 'jio_vip_expires', true));
|
||
$expired_level = min(2, max(0, (int) get_user_meta($user_id, 'jio_vip_level_expired', true)));
|
||
|
||
if ($has_explicit) {
|
||
if ($level > 0 && $expires > 0 && $expires <= time()) {
|
||
if (class_exists('\\Jio\\Content\\VipMembershipExpiry')) {
|
||
\Jio\Content\VipMembershipExpiry::process_user($user_id, 'resolver');
|
||
$level = min(2, max(0, (int) get_user_meta($user_id, 'jio_vip_level', true)));
|
||
$expires = max(0, (int) get_user_meta($user_id, 'jio_vip_expires', true));
|
||
} else {
|
||
$base_level = min(2, max(0, (int) get_user_meta($user_id, 'jio_vip_base_level', true)));
|
||
$base_expires = max(0, (int) get_user_meta($user_id, 'jio_vip_base_expires', true));
|
||
if ($base_level > 0 && ($base_expires === 0 || $base_expires > time())) {
|
||
update_user_meta($user_id, 'jio_vip_level', $base_level);
|
||
update_user_meta($user_id, 'jio_vip_expires', $base_expires);
|
||
delete_user_meta($user_id, 'jio_vip_base_level');
|
||
delete_user_meta($user_id, 'jio_vip_base_expires');
|
||
$level = $base_level;
|
||
$expires = $base_expires;
|
||
$expired_level = 0;
|
||
} else {
|
||
$expired_level = max($expired_level, $level);
|
||
update_user_meta($user_id, 'jio_vip_level_expired', $expired_level);
|
||
update_user_meta($user_id, 'jio_vip_level', 0);
|
||
update_user_meta($user_id, 'jio_vip_expires', 0);
|
||
delete_user_meta($user_id, 'jio_vip_base_level');
|
||
delete_user_meta($user_id, 'jio_vip_base_expires');
|
||
$level = 0;
|
||
$expires = 0;
|
||
}
|
||
}
|
||
}
|
||
$fallback_level = min(2, max(0, (int) get_user_meta($user_id, 'jio_vip_base_level', true)));
|
||
$fallback_expires = max(0, (int) get_user_meta($user_id, 'jio_vip_base_expires', true));
|
||
if ($level > 0) {
|
||
$level = $level >= 2 && $vip2_enabled ? 2 : ($vip1_enabled ? 1 : 0);
|
||
$current_state = $expires > 0 ? 'active' : 'permanent';
|
||
$fallback_state = $fallback_level > 0 ? ($fallback_expires > 0 ? 'active' : 'permanent') : 'none';
|
||
return [
|
||
'level' => $level,
|
||
'expires' => $expires,
|
||
'state' => $current_state,
|
||
'expired_level' => 0,
|
||
'source' => 'meta',
|
||
'has_explicit_state' => true,
|
||
'is_permanent' => $expires <= 0,
|
||
'current_level' => $level,
|
||
'current_expires' => $expires,
|
||
'current_state' => $current_state,
|
||
'fallback_level' => $fallback_level,
|
||
'fallback_expires' => $fallback_expires,
|
||
'fallback_state' => $fallback_state,
|
||
'has_fallback' => $fallback_level > 0,
|
||
'is_layered_upgrade' => $fallback_level > 0 && $fallback_level < $level,
|
||
];
|
||
}
|
||
return array_merge($empty, [
|
||
'state' => $expired_level > 0 ? 'expired' : 'unopened',
|
||
'expired_level' => $expired_level,
|
||
'source' => 'meta',
|
||
'has_explicit_state' => true,
|
||
'current_state' => $expired_level > 0 ? 'expired' : 'unopened',
|
||
]);
|
||
}
|
||
|
||
$paid = \Jio\Settings\Settings::get_paid_content_settings();
|
||
$user = get_userdata($user_id);
|
||
if ($user && array_intersect((array) $user->roles, (array) ($paid['vip_exempt_roles'] ?? []))) {
|
||
$level = $vip2_enabled ? 2 : ($vip1_enabled ? 1 : 0);
|
||
return array_merge($empty, [
|
||
'level' => $level,
|
||
'state' => $level > 0 ? 'permanent' : 'unopened',
|
||
'source' => 'exempt',
|
||
'is_permanent' => $level > 0,
|
||
'current_level' => $level,
|
||
'current_state' => $level > 0 ? 'permanent' : 'unopened',
|
||
]);
|
||
}
|
||
|
||
if (class_exists('\Jio\Payment\Order')) {
|
||
$orders = \Jio\Payment\Order::get_instance();
|
||
$order = method_exists($orders, 'get_valid_vip_order') ? $orders->get_valid_vip_order($user_id) : null;
|
||
if (is_array($order) && $order !== []) {
|
||
$order_meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
||
if ($order_meta === [] && is_string($order['note'] ?? null)) {
|
||
$decoded = json_decode($order['note'], true);
|
||
$order_meta = is_array($decoded) ? $decoded : [];
|
||
}
|
||
$level = (int) ($order_meta['level'] ?? 0);
|
||
$product_id = (int) ($order['product_id'] ?? 0);
|
||
if ($level <= 0 && $product_id > 0) {
|
||
$level = $product_id >= 1000 ? (int) floor($product_id / 1000) : 1;
|
||
}
|
||
$level = min(2, max(1, $level));
|
||
$level = $level >= 2 && $vip2_enabled ? 2 : ($vip1_enabled ? 1 : 0);
|
||
$order_expires = max(0, (int) ($order_meta['expires'] ?? $order_meta['vip_expires'] ?? 0));
|
||
return array_merge($empty, [
|
||
'level' => $level,
|
||
'expires' => $order_expires,
|
||
'state' => $level > 0 ? 'active' : 'unopened',
|
||
'source' => 'order',
|
||
'current_level' => $level,
|
||
'current_expires' => $order_expires,
|
||
'current_state' => $level > 0 ? 'active' : 'unopened',
|
||
]);
|
||
}
|
||
}
|
||
return $empty;
|
||
}
|
||
|
||
/**
|
||
* @param int $user_id User ID, or current user when zero.
|
||
* @return int 0|1|2
|
||
*/
|
||
function jio_get_user_vip_level(int $user_id = 0): int {
|
||
return (int) jio_get_user_vip_state($user_id)['level'];
|
||
}
|
||
|
||
/**
|
||
* 检查用户是否为 VIP(level >= 1)。
|
||
*
|
||
* @param int $user_id
|
||
* @return bool
|
||
*/
|
||
function jio_is_vip(int $user_id = 0): bool {
|
||
return jio_get_user_vip_level($user_id) >= 1;
|
||
}
|
||
|
||
/**
|
||
* 检查用户是否为 VIP2(level >= 2)。
|
||
*
|
||
* @param int $user_id
|
||
* @return bool
|
||
*/
|
||
function jio_is_vip2(int $user_id = 0): bool {
|
||
return jio_get_user_vip_level($user_id) >= 2;
|
||
}
|
||
|
||
/**
|
||
* 获取 VIP 徽标文本
|
||
*
|
||
* @return string
|
||
*/
|
||
function jio_get_vip_badge_text(int $user_id = 0): string {
|
||
if (!class_exists('\Jio\Settings\Settings')) {
|
||
return 'VIP';
|
||
}
|
||
|
||
$vip_settings = \Jio\Settings\Settings::get_payment_vip_settings();
|
||
$level = $user_id > 0 ? jio_get_user_vip_level($user_id) : 0;
|
||
if ($level >= 1 && !empty($vip_settings['levels'][$level]['name'])) {
|
||
return sanitize_text_field((string) $vip_settings['levels'][$level]['name']);
|
||
}
|
||
return sanitize_text_field($vip_settings['badge_text'] ?? 'VIP');
|
||
}
|
||
|
||
/**
|
||
* Daily free-download limit for a user (0 = unlimited).
|
||
* Aligns with legacy pay_download_limit / pay_download_limit_vip_{n}.
|
||
*/
|
||
if (!function_exists('jio_get_user_free_download_limit')) {
|
||
function jio_get_user_free_download_limit(int $user_id = 0): int {
|
||
if ($user_id <= 0) {
|
||
$user_id = get_current_user_id();
|
||
}
|
||
if ($user_id <= 0 || !class_exists('\Jio\Settings\Settings')) {
|
||
return 0;
|
||
}
|
||
|
||
$vip = \Jio\Settings\Settings::get_payment_vip_settings();
|
||
$level = function_exists('jio_get_user_vip_level') ? jio_get_user_vip_level($user_id) : 0;
|
||
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
|
||
$benefit = is_array($vip['benefit'] ?? null) ? $vip['benefit'] : [];
|
||
|
||
if ($level >= 1 && !empty($levels[$level]['enabled'])) {
|
||
return max(0, (int) ($levels[$level]['download_limit'] ?? 0));
|
||
}
|
||
|
||
return max(0, (int) ($benefit['download_limit'] ?? 0));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Count free downloads used today for a user (legacy pay_down_number).
|
||
*/
|
||
if (!function_exists('jio_get_user_free_downloaded_number')) {
|
||
function jio_get_user_free_downloaded_number(int $user_id = 0): int {
|
||
if ($user_id <= 0) {
|
||
$user_id = get_current_user_id();
|
||
}
|
||
if ($user_id <= 0) {
|
||
return 0;
|
||
}
|
||
|
||
$day = current_time('Y-m-d');
|
||
$mate = get_user_meta($user_id, 'jio_pay_down_number', true);
|
||
if (!is_array($mate) || empty($mate[$day]) || !is_array($mate[$day])) {
|
||
return 0;
|
||
}
|
||
return count($mate[$day]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Record a free download for daily quota (legacy pay_down_number).
|
||
*/
|
||
if (!function_exists('jio_record_user_free_download')) {
|
||
function jio_record_user_free_download(int $user_id, int $post_id): void {
|
||
if ($user_id <= 0 || $post_id <= 0) {
|
||
return;
|
||
}
|
||
$day = current_time('Y-m-d');
|
||
$mate = get_user_meta($user_id, 'jio_pay_down_number', true);
|
||
if (!is_array($mate)) {
|
||
$mate = [];
|
||
}
|
||
// Drop old days to keep meta small.
|
||
foreach (array_keys($mate) as $k) {
|
||
if ($k !== $day) {
|
||
unset($mate[$k]);
|
||
}
|
||
}
|
||
if (!isset($mate[$day]) || !is_array($mate[$day])) {
|
||
$mate[$day] = [];
|
||
}
|
||
$mate[$day][(string) $post_id] = 1;
|
||
update_user_meta($user_id, 'jio_pay_down_number', $mate);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Whether a free download is allowed under daily quota.
|
||
* Paid downloads (cost > 0) always pass.
|
||
*
|
||
* @return true|WP_Error
|
||
*/
|
||
if (!function_exists('jio_check_free_download_quota')) {
|
||
function jio_check_free_download_quota(int $user_id, int $cost = 0) {
|
||
if ($cost > 0) {
|
||
return true;
|
||
}
|
||
if ($user_id <= 0) {
|
||
return true; // login gate handled elsewhere
|
||
}
|
||
|
||
$limit = jio_get_user_free_download_limit($user_id);
|
||
if ($limit <= 0) {
|
||
return true; // unlimited
|
||
}
|
||
|
||
$used = jio_get_user_free_downloaded_number($user_id);
|
||
if ($used >= $limit) {
|
||
$data = [
|
||
'status' => 429,
|
||
'limit' => $limit,
|
||
'used' => $used,
|
||
];
|
||
// Optional overbuy price for free-quota exhausted (legacy download_limit_over_price).
|
||
// Callers may pass post context via filter.
|
||
$over = apply_filters('jio_download_limit_over_price', 0, $user_id);
|
||
if ($over > 0) {
|
||
$data['download_limit_over_price'] = $over;
|
||
$data['allow_limit_over'] = true;
|
||
}
|
||
return new WP_Error(
|
||
'jio_download_limit',
|
||
__('您今日下载免费资源个数已超限,请明日再下载。', 'jio'),
|
||
$data
|
||
);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Remaining free downloads today (null = unlimited).
|
||
*/
|
||
if (!function_exists('jio_get_free_download_surplus')) {
|
||
function jio_get_free_download_surplus(int $user_id = 0): ?int {
|
||
if ($user_id <= 0) {
|
||
$user_id = get_current_user_id();
|
||
}
|
||
$limit = jio_get_user_free_download_limit($user_id);
|
||
if ($limit <= 0) {
|
||
return null;
|
||
}
|
||
$used = jio_get_user_free_downloaded_number($user_id);
|
||
return max(0, $limit - $used);
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Render the configured VIP icon with the frontend identity-image contract.
|
||
*
|
||
* @param int $user_id User ID.
|
||
* @param string $class Additional image classes.
|
||
* @param bool $tooltip Whether to enable the tooltip trigger.
|
||
*/
|
||
function jio_get_vip_icon(int $user_id = 0, string $class = 'em12 ml3', bool $tooltip = true): string {
|
||
$level = jio_get_user_vip_level($user_id);
|
||
if ($level <= 0 || !class_exists('\Jio\Settings\Settings')) {
|
||
return '';
|
||
}
|
||
|
||
$vip_settings = \Jio\Settings\Settings::get_payment_vip_settings();
|
||
$icon_url = esc_url((string) ($vip_settings['levels'][$level]['img_icon'] ?? ''));
|
||
if ($icon_url === '') {
|
||
return '';
|
||
}
|
||
|
||
$name = sanitize_text_field((string) ($vip_settings['levels'][$level]['name'] ?? jio_get_vip_badge_text($user_id)));
|
||
$tooltip_attr = $tooltip ? ' data-toggle="tooltip"' : '';
|
||
|
||
return '<img' . jio_get_lazy_attr('lazy_other', $icon_url, trim('img-icon ' . $class), jio_get_lazy_thumb())
|
||
. $tooltip_attr . ' title="' . esc_attr($name) . '" alt="' . esc_attr($name) . '">';
|
||
}
|
||
|
||
/**
|
||
* 获取用户 VIP 等级信息(用于展示)
|
||
*
|
||
* @param int $user_id
|
||
* @return array{is_vip: bool, level: int, badge_text: string, badge_html: string, name: string, expires: int, expires_label: string}
|
||
*/
|
||
function jio_get_vip_info(int $user_id = 0): array {
|
||
$state = function_exists('jio_get_user_vip_state') ? jio_get_user_vip_state($user_id) : [];
|
||
$level = (int) ($state['level'] ?? 0);
|
||
$is_vip = $level >= 1;
|
||
$badge_text = jio_get_vip_badge_text($user_id);
|
||
$expires = max(0, (int) ($state['expires'] ?? 0));
|
||
$expires_label = __('未开通', 'jio');
|
||
if (($state['state'] ?? '') === 'expired') {
|
||
$expires_label = __('已过期', 'jio');
|
||
} elseif ($is_vip && $expires <= 0 && ($state['source'] ?? 'none') !== 'order') {
|
||
$expires_label = __('永久有效', 'jio');
|
||
} elseif ($is_vip) {
|
||
$expires_label = sprintf(__('到期时间:%s', 'jio'), wp_date('Y-m-d H:i', $expires));
|
||
}
|
||
|
||
$name = __('普通用户', 'jio');
|
||
if ($is_vip && class_exists('\Jio\Settings\Settings')) {
|
||
$vip = \Jio\Settings\Settings::get_payment_vip_settings();
|
||
$name = (string) ($vip['levels'][$level]['name'] ?? ($badge_text . __('会员', 'jio')));
|
||
} elseif ($is_vip) {
|
||
$name = $badge_text . __('会员', 'jio');
|
||
}
|
||
|
||
return [
|
||
'is_vip' => $is_vip,
|
||
'level' => $level,
|
||
'badge_text' => $badge_text,
|
||
'badge_html' => $is_vip ? jio_get_vip_icon($user_id) : '',
|
||
'name' => $name,
|
||
'expires' => $expires,
|
||
'expires_label' => $expires_label,
|
||
'state' => (string) ($state['state'] ?? 'unopened'),
|
||
'source' => (string) ($state['source'] ?? 'none'),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* VIP 开通页入口(用户中心 credits 面板)。
|
||
*/
|
||
if (!function_exists('jio_get_vip_purchase_url')) {
|
||
function jio_get_vip_purchase_url(): string {
|
||
if (function_exists('jio_get_user_center_tab_url')) {
|
||
return jio_get_user_center_tab_url('vip');
|
||
}
|
||
if (function_exists('jio_get_user_center_url')) {
|
||
return jio_get_user_center_url('vip');
|
||
}
|
||
return home_url('/');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 用户中心 /user/vip/ 页面的积分兑换会员入口。
|
||
*
|
||
* 在「我的会员」摘要与两张档位购买卡之后输出全宽入口横条:
|
||
* .c-yellow.jio-widget-box.flex.ac.jsb(jio-widget-box → 本主题 jio-panel 表面)
|
||
* ├─ .flex1.em12:points-color 图标 +「{自定义货币名}兑换会员」
|
||
* └─ .badg.radius.c-yellow:「立即兑换」+ fa-chevron-circle-right
|
||
* 门控:已是会员不展示($vip_level ? '' : link),且要求积分兑换会员开启、
|
||
* 至少一个会员等级开启并存在可兑换套餐(catalog.points_exchange 已内含
|
||
* credits 门控)。
|
||
*
|
||
* 入口文案的货币名取 catalog.currency_name(后台「积分配置 → 自定义货币名称」),
|
||
* 与它打开的兑换弹窗标题、会员弹窗内的入口保持同一命名。
|
||
*
|
||
* 入口点击经 data-jio-vip-points-open 由 pay-vip.js 打开 wp_footer 输出的
|
||
* 静态兑换弹窗(jio_render_vip_points_exchange_modal),与会员购买弹窗内
|
||
* 的入口共用同一弹窗与开合开关。
|
||
*
|
||
* @param int $user_id 当前用户 ID(<=0 返回空串)。
|
||
* @param int $vip_level 用户当前会员等级(>0 时返回空串;缺省 -1 表示自行解析)。
|
||
* @return string 空串表示不满足展示条件。
|
||
*/
|
||
if (!function_exists('jio_get_vip_points_exchange_entry_html')) :
|
||
function jio_get_vip_points_exchange_entry_html(int $user_id, int $vip_level = -1): string {
|
||
if ($user_id <= 0 || !class_exists('\Jio\Content\VipService')) {
|
||
return '';
|
||
}
|
||
if ($vip_level < 0) {
|
||
$vip_level = function_exists('jio_get_user_vip_level') ? jio_get_user_vip_level($user_id) : 0;
|
||
}
|
||
// 参考主题契约:已经是会员就不再展示积分兑换入口。
|
||
if ($vip_level > 0) {
|
||
return '';
|
||
}
|
||
|
||
$catalog = \Jio\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 '';
|
||
}
|
||
|
||
// 入口文案跟随自定义货币名称(后台「积分配置 → 自定义货币名称」),与它打开的
|
||
// 兑换弹窗标题(VipServiceModal)和会员弹窗内的入口(pay-vip.js)保持同一命名。
|
||
$currency = (string) ($catalog['currency_name'] ?? '');
|
||
if ($currency === '') {
|
||
$currency = class_exists('\Jio\Settings\Settings')
|
||
? \Jio\Settings\Settings::get_currency_name()
|
||
: __('积分', 'jio');
|
||
}
|
||
|
||
return '<a class="uc-vip-points-entry flex ac jsb" href="javascript:;" data-jio-vip-points-open>'
|
||
. '<span class="flex1 em12 c-yellow">'
|
||
. '<svg class="icon mr6 em12" aria-hidden="true"><use xlink:href="#icon-points-color"></use></svg>'
|
||
. esc_html(sprintf(__('%s兑换会员', 'jio'), $currency))
|
||
. '</span>'
|
||
. '<span class="badg radius c-yellow">' . esc_html__('立即兑换', 'jio')
|
||
. '<i class="fa fas fa-chevron-circle-right ml6" aria-hidden="true"></i></span></a>';
|
||
}
|
||
endif;
|
||
|
||
/**
|
||
* Format VIP equity HTML for front-end display.
|
||
*
|
||
* Admin field is free-form HTML (typically one or more <li>). Do NOT
|
||
* re-parse / re-split the content into synthetic list items by punctuation
|
||
* or Chinese keyword heuristics — that mangles intentional HTML.
|
||
*
|
||
* Rules:
|
||
* - Sanitize with WordPress post-safe HTML.
|
||
* - If markup already has <li>, wrap the items in <ul class="v-equity …">.
|
||
* - Otherwise output the sanitized HTML inside <div class="v-equity …">.
|
||
* - Plain multi-line text: each non-empty line becomes one <li> (only \n split).
|
||
* - Empty input → empty string.
|
||
*
|
||
* @param string $raw Equity HTML/plain from settings.
|
||
* @param string $class Extra classes on the wrapper (e.g. "mb10 relative").
|
||
*/
|
||
if (!function_exists('jio_format_vip_equity_html')) {
|
||
function jio_format_vip_equity_html(string $raw, string $class = ''): string {
|
||
$raw = trim($raw);
|
||
if ($raw === '') {
|
||
return '';
|
||
}
|
||
|
||
$clean = trim(wp_kses_post($raw));
|
||
if ($clean === '') {
|
||
return '';
|
||
}
|
||
|
||
$class = trim('v-equity ' . $class);
|
||
$class_attr = esc_attr($class);
|
||
|
||
// Already list items — preserve them, only re-wrap.
|
||
if (preg_match('/<li[\s>]/i', $clean)) {
|
||
$inner = preg_replace('#</?(ul|ol)[^>]*>#i', '', $clean) ?? $clean;
|
||
$inner = trim($inner);
|
||
if ($inner === '') {
|
||
return '';
|
||
}
|
||
return '<ul class="' . $class_attr . '">' . $inner . '</ul>';
|
||
}
|
||
|
||
// Other HTML (p/br/a/…) — keep as-is, no list fabrication.
|
||
if (preg_match('/<[^>]+>/', $clean)) {
|
||
return '<div class="' . $class_attr . '">' . $clean . '</div>';
|
||
}
|
||
|
||
// Plain text only: split by newlines (author-authored line breaks), never
|
||
// by commas / Chinese keywords.
|
||
$lines = preg_split('/\R+/u', $clean) ?: [];
|
||
$lines = array_values(array_filter(array_map('trim', $lines), static fn($s) => $s !== ''));
|
||
if ($lines === []) {
|
||
return '';
|
||
}
|
||
$html = '<ul class="' . $class_attr . '">';
|
||
foreach ($lines as $line) {
|
||
$html .= '<li>' . esc_html($line) . '</li>';
|
||
}
|
||
$html .= '</ul>';
|
||
return $html;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 全局积分兑换会员弹窗(参考主题 refresh_modal 同构静态壳)。
|
||
*
|
||
* 入口有两处:会员购买弹窗(.pay-vip → assets/js/pay-vip.js)内的「积分兑换
|
||
* 会员」按钮,以及 /user/vip/ 档位购买卡下方的入口横条
|
||
* (jio_get_vip_points_exchange_entry_html);二者均带
|
||
* data-jio-vip-points-open,由 pay-vip.js 文档级委托打开本弹窗。.pay-vip
|
||
* 触发点全站可见,因此弹窗壳在满足条件时随 wp_footer 全局输出一次;显隐由
|
||
* pay-vip.js 自包含开关驱动,静止态由 vip-modal.css 的
|
||
* .modal[data-jio-vip-points-modal] 显式隐藏。
|
||
*/
|
||
if (!function_exists('jio_render_vip_points_exchange_modal')) :
|
||
function jio_render_vip_points_exchange_modal(): void {
|
||
if (!is_user_logged_in() || !class_exists('\Jio\Content\VipService')) {
|
||
return;
|
||
}
|
||
|
||
$html = \Jio\Content\VipService::get_instance()->get_points_exchange_modal_html(get_current_user_id());
|
||
if ($html === '') {
|
||
return;
|
||
}
|
||
|
||
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 各字段已在生成时转义。
|
||
}
|
||
endif;
|
||
add_action('wp_footer', 'jio_render_vip_points_exchange_modal');
|