From b8ee6b6a43ea30b162e3cad115f0a34a04190df9 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 22 Jul 2026 10:32:00 +0800 Subject: [PATCH] fix(float): repair VIP rail entry front and admin Always open VIP modal for float pay_vip, drop forced admin link, normalize gem icon, harden pay-vip click binding and SPA form UX. --- .../pages/content/FloatingButtonsSettings.vue | 31 +++++++++--- assets/css/frontend/floating-buttons.css | 14 ++++++ .../css/frontend/nav/header-menu-buttons.css | 9 +++- assets/js/pay-vip.js | 34 +++++++++++-- inc/helpers/aksrt-runtime-win.php | 49 ++++++++++++++++--- src/AKSRT/Admin/GlobalControllerPart1.php | 18 ++++++- 6 files changed, 132 insertions(+), 23 deletions(-) diff --git a/assets/admin/src/pages/content/FloatingButtonsSettings.vue b/assets/admin/src/pages/content/FloatingButtonsSettings.vue index 87c22d7d..83b64306 100644 --- a/assets/admin/src/pages/content/FloatingButtonsSettings.vue +++ b/assets/admin/src/pages/content/FloatingButtonsSettings.vue @@ -203,6 +203,10 @@ + + 会员入口固定打开站点「开通/升级会员」弹窗,无需填写链接;访客点击会先走登录。 + + @@ -392,9 +396,12 @@ const addPresetOptions = computed(() => { }) }) -const needsLink = computed(() => ['custom', 'pay_vip', 'add', 'shop_cart'].includes(editingItem.type) && !editingItem.hover) -const supportsHover = computed(() => ['custom', 'pay_vip'].includes(editingItem.type)) +// pay_vip always opens the site VIP modal on the front-end — no link required. +const needsLink = computed(() => ['custom', 'add', 'shop_cart'].includes(editingItem.type) && !editingItem.hover) +// Custom hover HTML only; VIP uses modal, not free-form hover content. +const supportsHover = computed(() => editingItem.type === 'custom') const supportsActionMode = computed(() => ['service_qq', 'service_wechat'].includes(editingItem.type)) +const isPayVip = computed(() => editingItem.type === 'pay_vip') const isServiceQrcode = computed(() => ['service_qq', 'service_wechat'].includes(editingItem.type) && editingItem.action_mode === 'qrcode' ) @@ -435,7 +442,7 @@ function applyTypeDefaults(type) { qrcode: { icon: 'fas fa-qrcode', icon_dark: '', icon_light: '', text: '二维码', desc: '扫码继续浏览', link: '', hover: '', action_mode: 'qrcode', qr_size: 120 }, service_qq: { icon: 'fab fa-qq', icon_dark: '', icon_light: '', text: 'QQ', desc: 'QQ 联系', link: '', hover: '', action_mode: 'link', qr_size: 120 }, service_wechat: { icon: 'fab fa-weixin', icon_dark: '', icon_light: '', text: '微信', desc: '扫码添加微信', link: '', hover: '', action_mode: 'link', qr_size: 120 }, - pay_vip: { icon: 'fas fa-crown', icon_dark: '', icon_light: '', text: '会员', desc: '会员入口', link: '', hover: '', action_mode: 'link', qr_size: 120 }, + pay_vip: { icon: 'fas fa-gem', icon_dark: '', icon_light: '', text: '会员', desc: '开通/升级会员弹窗', link: '', hover: '', action_mode: 'link', qr_size: 120 }, add: { icon: 'fas fa-plus', icon_dark: '', icon_light: '', text: '发布', desc: '发布内容', link: '', hover: '', action_mode: 'link', qr_size: 120 }, shop_cart: { icon: 'fas fa-shopping-cart', icon_dark: '', icon_light: '', text: '购物车', desc: '购物车入口', link: '', hover: '', action_mode: 'link', qr_size: 120 }, } @@ -456,6 +463,14 @@ function handleTypeChange(type) { if (type === 'service_wechat' && editingItem.action_mode === 'qrcode') editingItem.link = '' if (['service_qq', 'service_wechat'].includes(type) && editingItem.action_mode === 'qrcode') editingItem.desc = '' if (!needsLink.value && !['service_qq', 'service_wechat'].includes(type)) editingItem.link = '' + // VIP float always uses modal path; never keep a stale custom link/hover. + if (type === 'pay_vip') { + editingItem.link = '' + editingItem.hover = '' + if (!editingItem.icon || editingItem.icon.includes('fa-diamond')) { + editingItem.icon = 'fas fa-gem' + } + } } function addItem() { @@ -551,6 +566,7 @@ function getItemSummary(row) { if (row.type === 'service_qq') return row.action_mode === 'qrcode' ? (row.qrcode_img || '未设置 QQ 二维码') : (row.link || row.qq || '未设置 QQ 号码') if (row.type === 'service_wechat') return row.action_mode === 'qrcode' ? (row.wechat_img || '未设置微信二维码') : (row.link || '未设置跳转链接') if (row.type === 'qrcode') return row.desc || '展示当前页面二维码' + if (row.type === 'pay_vip') return '点击打开会员开通弹窗(无需填写链接)' if (row.hover) return '使用 HTML 悬浮层内容' return row.link || row.desc || '未设置说明' } @@ -588,7 +604,7 @@ function saveItem() { const item = { id: editingItem.id || `float_${Date.now()}`, type: editingItem.type, - icon: editingItem.type === 'theme_mode' ? 'fas fa-sun' : editingItem.icon, + icon: editingItem.type === 'theme_mode' ? 'fas fa-sun' : (editingItem.type === 'pay_vip' ? (editingItem.icon || 'fas fa-gem') : editingItem.icon), icon_dark: '', icon_light: '', text: editingItem.text, @@ -597,14 +613,17 @@ function saveItem() { qq: editingItem.type === 'service_qq' && editingItem.action_mode !== 'qrcode' ? editingItem.qq : '', qrcode_img: editingItem.type === 'service_qq' && editingItem.action_mode === 'qrcode' ? editingItem.qrcode_img : '', wechat_img: editingItem.type === 'service_wechat' && editingItem.action_mode === 'qrcode' ? editingItem.wechat_img : '', - link: (needsLink.value || (editingItem.type === 'service_wechat' && editingItem.action_mode !== 'qrcode') || (editingItem.type === 'service_qq' && editingItem.action_mode !== 'qrcode' && !!editingItem.link)) ? editingItem.link : '', + // pay_vip never stores a custom link — front-end always opens the VIP modal. + link: editingItem.type === 'pay_vip' + ? '' + : ((needsLink.value || (editingItem.type === 'service_wechat' && editingItem.action_mode !== 'qrcode') || (editingItem.type === 'service_qq' && editingItem.action_mode !== 'qrcode' && !!editingItem.link)) ? editingItem.link : ''), add_items: editingItem.type === 'add' ? editingItem.add_items.filter(subItem => subItem.label || subItem.url).map((subItem, idx) => ({ id: subItem.id || `add_${idx}`, label: subItem.label || '', icon: subItem.icon || 'fas fa-pen', url: subItem.url || '', })) : [], - hover: supportsHover.value ? (editingItem.hover || '') : '', + hover: editingItem.type === 'pay_vip' ? '' : (supportsHover.value ? (editingItem.hover || '') : ''), qr_size: editingItem.type === 'qrcode' ? (editingItem.qr_size || 120) : 120, hover_width: supportsHover.value ? (editingItem.hover_width || 200) : 200, pc_s: editingItem.pc_s, diff --git a/assets/css/frontend/floating-buttons.css b/assets/css/frontend/floating-buttons.css index 4a1629a1..46931f2f 100644 --- a/assets/css/frontend/floating-buttons.css +++ b/assets/css/frontend/floating-buttons.css @@ -334,6 +334,20 @@ body.dark-theme .float-btn__icon-sun { display: none; } +/* VIP float rail entry */ +.float-right .float-btn.pay-vip { + color: var(--this-color, #f5a623); +} + +.float-right .float-btn.pay-vip:hover { + color: var(--this-color, #f5a623); + opacity: 0.9; +} + +.float-right .float-btn.signin-loader { + cursor: pointer; +} + .theme-dark .float-right, html.dark .float-right, body.dark-theme .float-right { diff --git a/assets/css/frontend/nav/header-menu-buttons.css b/assets/css/frontend/nav/header-menu-buttons.css index b6d41042..189b5527 100644 --- a/assets/css/frontend/nav/header-menu-buttons.css +++ b/assets/css/frontend/nav/header-menu-buttons.css @@ -680,8 +680,13 @@ body.theme-dark body > .drop-newadd .newadd-btns__item { display: none; } -.float-btn.pay-vip { - color: #f5a623; +.float-btn.pay-vip, +.float-right .float-btn.pay-vip { + color: var(--this-color, #f5a623); +} +.float-right .float-btn.pay-vip:hover { + color: var(--this-color, #f5a623); + opacity: 0.9; } .float-right .float-btn > i { diff --git a/assets/js/pay-vip.js b/assets/js/pay-vip.js index a33e6ecb..c78ecd90 100644 --- a/assets/js/pay-vip.js +++ b/assets/js/pay-vip.js @@ -242,23 +242,47 @@ } function onDocClick(e) { - var t = e.target.closest(".pay-vip"); + // Header CTA, float rail, UC buttons, VIP hover cards. + var t = e.target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]"); if (!t) return; - // Allow real links without intercept if they point elsewhere and lack pay-vip intent. + // Real external href without modal intent: leave alone. + var href = (t.getAttribute("href") || "").trim(); + if (href && href !== "#" && href.indexOf("javascript:") !== 0 && !t.classList.contains("pay-vip") && !t.hasAttribute("data-aksrt-pay-vip") && !t.hasAttribute("vip-level") && !t.hasAttribute("data-vip-level")) { + return; + } e.preventDefault(); e.stopPropagation(); vipPay(t); } function init() { + // Capture phase so float/header anchors with href never navigate first. document.addEventListener("click", onDocClick, true); } - if (window.AksrtFrontend && typeof window.AksrtFrontend.register === "function") { - window.AksrtFrontend.register("aksrt-pay-vip", init); + // Prefer module registry, but always bind once as hard fallback + // (float rail must work even if AksrtFrontend.rehydrate is a no-op). + if (window.__aksrtPayVipBound) { + // already initialized + } else if (window.AksrtFrontend && typeof window.AksrtFrontend.register === "function") { + window.AksrtFrontend.register("aksrt-pay-vip", function () { + if (window.__aksrtPayVipBound) return; + window.__aksrtPayVipBound = true; + init(); + }); + // Also bind immediately — float button is in footer, available now. + if (!window.__aksrtPayVipBound) { + window.__aksrtPayVipBound = true; + init(); + } } else if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", init); + document.addEventListener("DOMContentLoaded", function () { + if (window.__aksrtPayVipBound) return; + window.__aksrtPayVipBound = true; + init(); + }); } else { + window.__aksrtPayVipBound = true; init(); } diff --git a/inc/helpers/aksrt-runtime-win.php b/inc/helpers/aksrt-runtime-win.php index 6c38877c..18c5595d 100644 --- a/inc/helpers/aksrt-runtime-win.php +++ b/inc/helpers/aksrt-runtime-win.php @@ -55,6 +55,10 @@ endif; if (!function_exists('aksrt_get_float_pay_vip_html')) : /** * Floating VIP open button. + * + * Always opens the site VIP modal for logged-in non-max VIP users + * (class pay-vip → assets/js/pay-vip.js). Guests get signin-loader. + * Admin "link/hover" fields are intentionally ignored for this type. */ function aksrt_get_float_pay_vip_html(array $item = []): string { $show_vip = false; @@ -70,15 +74,34 @@ if (!function_exists('aksrt_get_float_pay_vip_html')) : if ($user_id > 0 && function_exists('aksrt_get_user_vip_level')) { $level = (int) aksrt_get_user_vip_level($user_id); } - // Hide for max VIP if we only have 2 tiers and already VIP. + // Already top tier — no upsell entry on float rail. if ($level >= 2) { return ''; } - $title = $level === 1 ? __('升级会员', 'aksrt') : __('开通会员', 'aksrt'); - $icon = !empty($item['icon']) ? (string) $item['icon'] : 'fas fa-gem'; - $cls = $user_id > 0 ? 'float-btn pay-vip' : 'float-btn signin-loader'; - // Modal path: .pay-vip opens VIP modal (Batch 2+); guests use signin-loader. - $href = 'javascript:;'; + + // Prefer admin title; fallback by upgrade vs open. + $admin_title = trim((string) ($item['text'] ?? '')); + if ($admin_title !== '') { + $title = $admin_title; + } else { + $title = $level === 1 ? __('升级会员', 'aksrt') : __('开通会员', 'aksrt'); + } + + $icon = trim((string) ($item['icon'] ?? '')); + if ($icon === '') { + $icon = 'fas fa-gem'; + } + // FA4 leftovers from older admin data. + $icon = strtr($icon, [ + 'fa-diamond' => 'fa-gem', + 'fa fa-' => 'fas fa-', + ]); + if (!preg_match('/\b(fas|far|fab|fal|fad)\b/', $icon)) { + $icon = 'fas ' . ltrim(preg_replace('/^fa\s+/', '', $icon) ?? $icon); + } + + // Logged-in: open modal. Guest: open login. + $cls = $user_id > 0 ? 'float-btn pay-vip nowave' : 'float-btn signin-loader nowave'; $style = []; if (!empty($item['color'])) { $style[] = '--this-color:' . esc_attr((string) $item['color']); @@ -86,9 +109,19 @@ if (!function_exists('aksrt_get_float_pay_vip_html')) : if (!empty($item['bg'])) { $style[] = '--this-bg:' . esc_attr((string) $item['bg']); } + // Default VIP accent when admin left color empty. + if (empty($item['color'])) { + $style[] = '--this-color:#f5a623'; + } $style_attr = $style ? ' style="' . implode(';', $style) . '"' : ''; - $vip_level_attr = $user_id > 0 ? ' vip-level="' . esc_attr((string) max(1, min(2, $level + 1))) . '"' : ''; - return '' + + // Prefer upgrading one tier when already VIP1. + $target_level = $user_id > 0 ? max(1, min(2, $level + 1)) : 1; + $vip_level_attr = $user_id > 0 + ? ' vip-level="' . esc_attr((string) $target_level) . '" data-vip-level="' . esc_attr((string) $target_level) . '"' + : ''; + + return '' . '' . ''; } diff --git a/src/AKSRT/Admin/GlobalControllerPart1.php b/src/AKSRT/Admin/GlobalControllerPart1.php index bf8863b5..e1aa6795 100644 --- a/src/AKSRT/Admin/GlobalControllerPart1.php +++ b/src/AKSRT/Admin/GlobalControllerPart1.php @@ -241,6 +241,20 @@ trait GlobalControllerPart1 { $icon_dark = ''; $icon_light = ''; } + // VIP float always uses modal path; drop stale link/hover and normalize icons. + $link = esc_url_raw($item['link'] ?? ''); + $hover = wp_kses_post($item['hover'] ?? ''); + if ($type === 'pay_vip') { + $link = ''; + $hover = ''; + if ($icon === '' || str_contains($icon, 'fa-diamond') || $icon === 'fa fa-diamond') { + $icon = 'fas fa-gem'; + } + // Ensure FA5 prefix when bare fa-* is stored. + if ($icon !== '' && !preg_match('/\b(fas|far|fab|fal|fad)\b/', $icon)) { + $icon = 'fas ' . ltrim(preg_replace('/^fa\s+/', '', $icon) ?? $icon); + } + } return [ 'id' => sanitize_text_field($item['id'] ?? uniqid('float_')), @@ -254,9 +268,9 @@ trait GlobalControllerPart1 { 'qq' => $qq, 'qrcode_img' => esc_url_raw($item['qrcode_img'] ?? ''), 'wechat_img' => esc_url_raw($item['wechat_img'] ?? ''), - 'link' => esc_url_raw($item['link'] ?? ''), + 'link' => $link, 'add_items' => $add_items, - 'hover' => wp_kses_post($item['hover'] ?? ''), + 'hover' => $hover, 'qr_size' => $qr_size, 'hover_width' => $hover_width, 'pc_s' => !empty($item['pc_s']),