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 = 'http://127.0.0.1:8091/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();
initCountdown();
}
// 把 teamId / activityId 写到 user-info 的 dataset 上,便于点击时读取
function makeItem(team, price) {
const { userId, targetCount, lockCount,
validTimeCountdown, teamId, activityId: tActId } = team;
const leftNum = Math.max(targetCount - lockCount, 0);
const timeText = validTimeCountdown || '00:00:00';
const div = document.createElement('div');
div.className = 'user-item';
div.innerHTML = `
${userId}
仅剩${leftNum}人成团
${timeText}
`;
return div;
}
/* =====================================================
* 2. 拼单列表纵向轮播
* =================================================== */
function initUserMarquee() {
const items = userList.querySelectorAll('.user-item');
if (items.length <= 1) return;
const itemH = items[0].offsetHeight;
userList.appendChild(items[0].cloneNode(true));
let idx = 0;
userList.addEventListener('transitionend', () => {
if (idx >= items.length) {
userList.style.transition = 'none';
userList.style.transform = 'translateY(0)';
idx = 0;
void userList.offsetWidth;
}
});
setInterval(() => {
idx++;
userList.style.transition = 'transform .5s ease';
userList.style.transform = `translateY(${-idx * itemH}px)`;
}, 3000);
}
/* =====================================================
* 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 PAY_MALL_URL = 'http://127.0.0.1:8092';
const CREATE_PAY_API = `${PAY_MALL_URL}/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();
}
});