diff --git a/assets/css/frontend/vip-modal.css b/assets/css/frontend/vip-modal.css
new file mode 100644
index 00000000..07ea7f1a
--- /dev/null
+++ b/assets/css/frontend/vip-modal.css
@@ -0,0 +1,257 @@
+/**
+ * VIP pay modal + hover cards (zibll payvip-modal / vip-card).
+ */
+
+.aksrt-vip-modal-open {
+ overflow: hidden;
+}
+
+.aksrt-vip-modal-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 1200;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ padding: 16px;
+ background: rgba(0, 0, 0, 0.5);
+}
+
+.aksrt-vip-modal-overlay.is-open {
+ display: flex;
+}
+
+.aksrt-vip-modal-dialog {
+ width: min(560px, 100%);
+ max-height: min(90vh, 760px);
+ overflow: auto;
+ background: var(--main-bg-color, #fff);
+ border-radius: 12px;
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.22);
+}
+
+.payvip-modal {
+ position: relative;
+ padding: 18px 16px 16px;
+}
+
+.payvip-modal .close,
+.aksrt-vip-modal-close {
+ position: absolute;
+ top: 8px;
+ right: 12px;
+ border: 0;
+ background: transparent;
+ font-size: 22px;
+ line-height: 1;
+ cursor: pointer;
+ color: inherit;
+ opacity: 0.65;
+}
+
+.vip-cardminis {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ gap: 8px;
+ display: flex;
+}
+
+.vip-cardminis > li {
+ flex: 1;
+}
+
+.vip-cardminis a.aksrt-vip-tab {
+ display: block;
+ text-align: center;
+ padding: 10px 8px;
+ border-radius: 8px;
+ background: var(--surface-2, rgba(0, 0, 0, 0.05));
+ color: inherit;
+ text-decoration: none;
+ font-weight: 600;
+}
+
+.vip-cardminis > li.active a.aksrt-vip-tab {
+ background: linear-gradient(90deg, #ff6b6b, #f04494);
+ color: #fff;
+}
+
+.pay-vip-form-row {
+ display: grid;
+ gap: 12px;
+}
+
+@media (min-width: 640px) {
+ .pay-vip-form-row {
+ grid-template-columns: 1fr 1.2fr;
+ }
+}
+
+.pay-vip-info {
+ padding: 12px;
+ border-radius: 10px;
+ background: var(--surface-2, rgba(0, 0, 0, 0.04));
+}
+
+.pay-vip-info.level-1 {
+ background: linear-gradient(135deg, rgba(255, 193, 7, 0.12), rgba(255, 152, 0, 0.08));
+}
+
+.pay-vip-info.level-2 {
+ background: linear-gradient(135deg, rgba(156, 39, 176, 0.12), rgba(63, 81, 181, 0.08));
+}
+
+.v-equity {
+ margin: 0;
+ padding-left: 1.1em;
+ font-size: 12px;
+ line-height: 1.6;
+}
+
+.aksrt-vip-products {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+}
+
+.aksrt-vip-product-label {
+ display: block;
+ cursor: pointer;
+ margin: 0;
+}
+
+.aksrt-vip-product-label input {
+ position: absolute;
+ opacity: 0;
+ pointer-events: none;
+}
+
+.vip-product {
+ position: relative;
+ padding: 12px 8px;
+ border-radius: 10px;
+ border: 1px solid rgba(0, 0, 0, 0.08);
+ background: var(--main-bg-color, #fff);
+ transition: border-color 0.15s, box-shadow 0.15s;
+}
+
+.aksrt-vip-product-label input:checked + .vip-product {
+ border-color: #f04494;
+ box-shadow: 0 0 0 1px #f04494;
+}
+
+.product-price {
+ font-size: 18px;
+ font-weight: 700;
+}
+
+.vip-tag {
+ position: absolute;
+ top: 0;
+ right: 0;
+ font-size: 11px;
+ padding: 2px 6px;
+ border-radius: 0 10px 0 8px;
+ background: #ffc107;
+ color: #5c3b00;
+}
+
+.aksrt-vip-submit {
+ width: 100%;
+ margin-top: 8px;
+ border: 0;
+ border-radius: 999px;
+ padding: 12px 16px;
+ background: linear-gradient(90deg, #ff6b6b, #f04494);
+ color: #fff;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+.aksrt-vip-submit.is-loading,
+.aksrt-vip-submit:disabled {
+ opacity: 0.7;
+ cursor: wait;
+}
+
+.aksrt-vip-modal-tip.is-error,
+.uc-vip-shop__tip.is-error {
+ color: #e74c3c;
+}
+
+/* Header hover VIP cards */
+.navbar-vip.hover-show {
+ position: relative;
+}
+
+.sub-vip-card {
+ display: none;
+ position: absolute;
+ top: calc(100% + 8px);
+ right: 0;
+ z-index: 60;
+ width: min(420px, 90vw);
+ padding: 10px;
+ border-radius: 12px;
+ background: var(--main-bg-color, #fff);
+ box-shadow: 0 12px 36px rgba(0, 0, 0, 0.16);
+ gap: 10px;
+}
+
+.navbar-vip.hover-show:hover .sub-vip-card,
+.navbar-vip.hover-show:focus-within .sub-vip-card {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.vip-card {
+ position: relative;
+ flex: 1 1 160px;
+ min-width: 150px;
+ padding: 14px 12px;
+ border-radius: 10px;
+ overflow: hidden;
+ background: linear-gradient(135deg, #2b2f3a, #1b1e27);
+ color: #fff;
+}
+
+.vip-card.vip-theme1 {
+ background: linear-gradient(135deg, #f6c453, #f39c12);
+ color: #3b2a00;
+}
+
+.vip-card.vip-theme2 {
+ background: linear-gradient(135deg, #8e44ad, #3f51b5);
+ color: #fff;
+}
+
+.vip-card .payvip-button {
+ display: inline-block;
+ margin-top: 6px;
+ padding: 6px 12px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.92);
+ color: #333;
+ text-decoration: none;
+ font-size: 12px;
+}
+
+.vipbg-v2 {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ padding: 8px 10px;
+ border-radius: 8px;
+ color: #fff;
+ background: linear-gradient(90deg, #ffb347, #ff6b6b);
+}
+
+.vipbg-v2.level-2 {
+ background: linear-gradient(90deg, #9b59b6, #3f51b5);
+}
+
+.vip-expdate-tag {
+ font-size: 12px;
+}
diff --git a/assets/js/pay-vip.js b/assets/js/pay-vip.js
new file mode 100644
index 00000000..a33e6ecb
--- /dev/null
+++ b/assets/js/pay-vip.js
@@ -0,0 +1,266 @@
+/**
+ * AKSRT VIP pay modal — zibll .pay-vip parity.
+ * Loads modal HTML from GET /aksrt/v1/pay/vip/modal then purchases via POST /pay/vip.
+ */
+(function () {
+ "use strict";
+
+ function restBase() {
+ var aksrt = window.aksrtData || window.aksrt || {};
+ var siteUrl = aksrt.siteUrl || aksrt.homeUrl || (window.location.origin + "/");
+ if (window.location.protocol === "https:" && siteUrl.substring(0, 5) === "http:") {
+ siteUrl = "https:" + siteUrl.substring(5);
+ }
+ if (aksrt.restUrl) {
+ return String(aksrt.restUrl).replace(/\/$/, "");
+ }
+ return siteUrl.replace(/\/$/, "") + "/wp-json/aksrt/v1";
+ }
+
+ function nonceHeaders() {
+ var aksrt = window.aksrtData || window.aksrt || {};
+ var n = aksrt.restNonce || aksrt.nonce || "";
+ return n ? { "X-WP-Nonce": n } : {};
+ }
+
+ function api(path, options) {
+ var merged = Object.assign(
+ {
+ credentials: "same-origin",
+ headers: Object.assign({ "Content-Type": "application/json" }, nonceHeaders()),
+ },
+ options || {}
+ );
+ return fetch(restBase() + path, merged).then(function (r) {
+ return r
+ .json()
+ .catch(function () {
+ return { message: "Invalid JSON (HTTP " + r.status + ")" };
+ })
+ .then(function (j) {
+ return { ok: r.ok, status: r.status, json: j };
+ });
+ });
+ }
+
+ function ensureModalShell() {
+ var el = document.getElementById("modal_pay_uservip");
+ if (el) return el;
+ el = document.createElement("div");
+ el.id = "modal_pay_uservip";
+ el.className = "aksrt-vip-modal-overlay";
+ el.setAttribute("aria-hidden", "true");
+ el.innerHTML =
+ '
';
+ document.body.appendChild(el);
+ el.addEventListener("click", function (e) {
+ if (e.target === el || e.target.closest(".aksrt-vip-modal-close")) {
+ closeModal();
+ }
+ });
+ document.addEventListener("keydown", function (e) {
+ if (e.key === "Escape") closeModal();
+ });
+ return el;
+ }
+
+ function openModal() {
+ var el = ensureModalShell();
+ el.classList.add("is-open");
+ el.setAttribute("aria-hidden", "false");
+ document.body.classList.add("aksrt-vip-modal-open");
+ }
+
+ function closeModal() {
+ var el = document.getElementById("modal_pay_uservip");
+ if (!el) return;
+ el.classList.remove("is-open");
+ el.setAttribute("aria-hidden", "true");
+ document.body.classList.remove("aksrt-vip-modal-open");
+ }
+
+ function bindModalUi(root) {
+ if (!root || root.dataset.aksrtVipBound === "1") return;
+ root.dataset.aksrtVipBound = "1";
+
+ root.addEventListener("click", function (e) {
+ var tab = e.target.closest("[data-vip-tab]");
+ if (tab) {
+ e.preventDefault();
+ var id = tab.getAttribute("data-vip-tab");
+ root.querySelectorAll(".aksrt-vip-tab").forEach(function (a) {
+ a.parentElement && a.parentElement.classList.remove("active");
+ });
+ tab.parentElement && tab.parentElement.classList.add("active");
+ root.querySelectorAll(".aksrt-vip-pane").forEach(function (p) {
+ var on = p.id === id;
+ p.hidden = !on;
+ p.classList.toggle("active", on);
+ });
+ return;
+ }
+
+ var submit = e.target.closest("[data-vip-submit]");
+ if (submit) {
+ e.preventDefault();
+ purchaseFromPane(submit.closest(".aksrt-vip-pane") || root, submit);
+ }
+ });
+ }
+
+ function selectedMethod(root) {
+ var fixed = root.querySelector("[data-aksrt-vip-method-fixed]");
+ if (fixed && fixed.value) return fixed.value;
+ var checked = root.querySelector('input[name="aksrt_vip_pay_method"]:checked');
+ if (checked) return checked.value;
+ var first = root.querySelector('input[name="aksrt_vip_pay_method"]');
+ return first ? first.value : "balance";
+ }
+
+ function selectedPackage(root) {
+ var checked = root.querySelector('input[name="vip_product_id"]:checked');
+ return checked ? parseInt(checked.value, 10) : 0;
+ }
+
+ function setTip(root, text, isError) {
+ var tip = root.querySelector("[data-aksrt-vip-tip]");
+ if (!tip) return;
+ tip.textContent = text || "";
+ tip.classList.toggle("is-error", !!isError);
+ }
+
+ function pollOrder(orderNumber) {
+ if (!orderNumber) return;
+ var tries = 0;
+ var timer = setInterval(function () {
+ tries += 1;
+ api("/pay/content/order/" + encodeURIComponent(orderNumber))
+ .then(function (r) {
+ if (r.ok && r.json && r.json.paid) {
+ clearInterval(timer);
+ window.location.reload();
+ }
+ })
+ .catch(function () {});
+ if (tries >= 40) clearInterval(timer);
+ }, 3000);
+ }
+
+ function purchaseFromPane(pane, btn) {
+ var packageId = selectedPackage(pane);
+ if (!packageId) {
+ setTip(pane, "请选择套餐", true);
+ return;
+ }
+ if (btn.disabled) return;
+ btn.disabled = true;
+ btn.classList.add("is-loading");
+ setTip(pane, "正在创建订单…");
+
+ api("/pay/vip", {
+ method: "POST",
+ body: JSON.stringify({
+ package_id: packageId,
+ payment_method: selectedMethod(pane),
+ }),
+ })
+ .then(function (r) {
+ btn.disabled = false;
+ btn.classList.remove("is-loading");
+ if (!r.ok || (r.json && r.json.code && !r.json.success && r.json.success !== true)) {
+ var msg = (r.json && r.json.message) || "开通失败";
+ if (r.status === 401) {
+ msg = "请先登录";
+ document.querySelector(".signin-loader") && document.querySelector(".signin-loader").click();
+ }
+ setTip(pane, msg, true);
+ return;
+ }
+ var data = r.json && r.json.data ? r.json.data : r.json;
+ if (data && data.redirect && (data.payment_url || data.qr_content)) {
+ if (data.payment_url) window.open(data.payment_url, "_blank");
+ setTip(pane, data.message || "请完成支付,支付成功后将自动刷新。");
+ if (data.order_number) pollOrder(data.order_number);
+ return;
+ }
+ setTip(pane, (data && data.message) || "开通成功,正在刷新…");
+ setTimeout(function () {
+ window.location.reload();
+ }, 400);
+ })
+ .catch(function () {
+ btn.disabled = false;
+ btn.classList.remove("is-loading");
+ setTip(pane, "网络错误,请重试", true);
+ });
+ }
+
+ var loading = false;
+
+ function vipPay(trigger) {
+ var level = 1;
+ if (trigger) {
+ level = parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1;
+ }
+ if (loading) return;
+ loading = true;
+ var shell = ensureModalShell();
+ var body = shell.querySelector("[data-aksrt-vip-modal-body]");
+ if (body) body.innerHTML = '加载中…
';
+ openModal();
+
+ api("/pay/vip/modal?vip_level=" + encodeURIComponent(String(level)))
+ .then(function (r) {
+ loading = false;
+ if (r.status === 401) {
+ closeModal();
+ var login = document.querySelector(".signin-loader");
+ if (login) login.click();
+ return;
+ }
+ if (!r.ok || !r.json || !r.json.html) {
+ if (body) {
+ body.innerHTML =
+ '' +
+ ((r.json && r.json.message) || "加载失败") +
+ "
";
+ }
+ return;
+ }
+ if (body) {
+ body.innerHTML = r.json.html;
+ bindModalUi(body);
+ }
+ })
+ .catch(function () {
+ loading = false;
+ if (body) {
+ body.innerHTML = '网络错误
';
+ }
+ });
+ }
+
+ function onDocClick(e) {
+ var t = e.target.closest(".pay-vip");
+ if (!t) return;
+ // Allow real links without intercept if they point elsewhere and lack pay-vip intent.
+ e.preventDefault();
+ e.stopPropagation();
+ vipPay(t);
+ }
+
+ function init() {
+ document.addEventListener("click", onDocClick, true);
+ }
+
+ if (window.AksrtFrontend && typeof window.AksrtFrontend.register === "function") {
+ window.AksrtFrontend.register("aksrt-pay-vip", init);
+ } else if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", init);
+ } else {
+ init();
+ }
+
+ window.aksrtVipPay = vipPay;
+})();
diff --git a/inc/helpers/aksrt-header-menu-buttons.php b/inc/helpers/aksrt-header-menu-buttons.php
index 4b4a907c..67d50542 100644
--- a/inc/helpers/aksrt-header-menu-buttons.php
+++ b/inc/helpers/aksrt-header-menu-buttons.php
@@ -175,12 +175,21 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
}
if (!$is_vip) {
$cls = $user_id > 0 ? 'pay-vip' : 'signin-loader';
+ $hover = '';
+ if (class_exists('\AKSRT\Content\VipService')) {
+ $svc = \AKSRT\Content\VipService::get_instance();
+ if (method_exists($svc, 'render_vip_card')) {
+ $cards = $svc->render_vip_card(1, $user_id) . $svc->render_vip_card(2, $user_id);
+ if ($cards !== '') {
+ $hover = '';
+ }
+ }
+ }
$html .= '';
- }
- }
+ $html .= '' . $hover . '';
+ } }
return $html;
}
diff --git a/inc/helpers/aksrt-vip.php b/inc/helpers/aksrt-vip.php
index 61870490..ffa105d2 100644
--- a/inc/helpers/aksrt-vip.php
+++ b/inc/helpers/aksrt-vip.php
@@ -161,12 +161,16 @@ function aksrt_is_vip2(int $user_id = 0): bool {
*
* @return string
*/
-function aksrt_get_vip_badge_text(): string {
+function aksrt_get_vip_badge_text(int $user_id = 0): string {
if (!class_exists('\AKSRT\Settings\Settings')) {
return 'VIP';
}
$vip_settings = \AKSRT\Settings\Settings::get_payment_vip_settings();
+ $level = $user_id > 0 ? aksrt_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');
}
@@ -344,10 +348,12 @@ function aksrt_render_vip_badge(int $user_id = 0, string $size = 'sm'): string {
return '';
}
- $badge_text = aksrt_get_vip_badge_text();
+ $level = aksrt_get_user_vip_level($user_id);
+ $badge_text = aksrt_get_vip_badge_text($user_id);
$size_class = 'aksrt-vip-badge--' . $size;
+ $level_class = $level >= 2 ? 'aksrt-vip-badge--vip2' : 'aksrt-vip-badge--vip1';
- return ' ' . esc_html($badge_text) . '';
+ return ' ' . esc_html($badge_text) . '';
}
/**
@@ -359,7 +365,7 @@ function aksrt_render_vip_badge(int $user_id = 0, string $size = 'sm'): string {
function aksrt_get_vip_info(int $user_id = 0): array {
$level = aksrt_get_user_vip_level($user_id);
$is_vip = $level >= 1;
- $badge_text = aksrt_get_vip_badge_text();
+ $badge_text = aksrt_get_vip_badge_text($user_id);
$expires = $user_id > 0 ? (int) get_user_meta($user_id, 'aksrt_vip_expires', true) : 0;
$expires_label = __('未开通', 'aksrt');
if ($level > 0) {
diff --git a/src/AKSRT/Content/VipService.php b/src/AKSRT/Content/VipService.php
index 031f0a7f..028fe826 100644
--- a/src/AKSRT/Content/VipService.php
+++ b/src/AKSRT/Content/VipService.php
@@ -11,6 +11,7 @@ class VipService {
use VipServiceRest;
use VipServicePurchase;
use VipServicePackages;
+ use VipServiceModal;
private static $instance = null;
diff --git a/src/AKSRT/Content/VipServiceModal.php b/src/AKSRT/Content/VipServiceModal.php
new file mode 100644
index 00000000..ae3775f1
--- /dev/null
+++ b/src/AKSRT/Content/VipServiceModal.php
@@ -0,0 +1,312 @@
+'
+ . esc_html__('请先登录后再开通会员。', 'aksrt')
+ . '';
+ }
+
+ $vip = class_exists('\AKSRT\Settings\Settings')
+ ? \AKSRT\Settings\Settings::get_payment_vip_settings()
+ : ['enabled' => false];
+ if (empty($vip['enabled'])) {
+ return ''
+ . esc_html__('会员功能未启用。', 'aksrt')
+ . '
';
+ }
+
+ $catalog = $this->get_catalog($user_id);
+ $user_level = (int) ($catalog['user_level'] ?? 0);
+ $prefer_level = max(1, min(2, $prefer_level));
+
+ if ($user_level > 0) {
+ return $this->render_modal_viped($user_id, $catalog, $vip, $prefer_level);
+ }
+ return $this->render_modal_pay($user_id, $catalog, $vip, $prefer_level);
+ }
+
+ /**
+ * Mini hover card for header (zibll zibpay_get_vip_card).
+ */
+ public function render_vip_card(int $level, int $user_id = 0): string {
+ $level = max(1, min(2, $level));
+ $vip = class_exists('\AKSRT\Settings\Settings')
+ ? \AKSRT\Settings\Settings::get_payment_vip_settings()
+ : [];
+ $levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
+ if (empty($levels[$level]['enabled'])) {
+ return '';
+ }
+ $name = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
+ $desc = (string) ($levels[$level]['desc'] ?? '');
+ $equity = (string) ($levels[$level]['equity'] ?? '');
+ $cls = $user_id > 0 ? 'pay-vip' : 'signin-loader';
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ return $html;
+ }
+
+ /**
+ * @param array $catalog
+ * @param array $vip
+ */
+ private function render_modal_pay(int $user_id, array $catalog, array $vip, int $prefer_level): string {
+ $user = get_userdata($user_id);
+ $name = $user ? $user->display_name : '';
+ $avatar = get_avatar($user_id, 42);
+ $desc = (string) ($vip['desc'] ?? $catalog['desc'] ?? '');
+ $more = (string) ($vip['more'] ?? '');
+ $groups = is_array($catalog['level_packages'] ?? null) ? $catalog['level_packages'] : [];
+ $methods = is_array($catalog['payment_methods'] ?? null) ? $catalog['payment_methods'] : [];
+ $levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
+
+ $active_lv = !empty($levels[$prefer_level]['enabled']) ? $prefer_level : 0;
+ if ($active_lv <= 0) {
+ foreach ([1, 2] as $lv) {
+ if (!empty($levels[$lv]['enabled'])) {
+ $active_lv = $lv;
+ break;
+ }
+ }
+ }
+
+ $tabs = '';
+ $panes = '';
+ foreach ([1, 2] as $lv) {
+ if (empty($levels[$lv]['enabled'])) {
+ continue;
+ }
+ $is_active = ($lv === $active_lv);
+ $lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
+ $tabs .= ''
+ . ''
+ . esc_html($lv_name)
+ . '';
+
+ $group_items = [];
+ foreach ($groups as $g) {
+ if ((int) ($g['level'] ?? 0) === $lv) {
+ $group_items = is_array($g['items'] ?? null) ? $g['items'] : [];
+ break;
+ }
+ }
+ $products = $this->render_product_radios($group_items);
+ $equity = (string) ($levels[$lv]['equity'] ?? '');
+ $lv_desc = (string) ($levels[$lv]['desc'] ?? '');
+
+ $panes .= '';
+ }
+
+ $points = '';
+ if (!empty($catalog['points_exchange']['enabled']) && !empty($catalog['points_exchange']['packages'])) {
+ $points = '';
+ }
+
+ $html = '';
+ $html .= '
';
+ $html .= '
- ' . $avatar . '
';
+ $html .= '- ' . esc_html($name) . '';
+ if ($desc !== '') {
+ $html .= '
' . esc_html($desc) . '
';
+ }
+ $html .= '
';
+ $html .= '
';
+ $html .= '
' . $panes . '
';
+ if ($more !== '') {
+ $html .= '
' . $more . '
';
+ }
+ $html .= $points;
+ $html .= '
';
+ return $html;
+ }
+
+ /**
+ * @param array $catalog
+ * @param array $vip
+ */
+ private function render_modal_viped(int $user_id, array $catalog, array $vip, int $prefer_level): string {
+ $user = get_userdata($user_id);
+ $name = $user ? $user->display_name : '';
+ $avatar = get_avatar($user_id, 42);
+ $exp_label = (string) ($catalog['user_expires_label'] ?? '');
+ $groups = is_array($catalog['level_packages'] ?? null) ? $catalog['level_packages'] : [];
+ $methods = is_array($catalog['payment_methods'] ?? null) ? $catalog['payment_methods'] : [];
+ $levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
+ $more = (string) ($vip['more'] ?? '');
+
+ $tabs = '';
+ $panes = '';
+ $active_set = false;
+ foreach ($groups as $g) {
+ $lv = (int) ($g['level'] ?? 0);
+ $action = (string) ($g['action'] ?? 'renew');
+ $items = is_array($g['items'] ?? null) ? $g['items'] : [];
+ if ($lv <= 0 || $items === []) {
+ continue;
+ }
+ $is_active = !$active_set;
+ if ($is_active) {
+ $active_set = true;
+ }
+ $lv_name = (string) ($g['name'] ?? ($levels[$lv]['name'] ?? ('VIP' . $lv)));
+ $badge = $action === 'upgrade' ? __('升级', 'aksrt') : __('续费', 'aksrt');
+ $tab_id = 'tab-payvip-' . $lv . '-' . $action;
+ $tabs .= ''
+ . ''
+ . esc_html($badge . $lv_name)
+ . '';
+ $products = $this->render_product_radios($items);
+ $equity = (string) ($levels[$lv]['equity'] ?? '');
+ $panes .= '';
+ }
+
+ if ($tabs === '') {
+ return ''
+ . esc_html__('当前没有可续费或升级的套餐。', 'aksrt')
+ . '
';
+ }
+
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= '
' . $panes . '
';
+ if ($more !== '') {
+ $html .= '
' . $more . '
';
+ }
+ $html .= '
';
+ return $html;
+ }
+
+ /**
+ * @param array> $items
+ */
+ private function render_product_radios(array $items): string {
+ if ($items === []) {
+ return ''
+ . esc_html__('暂无可用套餐', 'aksrt') . '
';
+ }
+ $html = '';
+ $i = 0;
+ foreach ($items as $item) {
+ $pid = (int) ($item['package_id'] ?? 0);
+ if ($pid <= 0) {
+ continue;
+ }
+ $i++;
+ $price = (string) ($item['price_label'] ?? '');
+ $show = (string) ($item['show_price_label'] ?? '');
+ $tag = (string) ($item['discount_tag'] ?? '');
+ $time = (string) ($item['days_label'] ?? '');
+ $title = (string) ($item['title'] ?? '');
+ $checked = $i === 1 ? ' checked' : '';
+ $html .= '
';
+ }
+ $html .= '
';
+ return $html;
+ }
+
+ /**
+ * @param array $methods
+ */
+ private function render_pay_methods(array $methods): string {
+ if ($methods === []) {
+ return '';
+ }
+ if (count($methods) === 1) {
+ $id = (string) ($methods[0]['id'] ?? 'balance');
+ return '';
+ }
+ $html = '';
+ $html .= '' . esc_html__('支付方式', 'aksrt') . '';
+ foreach ($methods as $i => $m) {
+ $mid = (string) ($m['id'] ?? '');
+ $mname = (string) ($m['name'] ?? $mid);
+ $html .= '';
+ }
+ $html .= '
';
+ return $html;
+ }
+}
diff --git a/src/AKSRT/Content/VipServiceRest.php b/src/AKSRT/Content/VipServiceRest.php
index 091315d3..3997580d 100644
--- a/src/AKSRT/Content/VipServiceRest.php
+++ b/src/AKSRT/Content/VipServiceRest.php
@@ -30,6 +30,40 @@ trait VipServiceRest {
'callback' => [$this, 'rest_points_exchange'],
'permission_callback' => [$this, 'check_rest_nonce'],
]);
+
+ register_rest_route('aksrt/v1', '/pay/vip/modal', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_modal'],
+ 'permission_callback' => [$this, 'check_rest_nonce'],
+ 'args' => [
+ 'vip_level' => [
+ 'type' => 'integer',
+ 'default' => 1,
+ ],
+ ],
+ ]);
+ }
+
+ /**
+ * GET /pay/vip/modal?vip_level=1
+ */
+ public function rest_modal(\WP_REST_Request $request) {
+ $uid = get_current_user_id();
+ if ($uid <= 0) {
+ return new \WP_Error('aksrt_need_login', __('请先登录后再开通会员。', 'aksrt'), [
+ 'status' => 401,
+ 'login_url' => function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(),
+ ]);
+ }
+ $level = (int) $request->get_param('vip_level');
+ if ($level < 1 || $level > 2) {
+ $level = 1;
+ }
+ $html = $this->render_modal_html($uid, $level);
+ return new \WP_REST_Response([
+ 'html' => $html,
+ 'user_level' => function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($uid) : 0,
+ ], 200);
}
diff --git a/src/AKSRT/Core/ScriptsPart1b.php b/src/AKSRT/Core/ScriptsPart1b.php
index 8deaf0cf..953db435 100644
--- a/src/AKSRT/Core/ScriptsPart1b.php
+++ b/src/AKSRT/Core/ScriptsPart1b.php
@@ -161,6 +161,31 @@ trait ScriptsPart1b {
);
}
+ // VIP modal (zibll .pay-vip)
+ $vip_on = class_exists('\AKSRT\Settings\Settings')
+ && !empty(\AKSRT\Settings\Settings::get_payment_vip_settings()['enabled']);
+ if ($vip_on) {
+ $vip_css = get_template_directory() . '/assets/css/frontend/vip-modal.css';
+ if (file_exists($vip_css)) {
+ wp_enqueue_style(
+ 'aksrt-vip-modal',
+ $base_uri . '/assets/css/frontend/vip-modal.css',
+ [$core_dep],
+ (string) filemtime($vip_css)
+ );
+ }
+ $vip_js = get_template_directory() . '/assets/js/pay-vip.js';
+ if (file_exists($vip_js)) {
+ wp_enqueue_script(
+ 'aksrt-pay-vip',
+ $base_uri . '/assets/js/pay-vip.js',
+ ['aksrt-script'],
+ (string) filemtime($vip_js),
+ true
+ );
+ }
+ }
+
// Print stylesheet — only load on singular pages
if (is_singular()) {