定位中…
新用户注册 · 赠优惠券
充值送现金优惠券
开通年卡会员
服务介绍

中泰式.经络疏通.肾脏保养.抓龙筋膜.身体磨砂.淋巴调理+盆腔护理
艾灸理疗.血罐理疗.拔罐.刮痧.身体磨砂.修脚.采耳.洗脸.耳炎调理

优先服务者
秀婷 (297)
已服务 176
阿珂 (554)
已服务 195
沐沐 (651)
已服务 63
丽莎 (744)
已服务 13
热门服务
本地按摩
多年从业经验,指压推拿手法娴熟,服务周到。精油 SPA 护理,尊享放松,环境舒适。...
越南按摩
越南籍技师,手法细腻,服务周到。
高端按摩
技术好 服务好
中国按摩
服务好技术好
定位中…
Q
💬
按摩 本地 越南 高端 中国 双人按 西班牙 韩国 日本
江微科技
定位中…
服务详情
丽达 NO.229 已实名
🇰🇭 柬埔寨 · 新人理疗师
★ 4.9 · 已服务 272 · 371.77m
高棉语 · 中文

基本中文沟通,性格温柔,耐心,手法熟练,擅长传统泰式按摩。

⚡ 快速响应 ✓ 安全保障 ★ 标准流程 $ 价格透明
热门服务
中式传统按摩
疏通经脉 · 活络气血
券后 $25 $35/60分钟
已售 33990
高棉古法按摩
高棉手法 · 缓解疲劳
券后 $30 $40/60分钟
已售 20500
正宗泰式按摩
传统泰式 · 休闲放松
券后 $45 $55/90分钟
已售 16997
芳香精油SPA
Bodia 精油 · 改善睡眠
券后 $55 $65/90分钟
已售 25967
星级评分
4.9 ★★★★★
全部评论(27)
非常满意 (12) 性价比高 (3) 流程熟练 (7) 着装规范 (6)
1 / 1
本人艺术照仅供参考
选择项目下单
中式传统按摩
$25
定位中…
全部 本地 精油SPA 推拿按摩
👤
王师傅推荐技师 · 可预约
项目q
$11
已售0单
泰式古法按摩
$398 $498
已售0单
全身经络疏通
$298 $398
已售0单
定位中…
全部 可服务 服务中 可预约
我的订单
全部 待支付 进行中 已完成 已取消

暂无订单

消息中心
服务者
在线
个人中心
未登录
点击登录
0待支付
0待服务
0已完成
0退款/售后
全部订单
我的钱包 $0.00
优惠券 0张
我的收藏
我的地址
🌐 语言
TG机器人
设置中心
🌐
语言
💰
钱包
📍
地址管理
🗺
定位系统
🪪
身份资讯
付款设定
🔒
隐私政策
📄
用户协议
💬
帮助与支持
退出登录
会员中心
-
暂未开通
📣 7.24K人已购
🔒 未解锁
会员年卡
开通后,畅享优惠价
有效期:365天
年卡特权 V
开通立刻发放,当天可用
$5 x6
年卡优惠券
每月发放,30天内有效
年卡会员x1年 $19.99
$19.99
优惠券 兑换码
未使用 已使用
$10满$20可用
闲时优惠券
有效期至: 2026-05-13
可用时段:06:00~18:00
$10满$20可用
闲时优惠券
有效期至: 2026-05-13
可用时段:06:00~18:00
$10满$20可用
闲时优惠券
有效期至: 2026-05-13
可用时段:06:00~18:00
暂无已使用优惠券
我的钱包 钱包明细
我的余额
$0
余额充值

只能用于平台消费支付,充值后不可退款

$100
开通会员
🔒 未解锁
会员年卡
开通后,畅享优惠价
👑
有效期: 365天
年卡特权 ▼
开通立刻发放,当天可用 %
$5 x6
年卡优惠券
每月发放,30天内有效
年卡会员 x 1年 $19.99
$19.99
选择支付方式
支付剩余时间:00:15:00
付款:$0.00
支付

正在跳转至支付页面,完成支付后请返回本页等待确认…

ABA · KHQR 收款
KHQR
$0.00
KHQR 收款码
WANG CHANGQING
USD 账户100001101399045
收款银行Canadia Bank
订单号

请使用柬埔寨任意银行 KHQR 扫码转账(USD),备注填写订单号。转账完成后点击「我已支付」,系统将自动确认并跳转。

正在发起支付…
支付成功
订单已支付,技师服务完成后收入将自动结算。
确认订单
📍 未设置默认地址, 去选择/添加地址 >
服务者
— >
服务时间
即刻出发 预约时间 >
项目名称: 高棉古法按摩
时长与价格
优惠券 -$0 >
小计 $30.00
VIP 开通年卡会员,立减 $5 $19.99
应付金额: $30.00
已优惠 $10
定位系统
当前城市
定位方式
当前国家
纬度
经度
精度
定位时间

打开网页将自动定位(GPS 优先,失败则用 IP 区域识别)。与手机号无关,适用于在不同国家使用国际号码的用户。也可手动切换城市。

地址管理
📋
暂无数据
📍 地图
📍
19m
浙江家常菜
Phum 01 Sangkat 04, Preah Sihanouk
📍
20m
Sky Sport Club
JG9H+7VC, 216, Preah Sihanouk
📍
520m
Cooper Hotel
Street 106, Kâmpóng Saôm
选择城市
仅查看该城市技师;技师在哪个城市定位,就只在该城市展示(全国各城均可)
选择国家/地区
常用
中国 +86
柬埔寨 +855
泰国 +66
越南 +84
中国香港 +852
马来西亚 +60
新加坡 +65
美国 +1
客户端工作台
小白按摩
客户端注册
小白按摩
'); } return; } alert('无法打开银行支付页面,请改用手机浏览器重试'); openPayPage(); } function handlePayInitResponse(orderId, payName, init) { if (!init || !init.ok) { hidePayLoading(); return; } if (init.alreadyPaid) { showPaySuccess(orderId, payName); return; } if (init.mode === 'balance') { hidePayLoading(); showPaySuccess(orderId, payName); if (typeof loadUserOrders === 'function') loadUserOrders(); return; } function ensurePayPoll() { if (init.poll) startPayPoll(orderId, init.poll, payName, { silent: isMobilePayDevice() }); else startPayPoll(orderId, {}, payName, { silent: isMobilePayDevice() }); } if (init.mode === 'form_post' && init.formPost) { hidePayLoading(); submitAcledaForm(init.formPost); ensurePayPoll(); return; } if (init.mode === 'alipay_html' && init.alipayHtml) { hidePayLoading(); var div = document.createElement('div'); div.innerHTML = init.alipayHtml; document.body.appendChild(div); var f = div.querySelector('form'); if (f) f.submit(); else alert('无法加载支付宝表单'); ensurePayPoll(); return; } if (init.mode === 'aba_collect' && init.abaCollect) { hidePayLoading(); openAbaCollectPage(init); return; } if (init.mode === 'aba_scheme' && (init.scheme || init.redirectUrl)) { launchAbaPayCheckout(orderId, payName, init); return; } if (init.mode === 'qrcode' && init.qrcode) { if (init.scheme || init.redirectUrl) { launchAbaPayCheckout(orderId, payName, init); return; } hidePayLoading(); ensurePayPoll(); var w = window.open('', '_blank'); if (w) { w.document.write('扫码支付

请扫码完成支付

'); } return; } if ((init.mode === 'webview' || init.mode === 'redirect') && init.redirectUrl) { hidePayLoading(); ensurePayPoll(); openPayCheckout(init.redirectUrl, 'Pay', payName || 'ABA Pay'); return; } if (init.mode === 'poll' && init.poll) { hidePayLoading(); startPayPoll(orderId, init.poll, payName); return; } hidePayLoading(); alert((init && init.message) || '支付渠道未返回跳转地址,请检查 payment_config 上游配置'); openPayPage(); } var __jwWalletBalance = null; function fetchClientWalletBalance() { if (typeof isUserAppLoggedIn === 'function' && !isUserAppLoggedIn()) { __jwWalletBalance = 0; return Promise.resolve(0); } return fetch(apiBase() + '/api/client/wallet?jwPkg=' + encodeURIComponent(JW_PKG), { headers: clientAuthHeaders({ Accept: 'application/json' }) }).then(function (r) { return r.json().then(function (j) { if (j && j.ok) { __jwWalletBalance = Math.round((Number(j.balance) || 0) * 100) / 100; } return __jwWalletBalance != null ? __jwWalletBalance : 0; }); }).catch(function () { return __jwWalletBalance != null ? __jwWalletBalance : 0; }); } function showPaySuccess(orderId, payName) { var sub = document.getElementById('pay-success-sub'); closeAbaCollectPage(); if (confirmOrderCheckout.isWalletRecharge) { var add = getCheckoutPayableAmount(); confirmOrderCheckout.isWalletRecharge = false; fetchClientWalletBalance().then(function () { refreshWalletBalanceUi(); }); if (sub) { sub.textContent = '充值成功!已到账 $' + add.toFixed(2) + ',可在「钱包支付」中使用。'; } } else if (sub) { sub.textContent = '订单 ' + (orderId || '') + ' 已支付(' + (payName || '') + ')。技师完成服务后,分润将自动入账。'; } if (payName === '钱包支付') fetchClientWalletBalance().then(function () { refreshWalletBalanceUi(); }); document.getElementById('page-pay').classList.remove('show'); document.getElementById('page-confirm-order').classList.remove('show'); document.getElementById('page-pay-success').classList.add('show'); if (window.JwOrderSound && typeof JwOrderSound.refresh === 'function') JwOrderSound.refresh(); syncCheckoutFlowNav(); } function closePaySuccess() { document.getElementById('page-pay-success').classList.remove('show'); syncCheckoutFlowNav(); if (confirmOrderCheckout.isWalletRecharge) { closePayPage(); if (typeof closeOverlayToReturn === 'function') closeOverlayToReturn(); else showPage('page-my'); return; } closeConfirmOrder(); setOrdersFilterTab('working'); showPage('page-orders'); if (typeof loadUserOrders === 'function') loadUserOrders(function () { setOrdersFilterTab('working'); renderUserOrders(__userOrdersCache); }); } function refreshWalletBalanceUi() { fetchClientWalletBalance().then(function (bal) { var el = document.querySelector('#page-wallet .hw-balance .amt'); if (el) { el.textContent = '$' + (Math.round(bal * 100) / 100).toFixed(bal % 1 === 0 ? 0 : 2); } var payLbl = document.querySelector('#pay-method-list [data-pay-id="钱包支付"] .pay-name'); if (payLbl) payLbl.textContent = '钱包支付 ($' + bal.toFixed(2) + ')'; }); } function parseServiceAmount(s) { if (!s || typeof s !== 'object') return 0; var amt = s.amount != null ? String(s.amount) : (s.price != null ? String(s.price) : ''); var n = parseFloat(String(amt).replace(/[^0-9.]/g, '')); return isNaN(n) ? 0 : n; } function formatTierMoney(n) { if (!isFinite(n)) return '0'; return n % 1 === 0 ? String(Math.round(n)) : n.toFixed(2); } function renderConfirmOrderDuration(items, selectedIdx) { var box = document.getElementById('confirm-order-duration'); if (!box) return; var list = Array.isArray(items) ? items.slice(0, 4) : []; if (!list.length) { box.innerHTML = '暂无价格档'; return; } box.innerHTML = list.map(function (s, i) { var amt = parseServiceAmount(s); var mins = serviceItemMinutes(s); var label = '$' + formatTierMoney(amt) + '/' + mins + ' 分钟'; return '' + escHtml(label) + ''; }).join(''); box.querySelectorAll('.opt[data-tier]').forEach(function (el) { el.onclick = function () { selectConfirmOrderTier(parseInt(el.getAttribute('data-tier'), 10) || 0); }; }); } function selectConfirmOrderTier(idx) { var st = confirmOrderCheckout; var items = st.serviceItems || []; if (!items[idx]) return; st.selectedTier = idx; st.baseAmount = parseServiceAmount(items[idx]); var nameEl = document.getElementById('confirm-order-name'); var tierName = String(items[idx].name || '').trim(); if (nameEl) nameEl.textContent = tierName || st.serviceName || '按摩服务'; renderConfirmOrderDuration(items, idx); refreshConfirmOrderTotals(); } window.selectConfirmOrderTier = selectConfirmOrderTier; function openConfirmOrder(serviceName, price, itemsOpt) { var el = document.getElementById('page-confirm-order'); var st = confirmOrderCheckout; st.vipSelected = false; st.orderId = ''; st.serviceName = serviceName || '按摩服务'; st.techId = __activeProviderId != null ? String(__activeProviderId) : ''; var g = st.techId ? findGirlById(st.techId) : null; st.techName = (g && g.name) ? String(g.name) : ''; var techEl = document.getElementById('confirm-order-tech'); if (techEl) techEl.textContent = (st.techName || '—') + ' >'; function finish(items) { st.serviceItems = Array.isArray(items) ? items.slice(0, 4) : []; var sel = 0; var want = parseCheckoutAmount(price); for (var i = 0; i < st.serviceItems.length; i++) { if (Math.abs(parseServiceAmount(st.serviceItems[i]) - want) < 0.01) { sel = i; break; } } st.selectedTier = sel; st.baseAmount = st.serviceItems[sel] ? parseServiceAmount(st.serviceItems[sel]) : want; var nameEl = document.getElementById('confirm-order-name'); var tierName = st.serviceItems[sel] ? String(st.serviceItems[sel].name || '').trim() : ''; if (nameEl) nameEl.textContent = tierName || st.serviceName; renderConfirmOrderDuration(st.serviceItems, sel); function applyDiscountThenShow() { var cat = svcLabelFromGirl(g) || __activeCatTag || '按摩'; st.couponOff = resolveCouponOffForCategory(cat); refreshConfirmOrderTotals(); if (el) el.classList.add('show'); syncCheckoutFlowNav(); } if (__memberDiscountCache) applyDiscountThenShow(); else loadMemberDiscounts(applyDiscountThenShow); } if (itemsOpt && itemsOpt.length) { finish(itemsOpt); return; } function resolveItems() { return resolveGirlServiceItems(g) || projectItemsForCat(svcLabelFromGirl(g) || __activeCatTag || '按摩') || []; } if (__projectItemsCache) finish(resolveItems()); else loadProjectItems(function () { finish(resolveItems()); }); } function closeConfirmOrder() { document.getElementById('page-confirm-order').classList.remove('show'); syncCheckoutFlowNav(); } function openAddressSelect() { document.getElementById('page-address-select').classList.add('show'); } function closeAddressSelect() { document.getElementById('page-address-select').classList.remove('show'); } function openAddressMgmt() { document.getElementById('page-address-mgmt').classList.add('show'); } function closeAddressMgmt() { document.getElementById('page-address-mgmt').classList.remove('show'); } function openLocSystem() { renderLocSystemPage(); document.getElementById('page-locate-system').classList.add('show'); } function closeLocSystem() { document.getElementById('page-locate-system').classList.remove('show'); } function openGoogleMap() { window.open('https://www.google.com/maps', '_blank'); } function pickAddress(name, addr) { var el = document.getElementById('confirm-order-addr'); if (el) el.textContent = name + ' ' + addr; closeAddressSelect(); } function goToPay() { var addrEl = document.getElementById('confirm-order-addr'); var addrText = (addrEl && addrEl.textContent) ? addrEl.textContent.trim() : ''; if (!addrText || /^未设置默认地址/.test(addrText)) { alert('请先选择或添加服务地址。\n\n已为您打开地址列表,点选一条地址后即可「去支付」。'); openAddressSelect(); return; } closeConfirmOrder(); openPayPage(); } function openVipPage() { if (typeof openOverlayPage === 'function') openOverlayPage('page-member-center', 'page-home'); else document.getElementById('page-vip').classList.add('show'); } function closeVipPage() { document.getElementById('page-vip').classList.remove('show'); } var __payCountdownTimer = null; var __payCountdownLeft = 0; var PAY_METHOD_GROUPS = [ { title: '银行支付', icon: '📱', items: [ { id: 'ABA Pay', label: 'ABA Pay', logo: 'assets/pay/aba-pay.svg', note: 'ABA 手机银行快捷支付' }, { id: 'KHQR', label: 'KHQR', logo: 'assets/pay/khqr.svg', note: '支持柬埔寨所有银行扫码' }, { id: 'WingBank', label: 'WingBank', logo: 'assets/pay/wingbank.svg', note: 'Required WingBank APP installed and registered', noteClass: 'warn' }, { id: 'ACLEDA mobile APP', label: 'ACLEDA mobile APP', logo: 'assets/pay/acleda.svg', note: 'Required ACLEDA APP installed and registered', noteClass: 'warn' } ] }, { title: '电子钱包', icon: '💳', items: [ { id: 'Pai', label: 'Pai', logo: 'assets/pay/pai.svg', note: '需要安装Pai APP支付', fee: '3%' } ] }, { title: '信用卡/借记卡', icon: '💳', items: [ { id: 'Credit/Debit Card', label: 'Credit/Debit Card', logo: 'assets/pay/card-intl.svg', note: 'VISA / Mastercard' } ] }, { title: '钱包支付', icon: '👛', items: [ { id: '钱包支付', label: '钱包支付', logo: 'assets/pay/wallet.svg', dynamicBalance: true } ] } ]; var BLOCKED_PAY_METHODS = ['微信支付', '支付宝', '银联卡']; var __abaCollectState = { orderId: '', payName: '' }; var __payConfigCache = null; function fetchPayConfig(force) { if (__payConfigCache && !force) return Promise.resolve(__payConfigCache); return fetch(apiBase() + '/api/pay/config?jwPkg=' + encodeURIComponent(JW_PKG), { headers: { Accept: 'application/json' } }) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (j) { if (j && j.ok) { __payConfigCache = j; if (Array.isArray(j.blockedMethods) && j.blockedMethods.length) { BLOCKED_PAY_METHODS = j.blockedMethods.slice(); } } return __payConfigCache; }) .catch(function () { return __payConfigCache; }); } function pickDefaultPayMethodId(lastId) { var cfg = __payConfigCache; var qrMethods = (cfg && cfg.staticCollectMethods) || []; if (cfg && cfg.hasToken === false && qrMethods.length) { if (lastId && qrMethods.indexOf(lastId) >= 0) return lastId; if (qrMethods.indexOf('KHQR') >= 0) return 'KHQR'; return qrMethods[0]; } return lastId || 'KHQR'; } function resolvePayQrUrl(u) { u = String(u || '').trim(); if (!u) return payAssetUrl('assets/pay/aba-collect-qr.png'); if (/^https?:\/\//i.test(u)) return u; if (u.indexOf('/api/') === 0) return apiBase() + u; return payAssetUrl(u.replace(/^\//, '')); } function payAssetUrl(rel) { var base = location.pathname.replace(/\/[^/]*$/, '/'); if (base === '/') base = './'; return base + String(rel || '').replace(/^\.\//, ''); } function getWalletBalanceDisplay() { if (__jwWalletBalance != null) return __jwWalletBalance; fetchClientWalletBalance(); return 0; } function getCheckoutPayableAmount() { var st = confirmOrderCheckout; var payable = st.baseAmount - st.couponOff; if (st.vipSelected) payable = st.baseAmount - st.couponOff + st.vipFee - st.vipOff; payable = Math.max(0, Math.round(payable * 100) / 100); var ship = getEzspaShippingFee(); if (ship > 0) payable = Math.round((payable + ship) * 100) / 100; return payable; } function getLastPayMethodId() { try { return (localStorage.getItem('jw_last_pay_method') || '').trim(); } catch (e) { return ''; } } function setLastPayMethodId(id) { try { if (id) localStorage.setItem('jw_last_pay_method', String(id)); } catch (e2) {} } function stopPayCountdown() { if (__payCountdownTimer) { clearInterval(__payCountdownTimer); __payCountdownTimer = null; } } function formatPayCountdown(sec) { sec = Math.max(0, Number(sec) || 0); var h = Math.floor(sec / 3600); var m = Math.floor((sec % 3600) / 60); var s = sec % 60; function pad(n) { return (n < 10 ? '0' : '') + n; } return pad(h) + ':' + pad(m) + ':' + pad(s); } function startPayCountdown(seconds) { stopPayCountdown(); __payCountdownLeft = Number(seconds) > 0 ? Number(seconds) : 900; var el = document.getElementById('pay-countdown-text'); function tick() { if (el) el.textContent = formatPayCountdown(__payCountdownLeft); if (__payCountdownLeft <= 0) { stopPayCountdown(); alert('支付超时,请重新下单'); closePayPage(); return; } __payCountdownLeft -= 1; } tick(); __payCountdownTimer = setInterval(tick, 1000); } function renderPayMethodLabel(item) { if (item.dynamicBalance) { var bal = getWalletBalanceDisplay(); return '钱包支付 ($' + bal.toFixed(2) + ')'; } if (item.fee) { return item.label + ' 手续费 ' + item.fee + 'i'; } return item.label; } function renderPayMethodsUi() { var host = document.getElementById('pay-methods-body'); if (!host) return; var blocked = BLOCKED_PAY_METHODS || []; var last = getLastPayMethodId(); if (last && blocked.indexOf(last) >= 0) last = ''; var defaultId = pickDefaultPayMethodId(last); var hintEl = document.getElementById('pay-no-token-hint'); if (hintEl) { var showHint = __payConfigCache && __payConfigCache.hasToken === false && (__payConfigCache.staticCollectMethods || []).length; hintEl.style.display = showHint ? '' : 'none'; } var html = ''; PAY_METHOD_GROUPS.forEach(function (grp) { var items = grp.items.filter(function (item) { return blocked.indexOf(item.id) < 0; }); if (!items.length) return; html += '
' + grp.icon + '' + grp.title + '
'; items.forEach(function (item) { var noteCls = item.noteClass ? (' note ' + item.noteClass) : ' note'; var noteHtml = item.note ? ('
' + item.note + '
') : ''; var recent = last && item.id === last ? '最近使用' : ''; html += '
' + '' + '
' + renderPayMethodLabel(item) + '
' + recent + '
' + noteHtml + '
'; }); html += '
'; }); host.innerHTML = html; var opts = host.querySelectorAll('.pay-option'); var picked = null; for (var i = 0; i < opts.length; i++) { if (opts[i].getAttribute('data-pay-method') === defaultId) { picked = opts[i]; break; } } if (!picked && opts.length) picked = opts[0]; if (picked) selectPay(picked, true); } function refreshPayPageUi() { var amtEl = document.getElementById('pay-amount-text'); if (amtEl) amtEl.textContent = '$' + getCheckoutPayableAmount().toFixed(2); fetchPayConfig(true).finally(function () { renderPayMethodsUi(); startPayCountdown(900); }); } function openPayPage() { var pm = document.getElementById('page-member-center'); if (pm) { pm.classList.remove('show'); pm.classList.remove('active'); } closeVipPage(); document.getElementById('page-pay').classList.add('show'); refreshPayPageUi(); syncCheckoutFlowNav(); } function closePayPage() { stopPayCountdown(); document.getElementById('page-pay').classList.remove('show'); syncCheckoutFlowNav(); } function selectPay(el, silent) { if (!el) return; document.querySelectorAll('#page-pay .pay-option').forEach(function (o) { o.classList.remove('selected'); }); el.classList.add('selected'); if (!silent) { var id = el.getAttribute('data-pay-method') || ''; if (id) setLastPayMethodId(id); } } function getSelectedPayName() { var opt = document.querySelector('#page-pay .pay-option.selected'); if (!opt) return ''; return opt.getAttribute('data-pay-method') || ''; } function getEzspaShippingFee() { return 0; } function getOrderServiceAmount(o) { if (!o) return 0; if (o.baseAmount != null && isFinite(Number(o.baseAmount))) { return Math.max(0, Math.round(Number(o.baseAmount) * 100) / 100); } var ship = Number(o.shipping != null ? o.shipping : getEzspaShippingFee()); if (!isFinite(ship)) ship = getEzspaShippingFee(); var amt = Number(o.amount); if (!isFinite(amt)) return 0; return Math.max(0, Math.round((amt - ship) * 100) / 100); } function getOrderPayableAmount(o) { if (!o) return 0; if (o.baseAmount != null && isFinite(Number(o.baseAmount))) { var base = Number(o.baseAmount) || 0; var off = Number(o.couponOff) || 0; var ship = Number(o.shipping != null ? o.shipping : getEzspaShippingFee()); if (!isFinite(ship)) ship = getEzspaShippingFee(); return Math.max(0, Math.round((base - off + ship) * 100) / 100); } var amt = Number(o.amount); return isFinite(amt) ? Math.max(0, Math.round(amt * 100) / 100) : 0; } function formatEzspaDateTime(d) { var dt = d instanceof Date ? d : new Date(d); var p = function (n) { return n < 10 ? '0' + n : String(n); }; return dt.getFullYear() + '-' + p(dt.getMonth() + 1) + '-' + p(dt.getDate()) + ' ' + p(dt.getHours()) + ':' + p(dt.getMinutes()) + ':' + p(dt.getSeconds()); } function buildEzspaPayExtras(st, payable, tier, serviceName) { var shipping = getEzspaShippingFee(); var now = new Date(); var cancel = new Date(now.getTime() + 10 * 60 * 1000); return { shipping: shipping, payMoney: Math.round((payable + shipping) * 100) / 100, itemsid: (tier && (tier.ezspaId || tier.itemsid || tier.id)) || '', itemsname: serviceName, itemsbrand: st.techId || __activeProviderId || '', nowTime: formatEzspaDateTime(now), cancelTime: formatEzspaDateTime(cancel), scene: 1 }; } function confirmPay() { var name = getSelectedPayName(); if (!name) { alert('请先选择支付方式后再确认。'); return; } if ((BLOCKED_PAY_METHODS || []).indexOf(name) >= 0) { alert('海外站点不支持微信/支付宝/银联等人民币支付,请使用 KHQR 或当地银行方式。'); return; } function runConfirmPayFlow() { setLastPayMethodId(name); stopPayCountdown(); var st = confirmOrderCheckout; var payable = st.baseAmount - st.couponOff; if (st.vipSelected) payable = st.baseAmount - st.couponOff + st.vipFee - st.vipOff; payable = Math.max(0, Math.round(payable * 100) / 100); var addrEl = document.getElementById('confirm-order-addr'); var remarkEl = document.querySelector('#page-confirm-order .co-remark'); var serviceName = (document.getElementById('confirm-order-name') || {}).textContent || '按摩服务'; var tier = (st.serviceItems && st.serviceItems[st.selectedTier]) ? st.serviceItems[st.selectedTier] : null; var ezspaPay = buildEzspaPayExtras(st, payable, tier, serviceName); var body = { serviceName: serviceName, techId: st.techId || __activeProviderId || '', techName: st.techName || '', amount: ezspaPay.payMoney, baseAmount: st.baseAmount, shipping: ezspaPay.shipping, couponOff: st.couponOff || 0, serviceMins: tier ? serviceItemMinutes(tier) : '', tierIndex: st.selectedTier, pay_method: name, address: addrEl ? String(addrEl.textContent || '').trim() : '', remark: remarkEl ? String(remarkEl.value || '').trim() : '', status: 'pending_pay', payment_status: 'unpaid', shipping: ezspaPay.shipping, itemsid: ezspaPay.itemsid, itemsname: ezspaPay.itemsname, itemsbrand: ezspaPay.itemsbrand, nowTime: ezspaPay.nowTime, cancelTime: ezspaPay.cancelTime, appointmentAt: formatEzspaDateTime(new Date()) }; showPayLoading('正在创建订单…'); function getPayUpstreamToken() { try { return (localStorage.getItem('jw_ezspa_token') || localStorage.getItem('ezspa_token') || '').trim(); } catch (eTok) { return ''; } } function saveEzspaToken(tok) { if (!tok) return; try { localStorage.setItem('jw_ezspa_token', tok); } catch (eSave) {} } function promptBindEzspaToken() { var hint = '支付需要 Ezspa 平台 Token(不是本站登录 JWT)。\n\n' + '1. 手机/电脑打开 https://m.ezspa.com 并登录\n' + '2. 打开开发者工具 → Network → 任意 api.99spa.com 请求\n' + '3. 复制 Request Headers 里的 Token(通常不是 eyJ 开头的 JWT)\n\n' + '请粘贴 Token:'; var tok = window.prompt(hint, ''); if (!tok || !String(tok).trim()) return Promise.resolve(false); return fetch(apiBase() + '/api/auth/ezspa/token?jwPkg=' + encodeURIComponent(JW_PKG), { method: 'POST', headers: clientAuthHeaders({ Accept: 'application/json' }), body: JSON.stringify({ token: String(tok).trim() }) }).then(function (r) { return r.json().then(function (j) { if (j && j.ok) { saveEzspaToken(String(tok).trim()); return true; } alert((j && (j.message || j.error)) || 'Token 保存失败'); return false; }); }).catch(function () { alert('网络错误'); return false; }); } function doPayInit(orderId) { var payTok = getPayUpstreamToken(); var initBody = { pay_method: name, payMoney: ezspaPay.payMoney, shipping: ezspaPay.shipping, itemsid: ezspaPay.itemsid, itemsname: ezspaPay.itemsname, itemsbrand: ezspaPay.itemsbrand, nowTime: ezspaPay.nowTime, cancelTime: ezspaPay.cancelTime, scene: ezspaPay.scene }; if (payTok) initBody.token = payTok; return fetch(apiBase() + '/api/orders/' + encodeURIComponent(orderId) + '/pay/init?jwPkg=' + encodeURIComponent(JW_PKG), { method: 'POST', headers: clientAuthHeaders({ Accept: 'application/json' }), body: JSON.stringify(initBody) }).then(function (r3) { return r3.json().then(function (j3) { return { ok: r3.ok, j: j3, orderId: orderId }; }); }); } function afterOrder(oid) { confirmOrderCheckout.orderId = oid; showPayLoading('正在跳转支付…'); return doPayInit(oid).then(function (initRes) { hidePayLoading(); if (!initRes.ok || !initRes.j || !initRes.j.ok) { var err = initRes.j || {}; if (err.needLogin || err.code === 402 || err.error === 'ezspa_token_missing' || err.error === 'ezspa_token_invalid') { alert(err.message || '当前未配置支付 Token,请改用 KHQR 扫码支付。'); openPayPage(); } else if (err.error === 'payment_not_configured') { alert('支付网关未配置,请联系管理员。'); openPayPage(); } else { alert(err.message || err.error || '发起支付失败'); openPayPage(); } return; } handlePayInitResponse(initRes.orderId, name, initRes.j); }); } if (st.orderId) { afterOrder(st.orderId).catch(function () { hidePayLoading(); alert('网络异常,请稍后重试'); }); return; } fetch(apiBase() + '/api/orders?jwPkg=' + encodeURIComponent(JW_PKG), { method: 'POST', headers: clientAuthHeaders({ Accept: 'application/json' }), body: JSON.stringify(body) }) .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); }) .then(function (res) { if (!res.ok || !res.j || !res.j.order) { hidePayLoading(); alert((res.j && (res.j.message || res.j.error)) || '下单失败,请重试'); return; } return afterOrder(res.j.order.id); }) .catch(function () { hidePayLoading(); alert('网络异常,请稍后重试'); }); } if (name === '钱包支付') { showPayLoading('正在校验钱包余额…'); fetchClientWalletBalance().then(function (bal) { hidePayLoading(); var need = getCheckoutPayableAmount(); if (bal < need) { alert('钱包余额不足(当前 $' + bal.toFixed(2) + '),请选择其它支付方式或先充值。'); return; } runConfirmPayFlow(); }).catch(function () { hidePayLoading(); alert('无法读取钱包余额,请稍后重试'); }); return; } runConfirmPayFlow(); } var __userOrdersCache = []; var __ordersFilterTab = 'all'; function parseOrderDate(s) { if (!s) return null; var t = Date.parse(String(s).replace(' ', 'T')); return isFinite(t) ? new Date(t) : null; } function applyPaidOrderToCache(order) { if (!order || order.id == null) return; var id = String(order.id); var found = false; __userOrdersCache = (__userOrdersCache || []).map(function (row) { if (String(row.id) !== id) return row; found = true; return Object.assign({}, row, order, { status: order.status || 'working', order_status: order.order_status || '进行中', payment_status: order.payment_status || 'paid' }); }); if (!found) { __userOrdersCache.unshift(Object.assign({}, order, { status: order.status || 'working', order_status: order.order_status || '进行中', payment_status: order.payment_status || 'paid' })); } updateMyOrderStats(__userOrdersCache); } function setOrdersFilterTab(tabKey) { __ordersFilterTab = tabKey || 'all'; var page = document.getElementById('page-orders'); if (!page) return; page.querySelectorAll('.ord-pc-tabs .tab[data-ord-filter]').forEach(function (t) { t.classList.toggle('active', (t.getAttribute('data-ord-filter') || 'all') === __ordersFilterTab); }); } function finishKhqrPaidFlow(orderId, payName, order) { if (order) applyPaidOrderToCache(order); else if (orderId) { applyPaidOrderToCache({ id: orderId, status: 'working', order_status: '进行中', payment_status: 'paid', pay_method: payName || 'KHQR' }); } stopPayPoll(); closeAbaCollectPage(); closePayPage(); closeConfirmOrder(); document.getElementById('page-pay-success').classList.remove('show'); setOrdersFilterTab('working'); renderUserOrders(__userOrdersCache); showPage('page-orders'); syncCheckoutFlowNav(); loadUserOrders(function () { setOrdersFilterTab('working'); renderUserOrders(__userOrdersCache); }); } function orderIsExpired(o) { var ct = parseOrderDate(o && o.cancelTime); if (!ct) return false; return ct.getTime() < Date.now(); } function orderBucket(o) { var st = String((o && o.status) || '').toLowerCase(); var pay = String((o && o.payment_status) || '').toLowerCase(); if (st === 'cancelled' || st === 'canceled' || st === '已取消') return 'cancelled'; if (/completed|done|finish|完成/.test(st)) return 'completed'; if (pay === 'paid' || st === 'paid' || /processing|working|服务中|进行中|in_progress/.test(st)) return 'working'; if (st === 'pending_pay' || pay === 'unpaid' || pay === 'pending' || pay === 'pending_confirm' || st === 'pending') { if (orderIsExpired(o)) return 'cancelled'; return 'pending'; } return 'pending'; } function orderStatusText(o) { var bucket = orderBucket(o); if (bucket === 'cancelled') return '已取消'; if (bucket === 'completed') return '已完成'; if (bucket === 'working') return '进行中'; return '待支付'; } function orderStatusClass(o) { var bucket = orderBucket(o); if (bucket === 'cancelled') return 'muted'; if (bucket === 'pending') return 'warn'; return ''; } function formatOrderMoney(n) { var v = Number(n); if (!isFinite(v)) v = 0; var s = (Math.round(v * 100) / 100).toFixed(2); if (s.slice(-3) === '.00') s = s.slice(0, -3); return '$' + s; } function orderGirlId(o) { if (!o) return ''; return String( o.techId != null ? o.techId : o.coach_id != null ? o.coach_id : o.girl_id != null ? o.girl_id : '' ).trim(); } function orderGirlFromOrder(o) { var id = orderGirlId(o); return id ? findGirlById(id) : null; } function orderTechLabel(o) { var name = String((o && o.techName) || '').trim() || '服务者'; var g = orderGirlFromOrder(o); var no = g && (g.no || g.display_no || g.serial || g.code); if (!no) { var id = orderGirlId(o); if (id) no = id.length > 3 ? id.slice(-3) : id; } return no ? (name + '(' + no + ')') : name; } function orderTechAvatarUrl(o) { var g = orderGirlFromOrder(o); var photos = girlPhotoList(g); if (photos[0]) return circleAvatarUrl(photos[0], 56, g); return TECH_PHOTO_PLACEHOLDER; } function orderThumbUrl(o) { var g = orderGirlFromOrder(o); var photos = girlPhotoList(g); var album = girlAlbumPhotoList(g); var pick = album[0] || photos[0] || ''; if (pick) return circleAvatarUrl(pick, 144, g); return TECH_PHOTO_PLACEHOLDER; } function orderApptText(o) { var ap = String((o && o.appointmentAt) || '').trim(); if (ap) return ap.replace('T', ' ').slice(0, 16); var cr = String((o && o.created_at) || '').trim(); return cr ? cr.replace('T', ' ').slice(0, 16) : '—'; } function updateMyOrderStats(list) { var stats = { pending: 0, working: 0, completed: 0, cancelled: 0 }; (list || []).forEach(function (o) { var b = orderBucket(o); if (stats[b] != null) stats[b] += 1; }); var els = document.querySelectorAll('#page-my .my-pc-statbox .my-pc-stat b'); if (els.length >= 4) { els[0].textContent = String(stats.pending); els[1].textContent = String(stats.working); els[2].textContent = String(stats.completed); els[3].textContent = String(stats.cancelled); } } function renderUserOrders(list) { var box = document.getElementById('ord-pc-list'); var empty = document.getElementById('ord-pc-empty'); if (!box || !empty) return; var filtered = (list || []).slice().sort(function (a, b) { var ta = parseOrderDate(a && a.created_at); var tb = parseOrderDate(b && b.created_at); return (tb ? tb.getTime() : 0) - (ta ? ta.getTime() : 0); }); if (__ordersFilterTab && __ordersFilterTab !== 'all') { filtered = filtered.filter(function (o) { return orderBucket(o) === __ordersFilterTab; }); } if (!filtered.length) { box.innerHTML = ''; box.hidden = true; empty.style.display = ''; var tip = empty.querySelector('.ord-pc-empty-tip'); if (tip) { tip.textContent = (list && list.length) ? '该分类暂无订单' : (isUserAppLoggedIn() ? '暂无订单' : '登录后查看订单'); } return; } empty.style.display = 'none'; box.hidden = false; box.innerHTML = filtered.map(function (o) { var bucket = orderBucket(o); var actions = ''; if (bucket === 'pending') { actions += ''; actions += ''; } actions += ''; actions += ''; var techAv = orderTechAvatarUrl(o); var techAvHtml = ''; var thumbUrl = orderThumbUrl(o); return ( '
' + '
' + '
' + techAvHtml + '' + escHtml(orderTechLabel(o)) + '
' + '
' + '' + '' + escHtml(orderStatusText(o)) + '' + '
' + '
' + '
' + '' + '
' + '
' + escHtml(o.serviceName || '按摩服务') + '
' + '
预约时间:' + escHtml(orderApptText(o)) + '
' + '
' + '
' + escHtml(formatOrderMoney(getOrderPayableAmount(o))) + '
' + '
' + '
' + actions + '
' + '
' ); }).join(''); box.querySelectorAll('[data-ord-pay]').forEach(function (btn) { btn.onclick = function () { resumeOrderPay(btn.getAttribute('data-ord-pay')); }; }); box.querySelectorAll('[data-ord-again]').forEach(function (btn) { btn.onclick = function () { reorderFromHistory(btn.getAttribute('data-ord-again')); }; }); box.querySelectorAll('[data-ord-del]').forEach(function (btn) { btn.onclick = function () { deleteUserOrder(btn.getAttribute('data-ord-del')); }; }); box.querySelectorAll('[data-ord-cancel]').forEach(function (btn) { btn.onclick = function () { cancelUserOrder(btn.getAttribute('data-ord-cancel')); }; }); box.querySelectorAll('[data-ord-chat]').forEach(function (btn) { btn.onclick = function () { openClientOrderChat(btn.getAttribute('data-ord-chat')); }; }); } var __clientChatOrderId = ''; var __clientChatPoll = null; var __clientChatCloseTimer = null; function stopClientChatPoll() { if (__clientChatPoll) { clearInterval(__clientChatPoll); __clientChatPoll = null; } } function stopClientChatCloseTimer() { if (__clientChatCloseTimer) { clearTimeout(__clientChatCloseTimer); __clientChatCloseTimer = null; } } function scheduleClientChatAutoClose(chat) { stopClientChatCloseTimer(); if (!chat || chat.closed || !chat.closeAt) return; var endMs = Date.parse(String(chat.closeAt).replace(' ', 'T')); if (!isFinite(endMs)) return; var delay = endMs - Date.now(); if (delay <= 0) { if (__clientChatOrderId) loadClientOrderChat(); return; } __clientChatCloseTimer = setTimeout(function () { if (__clientChatOrderId) loadClientOrderChat(); }, Math.min(delay, 2147483647)); } function openClientOrderInbox() { if (!isUserAppLoggedIn()) { alert('请先登录'); return; } openOverlayPage('page-order-inbox', getActivePageId()); loadClientOrderInbox(); } function closeClientOrderInbox() { try { if (window.JwOrderNotify) JwOrderNotify.onInboxClose(); } catch (eN) {} closeOverlayToReturn(); } function loadClientOrderInbox() { var box = document.getElementById('client-order-inbox-list'); if (!box) return; box.innerHTML = '
加载中…
'; fetch(apiBase() + '/api/order-chats/inbox?jwPkg=' + encodeURIComponent(JW_PKG), { headers: clientAuthHeaders({ Accept: 'application/json' }) }) .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); }) .then(function (res) { var items = res.ok && res.j && Array.isArray(res.j.items) ? res.j.items : []; if (!items.length) { box.innerHTML = '
暂无订单消息
'; return; } box.innerHTML = items.map(function (it) { var snippet = String(it.lastText || '').replace(/^【系统消息】/, '[系统消息]'); return '
' + '
' + escHtml(it.techName || '服务者') + '
' + '
' + escHtml(snippet) + '
' + '' + escHtml(it.displayAt || '') + '
'; }).join(''); box.querySelectorAll('[data-inbox-oid]').forEach(function (el) { el.onclick = function () { openClientOrderChat(el.getAttribute('data-inbox-oid')); }; }); try { if (window.JwOrderNotify) JwOrderNotify.onInboxOpen(items); } catch (eN) {} }) .catch(function () { box.innerHTML = '
加载失败
'; }); } function renderClientChatMessages(chat) { var body = document.getElementById('client-order-chat-body'); var foot = document.getElementById('client-order-chat-foot'); var closedTip = document.getElementById('client-order-chat-closed'); var inp = document.getElementById('client-order-chat-inp'); var sendBtn = document.getElementById('client-order-chat-send'); var statusEl = document.getElementById('client-chat-status'); if (!body) return; var msgs = (chat && chat.messages) || []; body.innerHTML = msgs.map(function (m) { var cls = 'ord-chat-msg'; if (m.role === 'system') cls += ' ord-chat-msg--sys'; else if (m.role === 'client') cls += ' ord-chat-msg--me'; else cls += ' ord-chat-msg--peer'; return '
' + escHtml(m.text) + '
' + (m.displayAt ? '
' + escHtml(m.displayAt) + '
' : '') + '
'; }).join(''); body.scrollTop = body.scrollHeight; var closed = !!(chat && (chat.closed || chat.status === 'closed')); var canSend = !closed && chat && chat.canSend !== false; if (foot) foot.style.display = canSend ? 'flex' : 'none'; if (closedTip) { closedTip.hidden = canSend; if (!canSend && !closed) closedTip.textContent = '预约服务时间已到,会话已关闭'; else if (closed) closedTip.textContent = '服务已经结束,无法继续发送消息'; } if (inp) inp.disabled = !canSend; if (sendBtn) sendBtn.disabled = !canSend; if (statusEl) { if (closed || !canSend) statusEl.textContent = '会话已结束'; else if (chat && chat.closeAtDisplay) statusEl.textContent = '服务至 ' + chat.closeAtDisplay; else statusEl.textContent = '在线'; } if (canSend) scheduleClientChatAutoClose(chat); else stopClientChatCloseTimer(); if ((closed || !canSend) && __clientChatOrderId) { stopClientChatPoll(); setTimeout(function () { if (__clientChatOrderId && String(chat.orderId || '') === __clientChatOrderId) closeClientOrderChat(); }, 2500); } } function loadClientOrderChat() { if (!__clientChatOrderId) return; fetch(apiBase() + '/api/orders/' + encodeURIComponent(__clientChatOrderId) + '/chat?jwPkg=' + encodeURIComponent(JW_PKG), { headers: clientAuthHeaders({ Accept: 'application/json' }) }) .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); }) .then(function (res) { if (!res.ok || !res.j || !res.j.chat) return; renderClientChatMessages(res.j.chat); try { if (window.JwOrderNotify && __clientChatOrderId) { var msgs = (res.j.chat.messages || []); var last = msgs.length ? msgs[msgs.length - 1] : null; JwOrderNotify.onChatOpen(__clientChatOrderId, last && (last.at || last.displayAt)); } } catch (eN) {} }) .catch(function () {}); } function openClientOrderChat(orderId) { orderId = String(orderId || ''); if (!orderId) return; if (!isUserAppLoggedIn()) { alert('请先登录'); return; } __clientChatOrderId = orderId; try { if (window.JwOrderNotify) JwOrderNotify.onChatOpen(orderId); } catch (eN) {} var o = findCachedOrder(orderId); var nameEl = document.getElementById('client-chat-name'); var avEl = document.getElementById('client-chat-avatar'); if (nameEl) nameEl.textContent = o ? orderTechLabel(o) : '服务者'; if (avEl) { avEl.style.visibility = ''; avEl.src = o ? orderTechAvatarUrl(o) : ''; } openOverlayPage('page-order-chat', getActivePageId()); loadClientOrderChat(); stopClientChatPoll(); __clientChatPoll = setInterval(loadClientOrderChat, 4000); } function closeClientOrderChat() { stopClientChatPoll(); stopClientChatCloseTimer(); __clientChatOrderId = ''; try { if (window.JwOrderNotify) JwOrderNotify.onChatClose(); } catch (eN) {} closeOverlayToReturn(); } function sendClientOrderChatMessage() { if (!__clientChatOrderId) return; var inp = document.getElementById('client-order-chat-inp'); var text = inp ? String(inp.value || '').trim() : ''; if (!text) return; fetch(apiBase() + '/api/orders/' + encodeURIComponent(__clientChatOrderId) + '/chat/messages?jwPkg=' + encodeURIComponent(JW_PKG), { method: 'POST', headers: clientAuthHeaders({ Accept: 'application/json' }), body: JSON.stringify({ text: text }) }) .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); }) .then(function (res) { if (!res.ok || !res.j || !res.j.ok) { alert((res.j && (res.j.message || res.j.error)) || '发送失败'); if (res.j && res.j.error === 'chat_closed') closeClientOrderChat(); return; } if (inp) inp.value = ''; renderClientChatMessages(res.j.chat); }) .catch(function () { alert('网络异常'); }); } window.openClientOrderInbox = openClientOrderInbox; window.openClientOrderChat = openClientOrderChat; window.closeClientOrderChat = closeClientOrderChat; window.sendClientOrderChatMessage = sendClientOrderChatMessage; function loadUserOrders(cb) { if (!isUserAppLoggedIn()) { __userOrdersCache = []; renderUserOrders([]); updateMyOrderStats([]); if (cb) cb([]); return; } fetch(apiBase() + '/api/orders?jwPkg=' + encodeURIComponent(JW_PKG), { headers: clientAuthHeaders({ Accept: 'application/json' }) }) .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); }) .then(function (res) { var list = Array.isArray(res.j) ? res.j : (res.j && Array.isArray(res.j.orders) ? res.j.orders : []); if (!res.ok) list = []; ensureGirlsLookupCache(function () { enrichOrdersWithGirlPhotos(list, function () { __userOrdersCache = list; renderUserOrders(list); updateMyOrderStats(list); if (cb) cb(list); }); }); }) .catch(function () { if (cb) cb(__userOrdersCache); }); } window.loadUserOrders = loadUserOrders; window.loadClientOrdersList = loadUserOrders; function findCachedOrder(id) { var sid = String(id || ''); for (var i = 0; i < __userOrdersCache.length; i++) { if (String(__userOrdersCache[i].id) === sid) return __userOrdersCache[i]; } return null; } function resumeOrderPay(orderId) { var o = findCachedOrder(orderId); if (!o) { alert('订单不存在'); return; } confirmOrderCheckout.orderId = String(o.id); confirmOrderCheckout.techId = o.techId != null ? String(o.techId) : ''; confirmOrderCheckout.techName = String(o.techName || ''); confirmOrderCheckout.serviceName = String(o.serviceName || '按摩服务'); confirmOrderCheckout.baseAmount = getOrderServiceAmount(o); confirmOrderCheckout.couponOff = Number(o.couponOff) || 0; confirmOrderCheckout.isWalletRecharge = /^【余额充值】/.test(confirmOrderCheckout.serviceName); if (o.pay_method) setLastPayMethodId(String(o.pay_method)); openPayPage(); } function reorderFromHistory(orderId) { var o = findCachedOrder(orderId); if (!o) { alert('订单不存在'); return; } if (o.techId != null) { __activeProviderId = String(o.techId); var g = findGirlById(__activeProviderId); if (g) { openConfirmOrder(o.serviceName || '按摩服务', getOrderServiceAmount(o), resolveGirlServiceItems(g)); return; } } openConfirmOrder(o.serviceName || '按摩服务', o.amount); } function deleteUserOrder(orderId) { if (!orderId) return; if (!confirm('确定删除该订单?')) return; fetch(apiBase() + '/api/orders/' + encodeURIComponent(orderId) + '?jwPkg=' + encodeURIComponent(JW_PKG), { method: 'DELETE', headers: clientAuthHeaders({ Accept: 'application/json' }) }) .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); }) .then(function (res) { if (!res.ok || !res.j || !res.j.ok) { alert((res.j && (res.j.message || res.j.error)) || '删除失败'); return; } loadUserOrders(); }) .catch(function () { alert('网络异常'); }); } function cancelUserOrder(orderId) { if (!orderId) return; if (!confirm('确定取消该订单?')) return; fetch(apiBase() + '/api/orders/' + encodeURIComponent(orderId) + '/cancel?jwPkg=' + encodeURIComponent(JW_PKG), { method: 'POST', headers: clientAuthHeaders({ Accept: 'application/json' }) }) .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); }) .then(function (res) { if (!res.ok || !res.j || !res.j.ok) { alert((res.j && (res.j.message || res.j.error)) || '取消失败'); return; } loadUserOrders(); }) .catch(function () { alert('网络异常'); }); } function wireOrderPageTabs() { var page = document.getElementById('page-orders'); if (!page) return; page.querySelectorAll('.ord-pc-tabs .tab[data-ord-filter]').forEach(function (tab) { tab.addEventListener('click', function () { page.querySelectorAll('.ord-pc-tabs .tab').forEach(function (t) { t.classList.remove('active'); }); tab.classList.add('active'); __ordersFilterTab = tab.getAttribute('data-ord-filter') || 'all'; renderUserOrders(__userOrdersCache); }); }); } /** 列表金圈:仅当 .avatar-wrap 含 class「online」时显示动效。详情页 .detail-avatar-frame 已取消金圈,本方法仅保留兼容(可改存 data 等)。 */ function setProviderAvatarOnline(avatarWrapEl, isOnline) { if (!avatarWrapEl) return; avatarWrapEl.classList.toggle('online', !!isOnline); } function setDetailHeadAvatarOnline(isOnline) { var hero = document.getElementById('detail-gallery-hero'); if (hero) hero.setAttribute('data-online', isOnline ? '1' : '0'); } window.setProviderAvatarOnline = setProviderAvatarOnline; window.setDetailHeadAvatarOnline = setDetailHeadAvatarOnline; var JW_APP_BRAND = '江微科技'; var __catReturnPage = 'page-square'; var __providerDetailReturnPage = 'page-square'; var __navHistorySilent = false; function isAgentEmbed() { return !!window.__JW_AGENT_EMBED; } function syncNavHistoryState(pageId) { if (__navHistorySilent || isAgentEmbed()) return; try { history.replaceState({ jwPage: pageId || getActivePageId(), t: Date.now() }, '', location.pathname + location.search); } catch (e) {} } function notifyAgentEmbedNav(pageId) { if (!isAgentEmbed()) return; try { window.parent.postMessage({ type: 'jw-agent-embed-nav', page: pageId || getActivePageId() }, '*'); } catch (e2) {} } function getActivePageId() { var p = document.querySelector('.page.active'); return p ? p.id : 'page-home'; } function resetCatTagsTo(label) { label = label || '按摩'; __activeCatTag = label; document.querySelectorAll('.cat-tags-sync .tag').forEach(function(t) { t.classList.toggle('active', t.getAttribute('data-tag') === label); }); } function pushAppNavHistory(pageId) { if (__navHistorySilent || isAgentEmbed()) return; try { history.pushState({ jwPage: pageId, t: Date.now() }, '', location.pathname + location.search); } catch (e) {} } function leaveCatPage(target) { target = target || __catReturnPage || 'page-square'; resetCatTagsTo('按摩'); showPage(target); if (target === 'page-square' || target === 'page-service' || target === 'page-tech' || target === 'page-home') { loadAndRenderTechnicians('按摩'); } syncNavHistoryState(target); notifyAgentEmbedNav(target); } function exitCatPage() { if (getActivePageId() !== 'page-cat') return; leaveCatPage(__catReturnPage || 'page-square'); } function goBackFromProviderDetail() { if (getActivePageId() !== 'page-provider-detail') return; var videoEl = document.getElementById('detail-gallery-video'); if (videoEl) { try { videoEl.pause(); } catch (eV) {} } var target = __providerDetailReturnPage || 'page-square'; showPage(target); syncNavHistoryState(target); notifyAgentEmbedNav(target); } window.exitCatPage = exitCatPage; window.goBackFromProviderDetail = goBackFromProviderDetail; function showCatPage(catName) { var fromId = getActivePageId(); if (fromId !== 'page-cat' && fromId !== 'page-provider-detail') { __catReturnPage = fromId; } document.querySelectorAll('.cat-tags-sync .tag').forEach(function(t) { t.classList.toggle('active', t.getAttribute('data-tag') === catName); }); __activeCatTag = catName; syncCategoryTagBars(catName); if (typeof loadAndRenderTechnicians === 'function') loadAndRenderTechnicians(catName); showPage('page-cat'); if (fromId !== 'page-cat') { pushAppNavHistory('page-cat'); } else { syncNavHistoryState('page-cat'); } } var JW_PKG = 'pkg9'; var DEFAULT_CAT_LABELS = ['按摩', '本地', '越南', '高端', '中国', '双人按', '西班牙', '韩国', '日本']; var __serviceCategoriesCache = null; var SVC_LABEL_TO_KEY = { '按摩': 'massage', '越南': 'vn', '高端': 'high', '中国': 'cn', '双人按': 'double', '西班牙': 'es', '韩国': 'kr', '日本': 'jp', '本地': 'local' }; var SVC_KEY_TO_LABEL = { massage: '按摩', vn: '越南', high: '高端', cn: '中国', double: '双人按', es: '西班牙', kr: '韩国', jp: '日本', local: '本地', 'vn-high': '越南高端' }; var __girlsCache = []; var __girlsCacheAll = []; var __girlsAllLoaded = false; var __workingTechIds = {}; var __projectItemsCache = null; var __memberDiscountCache = null; var PROJ_LABEL_ALIAS = { '双飞': '二人按摩', '双人按': '二人按摩', '二人按摩': '双人按' }; function normalizeProjectCatKey(lbl) { var s = String(lbl || '').trim(); if (!s) return ''; if (s === '双飞' || s === '双人按' || s === '二人按摩') return '二人按摩'; if (s !== '按摩' && s.length > 2 && s.slice(-2) === '按摩') return s.slice(0, -2); return s; } function projectLabelMatches(rowLabel, targetLabel) { var a = normalizeProjectCatKey(rowLabel); var b = normalizeProjectCatKey(targetLabel); return !!(a && b && a === b); } function catLabelToTypeKey(catLabel) { var lbl = String(catLabel || '').trim(); if (SVC_LABEL_TO_KEY[lbl]) return SVC_LABEL_TO_KEY[lbl]; var short = normalizeProjectCatKey(lbl); if (short && SVC_LABEL_TO_KEY[short]) return SVC_LABEL_TO_KEY[short]; if (lbl === '二人按摩' || lbl === '双人按' || lbl === '双飞') return 'double'; return ''; } function loadMemberDiscounts(cb) { fetch(apiBase() + '/api/member/discounts?jwPkg=' + encodeURIComponent(JW_PKG)) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (j) { if (j && j.ok && Array.isArray(j.rows)) __memberDiscountCache = j.rows; if (cb) cb(__memberDiscountCache); }) .catch(function () { if (cb) cb(null); }); } function resolveCouponOffForCategory(catLabel) { if (!__memberDiscountCache || !__memberDiscountCache.length) return 0; var lbl = String(catLabel || '按摩').trim(); for (var i = 0; i < __memberDiscountCache.length; i++) { var row = __memberDiscountCache[i]; if (!row) continue; var pl = String(row.label || '').trim(); if (projectLabelMatches(pl, lbl)) { var n = parseFloat(String(row.amountYuan != null ? row.amountYuan : '0').replace(/[^0-9.]/g, '')); return isNaN(n) ? 0 : Math.max(0, n); } } return 0; } function loadServiceCategories(cb) { fetch(apiBase() + '/api/service/categories?jwPkg=' + encodeURIComponent(JW_PKG)) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (j) { if (j && j.ok && Array.isArray(j.labels) && j.labels.length) { __serviceCategoriesCache = j.labels.slice(); } if (cb) cb(__serviceCategoriesCache || DEFAULT_CAT_LABELS); }) .catch(function () { if (cb) cb(DEFAULT_CAT_LABELS); }); } function clientCatDisplayLabel(lbl) { var s = String(lbl || '').trim(); if (!s || s === '按摩') return '按摩'; if (s === '双人按' || s === '双飞' || s === '二人按摩') return '双人按'; if (s.length >= 2 && s.slice(-2) === '按摩') return s; return s + '按摩'; } function catLabelFromAny(lbl) { var s = String(lbl || '').trim(); if (!s) return '按摩'; if (s === '双人按' || s === '双飞' || s === '二人按摩') return '双人按'; if (s.length >= 2 && s.slice(-2) === '按摩') return s.slice(0, -2); return s; } function uniqueClientCatLabels(labels) { var src = Array.isArray(labels) && labels.length ? labels : DEFAULT_CAT_LABELS; var out = []; var seen = {}; src.forEach(function (lbl) { var key = normalizeProjectCatKey(catLabelFromAny(lbl)) || catLabelFromAny(lbl); if (!key || seen[key]) return; seen[key] = 1; for (var i = 0; i < DEFAULT_CAT_LABELS.length; i++) { if (normalizeProjectCatKey(DEFAULT_CAT_LABELS[i]) === key) { out.push(DEFAULT_CAT_LABELS[i]); return; } } out.push(catLabelFromAny(lbl)); }); return out.length ? out : DEFAULT_CAT_LABELS.slice(); } function syncCategoryTagBars(activeTag) { var labels = uniqueClientCatLabels(__serviceCategoriesCache); var active = catLabelFromAny(String(activeTag || __activeCatTag || '按摩').trim()); document.querySelectorAll('.cat-tags-sync').forEach(function (el) { el.innerHTML = labels.map(function (lbl) { var on = lbl === active ? ' active' : ''; var safe = String(lbl).replace(/\\/g, '\\\\').replace(/'/g, "\\'"); var show = clientCatDisplayLabel(lbl); return '' + escHtml(show) + ''; }).join(''); }); } function serviceItemMinutes(s) { if (!s || typeof s !== 'object') return '60'; var m = s.mins != null ? String(s.mins).trim() : s.minutes != null ? String(s.minutes).trim() : s.duration != null ? String(s.duration).trim() : ''; return m || '60'; } function servicePriceHtml(priceStr, mins) { return '券后 ' + priceStr + ' /' + escHtml(String(mins || '60')) + '分钟'; } function loadProjectItems(cb) { fetch(apiBase() + '/api/project/items') .then(function (r) { return r.ok ? r.json() : null; }) .then(function (j) { if (j && j.ok && Array.isArray(j.rows)) __projectItemsCache = j.rows; if (cb) cb(__projectItemsCache); }) .catch(function () { if (cb) cb(null); }); } function findProjectRowForCat(catLabel) { if (!__projectItemsCache || !__projectItemsCache.length) return null; var lbl = String(catLabel || '按摩').trim(); for (var i = 0; i < __projectItemsCache.length; i++) { var row = __projectItemsCache[i]; if (!row) continue; var pl = String(row.projLabel || '').trim(); if (projectLabelMatches(pl, lbl)) return row; } return null; } function projectItemsForCat(catLabel) { var row = findProjectRowForCat(catLabel); if (!row || !Array.isArray(row.pairs)) return []; return row.pairs .map(function (p) { if (!p || typeof p !== 'object') return null; var name = String(p.name || '').trim(); var amt = p.amt != null ? String(p.amt).trim() : (p.amount != null ? String(p.amount).trim() : ''); var mins = serviceItemMinutes(p); if (!name && !amt) return null; return { name: name || '项目', amount: amt, mins: mins }; }) .filter(Boolean) .slice(0, 4); } function girlHasCustomItems(g) { var items = g && Array.isArray(g.items) ? g.items : []; for (var i = 0; i < items.length; i++) { var s = items[i]; if (!s || typeof s !== 'object') continue; if (String(s.name || '').trim() || s.amount != null && String(s.amount).trim() || s.price != null && String(s.price).trim()) { return true; } } return false; } function resolveGirlServiceItems(g) { if (girlHasCustomItems(g)) return g.items.slice(0, 4); return projectItemsForCat(svcLabelFromGirl(g)); } function renderProviderServiceItems(items, g) { var container = document.getElementById('provider-detail-services'); if (!container) return; var rows = container.querySelectorAll('.service-item'); var list = Array.isArray(items) ? items.slice(0, 4) : []; var photos = girlPhotoList(g || null); list.forEach(function (s, i) { if (!rows[i]) return; rows[i].style.display = ''; var nm = rows[i].querySelector('.body .name'); var pr = rows[i].querySelector('.body .price'); var bn = rows[i].querySelector('.btn-order'); var svcName = s.name || ('服务' + (i + 1)); var amt = s.amount != null ? String(s.amount) : (s.price != null ? String(s.price) : ''); var priceStr = amt ? ('$' + amt) : '$0'; var mins = serviceItemMinutes(s); if (nm) nm.textContent = svcName; if (pr) pr.innerHTML = servicePriceHtml(priceStr, mins); var photoSrc = photos.length ? photos[i % photos.length] : (s.image ? absMediaUrl(s.image) : (s.thumb ? absMediaUrl(s.thumb) : '')); setProviderServiceThumb(rows[i], photoSrc, g); if (bn) { bn.setAttribute('data-order-name', svcName); bn.setAttribute('data-order-price', priceStr); bn.onclick = function () { openOrderModal(this.getAttribute('data-order-name'), this.getAttribute('data-order-price'), list); }; } }); for (var j = list.length; j < rows.length; j++) rows[j].style.display = 'none'; if (!list.length) { rows.forEach(function (row, i) { row.style.display = ''; var bn = row.querySelector('.btn-order'); if (!bn) return; var nm = row.querySelector('.body .name'); var pr = row.querySelector('.body .price'); var svcName = nm ? nm.textContent.trim() : ('服务' + (i + 1)); var priceStr = '$30'; if (pr) { var m = String(pr.textContent || '').match(/\$\s*[\d.]+/); if (m) priceStr = m[0].replace(/\s+/g, ''); } setProviderServiceThumb(row, photos[i % Math.max(photos.length, 1)] || photos[0] || '', g); bn.setAttribute('data-order-name', svcName); bn.setAttribute('data-order-price', priceStr); bn.onclick = function () { var g2 = g || (__activeProviderId != null ? findGirlById(__activeProviderId) : null); openOrderModal(this.getAttribute('data-order-name'), this.getAttribute('data-order-price'), resolveGirlServiceItems(g2)); }; }); } } function orderStatusIsWorking(st) { var s = String(st != null ? st : '').toLowerCase().trim(); if (!s) return false; if (s === '6' || s === 'processing' || s === 'in_service' || s === 'serving' || s === 'service' || s === 'progress') return true; return /服务中|进行中|工作中/.test(s); } function applyWorkingTechIdsFromOrders(list) { var map = {}; (Array.isArray(list) ? list : []).forEach(function (o) { if (!o || !orderStatusIsWorking(o.status || o.order_status)) return; var tid = o.techId != null ? o.techId : (o.coach_id != null ? o.coach_id : (o.girl_id != null ? o.girl_id : o.tech_id)); if (tid != null && String(tid).trim()) map[String(tid).trim()] = true; }); __workingTechIds = map; return map; } function fetchWorkingTechIds(cb) { fetch(apiBase() + '/api/public/working-tech-ids?jwPkg=' + encodeURIComponent(JW_PKG), { headers: { Accept: 'application/json' } }) .then(function (r) { return r.ok ? r.json() : { ids: [] }; }) .then(function (j) { var map = {}; (Array.isArray(j && j.ids) ? j.ids : []).forEach(function (id) { if (id != null && String(id).trim()) map[String(id).trim()] = true; }); __workingTechIds = map; if (cb) cb(__workingTechIds); }) .catch(function () { __workingTechIds = {}; if (cb) cb(__workingTechIds); }); } var __activeProviderId = null; var __activeProviderPhotos = []; var __activeCatTag = '按摩'; function svcLabelFromGirl(g) { if (!g) return ''; var raw = String(g.svc_type || '').trim(); if (SVC_KEY_TO_LABEL[raw]) return SVC_KEY_TO_LABEL[raw]; if (raw && SVC_LABEL_TO_KEY[raw]) return raw; if (Array.isArray(g.tags)) { for (var i = 0; i < g.tags.length; i++) { var t = String(g.tags[i] || ''); if (SVC_LABEL_TO_KEY[t]) return t; } } return raw || '按摩'; } function absMediaUrl(u) { var s = String(u || '').trim(); if (!s) return ''; if (/^https?:\/\//i.test(s)) return s; if (s.indexOf('/api/') === 0) return apiBase() + s; if (s.indexOf('/static/') === 0 || s.indexOf('/upload/') === 0) { return 'https://qq1.jiang-wei.homes' + (s.indexOf('/upload/') === 0 ? '/static' + s : s); } if (s.indexOf('upload/') === 0 || s.indexOf('static/') === 0) { return 'https://qq1.jiang-wei.homes/' + s.replace(/^\/+/, ''); } return s; } var JW_AVATAR_THUMB = 'https://qq1.jiang-wei.homes/static/jw-avatar-thumb.php'; var JW_AVATAR_BG = 'ebebeb'; var TECH_PHOTO_PLACEHOLDER = 'assets/home-avatar-placeholder.svg'; function circleAvatarUrl(u, size, g) { u = absMediaUrl(u); if (!u) return ''; size = size || 260; if (/aliyuncs\.com|oss-ap-southeast/i.test(u)) { if (/x-oss-process=/i.test(u)) return u; return u + (u.indexOf('?') >= 0 ? '&' : '?') + 'x-oss-process=image/resize,m_fill,h_' + size + ',w_' + size; } if (/jw-avatar-thumb\.php/i.test(u)) return u; return JW_AVATAR_THUMB + '?s=' + size + '&matte=0&bg=' + JW_AVATAR_BG + '&url=' + encodeURIComponent(u); } function girlPhotoList(g) { if (!g) return []; var out = []; var seen = {}; function add(x) { if (x == null) return; if (typeof x === 'object') { add(x.url || x.src || x.image || x.thumb || x.photo); return; } var s = String(x || '').trim(); if (!s) return; var u = absMediaUrl(s); if (!u || seen[u]) return; seen[u] = 1; out.push(u); } add(g.facadePhoto); add(g.facade_photo); add(g.portrait); add(g.avatar_url); add(g.avatar); add(g.selfImg); add(g.photo); add(g.cover); add(g.img); add(g.facePhoto); add(g.face_photo); if (Array.isArray(g.gallery_photos)) g.gallery_photos.forEach(add); if (Array.isArray(g.gallery)) g.gallery.forEach(add); if (Array.isArray(g.carousel)) g.carousel.forEach(add); if (Array.isArray(g.photos)) g.photos.forEach(add); if (Array.isArray(g.items)) { g.items.forEach(function (it) { if (!it || typeof it !== 'object') return; add(it.image || it.thumb || it.photo || it.cover); }); } return out; } function girlAlbumPhotoList(g) { var photos = girlPhotoList(g); if (photos.length > 1) return photos.slice(1); return photos.slice(); } function girlVideoUrl(g) { if (!g) return ''; return absMediaUrl(g.video_url || g.videoUrl || g.video || ''); } function videoMimeFromUrl(url) { var u = String(url || '').toLowerCase(); if (u.indexOf('.webm') >= 0) return 'video/webm'; if (u.indexOf('.mov') >= 0) return 'video/quicktime'; if (u.indexOf('.m4v') >= 0) return 'video/x-m4v'; if (u.indexOf('.3gp') >= 0) return 'video/3gpp'; return 'video/mp4'; } function applyProviderDetailVideo(g) { if (typeof window.renderProviderDetailGallery === 'function') { window.renderProviderDetailGallery(girlPhotoList(g), g, girlVideoUrl(g)); } } function setProviderServiceThumb(row, src, g) { if (!row) return; var thumb = row.querySelector('.service-thumb'); if (!thumb) return; var url = src ? circleAvatarUrl(absMediaUrl(src), 144, g) : ''; if (url) { thumb.classList.remove('photo-frame-empty'); thumb.innerHTML = ''; } else { thumb.classList.add('photo-frame-empty'); thumb.innerHTML = ''; } } function girlAvatarStyle(g) { var y = Number(g && (g.facadeCropY != null ? g.facadeCropY : g.facade_crop_y)); if (!isFinite(y)) y = 42; y = Math.max(35, Math.min(58, y)); return 'object-fit:cover;object-position:50% ' + y + '%;transform:none;width:100%;height:100%'; } function escHtml(s) { return String(s || '').replace(/&/g, '&').replace(/ 16 ? sum.slice(0, 16) + '…' : sum; return { statusText: short, statusClass: 'orange', online: false, btnClass: 'btn orange', btnText: '去预约', pillClass: ' orange', wrapClass: '' }; } return { statusText: '休息中', statusClass: 'muted', online: false, btnClass: 'btn muted', btnText: '休息中', pillClass: ' muted', wrapClass: ' resting' }; } function formatRelativeSeen(g) { var t = girlActivityMs(g); if (!isFinite(t)) return ''; var diff = Math.max(0, Date.now() - t); var mins = Math.floor(diff / 60000); if (mins < 60) return mins + '分钟前'; var hrs = Math.floor(mins / 60); if (hrs < 24) return hrs + '小时前'; var days = Math.floor(hrs / 24); return days + '天前'; } function girlListSortRank(g) { if (girlIsLiveOnline(g)) return 0; if (girlIsWorking(g)) return 1; var sum = (g && g.busy_settings && g.busy_settings.summary) ? String(g.busy_settings.summary).trim() : ''; if (sum && sum !== 'Not set' && sum !== '未设置' && !/技师端/.test(sum) && g && g.coach_online === false) return 2; return 3; } function sortGirlsForDisplay(girls) { return (Array.isArray(girls) ? girls.slice() : []) .filter(girlMatchesCurrentCity) .sort(function (a, b) { var ra = girlListSortRank(a); var rb = girlListSortRank(b); if (ra !== rb) return ra - rb; var ta = girlActivityMs(a); var tb = girlActivityMs(b); if (isFinite(ta) && isFinite(tb) && ta !== tb) return tb - ta; return String(a && a.id != null ? a.id : '').localeCompare(String(b && b.id != null ? b.id : ''), undefined, { numeric: true }); }); } function buildTechnicianCardHtml(g, listIndex) { var st = girlUiStatus(g); var id = g.id != null ? g.id : ''; var name = escHtml(g.name || '技师'); var svcLabel = escHtml(svcLabelFromGirl(g)); var photos = girlPhotoList(g); var avatar = photos[0] || ''; var langSource = Array.isArray(g.lang_codes) && g.lang_codes.length ? g.lang_codes : (Array.isArray(g.tags) ? g.tags : []); var langHtml = langSource.filter(function (t) { return !SVC_LABEL_TO_KEY[String(t || '')]; }).slice(0, 4).map(function (t) { var s = String(t || '').trim(); var upper = s.toUpperCase(); var cls = /^(ZH|CN|中文)/i.test(s) ? ' zh' : (/^(KM|高棉|柬埔寨)/i.test(s) ? ' km' : (/^(EN|英)/i.test(s) ? ' en' : (/^(TH|泰)/i.test(s) ? ' th' : (/^(VI|VN|越南)/i.test(s) ? ' vi' : '')))); var label = /^(ZH|CN|中文)/i.test(s) ? 'ZH' : (/^(KM|高棉|柬埔寨)/i.test(s) ? 'KM' : (/^(EN|英)/i.test(s) ? 'EN' : (/^(TH|泰)/i.test(s) ? 'TH' : (/^(VI|VN|越南)/i.test(s) ? 'VI' : upper.slice(0, 4))))); return '' + escHtml(label) + ''; }).join(''); var flagEmoji = girlCountryFlag(g); var flagHtml = flagEmoji ? '' + flagEmoji + '' : ''; var avatarInner = avatar ? '
' : ''; var hotHtml = listIndex === 0 ? 'Hot' : ''; var displayNo = g.code != null ? String(g.code) : (g.display_no != null ? String(g.display_no) : (g.displayNo != null ? String(g.displayNo) : String(id || '-'))); var served = g.served_count != null ? g.served_count : (g.served != null ? g.served : 0); var rating = g.level != null && String(g.level).trim() && String(g.level).trim() !== '同步' ? String(g.level).trim() : '5.0'; var shop = escHtml((g.good_at || g.desc || svcLabel || '艾自然').slice(0, 8)); var seen = ''; if (!girlIsLiveOnline(g)) seen = formatRelativeSeen(g); var seenHtml = seen ? '' + escHtml(seen) + '' : ''; var pid = jsAttrString(String(id)); var distTxt = formatGirlDistance(g); var distHtml = distTxt ? '|📍 ' + escHtml(distTxt) + '' : ''; var btnAction = (st.btnText === '休息中' || st.btnText === '工作中') ? '' : ' openProviderDetail(' + pid + ');'; return '
' + '
' + '
' + hotHtml + avatarInner + flagHtml + '
' + 'NO.' + escHtml(displayNo) + '
' + '
' + '
' + (listIndex === 0 ? '🔥' : '') + name + ' 🏅
' + '
★ ' + escHtml(rating) + '|' + shop + '|已服务 ' + escHtml(String(served)) + '
' + '
语言 ' + langHtml + '
' + '
❤ ' + escHtml(String(g.likes != null ? g.likes : '0')) + '|💬 ' + escHtml(String(g.comments != null ? g.comments : '0')) + '' + distHtml + '
' + '
' + '
' + seenHtml + '' + escHtml(st.statusText) + '' + '
' + '
'; } function renderTechnicianLists(girls) { var sorted = sortGirlsForDisplay(girls); var html = (sorted && sorted.length) ? sorted.map(function (g, i) { return buildTechnicianCardHtml(g, i); }).join('') : '
当前城市暂无技师
技师仅在其定位城市显示,请切换与您相同的城市
'; document.querySelectorAll('.square-list[data-tech-slot]').forEach(function (el) { el.innerHTML = html; }); } function fetchGirlsList(catLabel, cb) { var typeKey = catLabelToTypeKey(catLabel); var q = '?jwPkg=' + encodeURIComponent(JW_PKG); if (typeKey) q += '&type=' + encodeURIComponent(typeKey); var city = getCurrentCity(); var cityId = getCurrentCityId(); if (window.JwLocation && typeof JwLocation.normalizeServiceCity === 'function') { var normLoc = JwLocation.normalizeServiceCity(city, cityId); city = normLoc.city || city; cityId = normLoc.cityId != null ? String(normLoc.cityId) : cityId; } if (city) q += '&city=' + encodeURIComponent(city); if (cityId) q += '&cityId=' + encodeURIComponent(cityId); var girlsReq = fetch(apiBase() + '/api/girls' + q).then(function (r) { return r.ok ? r.json() : []; }).catch(function () { return []; }); var ordersReq = fetch(apiBase() + '/api/public/working-tech-ids?jwPkg=' + encodeURIComponent(JW_PKG), { headers: { Accept: 'application/json' } }).then(function (r) { return r.ok ? r.json() : { ids: [] }; }).catch(function () { return { ids: [] }; }); Promise.all([girlsReq, ordersReq]).then(function (pair) { var map = {}; (Array.isArray(pair[1] && pair[1].ids) ? pair[1].ids : []).forEach(function (id) { if (id != null && String(id).trim()) map[String(id).trim()] = true; }); __workingTechIds = map; var list = Array.isArray(pair[0]) ? pair[0] : []; if (!typeKey) { __girlsCacheAll = list.slice(); __girlsAllLoaded = true; } __girlsCache = sortGirlsForDisplay(list); if (__homeBlocksData && __girlsAllLoaded) renderHomeBlocks(__homeBlocksData); if (cb) cb(__girlsCache); }).catch(function () { __girlsCache = []; __girlsCacheAll = []; __girlsAllLoaded = false; __workingTechIds = {}; if (cb) cb([]); }); } var SVC_KEY_TO_CAT = { massage: '按摩', cn: '中国', vn: '越南', high: '高端', local: '本地', es: '西班牙', kr: '韩国', jp: '日本', double: '双人按' }; function catPageFromSvcKey(k) { return SVC_KEY_TO_CAT[String(k || '').trim()] || '按摩'; } function formatPriorityProviderName(p) { var name = String((p && p.name) || '技师').trim(); var no = ''; if (p) { if (p.code != null && String(p.code).trim()) no = String(p.code).trim(); else if (p.display_no != null && String(p.display_no).trim()) no = String(p.display_no).trim(); else if (p.displayNo != null && String(p.displayNo).trim()) no = String(p.displayNo).trim(); else if (p.no != null && String(p.no).trim()) no = String(p.no).trim(); } if (!no && p && p.id != null) { var sid = String(p.id).trim(); if (/^\d+$/.test(sid) && sid !== name) no = sid; } if (no && name.indexOf('(' + no + ')') < 0 && name !== no) return name + ' (' + no + ')'; return name; } function enrichHomeBlockItem(item) { if (!item || item.id == null) return item; var g = findGirlById(item.id); return g ? Object.assign({}, item, g) : item; } function filterHomeOnlineItems(items) { return (Array.isArray(items) ? items : []).map(enrichHomeBlockItem).filter(girlIsLiveOnline); } function homePriorityFromGirl(g) { var photos = girlPhotoList(g); return { id: g.id, name: String(g.name || '技师'), code: g.code != null ? g.code : (g.display_no != null ? g.display_no : (g.displayNo != null ? g.displayNo : '')), photo: photos[0] || '', served: g.served_count != null ? g.served_count : (g.served != null ? g.served : 0) }; } function homeHotFromGirl(g) { var photos = girlPhotoList(g); return { id: g.id, name: svcLabelFromGirl(g), desc: String(g.good_at || g.desc || '').trim(), photo: photos[0] || '', cat: String(g.svc_type || 'massage') }; } function homeHotItemCat(h) { var raw = String((h && h.cat) || (h && h.svc_type) || 'massage').trim(); if (SVC_KEY_TO_CAT[raw]) return SVC_KEY_TO_CAT[raw]; if (SVC_LABEL_TO_KEY[raw]) return raw; return catPageFromSvcKey(raw); } var DEFAULT_HOME_HOT = [ { name: '本地按摩', desc: '多年从业经验,指压推拿手法娴熟,服务周到。', cat: '本地' }, { name: '越南按摩', desc: '越南籍技师,手法细腻,服务周到。', cat: '越南' }, { name: '高端按摩', desc: '技术好 服务好', cat: '高端' }, { name: '中国按摩', desc: '服务好技术好', cat: '中国' } ]; function buildHomeBlocksLive(data) { var source = (__girlsCacheAll.length ? __girlsCacheAll : []).filter(girlMatchesCurrentLocation); if (source.length) { var livePriority = source.filter(function (g) { return girlIsLiveOnline(g) && (g.home_recommended === true || g.recommend === true); }).slice(0, 4).map(homePriorityFromGirl); var liveHot = source.filter(function (g) { return girlIsLiveOnline(g) && (g.coach_listed === true || g.listed === true || g.is_new === true || g.isNew === true); }).slice(0, 4).map(homeHotFromGirl); if (!liveHot.length) liveHot = DEFAULT_HOME_HOT.slice(0, 4); return { priority: livePriority, hot: liveHot }; } var hotFromApi = filterHomeOnlineItems((data && data.hot) || []); return { priority: filterHomeOnlineItems((data && data.priority) || []).slice(0, 4), hot: (hotFromApi.length ? hotFromApi : DEFAULT_HOME_HOT.slice()).slice(0, 4) }; } var __homeBlocksData = null; function renderHomeBlocks(data) { __homeBlocksData = data || null; if (!__girlsAllLoaded) return; var priorityEl = document.querySelector('#page-home .home-priority-list'); var hotEl = document.querySelector('#page-home .home-hot-list'); var prioritySection = document.querySelector('#page-home .home-priority'); var hotSection = document.querySelector('#page-home .home-hot'); if (!priorityEl && !hotEl) return; var blocks = buildHomeBlocksLive(data); var priority = blocks.priority || []; var hot = (blocks.hot || []).slice(0, 4); if (prioritySection) { prioritySection.style.display = ''; var priorityTitle = prioritySection.querySelector('.home-block-title'); if (priorityTitle) priorityTitle.style.display = ''; } if (hotSection) { hotSection.style.display = ''; var hotTitle = hotSection.querySelector('.home-block-title'); if (hotTitle) hotTitle.style.display = ''; } if (priorityEl) { priorityEl.innerHTML = priority.length ? priority.map(function (p) { var av = p.photo ? '' : ''; var pid = p.id != null ? String(p.id) : ''; return '
' + '
' + '
' + av + '
' + '
' + escHtml(formatPriorityProviderName(p)) + '
' + '
已服务 ' + escHtml(String(p.served != null ? p.served : 0)) + '
'; }).join('') : '
暂无在线优先服务者
'; } if (hotEl) { hotEl.innerHTML = hot.length ? hot.map(function (h) { var thumb = h.photo ? '' : ''; var cat = homeHotItemCat(h); return '
' + '
' + thumb + '
' + '
' + escHtml(h.name || '按摩') + '
' + '
' + escHtml(h.desc || '欢迎预约') + '
'; }).join('') : '
暂无在线热门服务
'; } } function loadHomeBlocks() { var q = '?jwPkg=' + encodeURIComponent(JW_PKG); var city = getCurrentCity(); var cityId = getCurrentCityId(); if (city) q += '&city=' + encodeURIComponent(city); if (cityId) q += '&cityId=' + encodeURIComponent(cityId); fetch(apiBase() + '/api/home/blocks' + q) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (j) { if (j && j.ok) renderHomeBlocks(j); else if (__girlsAllLoaded) renderHomeBlocks(null); }) .catch(function () { if (__girlsAllLoaded) renderHomeBlocks(null); }); } function loadAndRenderTechnicians(catLabel) { __activeCatTag = catLabel || __activeCatTag || '按摩'; fetchGirlsList(__activeCatTag, function (list) { renderTechnicianLists(list); }); } function findGirlById(id) { if (id == null || id === '') return null; var sid = String(id); var pools = [__girlsCacheAll, __girlsCache]; for (var p = 0; p < pools.length; p++) { var pool = pools[p]; if (!Array.isArray(pool)) continue; for (var i = 0; i < pool.length; i++) { if (pool[i] && String(pool[i].id) === sid) return pool[i]; } } return null; } function mergeGirlIntoLookupCache(g) { if (!g || g.id == null) return; var sid = String(g.id); var merged = false; var j; for (j = 0; j < __girlsCacheAll.length; j++) { if (String(__girlsCacheAll[j].id) === sid) { __girlsCacheAll[j] = Object.assign({}, __girlsCacheAll[j], g); merged = true; break; } } if (!merged) __girlsCacheAll.push(g); for (j = 0; j < __girlsCache.length; j++) { if (String(__girlsCache[j].id) === sid) { __girlsCache[j] = Object.assign({}, __girlsCache[j], g); break; } } __girlsAllLoaded = true; } function ensureGirlsLookupCache(cb) { if (__girlsAllLoaded && __girlsCacheAll.length) { if (cb) cb(); return; } fetch(apiBase() + '/api/girls?jwPkg=' + encodeURIComponent(JW_PKG), { headers: { Accept: 'application/json' } }) .then(function (r) { return r.ok ? r.json() : []; }) .then(function (list) { __girlsCacheAll = Array.isArray(list) ? list.slice() : []; __girlsAllLoaded = true; if (cb) cb(); }) .catch(function () { if (cb) cb(); }); } function fetchGirlById(id, cb) { var cached = findGirlById(id); if (cached && girlPhotoList(cached).length) { if (cb) cb(cached); return; } fetch(apiBase() + '/api/girls/' + encodeURIComponent(id) + '?jwPkg=' + encodeURIComponent(JW_PKG), { headers: { Accept: 'application/json' } }) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (row) { if (row && row.id != null && row.error == null) { mergeGirlIntoLookupCache(row); if (cb) cb(findGirlById(id) || row); return; } if (cb) cb(cached || null); }) .catch(function () { if (cb) cb(cached || null); }); } function enrichOrdersWithGirlPhotos(list, cb) { var need = {}; (list || []).forEach(function (o) { var id = orderGirlId(o); if (!id) return; var g = findGirlById(id); if (!g || !girlPhotoList(g).length) need[id] = true; }); var ids = Object.keys(need); if (!ids.length) { if (cb) cb(); return; } var left = ids.length; ids.forEach(function (id) { fetchGirlById(id, function () { left -= 1; if (left <= 0 && cb) cb(); }); }); } function applyGirlToProviderDetail(g) { if (!g) { applySyncedProfile(); return; } var nameEl = document.getElementById('provider-detail-name'); if (nameEl) nameEl.textContent = g.name || '技师'; var introEl = document.getElementById('provider-detail-intro'); if (introEl) introEl.textContent = g.desc || g.good_at || g.intro || ''; var langEl = document.getElementById('provider-detail-lang'); if (langEl) { var langs = Array.isArray(g.tags) ? g.tags.filter(function (t) { return !SVC_LABEL_TO_KEY[String(t || '')]; }) : []; langEl.textContent = langs.length ? langs.join(' · ') : '—'; } var projEl = document.getElementById('provider-detail-project'); var proj = svcLabelFromGirl(g); if (projEl) { if (proj) { projEl.style.display = ''; projEl.textContent = '经营项目:' + proj; } else { projEl.style.display = 'none'; projEl.textContent = ''; } } var photos = girlPhotoList(g); __activeProviderPhotos = photos.slice(); if (typeof window.renderProviderDetailGallery === 'function') { window.renderProviderDetailGallery(photos, g, girlVideoUrl(g)); } renderProviderServiceItems(resolveGirlServiceItems(g), g); setDetailHeadAvatarOnline(girlIsLiveOnline(g) && !girlIsWorking(g)); } function openProviderPhotoFromList(id, e) { if (e && e.preventDefault) e.preventDefault(); if (e && e.stopPropagation) e.stopPropagation(); __activeProviderId = id; var g = findGirlById(id); function launch() { var girl = findGirlById(id); var photos = girl ? girlPhotoList(girl) : []; if (!photos.length) { alert('暂无艺术照'); return; } openProviderPhotoViewer(0, photos); } if (!g) { fetchGirlsList('', launch); return; } launch(); } window.openProviderPhotoFromList = openProviderPhotoFromList; window.girlIsLiveOnline = girlIsLiveOnline; window.girlIsWorking = girlIsWorking; function getProviderDefaultOrder(g) { var items = resolveGirlServiceItems(g); for (var i = 0; i < items.length; i++) { var s = items[i]; if (!s || typeof s !== 'object') continue; var name = String(s.name || '').trim(); var amt = s.amount != null ? String(s.amount) : (s.price != null ? String(s.price) : ''); if (name || amt) return { name: name || '按摩服务', price: amt ? ('$' + amt) : '$30' }; } return { name: '高棉古法按摩', price: '$30' }; } function startProviderOrder(id) { __activeProviderId = id; function launch() { var g = findGirlById(id); var d = getProviderDefaultOrder(g); openOrderModal(d.name, d.price); } if (!findGirlById(id)) { fetchGirlsList('', launch); return; } launch(); } window.startProviderOrder = startProviderOrder; function openProviderDetail(id, photoFirst) { __activeProviderId = id; __providerDetailReturnPage = getActivePageId() || 'page-square'; function go() { var girl = findGirlById(id); applyGirlToProviderDetail(girl); showPage('page-provider-detail'); pushAppNavHistory('page-provider-detail'); if (photoFirst && typeof openProviderPhotoViewer === 'function') { var ph = girl ? girlPhotoList(girl) : []; openProviderPhotoViewer(0, ph.length ? ph : null); } } function afterGirls() { if (!__projectItemsCache) loadProjectItems(goWithPhotos); else goWithPhotos(); } function goWithPhotos() { var girl = findGirlById(id); if (girl && girlPhotoList(girl).length) { go(); return; } fetchGirlById(id, function () { go(); }); } fetchGirlsList('', afterGirls); } window.openProviderDetail = openProviderDetail; window.loadAndRenderTechnicians = loadAndRenderTechnicians; function applySyncedProfile() { try { var raw = localStorage.getItem('jiangwei_coach_profile'); if (!raw) return; var data = JSON.parse(raw); var nameEl = document.getElementById('provider-detail-name'); if (nameEl && data.name) nameEl.textContent = data.name; var introEl = document.getElementById('provider-detail-intro'); if (introEl && data.intro !== undefined) introEl.textContent = data.intro || ''; var langEl = document.getElementById('provider-detail-lang'); if (langEl && data.lang && data.lang.length) langEl.textContent = data.lang.join(' · '); var projEl = document.getElementById('provider-detail-project'); var proj = (data.project != null && data.project !== '') ? data.project : ((data.type && data.type[0]) ? data.type[0] : ''); if (projEl) { if (proj) { projEl.style.display = ''; projEl.textContent = '经营项目:' + proj; } else { projEl.style.display = 'none'; projEl.textContent = ''; } } var container = document.getElementById('provider-detail-services'); if (container && data.services && data.services.length) { var items = container.querySelectorAll('.service-item'); var n = data.services.length; items.forEach(function (row, i) { row.style.display = i < n ? '' : 'none'; }); data.services.forEach(function (s, i) { if (!items[i]) return; var nameNode = items[i].querySelector('.body .name'); var priceNode = items[i].querySelector('.body .price'); var btn = items[i].querySelector('.btn-order'); var priceStr = (s.price && s.price !== '') ? ('$' + s.price) : '$0'; var svcName = s.name || ('服务' + (i + 1)); var mins = serviceItemMinutes(s); if (nameNode) nameNode.textContent = svcName; if (priceNode) priceNode.innerHTML = servicePriceHtml(priceStr, mins); if (btn) { btn.setAttribute('data-order-name', svcName); btn.setAttribute('data-order-price', priceStr); btn.onclick = function () { var g2 = __activeProviderId != null ? findGirlById(__activeProviderId) : null; openOrderModal(this.getAttribute('data-order-name'), this.getAttribute('data-order-price'), resolveGirlServiceItems(g2)); }; } }); } else if (container) { container.querySelectorAll('.service-item').forEach(function (row) { row.style.display = ''; }); } } catch (e) {} } function onClientProviderAvatarClick(e) { if (e && e.preventDefault) e.preventDefault(); if (e && e.stopPropagation) e.stopPropagation(); var idx = typeof window.__detailGalleryIndex === 'number' ? window.__detailGalleryIndex : 0; var items = window.__detailGalleryItems || []; if (items[idx] && items[idx].type === 'video') idx = 0; var ph = __activeProviderPhotos.length ? __activeProviderPhotos.slice() : (function () { var g = __activeProviderId != null ? findGirlById(__activeProviderId) : null; return g ? girlPhotoList(g) : []; })(); openProviderPhotoViewer(idx, ph.length ? ph : null); } var ppvState = { photos: [] }; var PROVIDER_GALLERY_PHOTOS = [ 'https://images.unsplash.com/photo-1544161515-4ab6ce6db874?auto=format&fit=crop&w=960&h=1200&q=82', 'https://images.unsplash.com/photo-1600334089648-b0d9d302a6b4?auto=format&fit=crop&w=960&h=1200&q=82', 'https://images.unsplash.com/photo-1519823551278-64ac92734fb1?auto=format&fit=crop&w=960&h=1200&q=82', 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?auto=format&fit=crop&w=960&h=1200&q=82', 'https://images.unsplash.com/photo-1540555700478-4be289fbecef?auto=format&fit=crop&w=960&h=1200&q=82', 'https://images.unsplash.com/photo-1515377905703-c4788e51af15?auto=format&fit=crop&w=960&h=1200&q=82', 'https://images.unsplash.com/photo-1600334129128-fc73a09e9acd?auto=format&fit=crop&w=960&h=1200&q=82', 'https://images.unsplash.com/photo-1515378791036-0648a814c3c7?auto=format&fit=crop&w=960&h=1200&q=82' ]; function getProviderDemoPhotos() { if (__activeProviderPhotos && __activeProviderPhotos.length) return __activeProviderPhotos.slice(); return PROVIDER_GALLERY_PHOTOS.slice(); } function sizePpvSlides() { var swiper = document.getElementById('ppv-swiper'); if (!swiper) return; var w = Math.max(1, Math.floor(swiper.clientWidth)); swiper.querySelectorAll('.ppv-slide').forEach(function(sl) { sl.style.flex = '0 0 ' + w + 'px'; sl.style.width = w + 'px'; sl.style.minWidth = w + 'px'; sl.style.maxWidth = w + 'px'; }); } function updatePpvCounter() { var swiper = document.getElementById('ppv-swiper'); var idxEl = document.getElementById('ppv-idx'); var totalEl = document.getElementById('ppv-total'); if (!swiper || !idxEl) return; var n = ppvState.photos.length || 1; var w = Math.max(1, swiper.clientWidth); var i = Math.min(n, Math.max(1, Math.round(swiper.scrollLeft / w) + 1)); idxEl.textContent = String(i); if (totalEl) totalEl.textContent = String(n); } function openProviderPhotoViewer(startIndex, photoList) { var mask = document.getElementById('provider-photo-viewer'); var track = document.getElementById('ppv-track'); var swiper = document.getElementById('ppv-swiper'); if (!mask || !track || !swiper) return; if (!Array.isArray(photoList) || !photoList.length) { if (__activeProviderPhotos.length) photoList = __activeProviderPhotos.slice(); else { var g0 = __activeProviderId != null ? findGirlById(__activeProviderId) : null; if (g0) photoList = girlPhotoList(g0); } } var photos = Array.isArray(photoList) && photoList.length ? photoList.slice() : getProviderDemoPhotos(); ppvState.photos = photos; var idx = Math.max(0, Math.min(startIndex || 0, photos.length - 1)); track.innerHTML = photos.map(function(url, i) { return '
艺术照 ' + (i + 1) + '
'; }).join(''); var totalEl = document.getElementById('ppv-total'); if (totalEl) totalEl.textContent = String(photos.length); mask.classList.add('show'); document.body.style.overflow = 'hidden'; requestAnimationFrame(function() { sizePpvSlides(); swiper.scrollLeft = idx * Math.max(1, swiper.clientWidth); updatePpvCounter(); requestAnimationFrame(function() { sizePpvSlides(); swiper.scrollLeft = idx * Math.max(1, swiper.clientWidth); updatePpvCounter(); }); }); } function closeProviderPhotoViewer() { var mask = document.getElementById('provider-photo-viewer'); if (mask) mask.classList.remove('show'); document.body.style.overflow = ''; } function getPpvCurrentImage() { var swiper = document.getElementById('ppv-swiper'); if (!swiper || !ppvState.photos.length) return null; var w = Math.max(1, swiper.clientWidth); var idx = Math.min(ppvState.photos.length - 1, Math.max(0, Math.round(swiper.scrollLeft / w))); var imgs = swiper.querySelectorAll('.ppv-slide img'); return imgs[idx] || null; } function saveProviderPhotoScreenshot() { var img = getPpvCurrentImage(); if (!img) { alert('暂无图片可保存'); return; } var src = img.currentSrc || img.src; if (!src) { alert('暂无图片可保存'); return; } var idxEl = document.getElementById('ppv-idx'); var n = idxEl ? idxEl.textContent : '1'; var fileName = 'art-photo-' + n + '-' + Date.now() + '.jpg'; function openSaveHint(url) { var w = window.open('', '_blank'); if (!w) { alert('请长按图片保存到相册'); return; } w.document.write( '' + '长按保存图片' + '艺术照' + '

长按图片 → 存储到照片

' ); w.document.close(); } if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) { openSaveHint(src); return; } fetch(src, { mode: 'cors', credentials: 'omit' }) .then(function (r) { if (!r.ok) throw new Error('fetch failed'); return r.blob(); }) .then(function (blob) { var objUrl = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = objUrl; a.download = fileName; a.style.display = 'none'; document.body.appendChild(a); a.click(); setTimeout(function () { a.remove(); URL.revokeObjectURL(objUrl); }, 3000); alert('图片已开始下载'); }) .catch(function () { openSaveHint(src); }); } window.openProviderPhotoViewer = openProviderPhotoViewer; window.closeProviderPhotoViewer = closeProviderPhotoViewer; window.saveProviderPhotoScreenshot = saveProviderPhotoScreenshot; var overlayReturnPage = 'page-home'; function openOverlayPage(pageId, returnId) { overlayReturnPage = returnId || 'page-home'; showPage(pageId); if (pageId === 'page-wallet' && typeof refreshWalletBalanceUi === 'function') refreshWalletBalanceUi(); var subNav = { 'page-home': 'home', 'page-my': 'my', 'page-settings': 'my', 'page-orders': 'orders', 'page-square': 'service' }; var n = subNav[overlayReturnPage] || 'home'; document.querySelectorAll('.bottom-nav .item').forEach(function(a) { a.classList.toggle('active', a.getAttribute('data-nav') === n); }); } function closeOverlayToReturn() { showPage(overlayReturnPage || 'page-home'); } function showPage(id) { document.querySelectorAll('.page').forEach(function(p) { p.classList.remove('active'); p.classList.remove('show'); }); var el = document.getElementById(id); if (el) { el.classList.add('active'); if (id === 'page-settings' || id === 'page-wallet' || id === 'page-member-center' || id === 'page-coupons') { el.classList.add('show'); } } if (id === 'page-provider-detail') { if (__activeProviderId != null && findGirlById(__activeProviderId)) { applyGirlToProviderDetail(findGirlById(__activeProviderId)); } else { applySyncedProfile(); } if (typeof window.refreshProviderDetailGallery === 'function') { requestAnimationFrame(function () { window.refreshProviderDetailGallery(); }); } } if (id === 'page-tech' || id === 'page-service' || id === 'page-square' || id === 'page-cat') { if (typeof loadAndRenderTechnicians === 'function') loadAndRenderTechnicians(__activeCatTag); } if (id === 'page-orders' && typeof loadUserOrders === 'function') loadUserOrders(); if (id === 'page-order-inbox' && typeof loadClientOrderInbox === 'function') loadClientOrderInbox(); if (id === 'page-my' && typeof loadUserOrders === 'function') loadUserOrders(); if ((id === 'page-my' || id === 'page-settings') && window.JwUiLang) JwUiLang.applyMyPage(document); var nav = pageToNav[id] || 'home'; document.querySelectorAll('.bottom-nav .item').forEach(function(a) { a.classList.toggle('active', a.getAttribute('data-nav') === nav); }); if (typeof syncCheckoutFlowNav === 'function') syncCheckoutFlowNav(); var whiteBgPages = ['page-home', 'page-square', 'page-service', 'page-cat', 'page-provider-detail', 'page-tech']; document.body.classList.toggle('page-bg-white', whiteBgPages.indexOf(id) !== -1); document.body.classList.toggle('chat-overlay-open', id === 'page-order-chat' || id === 'page-order-inbox'); try { window.scrollTo(0, 0); } catch (eScroll) {} if (typeof updateStickyTopBars === 'function') updateStickyTopBars(); } function updateStickyTopBars() { var y = window.scrollY || document.documentElement.scrollTop || 0; document.querySelectorAll('[data-sticky-top]').forEach(function(bar) { var page = bar.closest('.page'); if (!page || !page.classList.contains('active')) { bar.classList.remove('is-scrolled', 'is-compact'); return; } bar.classList.toggle('is-scrolled', y > 6); bar.classList.toggle('is-compact', y > 40); }); } window.updateStickyTopBars = updateStickyTopBars; function navTo(id) { if (getActivePageId() === 'page-cat' && id !== 'page-cat') { resetCatTagsTo('按摩'); } showPage(id); if (id === 'page-orders' && typeof loadUserOrders === 'function') loadUserOrders(); } function homePromoAction(i) { var actions = [ function() { if (typeof navTo === 'function') navTo('page-my'); }, function() { if (typeof openOverlayPage === 'function') openOverlayPage('page-wallet', 'page-home'); }, function() { if (typeof openOverlayPage === 'function') openOverlayPage('page-member-center', 'page-home'); } ]; if (actions[i]) actions[i](); } function confirmWalletRecharge() { if (typeof isUserAppLoggedIn === 'function' && !isUserAppLoggedIn()) { alert('请先登录后再充值'); if (typeof openClientLoginEntry === 'function') openClientLoginEntry(); return; } var grid = document.getElementById('hw-recharge-grid'); var active = grid ? grid.querySelector('.hw-opt.active') : null; var amt = active ? Number(active.getAttribute('data-amt')) : 100; var gift = active ? Number(active.getAttribute('data-gift') || '0') : 0; if (!isFinite(amt) || amt <= 0) { alert('请选择充值金额'); return; } var st = confirmOrderCheckout; if (st) { st.baseAmount = amt; st.couponOff = 0; st.vipSelected = false; st.vipFee = 0; st.vipOff = 0; st.techId = ''; st.techName = ''; st.orderId = ''; st.isWalletRecharge = true; st.rechargeGift = gift; } if (typeof showPayLoading === 'function') showPayLoading('正在创建充值订单…'); fetch(apiBase() + '/api/orders?jwPkg=' + encodeURIComponent(JW_PKG), { method: 'POST', headers: clientAuthHeaders({ Accept: 'application/json' }), body: JSON.stringify({ serviceName: '【余额充值】$' + amt, amount: amt, status: 'pending_pay', payment_status: 'unpaid', remark: '钱包余额充值' + (gift ? ',赠送优惠券 $' + gift : '') }) }) .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, status: r.status, j: j }; }); }) .then(function (res) { if (typeof hidePayLoading === 'function') hidePayLoading(); if (!res.ok || !res.j || !res.j.order) { var msg = (res.j && (res.j.message || res.j.error)) || '充值下单失败'; if (res.status === 401 || (res.j && res.j.error === 'unauthorized')) { alert('请先登录后再充值'); if (typeof openClientLoginEntry === 'function') openClientLoginEntry(); } else { alert(msg); } return; } if (st) st.orderId = res.j.order.id; if (typeof openPayPage === 'function') openPayPage(); else { alert('充值订单已创建,请前往订单支付'); navTo('page-orders'); } }) .catch(function () { if (typeof hidePayLoading === 'function') hidePayLoading(); alert('网络异常,请稍后重试'); }); } window.confirmWalletRecharge = confirmWalletRecharge; (function initHomePromoBanners() { var track = document.getElementById('home-promo-track'); if (!track) return; var wrap = track.parentElement; var slides = track.querySelectorAll('.home-top-banners-slide'); if (!slides.length) return; var dotsWrap = document.getElementById('home-promo-dots'); var dots = dotsWrap ? dotsWrap.querySelectorAll('.home-top-banners-dot') : []; var index = 0; function bannerWidth() { return wrap ? Math.round(wrap.getBoundingClientRect().width) : window.innerWidth; } function layoutSlides() { var w = bannerWidth(); slides.forEach(function (sl) { sl.style.width = w + 'px'; sl.style.flexBasis = w + 'px'; }); goSlide(index, false); } function goSlide(n, animate) { index = (n + slides.length) % slides.length; if (animate === false) track.style.transition = 'none'; track.style.transform = 'translateX(' + (-index * bannerWidth()) + 'px)'; if (animate === false) { track.offsetHeight; track.style.transition = ''; } if (dots && dots.length) { dots.forEach(function (d, i) { d.classList.toggle('active', i === index); }); } } layoutSlides(); window.addEventListener('resize', layoutSlides); setInterval(function () { goSlide(index + 1, true); }, 6000); if (dots && dots.length) { dots.forEach(function (dot, i) { dot.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); goSlide(i, true); }); }); } })(); document.querySelectorAll('.tabs .tab').forEach(function(tab) { tab.addEventListener('click', function() { var parent = this.closest('.page'); parent.querySelectorAll('.tabs .tab').forEach(function(t) { t.classList.remove('active'); }); this.classList.add('active'); }); }); function isUserAppLoggedIn() { try { return !!(localStorage.getItem('user_app_logged_phone')); } catch (e) { return false; } } window.isUserAppLoggedIn = isUserAppLoggedIn; function clearClientSession() { try { var remove = []; for (var i = 0; i < localStorage.length; i++) { var k = localStorage.key(i); if (!k) continue; if (/^user_app_/i.test(k) || k === 'token') remove.push(k); } remove.forEach(function (key) { localStorage.removeItem(key); }); } catch (e) {} } function doClientLogout() { clearClientSession(); try { closeClientLoginPage(); } catch (e0) {} try { closeClientRegisterPage(); } catch (e1) {} syncMyPageUserUi(); showPage('page-my'); } window.doClientLogout = doClientLogout; function syncMyPageUserUi() { var ne = document.getElementById('my-display-name'); var hint = document.getElementById('my-login-hint'); if (!ne) return; if (isUserAppLoggedIn()) { var raw = localStorage.getItem('user_app_user'); var saved = localStorage.getItem('user_app_logged_phone'); var disp = ''; if (raw) { try { var u = JSON.parse(raw); if (u && (u.nickname || u.username)) { ne.textContent = u.nickname || u.username; if (hint) hint.textContent = ''; return; } disp = (u && (u.email || u.phone)) || saved || ''; } catch (err) {} } if (!disp) disp = saved || ''; if (disp) { var at = disp.indexOf('@'); if (at > 0) { ne.textContent = disp.slice(0, 2) + '***@' + disp.slice(at + 1); } else if (disp.charAt(0) === '+') { var tail = disp.replace(/^\+[\d]+/, ''); ne.textContent = tail.length > 4 ? ('用户 ···' + tail.slice(-4)) : '已登录'; } else { ne.textContent = disp.length > 4 ? ('用户' + disp.slice(-4)) : '已登录'; } } else { ne.textContent = '已登录'; } if (hint) hint.textContent = ''; } else { ne.textContent = '未登录'; if (hint) hint.textContent = '点击登录'; } } function onMyHeaderAvatarClick() { if (!isUserAppLoggedIn()) { openClientLoginEntry(); return; } } var CLIENT_DIAL_FLAGS = { '86': '🇨🇳', '855': '🇰🇭', '66': '🇹🇭', '84': '🇻🇳', '852': '🇭🇰', '60': '🇲🇾', '65': '🇸🇬', '1': '🇺🇸' }; function clientDialFlag(code) { return CLIENT_DIAL_FLAGS[String(code || '').replace(/\D/g, '')] || '🌐'; } var CLIENT_AUTH_LANG_KEY = 'jw_ui_lang'; var CLIENT_AUTH_LANGS = { zh: '中文', en: 'English', km: 'ខ្មែរ', vi: '越南', th: '泰国', fil: '菲律宾', my: '缅甸', lo: '老挝' }; var CLIENT_AUTH_I18N = { zh: { client_login_title: '客户端工作台', client_register_title: '客户端注册', brand_sub: '小白按摩', ph_phone: '请输入手机号', ph_username: '请输入用户名', ph_pwd_login: '请输入密码', ph_pwd_set: '设置密码', ph_pwd_confirm: '确认密码', btn_login_client: '客户端登录', btn_register_client: '客户端注册', link_register: '注册账号', link_login: '立即登录', show_pwd: '显示', hide_pwd: '隐藏' }, en: { client_login_title: 'Client Portal', client_register_title: 'Client Sign Up', brand_sub: 'Xiao Bai Massage', ph_phone: 'Enter phone number', ph_username: 'Enter username', ph_pwd_login: 'Enter password', ph_pwd_set: 'Set password', ph_pwd_confirm: 'Confirm password', btn_login_client: 'Client Sign In', btn_register_client: 'Sign Up', link_register: 'Create account', link_login: 'Log in now', show_pwd: 'Show', hide_pwd: 'Hide' }, km: { client_login_title: 'កន្លែងធ្វើការអតិថិជន', client_register_title: 'ចុះឈ្មោះអតិថិជន', brand_sub: 'Xiao Bai Massage', ph_phone: 'បញ្ចូលលេខទូរស័ព្ទ', ph_username: 'បញ្ចូលឈ្មោះអ្នកប្រើ', ph_pwd_login: 'បញ្ចូលពាក្យសម្ងាត់', ph_pwd_set: 'កំណត់ពាក្យសម្ងាត់', ph_pwd_confirm: 'បញ្ជាក់ពាក្យសម្ងាត់', btn_login_client: 'ចូល', btn_register_client: 'ចុះឈ្មោះ', link_register: 'បង្កើតគណនី', link_login: 'ចូលឥឡូវ', show_pwd: 'បង្ហាញ', hide_pwd: 'លាក់' }, vi: { client_login_title: 'Cổng Khách Hàng', client_register_title: 'Đăng Ký Khách Hàng', brand_sub: 'Xiao Bai Massage', ph_phone: 'Nhập số điện thoại', ph_username: 'Nhập tên đăng nhập', ph_pwd_login: 'Nhập mật khẩu', ph_pwd_set: 'Đặt mật khẩu', ph_pwd_confirm: 'Xác nhận mật khẩu', btn_login_client: 'Đăng Nhập', btn_register_client: 'Đăng Ký', link_register: 'Tạo tài khoản', link_login: 'Đăng nhập ngay', show_pwd: 'Hiện', hide_pwd: 'Ẩn' }, th: { client_login_title: 'พอร์ทัลลูกค้า', client_register_title: 'สมัครลูกค้า', brand_sub: 'Xiao Bai Massage', ph_phone: 'กรอกเบอร์โทร', ph_username: 'กรอกชื่อผู้ใช้', ph_pwd_login: 'กรอกรหัสผ่าน', ph_pwd_set: 'ตั้งรหัสผ่าน', ph_pwd_confirm: 'ยืนยันรหัสผ่าน', btn_login_client: 'เข้าสู่ระบบ', btn_register_client: 'สมัคร', link_register: 'สร้างบัญชี', link_login: 'เข้าสู่ระบบเลย', show_pwd: 'แสดง', hide_pwd: 'ซ่อน' }, fil: { client_login_title: 'Portal ng Kliyente', client_register_title: 'Magrehistro bilang Kliyente', brand_sub: 'Xiao Bai Massage', ph_phone: 'Ilagay ang numero', ph_username: 'Ilagay ang username', ph_pwd_login: 'Ilagay ang password', ph_pwd_set: 'Itakda ang password', ph_pwd_confirm: 'Kumpirmahin ang password', btn_login_client: 'Mag-sign In', btn_register_client: 'Magrehistro', link_register: 'Gumawa ng account', link_login: 'Mag-login ngayon', show_pwd: 'Ipakita', hide_pwd: 'Itago' }, my: { client_login_title: 'ဖောက်သည် ပေါ်တယ်', client_register_title: 'ဖောက်သည် မှတ်ပုံတင်', brand_sub: 'Xiao Bai Massage', ph_phone: 'ဖုန်းနံပါတ်ထည့်ပါ', ph_username: 'အသုံးပြုသူအမည်ထည့်ပါ', ph_pwd_login: 'စကားဝှက်ထည့်ပါ', ph_pwd_set: 'စကားဝှက်သတ်မှတ်ပါ', ph_pwd_confirm: 'စကားဝှက်အတည်ပြုပါ', btn_login_client: 'ဝင်ရောက်ရန်', btn_register_client: 'မှတ်ပုံတင်ရန်', link_register: 'အကောင့်ဖွင့်ရန်', link_login: 'ယခုဝင်ရောက်ရန်', show_pwd: 'ပြပါ', hide_pwd: 'ဖျောက်ပါ' }, lo: { client_login_title: 'ພອດອນແມ່ນລູກຄ້າ', client_register_title: 'ລົງທະບຽນລູກຄ້າ', brand_sub: 'Xiao Bai Massage', ph_phone: 'ໃສ່ເບີໂທ', ph_username: 'ໃສ່ຊື່ຜູ້ໃຊ້', ph_pwd_login: 'ໃສ່ລະຫັດຜ່ານ', ph_pwd_set: 'ຕັ້ງລະຫັດຜ່ານ', ph_pwd_confirm: 'ຢືນຢັນລະຫັດຜ່ານ', btn_login_client: 'ເຂົ້າສູ່ລະບົບ', btn_register_client: 'ລົງທະບຽນ', link_register: 'ສ້າງບັນຊີ', link_login: 'ເຂົ້າສູ່ລະບົບດຽວນີ້', show_pwd: 'ສະແດງ', hide_pwd: 'ເຊື່ອງ' } }; function getClientAuthLang() { try { var l = localStorage.getItem(CLIENT_AUTH_LANG_KEY) || 'zh'; return CLIENT_AUTH_I18N[l] ? l : 'zh'; } catch (e) { return 'zh'; } } function clientAuthT(key) { var pack = CLIENT_AUTH_I18N[getClientAuthLang()] || CLIENT_AUTH_I18N.zh; return pack[key] != null ? pack[key] : (CLIENT_AUTH_I18N.zh[key] || key); } function applyClientAuthLang() { var lang = getClientAuthLang(); var loginTit = document.querySelector('#client-login-page .cl-auth-hero-tit'); var regTit = document.querySelector('#client-register-page .cl-auth-hero-tit'); if (loginTit) loginTit.textContent = clientAuthT('client_login_title'); if (regTit) regTit.textContent = clientAuthT('client_register_title'); document.querySelectorAll('#client-login-page .cl-auth-hero-sub, #client-register-page .cl-auth-hero-sub').forEach(function(el) { el.textContent = clientAuthT('brand_sub'); }); var lp = document.getElementById('client-login-phone'); var rp = document.getElementById('client-register-phone'); var ru = document.getElementById('client-register-username'); var lpd = document.getElementById('client-login-password'); var rpd = document.getElementById('client-register-password'); var rpd2 = document.getElementById('client-register-password2'); if (lp) lp.placeholder = clientAuthT('ph_phone'); if (rp) rp.placeholder = clientAuthT('ph_phone'); if (ru) ru.placeholder = clientAuthT('ph_username'); if (lpd) lpd.placeholder = clientAuthT('ph_pwd_login'); if (rpd) rpd.placeholder = clientAuthT('ph_pwd_set'); if (rpd2) rpd2.placeholder = clientAuthT('ph_pwd_confirm'); var loginBtn = document.querySelector('#client-login-page .cl-btn'); var regBtn = document.querySelector('#client-register-page .cl-btn'); if (loginBtn) loginBtn.textContent = clientAuthT('btn_login_client'); if (regBtn) regBtn.textContent = clientAuthT('btn_register_client'); var regLink = document.querySelector('#client-login-page .cl-reg-link button'); var loginLink = document.querySelector('#client-register-page .cl-reg-link button'); if (regLink) regLink.textContent = clientAuthT('link_register'); if (loginLink) loginLink.textContent = clientAuthT('link_login'); document.querySelectorAll('#client-login-page .cl-pass-toggle, #client-register-page .cl-pass-toggle').forEach(function(btn) { var inp = btn.previousElementSibling; if (inp && inp.tagName === 'INPUT') { btn.textContent = inp.type === 'password' ? clientAuthT('show_pwd') : clientAuthT('hide_pwd'); } }); document.querySelectorAll('.cl-lang-current').forEach(function(el) { el.textContent = CLIENT_AUTH_LANGS[lang] || lang; }); document.querySelectorAll('.cl-lang-drop button[data-lang]').forEach(function(btn) { btn.classList.toggle('active', btn.getAttribute('data-lang') === lang); }); } function closeClientAuthLangMenu() { document.querySelectorAll('.cl-lang-drop').forEach(function(d) { d.setAttribute('hidden', ''); }); } function toggleClientAuthLangMenu(ev) { if (window.JwUiLang && typeof JwUiLang.openSheet === 'function') { JwUiLang.openSheet(ev); return; } if (ev) ev.stopPropagation(); var btn = ev && ev.currentTarget; var drop = btn && btn.nextElementSibling; if (!drop) return; var open = drop.hasAttribute('hidden'); closeClientAuthLangMenu(); if (open) drop.removeAttribute('hidden'); } function setClientAuthLang(code) { if (!CLIENT_AUTH_I18N[code]) return; closeClientAuthLangMenu(); if (window.JwUiLang) { JwUiLang.setLang(code); } else { try { localStorage.setItem(CLIENT_AUTH_LANG_KEY, code); } catch (e) {} } applyClientAuthLang(); } document.addEventListener('click', closeClientAuthLangMenu); function toggleClientLoginPassword(btn) { toggleClientFieldPassword('client-login-password', btn); } function toggleClientFieldPassword(id, btn) { var inp = document.getElementById(id); if (!inp || !btn) return; if (inp.type === 'password') { inp.type = 'text'; btn.textContent = clientAuthT('hide_pwd'); } else { inp.type = 'password'; btn.textContent = clientAuthT('show_pwd'); } } function openClientLoginEntry() { var p = document.getElementById('client-login-page'); if (!p) { alert('登录界面未加载'); return; } var r = document.getElementById('client-register-page'); if (r) r.classList.remove('show'); p.classList.add('show'); refreshClientDialDisplays(); applyClientAuthLang(); var ph = document.getElementById('client-login-phone'); if (ph) ph.focus(); } window.openClientLoginEntry = openClientLoginEntry; function closeClientLoginPage() { var p = document.getElementById('client-login-page'); if (p) p.classList.remove('show'); } function openClientRegisterPage() { var p = document.getElementById('client-register-page'); var l = document.getElementById('client-login-page'); if (!p) { alert('注册界面未加载'); return; } if (l) l.classList.remove('show'); p.classList.add('show'); refreshClientDialDisplays(); applyClientAuthLang(); var ph = document.getElementById('client-register-phone'); if (ph) ph.focus(); } function closeClientRegisterPage() { var p = document.getElementById('client-register-page'); if (p) p.classList.remove('show'); } function openClientLoginFromRegister() { closeClientRegisterPage(); openClientLoginEntry(); } function refreshClientDialDisplays() { var c = '86'; try { c = localStorage.getItem('user_app_dial_code') || '86'; } catch (e) {} c = String(c).replace(/\D/g, '') || '86'; var flag = clientDialFlag(c); var a = document.getElementById('client-login-dial-display'); var b = document.getElementById('client-register-dial-display'); var fa = document.getElementById('client-login-flag'); var fb = document.getElementById('client-register-flag'); if (a) a.textContent = '+' + c; if (b) b.textContent = '+' + c; if (fa) fa.textContent = flag; if (fb) fb.textContent = flag; } function clientDialDigits() { try { return (localStorage.getItem('user_app_dial_code') || '86').replace(/\D/g, '') || '86'; } catch (e2) { return '86'; } } function clientFullPhoneFromInput(inputEl) { var p = (inputEl && inputEl.value ? inputEl.value : '').replace(/\D/g, ''); return '+' + clientDialDigits() + p; } function clientLocalCredGet(full) { try { var r = localStorage.getItem('user_app_local_creds'); if (!r) return null; var o = JSON.parse(r); var v = o[full]; if (v == null) return null; if (typeof v === 'string') return { password: v, username: '' }; return { password: v.password || '', username: v.username || '' }; } catch (e) { return null; } } function clientLocalCredSet(full, pass, username) { try { var o = {}; var r = localStorage.getItem('user_app_local_creds'); if (r) o = JSON.parse(r); o[full] = { password: pass, username: username || '' }; localStorage.setItem('user_app_local_creds', JSON.stringify(o)); } catch (e) {} } function openClientCountryMask(which) { window.__clientDialTarget = which || 'login'; var m = document.getElementById('client-login-country-mask'); if (m) m.classList.add('show'); var s = document.getElementById('clc-search-input'); if (s) { s.value = ''; filterClientCountryList(''); } } function closeClientCountryMask() { var m = document.getElementById('client-login-country-mask'); if (m) m.classList.remove('show'); } function filterClientCountryList(k) { k = (k || '').trim().toLowerCase(); var list = document.getElementById('clc-list'); if (!list) return; list.querySelectorAll('.clc-item').forEach(function(el) { var n = (el.getAttribute('data-name') || '').toLowerCase(); var c = (el.getAttribute('data-code') || ''); var show = !k || n.indexOf(k) >= 0 || ('+' + c).indexOf(k) >= 0 || c.indexOf(k) >= 0; el.classList.toggle('hide', !show); }); } function clientPickDial(code, name) { try { localStorage.setItem('user_app_dial_code', String(code).replace(/\D/g, '') || '86'); if (name) localStorage.setItem('user_app_dial_name', name); } catch (e) {} refreshClientDialDisplays(); closeClientCountryMask(); } var JW_CFG = window.__JW_SITE_CONFIG || {}; var TECH_APP_URL = (JW_CFG.coach || 'https://qq1.jiang-wei.homes').replace(/\/?$/, '/'); function openTechCoachPortal() { window.open(TECH_APP_URL, '_blank', 'noopener,noreferrer'); } var AGENT_APP_URL = (window.__JW_SITE_CONFIG && window.__JW_SITE_CONFIG.agent) ? window.__JW_SITE_CONFIG.agent.replace(/\/?$/, '/') : 'https://qq3.jiang-wei.homes/'; function openAgentPortal() { window.open(AGENT_APP_URL, '_blank', 'noopener,noreferrer'); } var PRODUCTION_API_ORIGIN = JW_CFG.api || 'https://qq2.jiang-wei.homes'; function clientAuthHeaders(extra) { if (window.JwApiAuth) return JwApiAuth.authHeaders('client', Object.assign({ 'Content-Type': 'application/json' }, extra || {})); var h = Object.assign({ 'Content-Type': 'application/json' }, extra || {}); try { var t = localStorage.getItem('jw_client_token') || localStorage.getItem('token') || ''; if (t) h.Authorization = 'Bearer ' + t; } catch (e) {} return h; } function apiBase() { if (typeof window.jwResolveApiRoot === 'function') return window.jwResolveApiRoot(location); return PRODUCTION_API_ORIGIN; } function clientReadFetchResponse(r) { return r.text().then(function (t) { var j = {}; if (t && String(t).trim()) { try { j = JSON.parse(t); } catch (e) { j = {}; } } return { ok: r.ok, status: r.status, j: j }; }); } function clientRegisterLooksConflict(res) { if (res.status === 409 || res.status === 422) return true; var m = String((res.j && (res.j.error || res.j.message)) || '').toLowerCase(); return /已注册|已存在|exists|exist|duplicate|already\s*registered|registered/.test(m); } function doClientPhoneLogin() { var phoneEl = document.getElementById('client-login-phone'); var pwd = (document.getElementById('client-login-password') && document.getElementById('client-login-password').value) || ''; if (!document.getElementById('client-login-agree').checked) { alert('请先勾选同意用户协议与隐私政策'); return; } var dial = clientDialDigits(); var full = clientFullPhoneFromInput(phoneEl); var digits = full.slice(('+' + dial).length); if (!digits || digits.length < 6) { alert('请输入正确的手机号'); return; } if (!pwd || pwd.length < 6) { alert('请输入密码(至少 6 位)'); return; } var body = { dialCode: clientDialDigits(), phone: digits, password: pwd, role: 'client' }; fetch(apiBase() + '/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) .then(function (r) { return clientReadFetchResponse(r); }) .then(function (res) { if (res.ok && res.j && (res.j.user || res.j.token)) { var u = res.j.user || {}; try { if (res.j.token && window.JwApiAuth) JwApiAuth.setToken('client', res.j.token); else if (res.j.token) localStorage.setItem('jw_client_token', res.j.token); if (res.j.ezspaToken) localStorage.setItem('jw_ezspa_token', res.j.ezspaToken); localStorage.setItem('user_app_logged_phone', u.phone || full); localStorage.setItem('user_app_user', JSON.stringify(u)); } catch (e) {} syncMyPageUserUi(); if (typeof refreshWalletBalanceUi === 'function') refreshWalletBalanceUi(); closeClientLoginPage(); alert('登录成功'); return; } var local = clientLocalCredGet(full); if (local && local.password === pwd) { try { localStorage.setItem('user_app_logged_phone', full); localStorage.setItem('user_app_user', JSON.stringify({ phone: full, username: local.username || '', nickname: local.username || ('用户' + digits.slice(-4)) })); } catch (e2) {} syncMyPageUserUi(); if (typeof refreshWalletBalanceUi === 'function') refreshWalletBalanceUi(); closeClientLoginPage(); alert('登录成功'); return; } alert((res.j && res.j.error) || '账号或密码不正确;请先注册'); }) .catch(function() { var local = clientLocalCredGet(full); if (local && local.password === pwd) { try { localStorage.setItem('user_app_logged_phone', full); localStorage.setItem('user_app_user', JSON.stringify({ phone: full, username: local.username || '', nickname: local.username || ('用户' + digits.slice(-4)) })); } catch (e3) {} syncMyPageUserUi(); closeClientLoginPage(); alert('登录成功'); return; } alert('无法连接服务器;演示模式下请先在「注册账号」创建本地账号'); }); } function doClientRegister() { try { var phoneEl = document.getElementById('client-register-phone'); var username = (document.getElementById('client-register-username') && document.getElementById('client-register-username').value || '').trim(); var p1 = (document.getElementById('client-register-password') && document.getElementById('client-register-password').value) || ''; var p2 = (document.getElementById('client-register-password2') && document.getElementById('client-register-password2').value) || ''; if (!document.getElementById('client-register-agree').checked) { alert('请先勾选同意用户协议与隐私政策'); return; } var dial = clientDialDigits(); var full = clientFullPhoneFromInput(phoneEl); var digits = full.slice(('+' + dial).length); if (!digits || digits.length < 6) { alert('请输入正确的手机号'); return; } if (!username || username.length < 2) { alert('请输入用户名(至少 2 个字符)'); return; } if (username.length > 20) { alert('用户名不能超过 20 个字符'); return; } if (!p1 || p1.length < 6) { alert('密码至少 6 位'); return; } if (p1 !== p2) { alert('两次输入的密码不一致'); return; } if (clientLocalCredGet(full)) { alert('该手机号已注册,请直接登录'); return; } var body = { dialCode: clientDialDigits(), phone: digits, password: p1, username: username, role: 'client' }; try { var ic = (localStorage.getItem('jw_invite_ref') || '').trim(); if (ic) body.inviteCode = ic; } catch (eInv) {} fetch(apiBase() + '/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) .then(function (r) { return clientReadFetchResponse(r); }) .then(function (res) { if (res.ok && res.j && (res.j.user || res.j.token)) { clientLocalCredSet(full, p1, username); try { var u = res.j.user || {}; if (res.j.token && window.JwApiAuth) JwApiAuth.setToken('client', res.j.token); else if (res.j.token) localStorage.setItem('jw_client_token', res.j.token); localStorage.setItem('user_app_logged_phone', u.phone || full); localStorage.setItem('user_app_user', JSON.stringify(res.j.user || { phone: full, username: username, nickname: username })); } catch (e) {} syncMyPageUserUi(); closeClientRegisterPage(); alert('注册成功'); return; } if (clientRegisterLooksConflict(res)) { alert((res.j && (res.j.error || res.j.message)) || '该手机号已被注册,请直接登录'); return; } clientLocalCredSet(full, p1, username); try { localStorage.setItem('user_app_logged_phone', full); localStorage.setItem('user_app_user', JSON.stringify({ phone: full, username: username, nickname: username })); } catch (e2) {} syncMyPageUserUi(); closeClientRegisterPage(); var srv = (res.j && (res.j.error || res.j.message)) ? String(res.j.error || res.j.message) : ''; if (!res.ok && srv) alert('服务端未接通或接口未实现(' + srv + ')。已为您保存本机演示账号,可直接使用。'); else if (!res.ok) alert('服务端未返回成功。已保存本机演示账号,可直接使用。'); else alert('已注册(演示模式)'); }) .catch(function () { clientLocalCredSet(full, p1, username); try { localStorage.setItem('user_app_logged_phone', full); localStorage.setItem('user_app_user', JSON.stringify({ phone: full, username: username, nickname: username })); } catch (e3) {} syncMyPageUserUi(); closeClientRegisterPage(); alert('网络异常。已注册(离线演示:账号已保存在本机)'); }); } catch (e) { alert('注册异常:' + (e && e.message ? e.message : String(e))); } } function mountTechnicianCards() { loadProjectItems(function () { loadMemberDiscounts(function () { loadServiceCategories(function () { syncCategoryTagBars(__activeCatTag || '按摩'); fetchGirlsList('', function () { loadHomeBlocks(); fetchGirlsList(__activeCatTag || '按摩', function (list) { renderTechnicianLists(list); }); }); }); }); }); } document.addEventListener('DOMContentLoaded', function() { try { var p = new URLSearchParams(location.search); var ref = (p.get('ref') || p.get('invite') || '').trim().toUpperCase(); if (ref) { localStorage.setItem('jw_invite_ref', ref); if (ref.indexOf('CU') === 0 && typeof openClientRegisterPage === 'function') openClientRegisterPage(); } } catch (eRef) {} document.body.classList.add('page-bg-white'); try { applyClientAuthLang(); } catch (eLang) {} try { if (window.JwUiLang) { JwUiLang.registerOnChange(function () { applyClientAuthLang(); }); JwUiLang.mountPicker('client-my-lang-slot'); JwUiLang.mountPicker('client-settings-lang-slot'); JwUiLang.applyMyPage(document); } } catch (eUiLang) {} try { if (window.JwOrderSound) { JwOrderSound.init({ role: 'client', getApiBase: apiBase, getAuthHeaders: clientAuthHeaders, isLoggedIn: isUserAppLoggedIn, getPkg: function () { return JW_PKG; }, getLang: function () { return window.JwUiLang ? JwUiLang.getLang() : 'zh'; }, soundBase: function () { return 'https://qq1.jiang-wei.homes'; } }); } } catch (eSound) {} try { if (window.JwOrderNotify) { JwOrderNotify.init({ role: 'client', getApiBase: apiBase, getAuthHeaders: clientAuthHeaders, isLoggedIn: isUserAppLoggedIn, getPkg: function () { return JW_PKG; }, badges: { msgBtn: 'client-ord-msg-badge', navOrders: 'client-nav-orders-badge' }, openChat: function (oid) { openClientOrderChat(oid); }, openInbox: function () { openClientOrderInbox(); } }); } } catch (eNotify) {} try { if (window.JwLocation) { JwLocation.init({ apiBase: (typeof apiBase === 'function' ? apiBase() : (JW_CFG.api || 'https://qq2.jiang-wei.homes')), jwPkg: JW_PKG, displaySelectors: ['.loc-city', '.jw-loc-display'], injectBar: false, onLocated: function () { __girlsAllLoaded = false; fetchGirlsList(typeof __activeCatTag !== 'undefined' ? __activeCatTag : '', function (list) { renderTechnicianLists(list); if (__homeBlocksData && __girlsAllLoaded) renderHomeBlocks(__homeBlocksData); }); }, onReady: function () { try { mountTechnicianCards(); } catch (eTech) {} } }); } else { loadJwGeoCities(function () { initJwLocationSystem(function () { try { mountTechnicianCards(); } catch (eTech) {} }); }); } } catch (eLoc) { try { mountTechnicianCards(); } catch (eTech) {} } try { window.addEventListener('scroll', function() { updateStickyTopBars(); }, { passive: true }); updateStickyTopBars(); } catch (eSticky) {} try { var ppvSwiper = document.getElementById('ppv-swiper'); if (ppvSwiper) { ppvSwiper.addEventListener('scroll', function() { updatePpvCounter(); }, { passive: true }); } var ppvResizeT; window.addEventListener('resize', function() { clearTimeout(ppvResizeT); ppvResizeT = setTimeout(function() { var mask = document.getElementById('provider-photo-viewer'); if (mask && mask.classList.contains('show')) { sizePpvSlides(); updatePpvCounter(); } }, 80); }); document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeProviderPhotoViewer(); }); } catch (ePpv) {} try { if (!history.state || !history.state.jwPage) { history.replaceState({ jwPage: getActivePageId(), t: Date.now() }, '', location.pathname + location.search); } window.addEventListener('popstate', function(e) { var viewer = document.getElementById('provider-photo-viewer'); if (viewer && viewer.classList.contains('show')) { closeProviderPhotoViewer(); return; } __navHistorySilent = true; var st = (e.state && e.state.jwPage) || ''; var activeId = getActivePageId(); if (activeId === 'page-provider-detail') { var pdTarget = __providerDetailReturnPage || (st && st !== 'page-provider-detail' ? st : 'page-square'); showPage(pdTarget); syncNavHistoryState(pdTarget); } else if (activeId === 'page-cat') { if (st && st !== 'page-cat' && st !== 'page-provider-detail') { resetCatTagsTo('按摩'); showPage(st); if (st === 'page-square' || st === 'page-service' || st === 'page-tech' || st === 'page-home') { loadAndRenderTechnicians('按摩'); } syncNavHistoryState(st); } else { leaveCatPage(__catReturnPage || 'page-square'); } } else if (st) { showPage(st); syncNavHistoryState(st); } __navHistorySilent = false; }); } catch (eHist) {} try { if (!localStorage.getItem('user_app_dial_code')) localStorage.setItem('user_app_dial_code', '86'); } catch (e0) {} try { refreshClientDialDisplays(); } catch (eDial) {} try { syncMyPageUserUi(); } catch (e) {} try { wireOrderPageTabs(); } catch (eOrdTabs) {} try { var cpTabs = document.getElementById('cp-tabs'); if (cpTabs) { cpTabs.querySelectorAll('span[data-cptab]').forEach(function(tab) { tab.addEventListener('click', function() { var key = tab.getAttribute('data-cptab'); cpTabs.querySelectorAll('span[data-cptab]').forEach(function(s) { s.classList.toggle('active', s.getAttribute('data-cptab') === key); }); var u = document.getElementById('cp-list-unused'); var used = document.getElementById('cp-list-used'); if (u) u.classList.toggle('hide', key !== 'unused'); if (used) used.classList.toggle('hide', key !== 'used'); }); }); } var grid = document.getElementById('hw-recharge-grid'); var footAmt = document.getElementById('hw-foot-amt'); if (grid && footAmt) { grid.querySelectorAll('.hw-opt').forEach(function(btn) { btn.addEventListener('click', function() { grid.querySelectorAll('.hw-opt').forEach(function(b) { b.classList.remove('active'); }); this.classList.add('active'); footAmt.textContent = '$' + this.getAttribute('data-amt'); }); }); } } catch (e2) {} }); (function bindProviderDetailGallery() { var hero = document.getElementById('detail-gallery-hero'); var bg = document.getElementById('detail-gallery-bg'); var mainImg = document.getElementById('detail-gallery-main'); var videoEl = document.getElementById('detail-gallery-video'); var thumbsRoot = document.getElementById('detail-gallery-thumbs'); if (!hero || !bg || !mainImg || !thumbsRoot) return; window.__detailGalleryItems = []; window.__detailGalleryIndex = 0; window.__detailGalleryGirl = null; function wireImgFallbacks(scope) { var root = scope || document.getElementById('page-provider-detail'); if (!root) return; root.querySelectorAll('img[data-img-fallbacks]').forEach(function(img) { if (img.getAttribute('data-fallback-wired') === '1') return; img.setAttribute('data-fallback-wired', '1'); var raw = img.getAttribute('data-img-fallbacks') || ''; var list = raw.split('|').map(function(s) { return s.trim(); }).filter(Boolean); var pos = 0; img.addEventListener('error', function onErr() { if (pos < list.length) { img.src = list[pos++]; } else { img.removeEventListener('error', onErr); } }); }); } function setHeroPhoto(url, g) { var src = url || TECH_PHOTO_PLACEHOLDER; hero.classList.remove('is-video'); mainImg.src = src; mainImg.style.cssText = g ? girlAvatarStyle(g) : ''; bg.style.backgroundImage = src ? ('url("' + String(src).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '")') : 'none'; if (videoEl) { try { videoEl.pause(); } catch (ePause) {} } } function setHeroVideo(url, poster, g) { if (!url) { setHeroPhoto(poster || TECH_PHOTO_PLACEHOLDER, g); return; } hero.classList.add('is-video'); if (poster) videoEl.setAttribute('poster', poster); else videoEl.removeAttribute('poster'); videoEl.innerHTML = ''; try { videoEl.load(); } catch (eLoad) {} if (poster) bg.style.backgroundImage = 'url("' + String(poster).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '")'; } window.selectProviderDetailGalleryIndex = function(i) { var items = window.__detailGalleryItems || []; if (!items.length) return; i = Math.max(0, Math.min(items.length - 1, i | 0)); window.__detailGalleryIndex = i; thumbsRoot.querySelectorAll('.detail-gallery-thumb').forEach(function(el, idx) { el.classList.toggle('active', idx === i); }); var item = items[i]; var g = window.__detailGalleryGirl; if (item && item.type === 'video') setHeroVideo(item.url, item.poster || '', g); else setHeroPhoto(item ? item.url : '', g); var active = thumbsRoot.querySelector('.detail-gallery-thumb.active'); if (active && active.scrollIntoView) { try { active.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } catch (eScroll) {} } }; window.renderProviderDetailGallery = function(photos, g, videoUrl) { window.__detailGalleryGirl = g || null; var list = []; (photos || []).forEach(function(u) { if (u) list.push({ type: 'photo', url: u }); }); if (videoUrl) { list.push({ type: 'video', url: videoUrl, poster: (photos && photos[0]) || '' }); } if (!list.length) { list.push({ type: 'photo', url: TECH_PHOTO_PLACEHOLDER }); } window.__detailGalleryItems = list; __activeProviderPhotos = (photos && photos.length) ? photos.slice() : list.filter(function(x) { return x.type === 'photo'; }).map(function(x) { return x.url; }); thumbsRoot.innerHTML = list.map(function(item, i) { var cls = 'detail-gallery-thumb' + (i === 0 ? ' active' : '') + (item.type === 'video' ? ' is-video' : ''); if (item.type === 'video') { var p = item.poster || TECH_PHOTO_PLACEHOLDER; return ''; } return ''; }).join(''); thumbsRoot.querySelectorAll('.detail-gallery-thumb').forEach(function(btn) { btn.addEventListener('click', function() { var idx = parseInt(btn.getAttribute('data-idx'), 10); if (!isNaN(idx)) window.selectProviderDetailGalleryIndex(idx); }); }); wireImgFallbacks(thumbsRoot); window.__detailGalleryIndex = 0; var first = list[0]; if (first.type === 'video') setHeroVideo(first.url, first.poster, g); else setHeroPhoto(first.url, g); }; window.refreshProviderDetailGallery = function() { window.selectProviderDetailGalleryIndex(window.__detailGalleryIndex || 0); }; window.setProviderDetailMediaTab = function() {}; window.setProviderAlbumPhotos = function(urls) { window.renderProviderDetailGallery(urls, window.__detailGalleryGirl, ''); }; window.refreshProviderDetailAlbumLayout = function() { window.refreshProviderDetailGallery(); }; wireImgFallbacks(document.getElementById('page-provider-detail')); })(); // 广场横向标签:仅切换选中态,不跳转页面 // 首页视频:先试根目录,失败可试 video/ 路径;播放成功则取消 fallback (function(){ var wrap = document.getElementById('home-video-wrap'); var video = document.getElementById('home-video'); var fallbackText = document.getElementById('home-video-fallback-text'); if (!video || !wrap) return; function showVideo() { wrap.classList.remove('video-fallback'); video.style.display = 'block'; if (fallbackText) fallbackText.style.display = 'none'; } video.addEventListener('loadeddata', showVideo, { once: true }); video.addEventListener('canplay', showVideo, { once: true }); var fallbackPath = 'video/8820c1651db6b11e9d6a2c61a960eae2.mp4'; video.addEventListener('error', function onErr() { if (this.getAttribute('data-tried-fallback')) return; this.setAttribute('data-tried-fallback', '1'); this.src = fallbackPath; this.load(); }, { once: true }); })();