document.addEventListener('DOMContentLoaded', () => { /* ========== 通用工具 ========== */ const getCookie = k => document.cookie .split(';') .map(c => c.trim()) .find(c => c.startsWith(k + '='))?.split('=')[1] || null; const $ = id => document.getElementById(id); /* ----------- 0. DOM 快捷引用 ----------- */ const currentPrice = $('currentPrice'); const originalPrice = $('originalPrice'); const dropPrice = $('dropPrice'); const soldBox = $('soldBox'); const groupTitle = $('groupTitle'); const userList = $('userList'); const singlePriceSpan = $('singlePrice'); const groupPriceSpan = $('groupPrice'); const btnSingle = $('btnSingle'); const btnGroup = $('btnGroup'); /* ===================================================== * 1. 取接口数据并渲染 * =================================================== */ const CFG_API = '/api/v1/gbm/index/query_group_buy_market_config'; // 登录检查 const loginToken = getCookie('loginToken'); if (!loginToken) { location.href = 'login.html'; return; } const username = loginToken; // 保存活动 id(开团 / 参团时要用) let activityId = null; const POST_BODY = { userId : username, source : 's01', channel: 'c01', goodsId: '9890001' }; fetch(CFG_API, { method : 'POST', headers: { 'Content-Type': 'application/json' }, body : JSON.stringify(POST_BODY) }) .then(r => r.json()) .then(({ code, info, data }) => { if (code !== '0000' || !data) { console.error(info); return; } activityId = data.activityId; renderGoods(data.goods); renderStatistic(data.teamStatistic); renderTeams(data.teamList, data.goods?.payPrice); }) .catch(console.error); /* ------------- 渲染商品信息 ------------- */ function renderGoods(g = {}) { const { originalPrice: op = 0, payPrice = 0, deductionPrice = 0 } = g; currentPrice.textContent = payPrice; originalPrice.textContent = op; dropPrice.textContent = `直降 ¥${deductionPrice}`; singlePriceSpan.textContent = `¥${op}`; groupPriceSpan.textContent = `¥${payPrice}`; btnSingle.dataset.price = op; btnGroup.dataset.price = payPrice; } /* ------------- 渲染统计信息 ------------- */ function renderStatistic(stat = {}) { const { allTeamUserCount = 0 } = stat; groupTitle.textContent = `${allTeamUserCount}人在抢,参与可立即拼成`; soldBox.textContent = `${allTeamUserCount}人再抢`; } /* ------------- 渲染拼团列表 ------------- */ function renderTeams(list = [], groupPrice = 0) { if (!list?.length) { groupTitle.textContent = '小伙伴,赶紧去开团吧,做村里最靓的仔。'; return; } userList.innerHTML = ''; list.forEach(t => userList.appendChild(makeItem(t, groupPrice))); initUserMarquee(3); // 显示 3 条 initCountdown(); } function obfuscateUserId(userId) { if (userId.length < 4) { if (userId.length <= 1) { return userId; } const first = userId.charAt(0); const last = userId.charAt(userId.length - 1); const middle = '*'.repeat(userId.length - 2); return first + middle + last; } else { const start = userId.slice(0, 2); const end = userId.slice(-1); const middle = '*'.repeat(userId.length - 4); return start + middle + end; } } // 把 teamId / activityId 写到 user-info 的 dataset 上,便于点击时读取 function makeItem(team, price) { const { userId, targetCount, lockCount, validTimeCountdown } = team; // ① 先对 userId 脱敏 const maskedId = obfuscateUserId(userId); // ② 继续原本逻辑 const leftNum = Math.max(targetCount - lockCount, 0); const timeText = validTimeCountdown || '00:00:00'; const div = document.createElement('div'); div.className = 'user-item'; div.innerHTML = `
${maskedId}
仅剩 ${leftNum} 人成团 ${timeText}
`; return div; } /* ===================================================== * 2. 拼单列表纵向轮播 * =================================================== */ function initUserMarquee(visibleCount = 3, interval = 3000, duration = 500) { const box = document.querySelector('.group-users'); const listEl = userList; let originals = Array.from(listEl.children); const total = originals.length; if (total === 0) return; // 如果原本就 <= 可见数,直接定高,不滚 if (total <= visibleCount) { const h0 = originals[0].offsetHeight; box.style.height = (h0 * visibleCount) + 'px'; return; } // 1. 复制整份加入末尾 originals.forEach(item => listEl.appendChild(item.cloneNode(true))); // 2. 重新测量单条高度(此时 DOM 完整) const itemH = listEl.children[0].offsetHeight; // 3. 设定窗口高度 box.style.height = (itemH * visibleCount) + 'px'; // 4. 状态 let index = 0; let ticking = false; function step() { index++; listEl.style.transition = `transform ${duration}ms ease`; listEl.style.transform = `translateY(-${index * itemH}px)`; } listEl.addEventListener('transitionend', () => { ticking = false; // 5. 到达复制段的“第一帧”(index === total)就无缝重置 if (index === total) { listEl.style.transition = 'none'; listEl.style.transform = 'translateY(0)'; index = 0; // 强制 reflow 再恢复 transition void listEl.offsetHeight; listEl.style.transition = `transform ${duration}ms ease`; } }); const timer = setInterval(() => { if (!ticking) { ticking = true; step(); } }, interval); // 可选:鼠标悬停暂停 listEl.addEventListener('mouseenter', () => clearInterval(timer)); } /* ===================================================== * 3. 倒计时 * =================================================== */ let countdownData = []; function initCountdown() { const els = document.querySelectorAll('.countdown'); countdownData = Array.from(els).map(el => ({ el, remain: toSec(el.textContent.trim()) })); setInterval(tick, 1000); } const toSec = t => { if (!t.includes(':')) return 0; const [h='00', m='00', s='00'] = t.split(':'); return +h*3600 + +m*60 + +s; }; const fmt = n => String(n).padStart(2,'0'); const format = s => `${fmt(s/3600|0)}:${fmt((s%3600)/60|0)}:${fmt(s%60)}`; function tick() { countdownData.forEach(c=>{ if (c.remain > 0) { c.remain--; c.el.textContent = format(c.remain); if(!c.remain) expire(c.el); } }); } function expire(el){ el.textContent = '00:00:00'; const item = el.closest('.user-item'); item?.classList.add('expired'); item?.querySelector('.buy-btn')?.setAttribute('disabled','disabled'); } /* ===================================================== * 4. 支付相关 * =================================================== */ const CREATE_PAY_API = `/api/v1/alipay/create_pay_order`; // 4.1 支付确认弹窗 function showPaymentConfirm(price){ if(document.querySelector('.payment-overlay')) return; const tpl = document.getElementById('tpl-payment'); const overlay = tpl.content.firstElementChild.cloneNode(true); overlay.querySelector('#priceText').textContent = `¥${price}`; overlay.querySelector('.copyable').onclick = function(){ navigator.clipboard.writeText(this.dataset.copy) .then(()=>alert('买家账号已复制到剪贴板')); }; overlay.querySelector('.cancel-btn').onclick = ()=>{ document.querySelectorAll('form').forEach(f=>f.remove()); overlay.remove(); }; overlay.querySelector('.confirm-btn').onclick = ()=>{ document.querySelector('form')?.submit(); overlay.remove(); }; overlay.addEventListener('click',e=>{ if(e.target===overlay) overlay.querySelector('.cancel-btn').click(); }); document.body.appendChild(overlay); } /* ---------- 4.2 单独购买 ---------- */ btnSingle.addEventListener('click', () => { if (!getCookie('loginToken')) { location.href = 'login.html'; return; } fetch(CREATE_PAY_API, { method : 'POST', headers: { 'Content-Type': 'application/json' }, body : JSON.stringify({ userId : username, productId : POST_BODY.goodsId, marketType: 0 }) }) .then(r=>r.json()) .then(json=>{ if (json.code!=='0000') return alert(json.info||'下单失败'); document.querySelectorAll('form').forEach(f=>f.remove()); document.body.insertAdjacentHTML('beforeend', json.data); showPaymentConfirm(btnSingle.dataset.price); }) .catch(console.error); }); /* ---------- 4.3 开团购买 ---------- */ btnGroup.addEventListener('click', () => { if (!getCookie('loginToken')) { location.href = 'login.html'; return; } fetch(CREATE_PAY_API, { method : 'POST', headers: { 'Content-Type': 'application/json' }, body : JSON.stringify({ userId : username, productId : POST_BODY.goodsId, marketType: 1, activityId: activityId }) }) .then(r=>r.json()) .then(json=>{ if (json.code!=='0000') return alert(json.info||'下单失败'); document.querySelectorAll('form').forEach(f=>f.remove()); document.body.insertAdjacentHTML('beforeend', json.data); showPaymentConfirm(btnGroup.dataset.price); }) .catch(console.error); }); /* ---------- 4.4 参与拼团( buy-btn )---------- */ document.body.addEventListener('click', e => { const joinBtn = e.target.closest('.buy-btn'); if (!joinBtn) return; if (!getCookie('loginToken')) { location.href = 'login.html'; return; } // 获取 teamId、activityId const userInfo = joinBtn.closest('.user-item')?.querySelector('.user-info'); const teamId = userInfo?.dataset.teamid; const actId = userInfo?.dataset.activityid || activityId; if (!teamId) { return alert('拼团信息已失效,请刷新页面'); } fetch(CREATE_PAY_API, { method : 'POST', headers: { 'Content-Type': 'application/json' }, body : JSON.stringify({ userId : username, productId : POST_BODY.goodsId, teamId : teamId, activityId: actId, marketType: 1 // 参团 }) }) .then(r=>r.json()) .then(json=>{ if (json.code!=='0000') return alert(json.info||'参团失败'); document.querySelectorAll('form').forEach(f=>f.remove()); document.body.insertAdjacentHTML('beforeend', json.data); showPaymentConfirm(joinBtn.dataset.price); }) .catch(console.error); }); /* ===================================================== * 5. 顶部横向轮播(原逻辑保留) * =================================================== */ const wrapper = document.querySelector('.swiper-wrapper'); const slides = [...wrapper.children]; const pagination = document.querySelector('.swiper-pagination'); const count = slides.length; let current = 0, startX = 0, dragging = false, timer; for (let i = 0; i < count; i++) { const dot = document.createElement('div'); dot.className = 'swiper-dot' + (i===0?' active':''); dot.onclick = () => goTo(i); pagination.appendChild(dot); } const dots = pagination.children; const goTo = i => { current = (i+count)%count; wrapper.style.transition = 'transform .3s ease'; wrapper.style.transform = `translateX(-${current*100}%)`; [...dots].forEach((d,j)=>d.classList.toggle('active',j===current)); }; const auto = () => { timer=setInterval(()=>goTo(current+1),3000); }; const stop = () => clearInterval(timer); auto(); const getX = e => e.touches?e.touches[0].clientX:e.clientX; wrapper.addEventListener('pointerdown', e=>{ stop(); dragging=true; startX=getX(e); wrapper.style.transition='none'; }); wrapper.addEventListener('pointermove', e=>{ if(!dragging) return; const diff=getX(e)-startX; wrapper.style.transform=`translateX(calc(${-current*100}% + ${diff}px))`; }); wrapper.addEventListener('pointerup',endSwipe); wrapper.addEventListener('pointercancel',endSwipe); wrapper.addEventListener('pointerleave',endSwipe); function endSwipe(e){ if(!dragging) return; dragging=false; const diff=getX(e)-startX; const limit=wrapper.offsetWidth*0.15; if(diff> limit) goTo(current-1); else if(diff<-limit) goTo(current+1); else goTo(current); auto(); } });