diff --git a/assets/admin/src/schema/sections/payment/vip.js b/assets/admin/src/schema/sections/payment/vip.js index 8958a2ca..6a8ef4a4 100644 --- a/assets/admin/src/schema/sections/payment/vip.js +++ b/assets/admin/src/schema/sections/payment/vip.js @@ -2,14 +2,24 @@ export const vipSchema = { key: 'payment.vip', title: 'VIP会员', icon: 'fas fa-crown', - description: '对齐 Zibll 双级会员:pay_user_vip_1 / pay_user_vip_2 与下载权益。', + description: '对齐 Zibll 双级会员:分档套餐、权益、续费/升级与积分兑换。', defaults: { pay_user_vip_1_s: true, pay_user_vip_1_name: '一级会员', + pay_user_vip_1_desc: '尊享一级会员特权', + pay_user_vip_1_equity: '
  • 免费下载部分资源
  • 会员专属身份标识
  • 付费内容会员价
  • ', + vip_1_product_s: true, + payment_vip_1_products: '30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡', + 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: '
  • 更高免费下载额度
  • 更高等级身份标识
  • 更多付费内容会员价
  • ', + 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_2_img_icon: '', pay_user_vip_desc: '', - payment_vip_packages: '30|30|月卡\n365|365|年卡', + pay_user_vip_more: '', pay_download_limit: 0, pay_download_speed: 0, pay_download_limit_vip_1: 0, @@ -19,6 +29,9 @@ export const vipSchema = { pay_vip_pass_charge_s: false, pay_vip_pass_charge_desc: '', pay_vip_pass_charge_only_password: true, + pay_vip_points_exchange_s: false, + pay_vip_points_exchange_desc: '', + payment_vip_points_packages: '1|500|30|积分月卡\n1|5000|0|积分永久', vip_renew: true, vip_renew_desc: '', vip_renew_price_type: 'discount', @@ -26,7 +39,7 @@ export const vipSchema = { vip_renew_reduce: 20, vip_upgrade: true, vip_upgrade_desc: '', - vip_upgrade_product: '', + vip_upgrade_product: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级', }, groups: [ { @@ -36,17 +49,58 @@ export const vipSchema = { fields: [ { 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: 'vip_1_img_icon', label: '一级会员图标 URL', type: 'input', default: '', dependsOn: { key: 'pay_user_vip_1_s', value: true } }, { 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_desc', label: '会员说明', type: 'textarea', default: '' }, + { 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: 'vip_2_img_icon', label: '二级会员图标 URL', type: 'input', default: '', dependsOn: { key: 'pay_user_vip_2_s', value: true } }, + { key: 'pay_user_vip_desc', label: '弹窗顶部会员说明', type: 'textarea', default: '' }, + { key: 'pay_user_vip_more', label: '弹窗底部补充说明 HTML', type: 'textarea', default: '' }, ], }, { key: 'packages', - title: '会员套餐', + title: '分档套餐', icon: 'fas fa-gem', fields: [ - { key: 'payment_vip_packages', label: '套餐列表', type: 'textarea', default: '30|30|月卡\n365|365|年卡', help: '每行一个套餐:价格|时长(天)|标题' }, + { key: 'vip_1_product_s', label: '允许购买一级会员', type: 'switch', default: true, dependsOn: { key: 'pay_user_vip_1_s', value: true } }, + { + key: 'payment_vip_1_products', + label: '一级会员套餐', + type: 'textarea', + default: '30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡', + dependsOn: { key: 'vip_1_product_s', value: true }, + help: '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久', + }, + { key: 'vip_2_product_s', label: '允许购买二级会员', type: 'switch', default: true, dependsOn: { key: 'pay_user_vip_2_s', value: true } }, + { + key: 'payment_vip_2_products', + label: '二级会员套餐', + type: 'textarea', + default: '58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡', + dependsOn: { key: 'vip_2_product_s', value: true }, + help: '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久', + }, + ], + }, + { + key: 'points_exchange', + title: '积分兑换会员', + icon: 'fas fa-coins', + fields: [ + { key: 'pay_vip_points_exchange_s', label: '启用积分兑换', type: 'switch', default: false }, + { + 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 } }, ], }, { @@ -73,7 +127,7 @@ export const vipSchema = { { key: 'vip_renew', label: '允许续费', type: 'switch', default: true }, { key: 'vip_renew_desc', label: '续费说明', type: 'textarea', default: '', dependsOn: { key: 'vip_renew', value: true } }, { key: 'vip_renew_price_type', label: '续费价格类型', type: 'radio', default: 'discount', options: [{ label: '相同价格', value: 'same' }, { label: '折扣价格', value: 'discount' }, { label: '优惠立减', value: 'reduce' }], dependsOn: { key: 'vip_renew', value: true } }, - { key: 'vip_renew_discount', label: '续费折扣(%)', type: 'number', default: 8, min: 1, max: 100, dependsOn: { key: 'vip_renew_price_type', value: 'discount' } }, + { key: 'vip_renew_discount', label: '续费折扣(折,如 8=八折)', type: 'number', default: 8, min: 1, max: 100, dependsOn: { key: 'vip_renew_price_type', value: 'discount' } }, { key: 'vip_renew_reduce', label: '续费减免金额', type: 'number', default: 20, min: 0, max: 9999, dependsOn: { key: 'vip_renew_price_type', value: 'reduce' } }, ], }, @@ -88,9 +142,9 @@ export const vipSchema = { key: 'vip_upgrade_product', label: '升级产品', type: 'textarea', - default: '', + default: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级', dependsOn: { key: 'vip_upgrade', value: true }, - help: '每行一条。永久VIP1升级:permanent|价格|标题;按剩余天:unit|每日单价|标题;限时跳转永久:jump|价格|标题;兼容旧格式:价格|天数|标题', + help: '每行一条。永久VIP1升级:permanent|价格|标题;按剩余天:unit|每日单价|标题;限时跳转永久:jump|价格|标题', }, ], }, 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/css/routes/user-center/credits.css b/assets/css/routes/user-center/credits.css index 9f4b1dbe..e328f3e8 100644 --- a/assets/css/routes/user-center/credits.css +++ b/assets/css/routes/user-center/credits.css @@ -1075,3 +1075,13 @@ body.uc-modal-open { height: 100%; background: linear-gradient(90deg, #22c55e, #16a34a); } + +/* VIP tab */ +.uc-vip-tab .user-content-myvip{position:relative;padding:16px;margin-bottom:16px} +.uc-vip-level-cards{display:flex;flex-wrap:wrap;gap:12px} +.uc-vip-level-card{position:relative;flex:1 1 240px;padding:18px 14px;overflow:hidden} +.uc-vip-level-card .v-equity{text-align:left;margin:10px 0 14px;padding-left:1.1em;font-size:13px;line-height:1.55} +.uc-vip-level-card .pay-vip{display:inline-block;margin-top:4px} +.uc-vip-assets-entry{gap:12px} +.jb-vip1{background:linear-gradient(90deg,#f6c453,#f39c12);color:#3b2a00;border:0} +.jb-vip2{background:linear-gradient(90deg,#8e44ad,#3f51b5);color:#fff;border:0} 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/header.php b/header.php index e80f6e78..fcbba869 100644 --- a/header.php +++ b/header.php @@ -276,8 +276,8 @@ if (!empty($aksrt_bg_styles)) { ?> - @@ -513,8 +513,8 @@ if (!empty($aksrt_bg_styles)) { diff --git a/inc/helpers/aksrt-header-menu-buttons.php b/inc/helpers/aksrt-header-menu-buttons.php index 87686a16..67d50542 100644 --- a/inc/helpers/aksrt-header-menu-buttons.php +++ b/inc/helpers/aksrt-header-menu-buttons.php @@ -161,13 +161,12 @@ if (!function_exists('aksrt_render_header_menu_buttons')) : } } - // VIP open CTA for guests / non-VIP. - $user_id = get_current_user_id(); - $vip_url = function_exists('aksrt_get_vip_purchase_url') ? aksrt_get_vip_purchase_url() : (function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('vip') : '#'); - $show_vip = true; - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_vip_settings')) { - $vs = \AKSRT\Settings\Settings::get_vip_settings(); - $show_vip = !empty($vs['enabled']) || !empty($vs['vip1_enabled']) || !empty($vs['vip2_enabled']); + // VIP open CTA for guests / non-VIP (zibll: .pay-vip modal). + $user_id = get_current_user_id(); + $show_vip = false; + if (class_exists('\AKSRT\Settings\Settings')) { + $vs = \AKSRT\Settings\Settings::get_payment_vip_settings(); + $show_vip = !empty($vs['enabled']); } if ($show_vip) { $is_vip = false; @@ -175,13 +174,22 @@ if (!function_exists('aksrt_render_header_menu_buttons')) : $is_vip = (int) aksrt_get_user_vip_level($user_id) > 0; } if (!$is_vip) { - $cls = $user_id ? 'pay-vip' : 'signin-loader'; + $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-header-userbox.php b/inc/helpers/aksrt-header-userbox.php index 0f293fa9..c5db481b 100644 --- a/inc/helpers/aksrt-header-userbox.php +++ b/inc/helpers/aksrt-header-userbox.php @@ -85,7 +85,7 @@ if (!function_exists('aksrt_header_user_wallet_mini')) : if (class_exists('\AKSRT\Credits\Balance')) { $wallet = (float) \AKSRT\Credits\Balance::balance($user_id); } - $center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('assets') : '#'; + $center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('credits') : '#'; $html = ''; $html .= ' ' @@ -116,29 +116,44 @@ endif; if (!function_exists('aksrt_header_payvip_bar')) : /** - * VIP status / open CTA bar. + * VIP status / open CTA bar (zibll zib_get_header_payvip_icon parity). */ function aksrt_header_payvip_bar(int $user_id = 0): string { $user_id = $user_id > 0 ? $user_id : get_current_user_id(); if ($user_id <= 0) { return ''; } - $badge = function_exists('aksrt_render_vip_badge') ? aksrt_render_vip_badge($user_id, 'md') : ''; - if ($badge !== '') { - return '
    ' . $badge . '
    '; - } - // Guest-style open VIP for non-members if VIP feature on. - $vip_url = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('vip') : '#'; - $enabled = true; - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_vip_settings')) { - $vs = \AKSRT\Settings\Settings::get_vip_settings(); - $enabled = !empty($vs['enabled']) || !empty($vs['vip1_enabled']) || !empty($vs['vip2_enabled']); - } - if (!$enabled) { + + $vip = class_exists('\AKSRT\Settings\Settings') + ? \AKSRT\Settings\Settings::get_payment_vip_settings() + : ['enabled' => false, 'levels' => []]; + if (empty($vip['enabled'])) { return ''; } + + $level = function_exists('aksrt_get_user_vip_level') ? (int) aksrt_get_user_vip_level($user_id) : 0; + if ($level > 0) { + $levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : []; + $name = (string) ($levels[$level]['name'] ?? ($level === 2 ? __('二级会员', 'aksrt') : __('一级会员', 'aksrt'))); + $exp_label = ''; + if (function_exists('aksrt_get_vip_info')) { + $info = aksrt_get_vip_info($user_id); + $exp_label = (string) ($info['expires_label'] ?? ''); + } + $html = '
    '; + $html .= ''; + $html .= ''; + $html .= '' . esc_html($name) . ''; + if ($exp_label !== '') { + $html .= '' + . esc_html($exp_label) . ''; + } + $html .= '
    '; + return $html; + } + return '
    ' - . '' + . '' . '' . esc_html__('开通会员 尊享会员权益', 'aksrt') . '
    '; diff --git a/inc/helpers/aksrt-runtime-win.php b/inc/helpers/aksrt-runtime-win.php index 354c2e0d..9771cd90 100644 --- a/inc/helpers/aksrt-runtime-win.php +++ b/inc/helpers/aksrt-runtime-win.php @@ -57,10 +57,10 @@ if (!function_exists('aksrt_get_float_pay_vip_html')) : * Floating VIP open button. */ function aksrt_get_float_pay_vip_html(array $item = []): string { - $show_vip = true; - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_vip_settings')) { - $vs = \AKSRT\Settings\Settings::get_vip_settings(); - $show_vip = !empty($vs['enabled']) || !empty($vs['vip1_enabled']) || !empty($vs['vip2_enabled']); + $show_vip = false; + if (class_exists('\AKSRT\Settings\Settings')) { + $vs = \AKSRT\Settings\Settings::get_payment_vip_settings(); + $show_vip = !empty($vs['enabled']); } if (!$show_vip) { return ''; @@ -74,13 +74,11 @@ if (!function_exists('aksrt_get_float_pay_vip_html')) : if ($level >= 2) { return ''; } - $vip_url = function_exists('aksrt_get_vip_purchase_url') - ? aksrt_get_vip_purchase_url() - : (function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('vip') : '#'); $title = $level === 1 ? __('升级会员', 'aksrt') : __('开通会员', 'aksrt'); $icon = !empty($item['icon']) ? (string) $item['icon'] : 'fa fa-diamond'; $cls = $user_id > 0 ? 'float-btn pay-vip' : 'float-btn signin-loader'; - $href = $user_id > 0 ? $vip_url : 'javascript:;'; + // Modal path: .pay-vip opens VIP modal (Batch 2+); guests use signin-loader. + $href = 'javascript:;'; $style = []; if (!empty($item['color'])) { $style[] = '--this-color:' . esc_attr((string) $item['color']); @@ -89,7 +87,8 @@ if (!function_exists('aksrt_get_float_pay_vip_html')) : $style[] = '--this-bg:' . esc_attr((string) $item['bg']); } $style_attr = $style ? ' style="' . implode(';', $style) . '"' : ''; - return '' + $vip_level_attr = $user_id > 0 ? ' vip-level="' . esc_attr((string) max(1, min(2, $level + 1))) . '"' : ''; + return '' . '' . ''; } @@ -152,28 +151,3 @@ if (!function_exists('aksrt_get_win_runtime_config')) : ]; } endif; - -if (!function_exists('aksrt_print_win_var')) : - /** - * Echo window._win + alias window.aksrtData bridge. - */ - function aksrt_print_win_var(): void { - static $done = false; - if ($done) { - return; - } - $done = true; - $cfg = aksrt_get_win_runtime_config(); - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo '' . "\n"; - } -endif; diff --git a/inc/helpers/aksrt-user-center.php b/inc/helpers/aksrt-user-center.php index 911521af..973cc8e7 100644 --- a/inc/helpers/aksrt-user-center.php +++ b/inc/helpers/aksrt-user-center.php @@ -169,6 +169,7 @@ if (!function_exists('aksrt_capture_user_center_view')) { 'comments' => 'comments', 'favorites' => 'favorites', 'credits' => 'credits', + 'vip' => 'vip', 'settings' => 'settings', 'header' => 'header', 'sidebar' => 'sidebar', @@ -193,7 +194,7 @@ if (!function_exists('aksrt_capture_user_center_panel')) { * Capture a built-in or registered user-center panel body. */ function aksrt_capture_user_center_panel(string $tab_key, array $tab, array $context): string { - $built_in_tabs = ['profile', 'posts', 'comments', 'favorites', 'credits', 'settings']; + $built_in_tabs = ['profile', 'posts', 'comments', 'favorites', 'credits', 'vip', 'settings']; if (in_array($tab_key, $built_in_tabs, true)) { return aksrt_capture_user_center_view($tab_key, $context); } @@ -296,10 +297,13 @@ if (!function_exists('aksrt_user_center_resolve_tabs')) { $keys = array_keys($tabs); $default = in_array('profile', $keys, true) ? 'profile' : ($keys[0] ?? 'profile'); $active = isset($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : $default; - if ('signin' === $active) { + if ('signin' === $active || 'assets' === $active) { $active = 'credits'; } elseif ('invite' === $active) { $active = 'settings'; + } elseif ('vip' === $active) { + // Prefer dedicated vip tab when registered; else credits (VIP shop). + $active = in_array('vip', $keys, true) ? 'vip' : 'credits'; } if (!in_array($active, $keys, true)) { $active = $default; @@ -353,7 +357,13 @@ if (!function_exists('aksrt_user_center_runtime_config')) { 'userId' => $user_id, 'defaultTab' => $default_tab, 'activeTab' => $active_tab, - 'tabAliases' => ['signin' => 'credits', 'invite' => 'settings'], + 'tabAliases' => [ + 'signin' => 'credits', + 'invite' => 'settings', + 'assets' => 'credits', + // vip → vip when panel exists; SPA may still map missing keys via server resolve. + 'vip' => 'vip', + ], 'tabUrls' => $tab_urls, 'i18n' => [ 'contactUrlPlaceholder' => __('链接 URL', 'aksrt'), diff --git a/inc/helpers/aksrt-vip.php b/inc/helpers/aksrt-vip.php index c8e9f927..e1987214 100644 --- a/inc/helpers/aksrt-vip.php +++ b/inc/helpers/aksrt-vip.php @@ -65,7 +65,8 @@ function aksrt_get_user_vip_level(int $user_id = 0): int { if ($meta_level > 0) { if ($expires > 0 && $expires < time()) { - // 过期:lazy 清 level,与 capabilities 读 meta 对齐。 + // 过期:lazy 清 level,记录上次等级(zibll vip_level_expired)。 + update_user_meta($user_id, 'aksrt_vip_level_expired', min(2, max(1, $meta_level))); update_user_meta($user_id, 'aksrt_vip_level', 0); $candidate = 0; } else { @@ -82,16 +83,36 @@ function aksrt_get_user_vip_level(int $user_id = 0): int { : null; if (is_array($valid_order) && !empty($valid_order)) { - $order_expires = trim((string) ($valid_order['expires_at'] ?? '')); - $order_expired = false; - if ($order_expires !== '') { - $ts = strtotime($order_expires); - if ($ts !== false && $ts < time()) { - $order_expired = true; + // Order.expires_at is payment checkout deadline, not VIP membership expiry. + // Prefer note/meta_payload level + aksrt_vip_expires if present. + $meta = []; + if (!empty($valid_order['meta_payload']) && is_array($valid_order['meta_payload'])) { + $meta = $valid_order['meta_payload']; + } elseif (!empty($valid_order['note']) && is_string($valid_order['note'])) { + $decoded = json_decode($valid_order['note'], true); + if (is_array($decoded)) { + $meta = $decoded; } } - if (!$order_expired) { - // 套餐无 level 列:有效 VIP 订单至少视为 VIP1。 + + $order_level = (int) ($meta['level'] ?? 0); + if ($order_level <= 0 && !empty($valid_order['product_id']) && class_exists('\AKSRT\Content\VipService')) { + // package_id encodes level: decode if possible via service helpers. + $pkg_id = (int) $valid_order['product_id']; + // Encode scheme: level * 1000 + index (see VipServicePackages). + if ($pkg_id >= 1000) { + $order_level = (int) floor($pkg_id / 1000); + } elseif ($pkg_id > 0) { + $order_level = 1; + } + } + $order_level = max(0, min(2, $order_level)); + + // Membership still valid if user meta expires is in future or permanent (0 with prior grant). + // Without restored meta, treat paid VIP order as at least the ordered level. + if ($order_level > 0) { + $candidate = $order_level; + } else { $candidate = 1; } } @@ -140,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'); } @@ -323,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) . ''; } /** @@ -338,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) { @@ -380,10 +407,10 @@ function aksrt_get_vip_info(int $user_id = 0): array { if (!function_exists('aksrt_get_vip_purchase_url')) { function aksrt_get_vip_purchase_url(): string { if (function_exists('aksrt_get_user_center_tab_url')) { - return aksrt_get_user_center_tab_url('credits'); + return aksrt_get_user_center_tab_url('vip'); } if (function_exists('aksrt_get_user_center_url')) { - return aksrt_get_user_center_url(); + return aksrt_get_user_center_url('vip'); } return home_url('/'); } diff --git a/inc/helpers/user-center-renderers/profile-credits.php b/inc/helpers/user-center-renderers/profile-credits.php index 8bd30da1..7bbd7b98 100644 --- a/inc/helpers/user-center-renderers/profile-credits.php +++ b/inc/helpers/user-center-renderers/profile-credits.php @@ -39,7 +39,15 @@ if ($show_wallet || $show_credits) : endif; if (!empty($show_vip)) { - require $dir . '/credits-vip.php'; + // Slim entry to dedicated VIP tab (full shop lives under tab=vip). + $vip_url = function_exists('aksrt_get_user_center_tab_url') + ? aksrt_get_user_center_tab_url('vip') + : '#'; + echo '
    '; + echo '
    ' . esc_html__('我的会员', 'aksrt') . '
    '; + echo '
    ' . esc_html__('查看权益、续费与升级', 'aksrt') . '
    '; + echo '' . esc_html__('进入会员中心', 'aksrt') . ''; + echo '
    '; } if ($show_credits) { require $dir . '/credits-signin.php'; diff --git a/inc/helpers/user-center-renderers/profile-vip.php b/inc/helpers/user-center-renderers/profile-vip.php new file mode 100644 index 00000000..f8ea303e --- /dev/null +++ b/inc/helpers/user-center-renderers/profile-vip.php @@ -0,0 +1,13 @@ + - + diff --git a/inc/helpers/user-center-renderers/vip-tab.php b/inc/helpers/user-center-renderers/vip-tab.php new file mode 100644 index 00000000..22b2e232 --- /dev/null +++ b/inc/helpers/user-center-renderers/vip-tab.php @@ -0,0 +1,152 @@ +' . esc_html__('会员功能未启用。', 'aksrt') . ''; + return; +} + +$user_id = (int) ($user_id ?? get_current_user_id()); +$level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0; +$expires = (int) get_user_meta($user_id, 'aksrt_vip_expires', true); +$expired_level = (int) get_user_meta($user_id, 'aksrt_vip_level_expired', true); +$levels = is_array($vip_settings['levels'] ?? null) ? $vip_settings['levels'] : []; +$desc_top = (string) ($vip_settings['desc'] ?? ''); + +$user = get_userdata($user_id); +$display = $user ? $user->display_name : ''; +$avatar = get_avatar($user_id, 56); + +// Status header box. +$status_html = ''; +$btn_type = 'pay'; +if ($level > 0) { + $lv_name = (string) ($levels[$level]['name'] ?? ('VIP' . $level)); + $lv_desc = (string) ($levels[$level]['desc'] ?? ''); + if ($expires <= 0) { + $badge = '' . esc_html__('永久会员', 'aksrt') . ''; + if ($level < 2) { + $btn_type = 'upgrade'; + } + } else { + $badge = '' + . esc_html(sprintf(__('到期时间:%s', 'aksrt'), wp_date('Y年m月d日', $expires))) . ''; + $btn_type = 'renew_' . $level; + } + $status_html = '
    ' + . '
    ' . esc_html($lv_name) . '
    ' + . $badge + . ($lv_desc !== '' ? '
    ' . esc_html($lv_desc) . '
    ' : '') + . '
    '; +} elseif ($expired_level > 0) { + $exp_label = $expires > 0 + ? sprintf(__('过期时间:%s', 'aksrt'), wp_date('Y年m月d日', $expires)) + : __('已过期', 'aksrt'); + $status_html = '
    ' + . '
    ' . esc_html__('会员已过期', 'aksrt') . '
    ' + . '
    ' . esc_html($exp_label) . '
    ' + . ($desc_top !== '' ? '
    ' . esc_html($desc_top) . '
    ' : '') + . '
    '; + $btn_type = 'pay'; +} else { + $status_html = '
    ' + . '
    ' . esc_html__('开通会员', 'aksrt') . '
    ' + . ($desc_top !== '' ? '
    ' . esc_html($desc_top) . '
    ' : '
    ' . esc_html__('开通会员,尊享专属特权', 'aksrt') . '
    ') + . '
    '; + $btn_type = 'pay'; +} + +// Level cards. +$cards = ''; +foreach ([1, 2] as $lv) { + if (empty($levels[$lv]['enabled'])) { + continue; + } + $lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv)); + $lv_desc = (string) ($levels[$lv]['desc'] ?? ''); + $equity = (string) ($levels[$lv]['equity'] ?? ''); + $cta = sprintf(__('开通%s', 'aksrt'), $lv_name); + $vip_attr = $lv; + if (str_starts_with($btn_type, 'renew_')) { + $renew_lv = (int) substr($btn_type, 6); + if ($lv === $renew_lv) { + $cta = sprintf(__('续费%s', 'aksrt'), $lv_name); + } elseif ($lv > $renew_lv) { + $cta = sprintf(__('升级%s', 'aksrt'), $lv_name); + } else { + continue; + } + } elseif ($btn_type === 'upgrade') { + if ($lv <= $level) { + continue; + } + $cta = sprintf(__('升级%s', 'aksrt'), $lv_name); + $vip_attr = 2; + } elseif ($level > 0 && $lv < $level) { + continue; + } elseif ($level > 0 && $lv === $level) { + $cta = sprintf(__('续费%s', 'aksrt'), $lv_name); + } + + $cards .= '
    '; + $cards .= '
    '; + $cards .= '
    '; + $cards .= '
    '; + $cards .= '
    ' . esc_html($lv_name) . '
    '; + if ($lv_desc !== '') { + $cards .= '
    ' . esc_html($lv_desc) . '
    '; + } + $cards .= '
    '; + if ($equity !== '') { + $cards .= ''; + } + $cards .= '' + . esc_html($cta) . ''; + $cards .= '
    '; +} + +// Points exchange (non-VIP only) — slim link; full shop stays optional under credits. +$points_html = ''; +if ($level <= 0 && !empty($vip_settings['points_exchange']['enabled'])) { + $points_html = ''; +} +?> +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    + + + + +
    + +
    + +
    diff --git a/inc/modules/user-parts/urls.php b/inc/modules/user-parts/urls.php index a4dbcbc3..e565c899 100644 --- a/inc/modules/user-parts/urls.php +++ b/inc/modules/user-parts/urls.php @@ -26,6 +26,17 @@ function aksrt_get_user_center_url(string $tab = ''): string { } if ($tab !== '') { + $tab = sanitize_key($tab); + // Deep-link aliases used by header wallet / VIP CTA. + $aliases = [ + 'assets' => 'credits', + 'vip' => 'vip', // dedicated vip tab when registered; resolver falls back if missing + 'signin' => 'credits', + 'invite' => 'settings', + ]; + if (isset($aliases[$tab])) { + $tab = $aliases[$tab]; + } $url = add_query_arg('tab', $tab, $url); } return $url; diff --git a/src/AKSRT/Admin/Schema/SchemaPayment.php b/src/AKSRT/Admin/Schema/SchemaPayment.php index 9477137d..83176572 100644 --- a/src/AKSRT/Admin/Schema/SchemaPayment.php +++ b/src/AKSRT/Admin/Schema/SchemaPayment.php @@ -100,18 +100,28 @@ class SchemaPayment { 'vip' => [ 'key' => 'vip', 'title' => 'VIP会员', - 'version' => '1.0.0', + 'version' => '1.1.0', 'option_key' => 'aksrt_settings', 'groups' => [ ['key' => 'basic', 'title' => '会员等级', 'fields' => [ ['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' => 'vip_1_img_icon', 'label' => '一级会员图标 URL', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]], ['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_desc', 'label' => '会员说明', 'type' => 'textarea', 'default' => ''], + ['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' => 'vip_2_img_icon', 'label' => '二级会员图标 URL', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]], + ['key' => 'pay_user_vip_desc', 'label' => '弹窗顶部会员说明', 'type' => 'textarea', 'default' => ''], + ['key' => 'pay_user_vip_more', 'label' => '弹窗底部补充说明 HTML', 'type' => 'textarea', 'default' => ''], ]], - ['key' => 'packages', 'title' => '会员套餐', 'fields' => [ - ['key' => 'payment_vip_packages', 'label' => '套餐列表', 'type' => 'textarea', 'default' => "30|30|月卡\n365|365|年卡", 'help' => '每行:现金价|天数|标题(天数为 0 表示永久)'], + ['key' => 'packages', 'title' => '分档套餐', 'fields' => [ + ['key' => 'vip_1_product_s', 'label' => '允许购买一级会员', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]], + ['key' => 'payment_vip_1_products', 'label' => '一级会员套餐', 'type' => 'textarea', 'default' => "30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡", 'dependsOn' => ['key' => 'vip_1_product_s', 'value' => true], 'help' => '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久'], + ['key' => 'vip_2_product_s', 'label' => '允许购买二级会员', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]], + ['key' => 'payment_vip_2_products', 'label' => '二级会员套餐', 'type' => 'textarea', 'default' => "58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡", 'dependsOn' => ['key' => 'vip_2_product_s', 'value' => true], 'help' => '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久'], ]], ['key' => 'points_exchange', 'title' => '积分兑换会员', 'fields' => [ ['key' => 'pay_vip_points_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false], @@ -137,13 +147,13 @@ class SchemaPayment { ['label' => '折扣价格', 'value' => 'discount'], ['label' => '优惠立减', 'value' => 'reduce'], ]], - ['key' => 'vip_renew_discount', 'label' => '续费折扣(%)', 'type' => 'number', 'default' => 8, 'min' => 1, 'max' => 100, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'discount']], + ['key' => 'vip_renew_discount', 'label' => '续费折扣(折,如 8=八折)', 'type' => 'number', 'default' => 8, 'min' => 1, 'max' => 100, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'discount']], ['key' => 'vip_renew_reduce', 'label' => '续费减免金额', 'type' => 'number', 'default' => 20, 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'reduce']], ]], ['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' => '', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '每行一条。永久VIP1:permanent|价格|标题;剩余天:unit|每日单价|标题;跳转永久:jump|价格|标题;兼容:价格|天数|标题'], + ['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|价格|标题'], ]], ], ], 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 .= '
    '; + $html .= '
    ' . esc_html($name) . '
    '; + if ($desc !== '') { + $html .= '
    ' . esc_html($desc) . '
    '; + } + if ($equity !== '') { + $html .= ''; + } + $html .= '' + . esc_html(sprintf(__('开通%s', 'aksrt'), $name)) + . ''; + $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 .= '
    '; + $panes .= '
    '; + $panes .= '
    '; + $panes .= '
    ' . esc_html($lv_name) . '
    '; + if ($lv_desc !== '') { + $panes .= '
    ' . esc_html($lv_desc) . '
    '; + } + if ($equity !== '') { + $panes .= '
      ' . $equity . '
    '; + } + $panes .= '
    '; + $panes .= $products; + $panes .= $this->render_pay_methods($methods); + $panes .= ''; + $panes .= '

    '; + $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 .= '
    '; + $panes .= '
    '; + $panes .= '
    '; + $panes .= '
    ' . esc_html($lv_name) . '
    '; + if ($equity !== '') { + $panes .= '
      ' . $equity . '
    '; + } + $panes .= '
    '; + $panes .= $products; + $panes .= $this->render_pay_methods($methods); + $cta = $action === 'upgrade' ? __('立即升级', 'aksrt') : __('立即续费', 'aksrt'); + $panes .= ''; + $panes .= '

    '; + $panes .= '
    '; + } + + if ($tabs === '') { + return '
    ' + . esc_html__('当前没有可续费或升级的套餐。', 'aksrt') + . '
    '; + } + + $html = '
    '; + $html .= ''; + $html .= '
    • ' . $avatar . '
    • '; + $html .= '
    • ' . esc_html($name) . ''; + $html .= '
      ' + . esc_html($exp_label) . '
    '; + $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/VipServicePackages.php b/src/AKSRT/Content/VipServicePackages.php index bce6fe77..d27527d4 100644 --- a/src/AKSRT/Content/VipServicePackages.php +++ b/src/AKSRT/Content/VipServicePackages.php @@ -12,11 +12,14 @@ trait VipServicePackages { /** - * Parse packages_raw lines: price|days|title + * Parse per-level product lines (zibll vip_product). * - * @return array + * Preferred: price|show_price|time|unit|tag|title + * Legacy: price|days|title + * + * @return array */ - public function parse_packages(string $raw): array { + public function parse_level_products(string $raw): array { $lines = preg_split('/\r\n|\r|\n/', $raw) ?: []; $out = []; foreach ($lines as $line) { @@ -26,20 +29,90 @@ trait VipServicePackages { } $parts = array_map('trim', explode('|', $line)); $price = isset($parts[0]) ? (float) $parts[0] : 0.0; - $days = isset($parts[1]) ? (int) $parts[1] : 0; - $title = isset($parts[2]) ? sanitize_text_field($parts[2]) : ''; if ($price <= 0) { continue; } - if ($title === '') { - $title = $days > 0 - ? sprintf(__('%d天会员', 'aksrt'), $days) - : __('永久会员', 'aksrt'); + + $n = count($parts); + // New format: price|show_price|time|unit|tag|title (6+) + // Or: price|show_price|time|unit|title (5) + if ($n >= 4 && (in_array(strtolower((string) ($parts[3] ?? '')), ['day', 'month', 'd', 'm'], true) + || (isset($parts[3]) && $parts[3] !== '' && !is_numeric($parts[3])))) { + $show_price = isset($parts[1]) ? (float) $parts[1] : 0.0; + $time = isset($parts[2]) ? (int) $parts[2] : 0; + $unit_raw = strtolower((string) ($parts[3] ?? 'month')); + $unit = ($unit_raw === 'day' || $unit_raw === 'd') ? 'day' : 'month'; + $tag = ''; + $title = ''; + if ($n >= 6) { + $tag = sanitize_text_field((string) ($parts[4] ?? '')); + $title = sanitize_text_field((string) ($parts[5] ?? '')); + } elseif ($n === 5) { + // price|show|time|unit|title + $title = sanitize_text_field((string) ($parts[4] ?? '')); + } + } else { + // Legacy: price|days|title + $show_price = 0.0; + $days_legacy = isset($parts[1]) ? (int) $parts[1] : 0; + $time = $days_legacy; + $unit = 'day'; + $tag = ''; + $title = isset($parts[2]) ? sanitize_text_field($parts[2]) : ''; } + + $time = max(0, $time); + $days = $this->duration_to_days($time, $unit); + if ($title === '') { + if ($time <= 0) { + $title = __('永久会员', 'aksrt'); + } elseif ($unit === 'day') { + $title = sprintf(__('%d天会员', 'aksrt'), $time); + } else { + $title = sprintf(__('%d个月会员', 'aksrt'), $time); + } + } + $out[] = [ - 'price' => round($price, 2), - 'days' => max(0, $days), - 'title' => $title, + 'price' => round($price, 2), + 'show_price' => max(0.0, round($show_price, 2)), + 'time' => $time, + 'unit' => $unit, + 'tag' => $tag, + 'tag_class' => '', + 'title' => $title, + 'days' => $days, + ]; + } + return $out; + } + + /** + * Approximate days for catalog labels / order meta; activate prefers time+unit calendar math. + */ + public function duration_to_days(int $time, string $unit = 'day'): int { + if ($time <= 0) { + return 0; + } + if ($unit === 'month') { + return $time * 30; + } + return $time; + } + + /** + * @deprecated Use parse_level_products — kept as thin adapter for upgrade fixed lines. + * + * @return array + */ + public function parse_packages(string $raw): array { + $parsed = $this->parse_level_products($raw); + $out = []; + foreach ($parsed as $p) { + $out[] = [ + 'price' => (float) $p['price'], + 'days' => (int) $p['days'], + 'title' => (string) $p['title'], ]; } return $out; diff --git a/src/AKSRT/Content/VipServicePurchase.php b/src/AKSRT/Content/VipServicePurchase.php index 4273f0e4..f8137703 100644 --- a/src/AKSRT/Content/VipServicePurchase.php +++ b/src/AKSRT/Content/VipServicePurchase.php @@ -17,12 +17,11 @@ trait VipServicePurchase { public function get_catalog(int $user_id = 0): array { $vip = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_payment_vip_settings() - : ['enabled' => false, 'packages_raw' => '', 'levels' => [], 'renew' => [], 'upgrade' => [], 'points_exchange' => []]; + : ['enabled' => false, 'levels' => [], 'renew' => [], 'upgrade' => [], 'points_exchange' => []]; $level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0; $expires = $user_id > 0 ? (int) get_user_meta($user_id, 'aksrt_vip_expires', true) : 0; $is_permanent = ($level > 0 && $expires <= 0); - $packages = $this->parse_packages((string) ($vip['packages_raw'] ?? '')); $levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : []; $renew = is_array($vip['renew'] ?? null) ? $vip['renew'] : []; $upgrade = is_array($vip['upgrade'] ?? null) ? $vip['upgrade'] : []; @@ -37,6 +36,8 @@ trait VipServicePurchase { if (empty($levels[$lv]['enabled'])) { continue; } + // Cash products can be disabled per level. + $product_s = !isset($levels[$lv]['product_s']) || !empty($levels[$lv]['product_s']); if ($is_permanent && $lv <= $level) { continue; } @@ -49,6 +50,9 @@ trait VipServicePurchase { $name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv)); $items = []; + $packages = $product_s + ? $this->parse_level_products((string) ($levels[$lv]['products_raw'] ?? '')) + : []; foreach ($packages as $idx => $pkg) { $base_price = (float) $pkg['price']; $days = (int) $pkg['days']; @@ -64,6 +68,22 @@ trait VipServicePurchase { continue; } + $show_price = (float) ($priced['show_price'] ?? 0); + if ($show_price <= 0 && !empty($pkg['show_price']) && $action === 'pay') { + $show_price = (float) $pkg['show_price']; + } + $discount_tag = (string) ($priced['tag'] ?? ''); + if ($discount_tag === '' && $action === 'pay' && !empty($pkg['tag'])) { + $discount_tag = (string) $pkg['tag']; + } + + $days_label = __('永久', 'aksrt'); + if ((int) $pkg['time'] > 0) { + $days_label = ((string) ($pkg['unit'] ?? 'day') === 'month') + ? sprintf(__('%d 个月', 'aksrt'), (int) $pkg['time']) + : sprintf(__('%d 天', 'aksrt'), (int) $pkg['time']); + } + $items[] = [ 'package_id' => $this->encode_package_id($lv, $idx + 1), 'level' => $lv, @@ -71,17 +91,17 @@ trait VipServicePurchase { 'action' => $action, 'price' => (float) $priced['price'], 'base_price' => $base_price, - 'show_price' => (float) ($priced['show_price'] ?? 0), - 'discount_tag' => (string) ($priced['tag'] ?? ''), + 'show_price' => $show_price, + 'discount_tag' => $discount_tag, 'days' => $days, + 'time' => (int) ($pkg['time'] ?? 0), + 'unit' => (string) ($pkg['unit'] ?? 'day'), 'title' => (string) $pkg['title'], 'price_label' => $this->format_cash_label((float) $priced['price']), - 'show_price_label' => !empty($priced['show_price']) - ? $this->format_cash_label((float) $priced['show_price']) + 'show_price_label' => $show_price > 0 + ? $this->format_cash_label($show_price) : '', - 'days_label' => ($days <= 0) - ? __('永久', 'aksrt') - : sprintf(__('%d 天', 'aksrt'), $days), + 'days_label' => $days_label, 'cta_label' => $action === 'renew' ? __('立即续费', 'aksrt') : ($action === 'upgrade' ? __('立即升级', 'aksrt') : __('立即开通', 'aksrt')), @@ -163,7 +183,7 @@ trait VipServicePurchase { $vip = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_payment_vip_settings() - : ['enabled' => false, 'packages_raw' => '', 'levels' => [], 'renew' => [], 'upgrade' => []]; + : ['enabled' => false, 'levels' => [], 'renew' => [], 'upgrade' => []]; if (empty($vip['enabled'])) { return new \WP_Error('aksrt_vip_disabled', __('会员功能未启用', 'aksrt'), ['status' => 400]); } @@ -203,6 +223,8 @@ trait VipServicePurchase { // Dedicated upgrade product: // 901 permanent/fixed, 902 unit remaining days, 903 jump permanent. $upgrade_mode = ''; + $pkg_time = 0; + $pkg_unit = 'day'; if (in_array($index, [901, 902, 903], true)) { $upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, (string) ($upgrade['product'] ?? '')); $match = null; @@ -222,18 +244,30 @@ trait VipServicePurchase { ]; $amount = round((float) $match['price'], 2); $days = (int) $match['days']; + $pkg_time = $days; // unit upgrade keeps days as remaining; jump/permanent use 0 + $pkg_unit = 'day'; $base_price = (float) ($match['base_price'] ?? $amount); $discount_tag = (string) ($match['discount_tag'] ?? __('升级专享', 'aksrt')); $upgrade_mode = sanitize_key((string) ($match['upgrade_mode'] ?? '')); $action = 'upgrade'; + if ($upgrade_mode === 'jump' || $upgrade_mode === 'permanent') { + $pkg_time = 0; + $days = 0; + } } else { - $packages = $this->parse_packages((string) ($vip['packages_raw'] ?? '')); + $level_cfg = is_array($levels[$level] ?? null) ? $levels[$level] : []; + if (isset($level_cfg['product_s']) && empty($level_cfg['product_s'])) { + return new \WP_Error('aksrt_vip_product_disabled', __('该等级暂不支持购买', 'aksrt'), ['status' => 400]); + } + $packages = $this->parse_level_products((string) ($level_cfg['products_raw'] ?? '')); $pkg = $packages[$index - 1] ?? null; if (!$pkg) { return new \WP_Error('aksrt_invalid_package', __('套餐不存在', 'aksrt'), ['status' => 400]); } $base_price = (float) $pkg['price']; $days = (int) $pkg['days']; + $pkg_time = (int) ($pkg['time'] ?? 0); + $pkg_unit = (string) ($pkg['unit'] ?? 'day'); $priced = $this->apply_action_price($base_price, $action, $renew, $upgrade, $level, $user_level, $expires, $is_permanent, $days); if ($priced === null) { return new \WP_Error('aksrt_invalid_package', __('当前无法购买该套餐', 'aksrt'), ['status' => 400]); @@ -259,6 +293,8 @@ trait VipServicePurchase { 'vip_pay' => 1, 'level' => $level, 'days' => $days, + 'time' => $pkg_time, + 'unit' => $pkg_unit, 'package_index' => $index, 'package_title' => (string) ($pkg['title'] ?? ''), 'package_id' => $package_id, @@ -401,6 +437,11 @@ trait VipServicePurchase { $level = (int) ($meta['level'] ?? 0); $days = (int) ($meta['days'] ?? 0); + $time = (int) ($meta['time'] ?? 0); + $unit = sanitize_key((string) ($meta['unit'] ?? 'day')); + if (!in_array($unit, ['day', 'month'], true)) { + $unit = 'day'; + } $upgrade_mode = sanitize_key((string) ($meta['upgrade_mode'] ?? '')); if ($level <= 0) { $decoded = $this->decode_package_id((int) ($paid_order['product_id'] ?? 0)); @@ -418,6 +459,9 @@ trait VipServicePurchase { // unit upgrade: raise level only, keep current expiry (zibll). if ($upgrade_mode === 'unit') { $this->activate_vip($user_id, $level, 0, true); + } elseif ($time > 0 || (isset($meta['time']) && (int) $meta['time'] === 0 && $days === 0)) { + // Prefer calendar time+unit when present (including permanent time=0). + $this->activate_vip_duration($user_id, $level, $time, $unit); } else { $this->activate_vip($user_id, $level, $days, false); } @@ -439,6 +483,8 @@ trait VipServicePurchase { $current_level = (int) get_user_meta($user_id, 'aksrt_vip_level', true); $new_level = max($current_level, $level); update_user_meta($user_id, 'aksrt_vip_level', $new_level); + // Clear expired memory when re-granting. + delete_user_meta($user_id, 'aksrt_vip_level_expired'); // Unit upgrade (zibll): keep vip_exp_date, only raise level. if ($keep_expiry) { @@ -460,6 +506,41 @@ trait VipServicePurchase { update_user_meta($user_id, 'aksrt_vip_expires', $base + ($days * DAY_IN_SECONDS)); } + /** + * Activate VIP using calendar duration (zibll month/day). + * time=0 → permanent. Extends from max(now, current_exp). + */ + public function activate_vip_duration(int $user_id, int $level, int $time, string $unit = 'day'): void { + if ($user_id <= 0) { + return; + } + $level = max(1, min(2, $level)); + $unit = $unit === 'month' ? 'month' : 'day'; + $current_level = (int) get_user_meta($user_id, 'aksrt_vip_level', true); + $new_level = max($current_level, $level); + update_user_meta($user_id, 'aksrt_vip_level', $new_level); + delete_user_meta($user_id, 'aksrt_vip_level_expired'); + + if ($time <= 0) { + update_user_meta($user_id, 'aksrt_vip_expires', 0); + return; + } + + $current_exp = (int) get_user_meta($user_id, 'aksrt_vip_expires', true); + if ($current_exp === 0 && $current_level > 0) { + return; // keep permanent + } + $base = max($current_exp, time()); + $modifier = $unit === 'month' + ? ('+' . $time . ' months') + : ('+' . $time . ' days'); + $new_exp = strtotime($modifier, $base); + if ($new_exp === false) { + $new_exp = $base + ($this->duration_to_days($time, $unit) * DAY_IN_SECONDS); + } + update_user_meta($user_id, 'aksrt_vip_expires', (int) $new_exp); + } + /** 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()) { diff --git a/src/AKSRT/Payment/OrderRepositoryCore.php b/src/AKSRT/Payment/OrderRepositoryCore.php index e3c498d6..ebb37522 100644 --- a/src/AKSRT/Payment/OrderRepositoryCore.php +++ b/src/AKSRT/Payment/OrderRepositoryCore.php @@ -29,6 +29,18 @@ trait OrderRepositoryCore { $order_number = $data['order_number'] ?? $this->generate_order_number(); + // Preserve JSON pay meta (VIP/content/coupon). Only strip tags for plain text notes. + $note_raw = (string) ($data['note'] ?? ''); + $note_trim = trim($note_raw); + if ($note_trim !== '' && ($note_trim[0] === '{' || $note_trim[0] === '[')) { + $decoded = json_decode($note_trim, true); + $note = is_array($decoded) + ? (string) wp_json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + : sanitize_textarea_field($note_raw); + } else { + $note = sanitize_textarea_field($note_raw); + } + $wpdb->insert($this->table(), [ 'order_number' => $order_number, 'user_id' => (int) ($data['user_id'] ?? 0), @@ -42,7 +54,7 @@ trait OrderRepositoryCore { 'created_at' => $data['created_at'] ?? current_time('mysql'), 'paid_at' => $data['paid_at'] ?? null, 'expires_at' => $data['expires_at'] ?? null, - 'note' => sanitize_textarea_field((string) ($data['note'] ?? '')), + 'note' => $note, ], ['%s', '%d', '%s', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']); if ($wpdb->insert_id) { diff --git a/src/AKSRT/Settings/CreditsPaymentGroups.php b/src/AKSRT/Settings/CreditsPaymentGroups.php index e9213afa..37cf5cc5 100644 --- a/src/AKSRT/Settings/CreditsPaymentGroups.php +++ b/src/AKSRT/Settings/CreditsPaymentGroups.php @@ -153,12 +153,23 @@ trait CreditsPaymentGroups { } /** - * @return array{enabled:bool,name:string,badge_text:string,packages_raw:string,package_count:int,help_text:string} + * Dual-level VIP settings (Zibll pay_user_vip_1 / pay_user_vip_2). + * + * @return array */ public static function get_payment_vip_settings(): array { return PaymentRuntime::get_payment_vip_settings(); } + /** + * Alias for callers that expect get_vip_settings() (header CTA, float buttons). + * + * @return array + */ + public static function get_vip_settings(): array { + return self::get_payment_vip_settings(); + } + /** * @return array{enabled:bool,share_ratio:int,settlement_threshold:int,cycle_days:int,note:string} */ diff --git a/src/AKSRT/Settings/FacadeBulk.php b/src/AKSRT/Settings/FacadeBulk.php index 58a91bb6..86e575f0 100644 --- a/src/AKSRT/Settings/FacadeBulk.php +++ b/src/AKSRT/Settings/FacadeBulk.php @@ -58,7 +58,8 @@ trait FacadeBulk { $s = self::get_all(); return [ - 'enabled' => !empty($s['header_newposts_enabled']), + // Default on like zibll nav_newposts when key unset. + 'enabled' => !isset($s['header_newposts_enabled']) || !empty($s['header_newposts_enabled']), 'text' => trim((string) ($s['header_newposts_text'] ?? ' 发布')), 'radius' => max(0, min(30, (int) ($s['header_newposts_radius'] ?? 20))), 'class' => sanitize_html_class((string) ($s['header_newposts_class'] ?? '')), diff --git a/src/AKSRT/Settings/PaymentRuntime.php b/src/AKSRT/Settings/PaymentRuntime.php index 78b2d41b..198d9e37 100644 --- a/src/AKSRT/Settings/PaymentRuntime.php +++ b/src/AKSRT/Settings/PaymentRuntime.php @@ -150,27 +150,60 @@ class PaymentRuntime { $renew_price_type = 'discount'; } - $packages = (string) ($settings['payment_vip_packages'] ?? "30|30|月卡\n365|365|年卡"); + $default_l1 = "30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡"; + $default_l2 = "58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡"; + $products_1 = sanitize_textarea_field((string) ($settings['payment_vip_1_products'] ?? $default_l1)); + $products_2 = sanitize_textarea_field((string) ($settings['payment_vip_2_products'] ?? $default_l2)); + // Restore newlines lost only if empty after sanitize — sanitize_textarea_field keeps \n. + if (trim($products_1) === '') { + $products_1 = $default_l1; + } + if (trim($products_2) === '') { + $products_2 = $default_l2; + } + $desc = sanitize_textarea_field((string) ($settings['pay_user_vip_desc'] ?? '')); + $more = (string) ($settings['pay_user_vip_more'] ?? ''); + $allowed_equity = [ + 'li' => [], + 'ul' => [], + 'ol' => [], + 'strong' => [], + 'b' => [], + 'em' => [], + 'i' => ['class' => true], + 'span' => ['class' => true], + 'br' => [], + 'a' => ['href' => true, 'target' => true, 'rel' => true, 'class' => true], + ]; return [ 'enabled' => $enabled, 'name' => $vip1_enabled ? $name1 : $badge, 'badge_text' => $badge, 'desc' => $desc, - 'packages_raw' => $packages, - 'package_count' => self::count_structured_lines($packages), + 'more' => wp_kses($more, $allowed_equity + ['div' => ['class' => true], 'p' => ['class' => true]]), 'help_text' => $desc !== '' ? $desc : __('会员权益以免扣下载和身份展示为主,开通后立即生效。', 'aksrt'), 'levels' => [ 1 => [ - 'enabled' => $vip1_enabled, - 'name' => $name1, + 'enabled' => $vip1_enabled, + 'name' => $name1, + 'desc' => sanitize_text_field((string) ($settings['pay_user_vip_1_desc'] ?? '')), + 'equity' => wp_kses((string) ($settings['pay_user_vip_1_equity'] ?? ''), $allowed_equity), + 'img_icon' => esc_url_raw((string) ($settings['vip_1_img_icon'] ?? '')), + 'product_s' => !isset($settings['vip_1_product_s']) || !empty($settings['vip_1_product_s']), + 'products_raw' => $products_1, 'download_limit' => max(0, min(9999, (int) ($settings['pay_download_limit_vip_1'] ?? 0))), 'download_speed' => max(0, min(99999, (int) ($settings['pay_download_speed_vip_1'] ?? 0))), ], 2 => [ - 'enabled' => $vip2_enabled, - 'name' => $name2, + 'enabled' => $vip2_enabled, + 'name' => $name2, + 'desc' => sanitize_text_field((string) ($settings['pay_user_vip_2_desc'] ?? '')), + 'equity' => wp_kses((string) ($settings['pay_user_vip_2_equity'] ?? ''), $allowed_equity), + 'img_icon' => esc_url_raw((string) ($settings['vip_2_img_icon'] ?? '')), + 'product_s' => !isset($settings['vip_2_product_s']) || !empty($settings['vip_2_product_s']), + 'products_raw' => $products_2, 'download_limit' => max(0, min(9999, (int) ($settings['pay_download_limit_vip_2'] ?? 0))), 'download_speed' => max(0, min(99999, (int) ($settings['pay_download_speed_vip_2'] ?? 0))), ], @@ -192,12 +225,13 @@ class PaymentRuntime { 'upgrade' => [ 'enabled' => !isset($settings['vip_upgrade']) || !empty($settings['vip_upgrade']), 'desc' => sanitize_textarea_field((string) ($settings['vip_upgrade_desc'] ?? '')), - 'product' => sanitize_text_field((string) ($settings['vip_upgrade_product'] ?? '')), + // Multi-line upgrade product rules — must not collapse newlines. + 'product' => sanitize_textarea_field((string) ($settings['vip_upgrade_product'] ?? '')), ], 'points_exchange' => [ 'enabled' => !empty($settings['pay_vip_points_exchange_s']), 'desc' => sanitize_textarea_field((string) ($settings['pay_vip_points_exchange_desc'] ?? '')), - 'packages_raw' => (string) ($settings['payment_vip_points_packages'] ?? "1|500|30|积分月卡\n1|5000|0|积分永久"), + 'packages_raw' => sanitize_textarea_field((string) ($settings['payment_vip_points_packages'] ?? "1|500|30|积分月卡\n1|5000|0|积分永久")), ], ]; } diff --git a/src/AKSRT/User/Center.php b/src/AKSRT/User/Center.php index 4b16105d..7b6e651f 100644 --- a/src/AKSRT/User/Center.php +++ b/src/AKSRT/User/Center.php @@ -73,6 +73,12 @@ class Center { 'priority' => 50, 'type' => 'panel', ], + 'vip' => [ + 'label' => __('我的会员', 'aksrt'), + 'icon' => 'crown', + 'priority' => 45, + 'type' => 'panel', + ], 'notif' => [ 'label' => __('消息通知', 'aksrt'), 'icon' => 'bell', @@ -119,12 +125,26 @@ class Center { $this->panels['credits'] = $panel; } + // Dedicated VIP tab when membership is enabled. + $vip_on = class_exists('\AKSRT\Settings\Settings') + && !empty(\AKSRT\Settings\Settings::get_payment_vip_settings()['enabled']); + if ($vip_on && !isset($this->panels['vip'])) { + $this->panels['vip'] = $definitions['vip']; + } elseif (!$vip_on && isset($this->panels['vip'])) { + unset($this->panels['vip']); + } + if (!isset($this->panels['settings'])) { $this->panels['settings'] = $definitions['settings']; } $this->panels['notif'] = $definitions['notif']; + // Keep stable order by priority. + uasort($this->panels, static function (array $a, array $b): int { + return ((int) ($a['priority'] ?? 100)) <=> ((int) ($b['priority'] ?? 100)); + }); + /** * 允许插件/子主题注册额外面板 *