From 7f556892ad0e70194e7be7f14c20ddcd54961fe1 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 18 Oct 2026 14:30:15 +0800 Subject: [PATCH] fix(vip): align modal copy fields with reference theme backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename and reposition the two VIP modal copy settings to match the zibll reference backend, drop dead renew/upgrade desc config, and align copy defaults. - pay_user_vip_desc: modal top intro -> 会员介绍, textarea -> input (one-line), zibll help text, default copy from zibll - pay_user_vip_more: bottom note -> 开通会员更多内容, rows 4 + sanitize_type html (HTML was stripped on save), zibll help text and three-div default - Both fields move from the member-level group to a new 界面文案 (modal-copy) group at the end of the VIP section; dependsOn any-of-two-levels adapted from zibll's pay_user_vip_1_s dependency - Copy defaults synced in three places (SPA schema, PHP schema, PaymentRuntime fallbacks, since Settings::get_all() does not merge schema defaults): 黄金会员/钻石会员, 尊享专属特权, zibll four-item equity list - Remove vip_renew_desc / vip_upgrade_desc: AKSRT has no renew/upgrade card render surface (render_vip_card takes no type) and zero consumers repo-wide; zibll renders them on page-level cards only - pay_user_vip_1/2_equity and pay_vip_pass_charge_desc gain sanitize_type html so admins no longer lose list/HTML markup on save Verification: new tests/widget-system/vip-modal-copy-fields.test.cjs (6 contract tests); node --test tests/**/*.test.cjs 634/634 pass; php -l clean on both changed PHP files; assets/admin npm run build succeeds --- .../src/schema/sections/payment/recharge.js | 2 +- .../admin/src/schema/sections/payment/vip.js | 59 ++++--- src/Admin/Schema/SchemaPayment.php | 22 +-- src/Settings/PaymentRuntime.php | 18 +-- .../vip-modal-copy-fields.test.cjs | 145 ++++++++++++++++++ 5 files changed, 204 insertions(+), 42 deletions(-) create mode 100644 tests/widget-system/vip-modal-copy-fields.test.cjs diff --git a/assets/admin/src/schema/sections/payment/recharge.js b/assets/admin/src/schema/sections/payment/recharge.js index 3925803e..6bb0f2ec 100644 --- a/assets/admin/src/schema/sections/payment/recharge.js +++ b/assets/admin/src/schema/sections/payment/recharge.js @@ -111,7 +111,7 @@ export const rechargeSchema = { { key: 'points_pass_exchange_desc', label: '卡密兑换积分说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'points_pass_exchange_s', value: true }, help: '用户使用卡密兑换积分时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' }, { key: 'points_pass_exchange_only_password', label: '积分卡密单密码模式', type: 'switch', default: false, dependsOn: { key: 'points_pass_exchange_s', value: true }, help: '积分卡密仅需输入密码,不需要卡号' }, { key: 'pay_vip_pass_charge_s', label: '启用卡密兑换会员功能', type: 'switch', default: false, help: '开启后用户可在会员购买/续费弹窗选择「卡密」渠道,使用会员卡密直接兑换会员' }, - { key: 'pay_vip_pass_charge_desc', label: '卡密兑换会员说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' }, + { key: 'pay_vip_pass_charge_desc', label: '卡密兑换会员说明', type: 'textarea', sanitize_type: 'html', default: '', rows: 2, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' }, { key: 'pay_vip_pass_charge_only_password', label: '会员卡密单密码模式', type: 'switch', default: false, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '会员卡密仅需输入密码,不需要卡号;默认关闭,会员卡需输入卡号和密码(与其他卡密兑换一致)' }, ], }, diff --git a/assets/admin/src/schema/sections/payment/vip.js b/assets/admin/src/schema/sections/payment/vip.js index 2fc076cd..f1b97026 100644 --- a/assets/admin/src/schema/sections/payment/vip.js +++ b/assets/admin/src/schema/sections/payment/vip.js @@ -11,17 +11,17 @@ export const vipSchema = { nav_pay_vip: true, nav_user_pay_vip: true, pay_user_vip_1_s: true, - pay_user_vip_1_name: '一级会员', - pay_user_vip_1_desc: '尊享一级会员特权', - pay_user_vip_1_equity: '
  • 免费下载部分资源
  • 会员专属身份标识
  • 付费内容会员价
  • ', + pay_user_vip_1_name: '黄金会员', + pay_user_vip_1_desc: '尊享专属特权', + pay_user_vip_1_equity: '
  • 全站资源折扣购买
  • 部分内容免费阅读
  • 一对一技术指导
  • VIP用户专属QQ群
  • ', vip_1_img_icon: '', pay_user_vip_2_s: true, - pay_user_vip_2_name: '二级会员', - pay_user_vip_2_desc: '尊享二级会员特权', - pay_user_vip_2_equity: '
  • 更高免费下载额度
  • 更高等级身份标识
  • 更多付费内容会员价
  • ', + pay_user_vip_2_name: '钻石会员', + pay_user_vip_2_desc: '尊享专属特权', + pay_user_vip_2_equity: '
  • 全站资源折扣购买
  • 部分内容免费阅读
  • 一对一技术指导
  • VIP用户专属QQ群
  • ', vip_2_img_icon: '', - pay_user_vip_desc: '', - pay_user_vip_more: '', + pay_user_vip_desc: '开通VIP会员,享受会员专属折扣以及多项特权', + pay_user_vip_more: '
    购买后不支持退款
    VIP权益仅适用于本站
    欢迎与站长联系
    ', pay_download_limit: 0, pay_download_speed: 0, pay_download_limit_vip_1: 0, @@ -33,12 +33,10 @@ export const vipSchema = { vip_2_product_s: true, payment_vip_2_products: '58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡', vip_renew: true, - vip_renew_desc: '', vip_renew_price_type: 'discount', vip_renew_discount: 8, vip_renew_reduce: '20.00', vip_upgrade: true, - vip_upgrade_desc: '', vip_upgrade_product: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级', pay_vip_points_exchange_s: false, payment_vip_points_packages: '1|500|30|积分月卡\n1|5000|0|积分永久', @@ -67,17 +65,15 @@ export const vipSchema = { { key: 'nav_pay_vip', label: '导航栏购买按钮', type: 'switch', default: true, help: '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。' }, { key: 'nav_user_pay_vip', label: '用户框购买按钮', type: 'switch', default: true, help: '在导航栏用户框内显示开通会员按钮。' }, { key: 'pay_user_vip_1_s', label: '启用一级会员', type: 'switch', default: true }, - { key: 'pay_user_vip_1_name', label: '一级会员显示名称', type: 'input', default: '一级会员', dependsOn: { key: 'pay_user_vip_1_s', value: true } }, - { key: 'pay_user_vip_1_desc', label: '一级会员一句话简介', type: 'input', default: '尊享一级会员特权', dependsOn: { key: 'pay_user_vip_1_s', value: true } }, - { key: 'pay_user_vip_1_equity', label: '一级会员权益 HTML', type: 'textarea', default: '
  • 免费下载部分资源
  • 会员专属身份标识
  • 付费内容会员价
  • ', dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '建议使用
  • 列表项' }, + { key: 'pay_user_vip_1_name', label: '一级会员显示名称', type: 'input', default: '黄金会员', dependsOn: { key: 'pay_user_vip_1_s', value: true } }, + { key: 'pay_user_vip_1_desc', label: '一级会员一句话简介', type: 'input', default: '尊享专属特权', dependsOn: { key: 'pay_user_vip_1_s', value: true } }, + { key: 'pay_user_vip_1_equity', label: '一级会员权益 HTML', type: 'textarea', sanitize_type: 'html', default: '
  • 全站资源折扣购买
  • 部分内容免费阅读
  • 一对一技术指导
  • VIP用户专属QQ群
  • ', rows: 4, dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '建议使用
  • 列表项' }, { key: 'vip_1_img_icon', label: '一级会员图标 URL', type: 'input', default: '', dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '留空使用内置 vip-1.svg 图标' }, { key: 'pay_user_vip_2_s', label: '启用二级会员', type: 'switch', default: true }, - { key: 'pay_user_vip_2_name', label: '二级会员显示名称', type: 'input', default: '二级会员', dependsOn: { key: 'pay_user_vip_2_s', value: true } }, - { key: 'pay_user_vip_2_desc', label: '二级会员一句话简介', type: 'input', default: '尊享二级会员特权', dependsOn: { key: 'pay_user_vip_2_s', value: true } }, - { key: 'pay_user_vip_2_equity', label: '二级会员权益 HTML', type: 'textarea', default: '
  • 更高免费下载额度
  • 更高等级身份标识
  • 更多付费内容会员价
  • ', dependsOn: { key: 'pay_user_vip_2_s', value: true } }, + { key: 'pay_user_vip_2_name', label: '二级会员显示名称', type: 'input', default: '钻石会员', dependsOn: { key: 'pay_user_vip_2_s', value: true } }, + { key: 'pay_user_vip_2_desc', label: '二级会员一句话简介', type: 'input', default: '尊享专属特权', dependsOn: { key: 'pay_user_vip_2_s', value: true } }, + { key: 'pay_user_vip_2_equity', label: '二级会员权益 HTML', type: 'textarea', sanitize_type: 'html', default: '
  • 全站资源折扣购买
  • 部分内容免费阅读
  • 一对一技术指导
  • VIP用户专属QQ群
  • ', rows: 4, dependsOn: { key: 'pay_user_vip_2_s', value: true } }, { key: 'vip_2_img_icon', label: '二级会员图标 URL', type: 'input', default: '', dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '留空使用内置 vip-2.svg 图标' }, - { key: 'pay_user_vip_desc', label: '弹窗顶部会员说明', type: 'textarea', default: '' }, - { key: 'pay_user_vip_more', label: '弹窗底部补充说明 HTML', type: 'textarea', default: '' }, ], }, { @@ -163,7 +159,6 @@ export const vipSchema = { title: '会员续费', fields: [ { key: 'vip_renew', label: '允许续费', type: 'switch', default: true, help: '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费' }, - { key: 'vip_renew_desc', label: '续费说明', type: 'textarea', default: '', dependsOn: { key: 'vip_renew', value: true } }, { key: 'vip_renew_price_type', label: '续费价格类型', @@ -185,7 +180,6 @@ export const vipSchema = { title: '会员升级', fields: [ { key: 'vip_upgrade', label: '允许升级', type: 'switch', default: true }, - { key: 'vip_upgrade_desc', label: '升级说明', type: 'textarea', default: '', dependsOn: { key: 'vip_upgrade', value: true } }, { key: 'vip_upgrade_product', label: '升级产品', @@ -217,5 +211,30 @@ export const vipSchema = { { key: 'pay_vip_points_exchange_desc', label: '兑换说明', type: 'textarea', default: '', dependsOn: { key: 'pay_vip_points_exchange_s', value: true } }, ], }, + { + key: 'modal-copy', + title: '界面文案', + icon: 'fa fas fa-align-left', + fields: [ + { + key: 'pay_user_vip_desc', + label: '会员介绍', + type: 'input', + default: '开通VIP会员,享受会员专属折扣以及多项特权', + dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] }, + help: '显示在开通界面顶部一句话简介,可以为会员权益简介或者活动介绍', + }, + { + key: 'pay_user_vip_more', + label: '开通会员更多内容', + type: 'textarea', + sanitize_type: 'html', + default: '
    购买后不支持退款
    VIP权益仅适用于本站
    欢迎与站长联系
    ', + rows: 4, + dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] }, + help: '显示在开通界面底部位置,可以为提醒事项、用户协议等,支持HTML代码', + }, + ], + }, ], } diff --git a/src/Admin/Schema/SchemaPayment.php b/src/Admin/Schema/SchemaPayment.php index 73762713..e12d665a 100644 --- a/src/Admin/Schema/SchemaPayment.php +++ b/src/Admin/Schema/SchemaPayment.php @@ -159,7 +159,7 @@ class SchemaPayment { ['key' => 'points_pass_exchange_desc', 'label' => '卡密兑换积分说明', 'type' => 'textarea', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'points_pass_exchange_s', 'value' => true], 'help' => '用户使用卡密兑换积分时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'], ['key' => 'points_pass_exchange_only_password', 'label' => '积分卡密单密码模式', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'points_pass_exchange_s', 'value' => true], 'help' => '积分卡密仅需输入密码,不需要卡号'], ['key' => 'pay_vip_pass_charge_s', 'label' => '启用卡密兑换会员功能', 'type' => 'switch', 'default' => false, 'help' => '开启后用户可在会员购买/续费弹窗选择「卡密」渠道,使用会员卡密直接兑换会员'], - ['key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换会员说明', 'type' => 'textarea', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true], 'help' => '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'], + ['key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换会员说明', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true], 'help' => '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'], ['key' => 'pay_vip_pass_charge_only_password', 'label' => '会员卡密单密码模式', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true], 'help' => '会员卡密仅需输入密码,不需要卡号;默认关闭,会员卡需输入卡号和密码(与其他卡密兑换一致)'], ]], ['key' => 'points-exchange', 'title' => '余额兑换积分', 'fields' => [ @@ -188,17 +188,15 @@ class SchemaPayment { ['key' => 'nav_pay_vip', 'label' => '导航栏购买按钮', 'type' => 'switch', 'default' => true, 'help' => '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。'], ['key' => 'nav_user_pay_vip', 'label' => '用户框购买按钮', 'type' => 'switch', 'default' => true, 'help' => '在导航栏用户框内显示开通会员按钮。'], ['key' => 'pay_user_vip_1_s', 'label' => '启用一级会员', 'type' => 'switch', 'default' => true], - ['key' => 'pay_user_vip_1_name', 'label' => '一级会员显示名称', 'type' => 'input', 'default' => '一级会员', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]], - ['key' => 'pay_user_vip_1_desc', 'label' => '一级会员一句话简介', 'type' => 'input', 'default' => '尊享一级会员特权', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]], - ['key' => 'pay_user_vip_1_equity', 'label' => '一级会员权益 HTML', 'type' => 'textarea', 'default' => '
  • 免费下载部分资源
  • 会员专属身份标识
  • 付费内容会员价
  • ', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]], + ['key' => 'pay_user_vip_1_name', 'label' => '一级会员显示名称', 'type' => 'input', 'default' => '黄金会员', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]], + ['key' => 'pay_user_vip_1_desc', 'label' => '一级会员一句话简介', 'type' => 'input', 'default' => '尊享专属特权', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]], + ['key' => 'pay_user_vip_1_equity', 'label' => '一级会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '
  • 全站资源折扣购买
  • 部分内容免费阅读
  • 一对一技术指导
  • VIP用户专属QQ群
  • ', 'rows' => 4, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]], ['key' => 'vip_1_img_icon', 'label' => '一级会员图标 URL', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true], 'help' => '留空使用内置 vip-1.svg 图标'], ['key' => 'pay_user_vip_2_s', 'label' => '启用二级会员', 'type' => 'switch', 'default' => true], - ['key' => 'pay_user_vip_2_name', 'label' => '二级会员显示名称', 'type' => 'input', 'default' => '二级会员', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]], - ['key' => 'pay_user_vip_2_desc', 'label' => '二级会员一句话简介', 'type' => 'input', 'default' => '尊享二级会员特权', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]], - ['key' => 'pay_user_vip_2_equity', 'label' => '二级会员权益 HTML', 'type' => 'textarea', 'default' => '
  • 更高免费下载额度
  • 更高等级身份标识
  • 更多付费内容会员价
  • ', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]], + ['key' => 'pay_user_vip_2_name', 'label' => '二级会员显示名称', 'type' => 'input', 'default' => '钻石会员', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]], + ['key' => 'pay_user_vip_2_desc', 'label' => '二级会员一句话简介', 'type' => 'input', 'default' => '尊享专属特权', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]], + ['key' => 'pay_user_vip_2_equity', 'label' => '二级会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '
  • 全站资源折扣购买
  • 部分内容免费阅读
  • 一对一技术指导
  • VIP用户专属QQ群
  • ', 'rows' => 4, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]], ['key' => 'vip_2_img_icon', 'label' => '二级会员图标 URL', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true], 'help' => '留空使用内置 vip-2.svg 图标'], - ['key' => 'pay_user_vip_desc', 'label' => '弹窗顶部会员说明', 'type' => 'textarea', 'default' => ''], - ['key' => 'pay_user_vip_more', 'label' => '弹窗底部补充说明 HTML', 'type' => 'textarea', 'default' => ''], ]], ['key' => 'benefit', 'title' => '会员权益', 'fields' => [ ['key' => 'vip_benefit', 'label' => '会员权益', 'type' => 'tabbed', 'tabs' => [ @@ -228,7 +226,6 @@ class SchemaPayment { ]], ['key' => 'renew', 'title' => '会员续费', 'fields' => [ ['key' => 'vip_renew', 'label' => '允许续费', 'type' => 'switch', 'default' => true, 'help' => '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费'], - ['key' => 'vip_renew_desc', 'label' => '续费说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'vip_renew', 'value' => true]], ['key' => 'vip_renew_price_type', 'label' => '续费价格类型', 'type' => 'radio', 'default' => 'discount', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'options' => [ ['label' => '相同价格', 'value' => 'same'], ['label' => '折扣价格', 'value' => 'discount'], @@ -239,7 +236,6 @@ class SchemaPayment { ]], ['key' => 'upgrade', 'title' => '会员升级', 'fields' => [ ['key' => 'vip_upgrade', 'label' => '允许升级', 'type' => 'switch', 'default' => true], - ['key' => 'vip_upgrade_desc', 'label' => '升级说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true]], ['key' => 'vip_upgrade_product', 'label' => '升级产品', 'type' => 'textarea', 'default' => "unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级", 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '每行一条。永久VIP1:permanent|价格|标题;剩余天:unit|每日单价|标题;跳转永久:jump|价格|标题'], ]], ]], @@ -249,6 +245,10 @@ class SchemaPayment { ['key' => 'payment_vip_points_packages', 'label' => '兑换套餐', 'type' => 'textarea', 'default' => "1|500|30|积分月卡\n1|5000|0|积分永久", 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true], 'help' => '每行:等级|积分|天数|标题(天数 0=永久;普通用户可开通,已有会员可续费同等级或升级更高等级)'], ['key' => 'pay_vip_points_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true]], ]], + ['key' => 'modal-copy', 'title' => '界面文案', 'fields' => [ + ['key' => 'pay_user_vip_desc', 'label' => '会员介绍', 'type' => 'input', 'default' => '开通VIP会员,享受会员专属折扣以及多项特权', 'dependsOn' => ['any' => [['key' => 'pay_user_vip_1_s', 'value' => true], ['key' => 'pay_user_vip_2_s', 'value' => true]]]], + ['key' => 'pay_user_vip_more', 'label' => '开通会员更多内容', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '
    购买后不支持退款
    VIP权益仅适用于本站
    欢迎与站长联系
    ', 'rows' => 4, 'dependsOn' => ['any' => [['key' => 'pay_user_vip_1_s', 'value' => true], ['key' => 'pay_user_vip_2_s', 'value' => true]]]], + ]], ], ], diff --git a/src/Settings/PaymentRuntime.php b/src/Settings/PaymentRuntime.php index aa955a89..c5e4d7be 100644 --- a/src/Settings/PaymentRuntime.php +++ b/src/Settings/PaymentRuntime.php @@ -171,10 +171,10 @@ class PaymentRuntime { $name1 = trim((string) ($settings['pay_user_vip_1_name'] ?? '')); $name2 = trim((string) ($settings['pay_user_vip_2_name'] ?? '')); if ($name1 === '') { - $name1 = __('一级会员', 'aksrt'); + $name1 = __('黄金会员', 'aksrt'); } if ($name2 === '') { - $name2 = __('二级会员', 'aksrt'); + $name2 = __('钻石会员', 'aksrt'); } $badge = $vip2_enabled ? $name2 : ($vip1_enabled ? $name1 : __('VIP', 'aksrt')); @@ -196,8 +196,8 @@ class PaymentRuntime { $products_2 = $default_l2; } - $desc = sanitize_textarea_field((string) ($settings['pay_user_vip_desc'] ?? '')); - $more = (string) ($settings['pay_user_vip_more'] ?? ''); + $desc = sanitize_text_field((string) ($settings['pay_user_vip_desc'] ?? __('开通VIP会员,享受会员专属折扣以及多项特权', 'aksrt'))); + $more = (string) ($settings['pay_user_vip_more'] ?? '
    购买后不支持退款
    VIP权益仅适用于本站
    欢迎与站长联系
    '); $allowed_equity = [ 'li' => [], 'ul' => [], @@ -223,8 +223,8 @@ class PaymentRuntime { 1 => [ 'enabled' => $vip1_enabled, 'name' => $name1, - 'desc' => sanitize_text_field((string) ($settings['pay_user_vip_1_desc'] ?? __('尊享一级会员特权', 'aksrt'))), - 'equity' => wp_kses((string) (($settings['pay_user_vip_1_equity'] ?? '') !== '' ? $settings['pay_user_vip_1_equity'] : '
  • 免费下载部分资源
  • 会员专属身份标识
  • 付费内容会员价
  • '), $allowed_equity), + 'desc' => sanitize_text_field((string) ($settings['pay_user_vip_1_desc'] ?? __('尊享专属特权', 'aksrt'))), + 'equity' => wp_kses((string) (($settings['pay_user_vip_1_equity'] ?? '') !== '' ? $settings['pay_user_vip_1_equity'] : '
  • 全站资源折扣购买
  • 部分内容免费阅读
  • 一对一技术指导
  • VIP用户专属QQ群
  • '), $allowed_equity), 'img_icon' => self::get_vip_icon_url(1, (string) ($settings['vip_1_img_icon'] ?? '')), 'product_s' => !isset($settings['vip_1_product_s']) || !empty($settings['vip_1_product_s']), 'products_raw' => $products_1, @@ -233,8 +233,8 @@ class PaymentRuntime { 2 => [ 'enabled' => $vip2_enabled, 'name' => $name2, - 'desc' => sanitize_text_field((string) ($settings['pay_user_vip_2_desc'] ?? __('尊享二级会员特权', 'aksrt'))), - 'equity' => wp_kses((string) (($settings['pay_user_vip_2_equity'] ?? '') !== '' ? $settings['pay_user_vip_2_equity'] : '
  • 更高免费下载额度
  • 更高等级身份标识
  • 更多付费内容会员价
  • '), $allowed_equity), + 'desc' => sanitize_text_field((string) ($settings['pay_user_vip_2_desc'] ?? __('尊享专属特权', 'aksrt'))), + 'equity' => wp_kses((string) (($settings['pay_user_vip_2_equity'] ?? '') !== '' ? $settings['pay_user_vip_2_equity'] : '
  • 全站资源折扣购买
  • 部分内容免费阅读
  • 一对一技术指导
  • VIP用户专属QQ群
  • '), $allowed_equity), 'img_icon' => self::get_vip_icon_url(2, (string) ($settings['vip_2_img_icon'] ?? '')), 'product_s' => !isset($settings['vip_2_product_s']) || !empty($settings['vip_2_product_s']), 'products_raw' => $products_2, @@ -246,14 +246,12 @@ class PaymentRuntime { ], 'renew' => [ 'enabled' => !isset($settings['vip_renew']) || !empty($settings['vip_renew']), - 'desc' => sanitize_textarea_field((string) ($settings['vip_renew_desc'] ?? '')), 'price_type' => $renew_price_type, 'discount' => max(1, min(100, (int) ($settings['vip_renew_discount'] ?? 8))), 'reduce' => self::money_decimal($settings['vip_renew_reduce'] ?? '20.00', '20.00', 999900), ], 'upgrade' => [ 'enabled' => !isset($settings['vip_upgrade']) || !empty($settings['vip_upgrade']), - 'desc' => sanitize_textarea_field((string) ($settings['vip_upgrade_desc'] ?? '')), // Multi-line upgrade product rules — must not collapse newlines. 'product' => sanitize_textarea_field((string) ($settings['vip_upgrade_product'] ?? '')), ], diff --git a/tests/widget-system/vip-modal-copy-fields.test.cjs b/tests/widget-system/vip-modal-copy-fields.test.cjs new file mode 100644 index 00000000..27a43827 --- /dev/null +++ b/tests/widget-system/vip-modal-copy-fields.test.cjs @@ -0,0 +1,145 @@ +/** + * VIP 后台设置弹窗文案字段契约(对照参考主题 zibll 修正)。 + * + * 硬约束: + * 1) 弹窗文案字段更名为「会员介绍」(pay_user_vip_desc, input) 与 + * 「开通会员更多内容」(pay_user_vip_more, textarea rows 4 + + * sanitize_type html),并从「会员等级」组移到分区末尾的 + * 「界面文案」(modal-copy) 独立分组; + * 2) 文案默认值对齐 zibll(黄金会员/钻石会员、尊享专属特权、权益四项、 + * 会员介绍与开通会员更多内容默认值),SPA schema / PHP schema / + * PaymentRuntime 回退三处一致——Settings::get_all() 不合并 + * schema 默认值,运行时回退即实际默认值; + * 3) vip_renew_desc / vip_upgrade_desc 为死配置(AKSRT 无续费/升级 + * 卡渲染面,全仓零消费方),两份 schema 与 PaymentRuntime 均已删除; + * 4) 权益与卡密说明字段保存时保留 HTML(sanitize_type html), + * 不再被 sanitize_textarea_field 剥掉标签。 + */ +const test = require('node:test'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); + +const root = path.join(__dirname, '..', '..'); +const read = (...seg) => fs.readFileSync(path.join(root, ...seg), 'utf8'); + +const vipSchemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'vip.js'); +const schemaPhp = read('src', 'Admin', 'Schema', 'SchemaPayment.php'); +const paymentRuntime = read('src', 'Settings', 'PaymentRuntime.php'); +const rechargeJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'recharge.js'); + +const DESC_DEFAULT = '开通VIP会员,享受会员专属折扣以及多项特权'; +const MORE_DEFAULT = '
    购买后不支持退款
    VIP权益仅适用于本站
    欢迎与站长联系
    '; +const EQUITY_DEFAULT = '
  • 全站资源折扣购买
  • 部分内容免费阅读
  • 一对一技术指导
  • VIP用户专属QQ群
  • '; + +// 取两个标记之间的源码段(分组切片,避免断言误伤其它功能的同名键)。 +function slice(source, from, to) { + const start = source.indexOf(from); + assert.notStrictEqual(start, -1, `missing ${from}`); + const end = source.indexOf(to, start + from.length); + return source.slice(start, end === -1 ? undefined : end); +} + +test('弹窗文案字段更名为会员介绍/开通会员更多内容并改为正确类型', () => { + for (const [name, src] of [['vip.js', vipSchemaJs], ['SchemaPayment.php', schemaPhp]]) { + assert.ok(!src.includes('弹窗顶部会员说明') && !src.includes('弹窗底部补充说明 HTML'), `${name} 旧名称应已移除`); + assert.ok(src.includes('会员介绍') && src.includes('开通会员更多内容'), `${name} 应含新名称`); + } + // 会员介绍为单行 input(zibll text「一句话简介」);开通会员更多内容为 rows 4 的 HTML textarea。 + assert.match( + vipSchemaJs, + /key: 'pay_user_vip_desc',\s*\n\s*label: '会员介绍',\s*\n\s*type: 'input',\s*\n\s*default: '开通VIP会员,享受会员专属折扣以及多项特权'/ + ); + assert.match( + vipSchemaJs, + /key: 'pay_user_vip_more',\s*\n\s*label: '开通会员更多内容',\s*\n\s*type: 'textarea',\s*\n\s*sanitize_type: 'html'[\s\S]*?rows: 4/ + ); + assert.match( + schemaPhp, + /'key' => 'pay_user_vip_desc', 'label' => '会员介绍', 'type' => 'input', 'default' => '开通VIP会员,享受会员专属折扣以及多项特权'/ + ); + assert.match( + schemaPhp, + /'key' => 'pay_user_vip_more', 'label' => '开通会员更多内容', 'type' => 'textarea', 'sanitize_type' => 'html'[\s\S]*?'rows' => 4/ + ); + // 帮助文本对齐 zibll 的 desc。 + assert.ok(vipSchemaJs.includes('显示在开通界面顶部一句话简介,可以为会员权益简介或者活动介绍')); + assert.ok(vipSchemaJs.includes('显示在开通界面底部位置,可以为提醒事项、用户协议等,支持HTML代码')); +}); + +test('两个文案字段位于分区末尾的界面文案分组且不再属于会员等级组', () => { + const modalJs = vipSchemaJs.indexOf("key: 'modal-copy'"); + const modalPhp = schemaPhp.indexOf("'key' => 'modal-copy'"); + assert.notStrictEqual(modalJs, -1, 'vip.js 应含 modal-copy 分组'); + assert.notStrictEqual(modalPhp, -1, 'SchemaPayment.php 应含 modal-copy 分组'); + assert.ok(modalJs > vipSchemaJs.indexOf("key: 'points-exchange'"), 'vip.js modal-copy 应在分区末尾'); + assert.ok(modalPhp > schemaPhp.indexOf("'key' => 'points-exchange'"), 'SchemaPayment.php modal-copy 应在分区末尾'); + // modal-copy 之后不再出现其它分组/字段 key。 + // modal-copy 之后(至分区末尾)不再出现其它分组/字段 key;dependsOn 引用的等级开关除外。 + assert.doesNotMatch(vipSchemaJs.slice(modalJs), /key: '(?!modal-copy|pay_user_vip_desc|pay_user_vip_more|pay_user_vip_1_s|pay_user_vip_2_s)/); + const phpTail = slice(schemaPhp, "'key' => 'modal-copy'", "'revenue'"); + assert.doesNotMatch(phpTail, /'key' => '(?!modal-copy|pay_user_vip_desc|pay_user_vip_more|pay_user_vip_1_s|pay_user_vip_2_s)/); + // 「会员等级」组不再包含这两个字段。 + assert.doesNotMatch(slice(vipSchemaJs, "key: 'basic'", "key: 'benefit'"), /pay_user_vip_desc|pay_user_vip_more/); + assert.doesNotMatch(slice(schemaPhp, "'key' => 'basic'", "'key' => 'benefit'"), /pay_user_vip_desc|pay_user_vip_more/); + // 依赖:任一等级启用时显示(zibll pay_user_vip_1_s 依赖的双等级适配)。 + assert.ok( + vipSchemaJs.includes("dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] }"), + 'vip.js 应为 any 双等级依赖' + ); + assert.ok( + schemaPhp.includes("'dependsOn' => ['any' => [['key' => 'pay_user_vip_1_s', 'value' => true], ['key' => 'pay_user_vip_2_s', 'value' => true]]]"), + 'SchemaPayment.php 应为 any 双等级依赖' + ); +}); + +test('续费/升级说明死配置已从两份 schema 与运行时删除', () => { + for (const [name, src] of [['vip.js', vipSchemaJs], ['SchemaPayment.php', schemaPhp], ['PaymentRuntime.php', paymentRuntime]]) { + assert.ok(!src.includes('vip_renew_desc'), `${name} 不应再含 vip_renew_desc`); + assert.ok(!src.includes('vip_upgrade_desc'), `${name} 不应再含 vip_upgrade_desc`); + } +}); + +test('文案默认值三处一致并对齐 zibll', () => { + // SPA defaults 块与字段级 default。 + assert.ok(vipSchemaJs.includes("pay_user_vip_1_name: '黄金会员'")); + assert.ok(vipSchemaJs.includes("pay_user_vip_2_name: '钻石会员'")); + assert.ok(vipSchemaJs.includes("pay_user_vip_1_desc: '尊享专属特权'")); + assert.ok(vipSchemaJs.includes("pay_user_vip_2_desc: '尊享专属特权'")); + assert.ok(vipSchemaJs.includes(`pay_user_vip_desc: '${DESC_DEFAULT}'`)); + assert.ok(vipSchemaJs.includes(`pay_user_vip_more: '${MORE_DEFAULT}'`)); + assert.ok(vipSchemaJs.includes(`default: '${EQUITY_DEFAULT}'`)); + // PHP schema 字段 default。 + assert.ok(schemaPhp.includes("'default' => '黄金会员'")); + assert.ok(schemaPhp.includes("'default' => '钻石会员'")); + assert.ok(schemaPhp.includes(`'default' => '${DESC_DEFAULT}'`)); + assert.ok(schemaPhp.includes(`'default' => '${MORE_DEFAULT}'`)); + // PaymentRuntime 运行时回退(get_all 不合并 schema 默认值,回退即实际默认值)。 + assert.ok(paymentRuntime.includes(`$desc = sanitize_text_field((string) ($settings['pay_user_vip_desc'] ?? __('${DESC_DEFAULT}', 'aksrt')))`)); + assert.ok(paymentRuntime.includes(`$more = (string) ($settings['pay_user_vip_more'] ?? '${MORE_DEFAULT}')`)); + assert.ok(paymentRuntime.includes("$name1 = __('黄金会员', 'aksrt');")); + assert.ok(paymentRuntime.includes("$name2 = __('钻石会员', 'aksrt');")); + assert.ok(paymentRuntime.includes("$settings['pay_user_vip_1_desc'] ?? __('尊享专属特权', 'aksrt')")); + assert.ok(paymentRuntime.includes("$settings['pay_user_vip_2_desc'] ?? __('尊享专属特权', 'aksrt')")); + assert.ok(paymentRuntime.includes(EQUITY_DEFAULT)); + // 旧默认值不再出现在任何一处。 + for (const [name, src] of [['vip.js', vipSchemaJs], ['SchemaPayment.php', schemaPhp], ['PaymentRuntime.php', paymentRuntime]]) { + assert.ok(!/尊享一级会员特权|尊享二级会员特权|免费下载部分资源|更高免费下载额度/.test(src), `${name} 旧默认值应已移除`); + } +}); + +test('权益与卡密说明字段保存时保留 HTML', () => { + assert.ok(vipSchemaJs.includes("key: 'pay_user_vip_1_equity', label: '一级会员权益 HTML', type: 'textarea', sanitize_type: 'html'")); + assert.ok(vipSchemaJs.includes("key: 'pay_user_vip_2_equity', label: '二级会员权益 HTML', type: 'textarea', sanitize_type: 'html'")); + assert.ok(schemaPhp.includes("'key' => 'pay_user_vip_1_equity', 'label' => '一级会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html'")); + assert.ok(schemaPhp.includes("'key' => 'pay_user_vip_2_equity', 'label' => '二级会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html'")); + assert.ok(rechargeJs.includes("key: 'pay_vip_pass_charge_desc', label: '卡密兑换会员说明', type: 'textarea', sanitize_type: 'html'")); + assert.ok(schemaPhp.includes("'key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换会员说明', 'type' => 'textarea', 'sanitize_type' => 'html'")); +}); + +test('会员介绍运行时按单行文本净化', () => { + // 只收敛 VIP 设置函数:充值等其它设置的 $desc 与本文无关。 + const vipSettings = slice(paymentRuntime, 'public static function get_payment_vip_settings', 'public static function get_payment_revenue_settings'); + assert.ok(vipSettings.includes("$desc = sanitize_text_field((string) ($settings['pay_user_vip_desc']")); + assert.ok(!vipSettings.includes('$desc = sanitize_textarea_field')); +});