fix(vip): render points exchange description

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-10-19 14:04:30 +08:00
1 parent 0d51beeaad
commit a120f61e13
7 files changed
+112 -4

No files matched your search

+21
View File
@@ -64,6 +64,27 @@
z-index: 100055;
}
/* public.css redeclares .muted-box after legacy utilities, overriding its
background, radius, and padding utilities. Restore the reference box contract. */
.modal[data-aksrt-vip-points-modal] .muted-box.padding-h10 {
background-color: var(--this-bg, var(--muted-border-color));
border-radius: var(--main-radius);
padding: 10px 15px;
}
.modal[data-aksrt-vip-points-modal] .muted-box.padding-10,
.modal[data-aksrt-vip-points-modal] .aksrt-vip-points-desc {
background-color: var(--this-bg, var(--muted-border-color));
border-radius: var(--main-radius);
padding: 10px;
}
.modal[data-aksrt-vip-points-modal] .aksrt-vip-points-desc {
display: block;
margin-top: 20px;
color: var(--muted-2-color, #999);
}
.pay-vip-form-row {
display: grid;
gap: 12px;
+2 -2
View File
@@ -239,11 +239,11 @@ trait VipServiceModal {
return '';
}
$desc_text = trim((string) ($points_ex['desc'] ?? ''));
$desc_text = trim((string) ($vip['points_exchange']['desc'] ?? ''));
$desc = $desc_text !== ''
// 管理员编辑的富文本说明:保存端已 wp_kses_post,这里同级别输出
// (参考主题 zibpay_get_vip_points_exchange_modal 原样拼接)。
? '<div class="muted-box muted-2-color padding-10 em09 mt20">' . wp_kses_post($desc_text) . '</div>'
? '<div class="muted-box muted-2-color padding-10 em09 mt20 aksrt-vip-points-desc">' . wp_kses_post($desc_text) . '</div>'
: '';
// 最低价套餐仍超出用户积分 → 参考主题的 badg c-red btn-block 提示。
@@ -224,7 +224,8 @@ test('积分兑换说明按 post 级 HTML 放行并原样渲染', () => {
assert.match(schemaPhp, /'key' => 'pay_vip_points_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'sanitize_type' => 'html'/);
const vipSettings = slice(paymentRuntime, 'public static function get_payment_vip_settings', 'public static function get_payment_revenue_settings');
assert.match(vipSettings, /'desc'\s+=> wp_kses_post\(\(string\) \(\$settings\['pay_vip_points_exchange_desc'\]/);
// 弹窗渲染端不再 esc_html 剥标签。
assert.match(modalSource, /<div class="muted-box muted-2-color padding-10 em09 mt20">' \. wp_kses_post\(\$desc_text\)/);
// 弹窗从同一份 VIP 设置上下文读取说明,不经 catalog 投影遗漏;说明容器保留参考主题的 HTML 与定位类。
assert.match(modalSource, /\$desc_text = trim\(\(string\) \(\$vip\['points_exchange'\]\['desc'\] \?\? ''\)\);/);
assert.match(modalSource, /<div class="muted-box muted-2-color padding-10 em09 mt20 aksrt-vip-points-desc">' \. wp_kses_post\(\$desc_text\)/);
assert.doesNotMatch(modalSource, /esc_html\(\$desc_text\)/);
});
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace {
define('ABSPATH', __DIR__ . '/');
function __(string $text, string $domain = ''): string { return $text; }
function esc_html($value): string { return (string) $value; }
function esc_html__($value, string $domain = ''): string { return (string) $value; }
function esc_attr($value): string { return (string) $value; }
function esc_attr__($value, string $domain = ''): string { return (string) $value; }
function esc_url($value): string { return (string) $value; }
function wp_kses_post($value): string { return (string) $value; }
function aksrt_get_user_center_tab_url(string $tab): string { return '/user/' . $tab; }
}
namespace AKSRT\Settings {
final class Settings {
public static function credits_enabled(): bool { return true; }
}
}
namespace {
require dirname(__DIR__, 2) . '/src/Content/VipServiceModal.php';
final class VipPointsDescriptionProbe {
use \AKSRT\Content\VipServiceModal;
/** @return array<string,mixed> */
private function get_vip_settings_context(): array {
return [
'enabled' => true,
'levels' => [1 => ['enabled' => true, 'name' => 'VIP1']],
'points_exchange' => [
'enabled' => true,
'desc' => '<p>后台配置的兑换说明</p>',
],
];
}
/** @return array<string,mixed> */
public function get_catalog(int $user_id = 0): array {
return [
'points_exchange' => [
'packages' => [[
'package_id' => 6001,
'level' => 1,
'points' => 500,
'time' => 30,
'unit' => 'day',
]],
],
'user_points' => 1000,
'currency_name' => '积分',
];
}
}
echo (new VipPointsDescriptionProbe())->get_points_exchange_modal_html(1);
}
@@ -22,6 +22,7 @@ const test = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const root = path.join(__dirname, '..', '..');
const read = (...seg) => fs.readFileSync(path.join(root, ...seg), 'utf8');
@@ -80,6 +81,28 @@ test('弹窗壳随 wp_footer 全局输出且不进入 catalog 载荷', () => {
assert.doesNotMatch(modal, /'vip_url'/);
});
test('我的积分盒恢复参考主题 muted-box 的背景、圆角与内边距', () => {
assert.match(vipCss, /\.modal\[data-aksrt-vip-points-modal\]\s+\.muted-box\.padding-h10\s*\{\s*background-color:\s*var\(--this-bg,\s*var\(--muted-border-color\)\);\s*border-radius:\s*var\(--main-radius\);\s*padding:\s*10px 15px;\s*\}/);
assert.match(vipCss, /\.modal\[data-aksrt-vip-points-modal\]\s+\.muted-box\.padding-10,\s*\.modal\[data-aksrt-vip-points-modal\]\s+\.aksrt-vip-points-desc\s*\{\s*background-color:\s*var\(--this-bg,\s*var\(--muted-border-color\)\);\s*border-radius:\s*var\(--main-radius\);\s*padding:\s*10px;\s*\}/);
});
test('后台兑换说明直接从 VIP 设置读取并作为富文本置于套餐与提交动作之间', () => {
assert.match(modal, /\$desc_text = trim\(\(string\) \(\$vip\['points_exchange'\]\['desc'\] \?\? ''\)\);/);
assert.match(modal, /aksrt-vip-points-desc/);
assert.match(modal, /wp_kses_post\(\$desc_text\)/);
assert.match(modal, /\. \$header\s*\. '<form class="" data-aksrt-vip-points-form>' \. \$user_box \. \$lists \. \$desc \. \$form/);
assert.match(vipCss, /\.modal\[data-aksrt-vip-points-modal\]\s+\.aksrt-vip-points-desc\s*\{[^}]*display:\s*block;[^}]*margin-top:\s*20px;[^}]*color:\s*var\(--muted-2-color,\s*#999\);/s);
assert.match(vipCss, /\.modal\[data-aksrt-vip-points-modal\]\s+\.muted-box\.padding-10,\s*\.modal\[data-aksrt-vip-points-modal\]\s+\.aksrt-vip-points-desc\s*\{[^}]*padding:\s*10px;/s);
});
test('兑换说明从后台设置实际输出,保留 HTML 并显示在卡片与 CTA 之间', () => {
const rendered = execFileSync(process.env.PHP_BINARY || 'php', [
path.join(__dirname, 'vip-points-exchange-description-harness.php'),
], { encoding: 'utf8' });
assert.match(rendered, /class="muted-box muted-2-color padding-10 em09 mt20 aksrt-vip-points-desc"><p>后台配置的兑换说明<\/p><\/div>/);
assert.ok(rendered.indexOf('aksrt-vip-points-desc') < rendered.indexOf('data-aksrt-vip-points-submit'));
});
test('弹窗壳静止态必须被 vip-modal.css 显式隐藏(.flex 覆盖回归护栏)', () => {
assert.match(vipCss, /\.modal\[data-aksrt-vip-points-modal\]\s*\{\s*display:\s*none;/);
assert.match(vipCss, /\.modal\[data-aksrt-vip-points-modal\]\.in\s*\{\s*display:\s*flex;/);
@@ -371,6 +371,7 @@ test('运行时归一化:默认值、价格净化、零价与非法行剔除',
assert.strictEqual(custom.upgrade.permanent_tag_class, 'jb-vip2');
// 积分兑换:非法行(积分为 0 / 等级越界 / 时长为空)被剔除。
assert.strictEqual(custom.points_exchange.packages.length, 3);
assert.strictEqual(custom.points_exchange.desc, '<p>积分兑换说明</p>');
assert.strictEqual(custom.points_exchange.packages[0].days, 30);
assert.strictEqual(custom.points_exchange.packages[1].is_permanent, true);
assert.strictEqual(custom.points_exchange.packages[2].days, 90);
@@ -155,6 +155,7 @@ namespace {
],
'vip_2_renew_product' => [],
'pay_vip_points_exchange_s' => true,
'pay_vip_points_exchange_desc' => '<p>积分兑换说明</p>',
'pay_vip_points_exchange_product' => [
['points' => 800, 'level' => 1, 'time' => 30, 'unit' => 'day'],
['points' => 9000, 'level' => 2, 'time' => 'Permanent'],