feat(vip): Batch 2 pay-vip modal REST, JS, and CSS
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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 =
|
||||
'<div class="aksrt-vip-modal-dialog" role="dialog" aria-modal="true">' +
|
||||
'<div class="aksrt-vip-modal-content" data-aksrt-vip-modal-body></div></div>';
|
||||
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 = '<div class="text-center padding-20 muted-2-color">加载中…</div>';
|
||||
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 =
|
||||
'<div class="text-center padding-20 c-red">' +
|
||||
((r.json && r.json.message) || "加载失败") +
|
||||
"</div>";
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (body) {
|
||||
body.innerHTML = r.json.html;
|
||||
bindModalUi(body);
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
loading = false;
|
||||
if (body) {
|
||||
body.innerHTML = '<div class="text-center padding-20 c-red">网络错误</div>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
})();
|
||||
@@ -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 = '<div class="sub-menu hover-show-con sub-vip-card">' . $cards . '</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
$html .= '<div class="navbar-form navbar-right navbar-vip hover-show inline-block">';
|
||||
$html .= '<a class="but jb-red radius payvip-icon ml10 ' . esc_attr($cls) . '" href="javascript:;"' . ($user_id > 0 ? ' vip-level="1"' : '') . '>';
|
||||
$html .= '<i class="fa fa-diamond em12 mr3" aria-hidden="true"></i>' . esc_html__('开通会员', 'aksrt');
|
||||
$html .= '</a></div>';
|
||||
}
|
||||
}
|
||||
$html .= '</a>' . $hover . '</div>';
|
||||
} }
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
@@ -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 '<span class="aksrt-vip-badge aksrt-vip-badge--gold ' . esc_attr($size_class) . '"><i class="fas fa-crown"></i> ' . esc_html($badge_text) . '</span>';
|
||||
return '<span class="aksrt-vip-badge aksrt-vip-badge--gold ' . esc_attr($size_class . ' ' . $level_class) . '"><i class="fas fa-crown"></i> ' . esc_html($badge_text) . '</span>';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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) {
|
||||
|
||||
@@ -11,6 +11,7 @@ class VipService {
|
||||
use VipServiceRest;
|
||||
use VipServicePurchase;
|
||||
use VipServicePackages;
|
||||
use VipServiceModal;
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
/**
|
||||
* VIP modal + card HTML (zibll pay-vip modal / vip-card parity).
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Content;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait VipServiceModal {
|
||||
|
||||
/**
|
||||
* Build pay / renew / upgrade modal HTML for a user.
|
||||
*/
|
||||
public function render_modal_html(int $user_id, int $prefer_level = 1): string {
|
||||
if ($user_id <= 0) {
|
||||
return '<div class="box-body payvip-modal text-center padding-20">'
|
||||
. esc_html__('请先登录后再开通会员。', 'aksrt')
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
$vip = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::get_payment_vip_settings()
|
||||
: ['enabled' => false];
|
||||
if (empty($vip['enabled'])) {
|
||||
return '<div class="box-body payvip-modal text-center padding-20">'
|
||||
. esc_html__('会员功能未启用。', 'aksrt')
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
$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 = '<div class="vip-card pointer level-' . $level . ' vip-theme' . $level . ' ' . esc_attr($cls) . '" vip-level="' . $level . '">';
|
||||
$html .= '<div class="abs-center vip-baicon"><i class="fa fa-diamond" aria-hidden="true"></i></div>';
|
||||
$html .= '<div class="relative">';
|
||||
$html .= '<div class="vip-name font-bold">' . esc_html($name) . '</div>';
|
||||
if ($desc !== '') {
|
||||
$html .= '<div class="muted-2-color em09 mb6">' . esc_html($desc) . '</div>';
|
||||
}
|
||||
if ($equity !== '') {
|
||||
$html .= '<ul class="mb10 relative em09 v-equity">' . $equity . '</ul>';
|
||||
}
|
||||
$html .= '<a class="but jb-blue radius payvip-button" href="javascript:;">'
|
||||
. esc_html(sprintf(__('开通%s', 'aksrt'), $name))
|
||||
. '</a>';
|
||||
$html .= '</div></div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $catalog
|
||||
* @param array<string,mixed> $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 .= '<li class="relative' . ($is_active ? ' active' : '') . '">'
|
||||
. '<a class="aksrt-vip-tab" href="javascript:;" data-vip-tab="tab-payvip-' . $lv . '">'
|
||||
. esc_html($lv_name)
|
||||
. '</a></li>';
|
||||
|
||||
$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 .= '<div class="tab-pane aksrt-vip-pane' . ($is_active ? ' active' : '') . '" id="tab-payvip-' . $lv . '"' . ($is_active ? '' : ' hidden') . '>';
|
||||
$panes .= '<div class="pay-vip-form-row">';
|
||||
$panes .= '<div class="pay-vip-info level-' . $lv . '">';
|
||||
$panes .= '<div class="v-name font-bold">' . esc_html($lv_name) . '</div>';
|
||||
if ($lv_desc !== '') {
|
||||
$panes .= '<div class="muted-2-color em09 mb6">' . esc_html($lv_desc) . '</div>';
|
||||
}
|
||||
if ($equity !== '') {
|
||||
$panes .= '<ul class="v-equity">' . $equity . '</ul>';
|
||||
}
|
||||
$panes .= '</div><div class="pay-vip-products">';
|
||||
$panes .= $products;
|
||||
$panes .= $this->render_pay_methods($methods);
|
||||
$panes .= '<button type="button" class="but jb-red btn-block padding-lg radius aksrt-vip-submit" data-vip-submit>'
|
||||
. esc_html__('立即开通', 'aksrt') . '</button>';
|
||||
$panes .= '<p class="aksrt-vip-modal-tip muted-2-color em09 mt10" data-aksrt-vip-tip></p>';
|
||||
$panes .= '</div></div></div>';
|
||||
}
|
||||
|
||||
$points = '';
|
||||
if (!empty($catalog['points_exchange']['enabled']) && !empty($catalog['points_exchange']['packages'])) {
|
||||
$points = '<div class="mt10"><a class="but c-green btn-block" href="'
|
||||
. esc_url(function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('vip') : '#')
|
||||
. '">' . esc_html__('积分兑换会员', 'aksrt') . '</a></div>';
|
||||
}
|
||||
|
||||
$html = '<div class="box-body payvip-modal" data-aksrt-vip-modal>';
|
||||
$html .= '<button type="button" class="close aksrt-vip-modal-close" aria-label="Close">×</button>';
|
||||
$html .= '<ul class="list-inline user-box flex ac mb10"><li class="avatar-img">' . $avatar . '</li>';
|
||||
$html .= '<li><b class="em12">' . esc_html($name) . '</b>';
|
||||
if ($desc !== '') {
|
||||
$html .= '<div class="muted-2-color em09">' . esc_html($desc) . '</div>';
|
||||
}
|
||||
$html .= '</li></ul>';
|
||||
$html .= '<ul class="flex jsb mt10 mb20 vip-cardminis gutters-5 nav">' . $tabs . '</ul>';
|
||||
$html .= '<div class="tab-content mt10">' . $panes . '</div>';
|
||||
if ($more !== '') {
|
||||
$html .= '<div class="muted-2-color mb10 em09 muted-box padding-10 mini-radius">' . $more . '</div>';
|
||||
}
|
||||
$html .= $points;
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $catalog
|
||||
* @param array<string,mixed> $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 .= '<li class="relative' . ($is_active ? ' active' : '') . '">'
|
||||
. '<a class="aksrt-vip-tab" href="javascript:;" data-vip-tab="' . esc_attr($tab_id) . '">'
|
||||
. esc_html($badge . $lv_name)
|
||||
. '</a></li>';
|
||||
$products = $this->render_product_radios($items);
|
||||
$equity = (string) ($levels[$lv]['equity'] ?? '');
|
||||
$panes .= '<div class="tab-pane aksrt-vip-pane' . ($is_active ? ' active' : '') . '" id="' . esc_attr($tab_id) . '"' . ($is_active ? '' : ' hidden') . '>';
|
||||
$panes .= '<div class="pay-vip-form-row">';
|
||||
$panes .= '<div class="pay-vip-info level-' . $lv . '">';
|
||||
$panes .= '<div class="v-name font-bold">' . esc_html($lv_name) . '</div>';
|
||||
if ($equity !== '') {
|
||||
$panes .= '<ul class="v-equity">' . $equity . '</ul>';
|
||||
}
|
||||
$panes .= '</div><div class="pay-vip-products">';
|
||||
$panes .= $products;
|
||||
$panes .= $this->render_pay_methods($methods);
|
||||
$cta = $action === 'upgrade' ? __('立即升级', 'aksrt') : __('立即续费', 'aksrt');
|
||||
$panes .= '<button type="button" class="but jb-red btn-block padding-lg radius aksrt-vip-submit" data-vip-submit>'
|
||||
. esc_html($cta) . '</button>';
|
||||
$panes .= '<p class="aksrt-vip-modal-tip muted-2-color em09 mt10" data-aksrt-vip-tip></p>';
|
||||
$panes .= '</div></div></div>';
|
||||
}
|
||||
|
||||
if ($tabs === '') {
|
||||
return '<div class="box-body payvip-modal text-center padding-20">'
|
||||
. esc_html__('当前没有可续费或升级的套餐。', 'aksrt')
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
$html = '<div class="box-body payvip-modal" data-aksrt-vip-modal>';
|
||||
$html .= '<button type="button" class="close aksrt-vip-modal-close" aria-label="Close">×</button>';
|
||||
$html .= '<ul class="list-inline user-box flex ac mb10"><li class="avatar-img">' . $avatar . '</li>';
|
||||
$html .= '<li><b class="em12">' . esc_html($name) . '</b>';
|
||||
$html .= '<div class="muted-2-color em09"><span class="badg c-yellow vip-expdate-tag">'
|
||||
. esc_html($exp_label) . '</span></div></li></ul>';
|
||||
$html .= '<ul class="flex jsb mt10 mb20 vip-cardminis gutters-5 nav">' . $tabs . '</ul>';
|
||||
$html .= '<div class="tab-content mt10">' . $panes . '</div>';
|
||||
if ($more !== '') {
|
||||
$html .= '<div class="muted-2-color mb10 em09 muted-box padding-10 mini-radius">' . $more . '</div>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array<string,mixed>> $items
|
||||
*/
|
||||
private function render_product_radios(array $items): string {
|
||||
if ($items === []) {
|
||||
return '<div class="muted-2-color text-center padding-10">'
|
||||
. esc_html__('暂无可用套餐', 'aksrt') . '</div>';
|
||||
}
|
||||
$html = '<div class="mb10 label-box charge-box aksrt-vip-products">';
|
||||
$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 .= '<label class="aksrt-vip-product-label">';
|
||||
$html .= '<input class="hide vip-product-input" type="radio" name="vip_product_id" value="'
|
||||
. esc_attr((string) $pid) . '"' . $checked . '>';
|
||||
$html .= '<div class="zib-widget vip-product relative text-center product-box">';
|
||||
if ($tag !== '') {
|
||||
$html .= '<div class="abs-right vip-tag badg jb-yellow">' . esc_html($tag) . '</div>';
|
||||
}
|
||||
if ($title !== '') {
|
||||
$html .= '<div class="em09 mb3">' . esc_html($title) . '</div>';
|
||||
}
|
||||
$html .= '<div class="flex abl gap6 jc"><div class="product-price c-red">' . esc_html($price) . '</div>';
|
||||
if ($show !== '') {
|
||||
$html .= '<div class="original-price relative muted-2-color"><s>' . esc_html($show) . '</s></div>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
if ($time !== '') {
|
||||
$html .= '<div class="muted-2-color c-blue em09 p2-10 badg radius">' . esc_html($time) . '</div>';
|
||||
}
|
||||
$html .= '</div></label>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array{id?:string,name?:string}> $methods
|
||||
*/
|
||||
private function render_pay_methods(array $methods): string {
|
||||
if ($methods === []) {
|
||||
return '<input type="hidden" name="aksrt_vip_pay_method" value="balance" data-aksrt-vip-method-fixed>';
|
||||
}
|
||||
if (count($methods) === 1) {
|
||||
$id = (string) ($methods[0]['id'] ?? 'balance');
|
||||
return '<input type="hidden" name="aksrt_vip_pay_method" value="' . esc_attr($id) . '" data-aksrt-vip-method-fixed>';
|
||||
}
|
||||
$html = '<div class="uc-vip-shop__methods mb10" data-aksrt-vip-methods>';
|
||||
$html .= '<span class="uc-vip-shop__methods-label">' . esc_html__('支付方式', 'aksrt') . '</span>';
|
||||
foreach ($methods as $i => $m) {
|
||||
$mid = (string) ($m['id'] ?? '');
|
||||
$mname = (string) ($m['name'] ?? $mid);
|
||||
$html .= '<label class="uc-vip-shop__method"><input type="radio" name="aksrt_vip_pay_method" value="'
|
||||
. esc_attr($mid) . '"' . ($i === 0 ? ' checked' : '') . '> <span>'
|
||||
. esc_html($mname) . '</span></label>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user