DOING Market

發現好活動,遇見好日常

市集・體驗・合作・寄賣・團購,讓生活更有趣

近期活動

載入中…

關於 DOING 系統

DOING 是給市集、活動與主辦團隊使用的一站式營運系統,從活動管理、報名收款、現場報到到營運分析,都集中在同一個工作空間。

📅 活動管理場次建立與管理 💳 報名收款線上報名與金流 ▦ 現場報到QR Code 報到 📊 數據分析營運統計
場次列表
載入中…
報名表
🎪
會員專區

需要幫助嗎?

DOING 小幫手在這裡!

📝

報名前必看

審核資料、照片、社群連結

💳

審核/錄取/付款

進度查詢、錄取通知、付款回報

🎪

設備/攤位/選位

桌椅設備、選位、現場配置

📸

報到/撤場/保證金

開攤照、清場照、押金退還

取消/退費/現場規範

退費規則、搬運、現場秩序

📣

寄放/寄賣/公告

寄物、百貨寄賣、抽獎、大群組

💬

聯絡該場次官方 LINE

付款、異動、退費、設備、現場問題

🤝

合作邀請

活動邀約、異業合作、商業合作

'); w.document.close(); toast&&toast('請長按圖片,選「加入照片」即可存檔',7000); }else toast&&toast('請允許開啟新分頁,或改按「分享」儲存',7000); return; } const a=document.createElement('a'); a.href=PF.shotDataUrl; a.download=photoFileName(); document.body.appendChild(a); a.click(); document.body.removeChild(a); toast&&toast('已下載'); } async function sharePhoto(){ if(!PF.shotDataUrl){ toast&&toast('尚未拍照'); return; } try{ const blob=await (await fetch(PF.shotDataUrl)).blob(); const file=new File([blob],photoFileName(),{type:'image/png'}); if(navigator.share && (!navigator.canShare || navigator.canShare({files:[file]}))){ await navigator.share({files:[file],title:(PF.frame&&PF.frame.name)||'活動拍照'}); return; } }catch(e){ if(e && e.name==='AbortError') return; } await downloadPhoto(); } async function api(params){ params = {...(params||{})}; if (!params.tenant && !params.tenantId) params.tenant = TENANT_ID; const _memberToken=memberSafeToken(); if(_memberToken && !params.member_token && !params.memberToken && !params.token) params.member_token=_memberToken; if (!params._t) params._t = Date.now(); const qs=Object.entries(params).map(([k,v])=>k+'='+encodeURIComponent(v)).join('&'); // 逾時保護:沒有這個的話,連線卡住就永遠不回,畫面上的按鈕會一直停用(使用者以為「按不下去」) const ctrl=(typeof AbortController!=='undefined')?new AbortController():null; const _to=Number(params.__timeout)||20000; if(params.__timeout) delete params.__timeout; const timer=setTimeout(()=>{ try{ ctrl&&ctrl.abort(); }catch(e){} }, _to); try{ const r=await fetch(API_BASE+'?'+qs,{cache:'no-store',signal:ctrl?ctrl.signal:undefined}); if(!r.ok){toast&&toast('⚠️ 連線失敗,請稍後再試');return{error:'fail'};} return await r.json(); } catch(e){ const aborted=(e&&(e.name==='AbortError')); toast&&toast(aborted?'⚠️ 連線逾時,請檢查網路後再試一次':'⚠️ 網路連線失敗'); return {error: aborted?'timeout':'fail'}; } finally{ clearTimeout(timer); } } async function apiPost(body, timeoutMs=30000){ body={...(body||{})}; if (!body.tenant && !body.tenantId) body.tenant = TENANT_ID; const _memberToken=memberSafeToken(); if(_memberToken && !body.member_token && !body.memberToken && !body.token) body.member_token=_memberToken; const ctrl = new AbortController(); const timer = setTimeout(()=>ctrl.abort(), timeoutMs); try{ const r=await fetch(API_BASE+'?_t='+Date.now(),{method:'POST',headers:{'Content-Type':'application/json;charset=utf-8','Cache-Control':'no-store'},body:JSON.stringify(body),signal:ctrl.signal,cache:'no-store'}); const txt=await r.text(); let data={}; try{ data=txt?JSON.parse(txt):{}; }catch(e){ data={error:'後端回傳格式錯誤', raw:txt}; } if(!r.ok){ toast('⚠️ 連線失敗'); return data.error?data:{error:'連線失敗'}; } return data; }catch(e){ console.error('[apiPost error]', e); if(e && e.name==='AbortError'){ toast('⚠️ 送出逾時,請稍後查詢我的紀錄'); return {error:'送出逾時:後端沒有在 15 秒內回應。系統可能仍在處理,請先到「我的紀錄」確認是否已出現報名;若沒有,請截圖此畫面給官方 LINE。'}; } toast('⚠️ 網路錯誤'); return {error:'網路錯誤,請稍後再試'}; }finally{ clearTimeout(timer); } } function isAmbiguousRegisterTransportError(message){ const s=String(message||''); return ['送出逾時','網路錯誤','連線失敗','後端回傳格式錯誤'].some(x=>s.includes(x)); } // POST 回覆若在手機網路中途遺失,先回查正式 registrations;查到就視為成功, // 避免資料已入庫、畫面卻叫使用者重送而造成重複報名。 async function recoverCommittedRegistration(sessionId,email,phone){ try{ const rows=await api({action:'getMyRegs',...(memberSafeToken()?{member_token:memberSafeToken()}:{email,phone}),_t:Date.now(),__timeout:12000}); if(!Array.isArray(rows)) return null; return rows.find(r=>String(r.sessionId||r.session_id||'')===String(sessionId) && !['不錄取','已取消'].includes(String(r.status||r.reviewStatus||'')) && !['已退費','已退款'].includes(String(r.transferStatus||r.transfer_status||''))) || null; }catch(_e){ return null; } } function openModal(id){const el=$(id);if(el){el.classList.add('show');el.classList.add('open');}} function closeModal(id){const el=$(id);if(el){el.classList.remove('show');el.classList.remove('open');}} function openLine(){ if(!LINE_URL){ toast('此主辦尚未設定 LINE,請聯繫主辦'); return; } window.open(LINE_URL,'_blank','noopener'); } function openCooperationEmail(){ const subject = encodeURIComponent('合作邀請/商業合作洽詢'); const body = encodeURIComponent('您好,我想洽詢活動邀約/異業合作/商業合作,以下為合作需求說明:\n\n聯絡人:\n單位/品牌名稱:\n聯絡電話:\n合作需求:\n預計活動時間/地點:\n'); const gmailUrl = `https://mail.google.com/mail/?view=cm&fs=1&to=ndiangrace@gmail.com&su=${subject}&body=${body}`; const mailtoUrl = `mailto:ndiangrace@gmail.com?subject=${subject}&body=${body}`; const opened = window.open(gmailUrl, '_blank', 'noopener'); if(!opened){ window.location.href = mailtoUrl; } } function esc(v){return String(v??'').replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/"/g,'"').replace(//g,'>');} // ── 頁面切換 ── // ── 統一返回 ── function goBack(){ history.back(); } // ── UI v2:底部五分類導覽 ── async function navToTab(tab){ try{ localStorage.setItem('doing_market_current_tab', tab); localStorage.setItem('doing_market_page_state', JSON.stringify({tab})); }catch(e){} ['botHome','botMember','botSupport'].forEach(id=>{ const el=document.getElementById(id); if(el) el.classList.remove('active'); }); const tabMap={home:'botHome',member:'botMember',support:'botSupport'}; const activeBtn=document.getElementById(tabMap[tab]); if(activeBtn) activeBtn.classList.add('active'); if(tab==='home'){ navTo('home'); return; } if(tab==='member'){ const email=String(ST.memberEmail||memberSafeEmail()||'').trim(); const phone=String(memberSafePhone(email)||(ST.member&&ST.member.phone)||'').trim(); const token=memberSafeToken(); navTo('member'); if(token||(email&&phone)){ memberShowArea('regs'); await safeLoadMemberPage(email,{phone,forceRegs:true,navOptions:{noPush:true}}); }else{ memberShowLogin(); const mailInput=$('memberLookupEmail'); const phoneInput=$('memberLookupPhone'); const err=$('memberLoginErr'); if(mailInput) mailInput.value=email||memberSafeEmail()||''; if(phoneInput) phoneInput.value=phone||''; if(err&&email&&!phone){ err.style.display='block'; err.textContent='請輸入手機號碼完成身分驗證,即可查看原有報名紀錄。'; } } return; } if(tab==='support'){ document.querySelectorAll('.page').forEach(p=>p.classList.remove('active')); const pg=document.getElementById('pageSupport'); if(pg) pg.classList.add('active'); } } // ── 探索頁篩選 ── // ── 近期活動渲染(首頁第四區塊)── function renderRecentSessions(){ const el=document.getElementById('recentSessionList'); if(!el) return; const allSessions = (window.ST && ST.allSessionsData) ? ST.allSessionsData : []; let sessions = allSessions .filter(s=>{ if(s.status==='closed'||s.status==='archived'||s.status==='cancelled'||s.isHidden||s.testMode||s.internal) return false; return true; }); sessions = sortSessionsByUpcoming(sessions); if(!sessions.length){ el.innerHTML='
目前無近期活動
'; return; } el.innerHTML = sessions.map(s=>renderActCard(s,'full')).join(''); } // ── 活動卡片渲染(共用)── function renderActCard(s, mode){ const isFull = mode==='full'; const cls = isFull ? 'act-card full' : 'act-card'; const id = s.id||s.sessionId||''; const name = s.name||s.title||'未命名活動'; const dateStr = s.date||s.startDate||s.eventDate||''; const timeStr = s.startTime||s.time||''; const loc = s.location||s.venue||s.place||''; const fee = s.baseFee!=null ? 'NT$'+Number(s.baseFee).toLocaleString()+(s.hasFeeRange?'起':'') : (s.feeText||s.fee||'依項目計算'); const regType = s.regType||s.type||s.category||''; const cover = s.cover||''; const isOpen = s.regOpen!==false && s.status!=='closed' && s.status!=='cancelled'; const isClosed = !isOpen; // 狀態文字 let statusTxt='報名中', statusCls=''; if(s.status==='closed'||s.regOpen===false){ statusTxt='已截止'; statusCls='closed'; } else if(s.isFull||s.remainSlots===0){ statusTxt='額滿'; statusCls='full-s'; } else if(s.regStartDate && new Date(s.regStartDate)>new Date()){ statusTxt='即將開放'; statusCls='soon'; } else if(s.regType==='合作洽詢'||s.regType==='審核制'){ statusTxt='審核制'; } else if(s.status==='coming'){ statusTxt='即將開始'; statusCls='soon'; } // 分類標籤 class let tagCls='market'; if(regType.includes('寄賣')) tagCls='consign'; else if(regType.includes('體驗')) tagCls='exp'; else if(regType.includes('合作')) tagCls='collab'; // 圖片 const imgHtml = cover ? `${name}` : `
🎪
`; // 報名按鈕 const regBtnTxt = isClosed ? statusTxt : '立即報名'; const regBtnCls = isClosed ? 'act-card-reg disabled' : 'act-card-reg'; const regAction = isClosed ? '' : `onclick="openSessionById('${id}')"`; return `
${imgHtml}
${statusTxt}
${name}
${dateStr?`📅 ${dateStr}${timeStr?' '+timeStr:''}`:''} ${loc?`📍 ${loc}`:''}
${(typeof myRegBadge==='function'?myRegBadge(s):'')} ${(typeof frontBundleBadgeHtml==='function'?frontBundleBadgeHtml(s):'')} ${regType?`${regType}`:''}
${(typeof photoFrameBtnHtml==='function'?photoFrameBtnHtml(s):'')}
`; } // ── 分享(卡片版)── function shareSessionCard(id, name, event){ if(event) event.stopPropagation(); openSharePanel('session', id, name); } // ── openSessionById(開啟單場報名表)── function uiNotice(message){ return new Promise(function(resolve){ var old=document.getElementById('uiNoticeOverlay'); if(old){ try{old.remove();}catch(e){} } var ov=document.createElement('div'); ov.id='uiNoticeOverlay'; ov.style.cssText='position:fixed;inset:0;z-index:100000;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;padding:20px'; var box=document.createElement('div'); box.style.cssText='width:min(100%,420px);background:#fff;border-radius:18px;padding:20px;box-shadow:0 18px 48px rgba(0,0,0,.22)'; var msg=document.createElement('div'); msg.style.cssText='white-space:pre-wrap;margin-bottom:18px;font-weight:800;line-height:1.7;color:#222'; msg.textContent=String(message||''); var ok=document.createElement('button'); ok.type='button'; ok.textContent='知道了'; ok.style.cssText='width:100%;padding:13px;border-radius:12px;border:none;background:#628E87;color:#fff;font-size:17px;font-weight:900;cursor:pointer;font-family:inherit'; var done=function(){ try{ov.remove();}catch(e){} resolve(true); }; ok.onclick=done; ov.onclick=function(e){ if(e.target===ov) done(); }; box.appendChild(msg); box.appendChild(ok); ov.appendChild(box); document.body.appendChild(ov); }); } function showMemberSocialReviewNoticeOnce(){ if(ST._memberSocialReviewNoticeShown) return Promise.resolve(); ST._memberSocialReviewNoticeShown=true; return uiNotice('FB、IG 或官網至少建議填寫一項\n方便主辦審核品牌與商品內容。\n若完全未填寫,審核將不會通過。'); } function openSessionById(id){ if(!id) return; openSession(id); } function navTo(tab, opts={}){ if(typeof removePayCartBar==='function' && tab!=='member') removePayCartBar(); const el=$('page'+tab.charAt(0).toUpperCase()+tab.slice(1)); if(!el) return; document.querySelectorAll('.page').forEach(p=>p.classList.remove('active')); el.classList.add('active'); window.scrollTo(0,0); // 儲存完整狀態(含 portal 分類) try{ const state = { tab }; if(tab==='portal' && ST.currentCatFilter) state.catFilter = ST.currentCatFilter; localStorage.setItem('doing_market_current_tab', tab); localStorage.setItem('doing_market_page_state', JSON.stringify(state)); }catch(e){} // 返回首頁時清除背景和報名表狀態 if(tab==='home'){ const pp=$('pagePortal');if(pp)pp.style.background=''; document.body.style.background='var(--wh)'; try{ localStorage.removeItem('doing_market_page_state'); localStorage.setItem('doing_market_page_state', JSON.stringify({tab:'home'})); }catch(e){} cleanFrontHomeUrl(); } if(!opts.noPush){ if(tab==='home') history.pushState({page:tab}, '', location.pathname + location.search + location.hash); else history.pushState({page:tab}, '', location.href); } } function memberSafeEmail(){ try{return String(localStorage.getItem('doing_market_member_email')||'').trim();}catch(e){return '';} } const MEMBER_TOKEN_KEY='doing_market_member_token'; function handleDoingLineCallback(){ try{ const u=new URL(location.href); const token=String(u.searchParams.get('member_token')||'').trim(); const pending=String(u.searchParams.get('line_pending')||'').trim(); const memberStatus=String(u.searchParams.get('member_status')||'').trim(); const err=String(u.searchParams.get('member_login_error')||u.searchParams.get('login_error')||'').trim(); ['member_token','member_status','member_login_error','login_error','line_pending'].forEach(k=>u.searchParams.delete(k)); if(token){ memberSaveToken(token); sessionStorage.removeItem('doing_line_pending'); history.replaceState({},'',u.pathname+(u.searchParams.toString()?'?'+u.searchParams.toString():'')+u.hash); setTimeout(()=>safeLoadMemberPage('',{navOptions:{noPush:true},forceRegs:memberStatus!=='profile_required'}),40); return; } if(pending){ sessionStorage.setItem('doing_line_pending',pending); history.replaceState({},'',u.pathname+(u.searchParams.toString()?'?'+u.searchParams.toString():'')+u.hash); setTimeout(()=>{ navTo('member',{noPush:true}); memberShowLogin(); const box=$('doingLineFirstBind'), normal=$('memberLookupBtn'), errEl=$('memberLoginErr'); if(box) box.style.display='block'; if(normal) normal.style.display='none'; const em=$('doingLineBindEmail'); if(em && !em.value) em.value=memberSafeEmail(); if(errEl){errEl.style.display='block';errEl.style.color='#555';errEl.textContent='LINE 驗證成功,第一次使用請完成會員綁定。';} },40); return; } if(err){ history.replaceState({},'',u.pathname+(u.searchParams.toString()?'?'+u.searchParams.toString():'')+u.hash); setTimeout(()=>{navTo('member',{noPush:true});memberShowLogin();const e=$('memberLoginErr');if(e){e.style.display='block';e.style.color='#c0392b';e.textContent='LINE 登入失敗:'+err;}},40); } }catch(_e){} } async function completeDoingLineBind(){ const pending=String(sessionStorage.getItem('doing_line_pending')||'').trim(); const email=String(($('doingLineBindEmail')||{}).value||'').trim(); const phone=String(($('doingLineBindPhone')||{}).value||'').trim(); const err=$('memberLoginErr'), btn=$('doingLineBindBtn'); if(!pending){ if(err){err.style.display='block';err.textContent='LINE 綁定已失效,請重新使用 LINE 登入。';} return; } if(!/\S+@\S+\.\S+/.test(email)){ if(err){err.style.display='block';err.textContent='請填寫正確 Email。';} return; } if(!phone){ if(err){err.style.display='block';err.textContent='請填寫手機號碼。';} return; } if(btn){btn.disabled=true;btn.textContent='綁定中…';} try{ const res=await apiPost({action:'completeLineMemberLogin',linePendingToken:pending,email,phone},20000); if(!res||res.error) throw new Error((res&&res.error)||'LINE 綁定失敗'); const token=String(res.token||res.member_token||'').trim(); if(!token) throw new Error('登入憑證未建立'); memberSaveToken(token); memberSaveEmail(email); memberSavePhone(phone,email); ST.member=res.member||{email,phone}; ST.memberEmail=email; ST._verifiedMemberPhone=phone; sessionStorage.removeItem('doing_line_pending'); const box=$('doingLineFirstBind'), normal=$('memberLookupBtn'); if(box) box.style.display='none'; if(normal) normal.style.display='block'; await safeLoadMemberPage(email,{navOptions:{noPush:true},forceRegs:true}); }catch(e){ if(err){err.style.display='block';err.style.color='#c0392b';err.textContent=e.message||'LINE 綁定失敗';} } finally{ if(btn){btn.disabled=false;btn.textContent='完成綁定並登入';} } } window.completeDoingLineBind=completeDoingLineBind; function memberSafeToken(){ try{return String(localStorage.getItem(MEMBER_TOKEN_KEY)||'').trim();}catch(e){return '';} } function memberSaveToken(token){ const value=String(token||'').trim(); try{ if(value) localStorage.setItem(MEMBER_TOKEN_KEY,value); else localStorage.removeItem(MEMBER_TOKEN_KEY); }catch(e){} ST.memberToken=value; } function memberClearToken(){ memberSaveToken(''); } function memberSaveEmail(email){ const next=String(email||'').trim(); try{localStorage.setItem('doing_market_member_email', next); localStorage.removeItem('tb_member');}catch(e){} } function memberSafePhone(email){ const expectedEmail=String(email||(ST&&ST.memberEmail)||memberSafeEmail()||'').trim().toLowerCase(); const memoryPhone=String((ST&&ST.member&&ST.member.phone)||'').trim(); const memoryEmail=String((ST&&ST.member&&ST.member.email)||(ST&&ST.memberEmail)||'').trim().toLowerCase(); if(memoryPhone&&(!expectedEmail||!memoryEmail||memoryEmail===expectedEmail)) return memoryPhone; return ''; } function memberSavePhone(phone,email){ const value=String(phone||'').trim(); const ownerEmail=String(email||(ST&&ST.member&&ST.member.email)||(ST&&ST.memberEmail)||memberSafeEmail()||'').trim().toLowerCase(); ST.member={...(ST.member||{}),email:(ST.member&&ST.member.email)||ownerEmail,phone:value}; } function memberShowLogin(){ const loginArea=$('memberLoginArea'); const memberArea=$('memberArea'); if(loginArea) loginArea.style.display='block'; if(memberArea) memberArea.style.display='none'; } function memberShowArea(mode){ const loginArea=$('memberLoginArea'); const memberArea=$('memberArea'); if(loginArea) loginArea.style.display='none'; if(memberArea) memberArea.style.display='block'; switchMemberMode(mode||'regs'); ['botHome','botMember','botSupport'].forEach(id=>{ const el=document.getElementById(id); if(el) el.classList.remove('active'); }); const btn=document.getElementById('botMember'); if(btn) btn.classList.add('active'); } function switchMemberMode(mode){ const regsDiv = $('myRegsMode'); const profileDiv = $('profileMode'); const tabRegs = $('tabMyRegs'); const tabProfile = $('tabProfile'); const hero = $('memberHeroCard'); const heroIp = $('memberHeroIp'); const editBtn = $('memberHeroEditBtn'); const isRegs = mode === 'regs'; if(regsDiv) regsDiv.style.display = isRegs ? 'block' : 'none'; if(profileDiv) profileDiv.style.display = isRegs ? 'none' : 'block'; if(tabRegs){ tabRegs.classList.toggle('active-record', isRegs); tabRegs.classList.toggle('active-profile', false); } if(tabProfile){ tabProfile.classList.toggle('active-profile', !isRegs); tabProfile.classList.toggle('active-record', false); } if(hero){ hero.classList.toggle('m-hero-record', isRegs); hero.classList.toggle('m-hero-profile', !isRegs); } if(editBtn){ editBtn.classList.toggle('m-btn-edit-green', isRegs); editBtn.classList.toggle('m-btn-edit-purple', !isRegs); } if(heroIp){ heroIp.src = isRegs ? 'IP會員.png' : 'IP資料.png'; } if(!isRegs) renderMemberProfile(); } function memberVal(m, ...keys){ for(const k of keys){ const v=m && m[k]; if(v!==undefined && v!==null && String(v).trim()!=='') return String(v); } return ''; } function memberDisplayValue(m, keys, fallback='—'){ const val = Array.isArray(keys) ? memberVal(m, ...keys) : memberVal(m, keys); const str = String(val || '').trim(); return str || fallback; } function memberProfileSocialHtml(type, url, label){ const clean = String(url || '').trim(); if(!clean) return ''; const icon = type === 'fb' ? 'f' : type === 'ig' ? '◎' : '🔗'; return `${icon}`; } function memberProfileRow(icon, label, value){ return `
${icon}
${escHtml(label)}
${value}
`; } function renderMemberProfile(){ const m = ST.member || {}; const email = String(m.email || ST.memberEmail || memberSafeEmail() || '').trim(); const el = $('memberProfileContent'); if(!el) return; const name = memberDisplayValue(m, ['name','displayName'], '—'); const phone = memberDisplayValue(m, ['phone'], '—'); const brand = memberDisplayValue(m, ['brand','brandName','brand_name'], '—'); const fb = String(memberVal(m,'fb','fb_url') || '').trim(); const ig = String(memberVal(m,'ig','ig_url') || '').trim(); const web = String(memberVal(m,'collabUrl','collab_url','website','web') || '').trim(); const socialHtml = [ memberProfileSocialHtml('fb', fb, 'Facebook'), memberProfileSocialHtml('ig', ig, 'Instagram'), memberProfileSocialHtml('web', web, '官網') ].filter(Boolean).join('') || '尚未填寫'; const invoiceType = memberDisplayValue(m, ['invoiceType','invoice_type'], '電子發票'); const taxId = memberDisplayValue(m, ['taxId','tax_id'], '—'); const company = memberDisplayValue(m, ['company','invoiceTitle','invoice_title'], brand !== '—' ? brand : '—'); const carrier = memberDisplayValue(m, ['invoiceCarrier','invoice_carrier'], '—'); const invoiceEmail = memberDisplayValue(m, ['invoiceEmail','invoice_email'], email || '—'); const hasName = !!memberVal(m,'name','displayName'); const hasBrand = !!memberVal(m,'brand','brandName','brand_name'); const hasSocial = !!(fb || ig || web); const warn = (!hasName || !hasBrand || !hasSocial) ? `
請補齊會員資料:${!hasName?'姓名 ':''}${!hasBrand?'品牌名稱 ':''}${!hasSocial?'FB、IG 或官網 ':''}
補齊並儲存後,報名表會自動帶入資料。
` : ''; el.innerHTML = `
${warn}
👤

個人基本資料

🦋
${memberProfileRow('♡','姓名', escHtml(name))} ${memberProfileRow('✉','Email', escHtml(email || '—'))} ${memberProfileRow('☎','手機', escHtml(phone))} ${memberProfileRow('◇','品牌名稱', escHtml(brand))} ${memberProfileRow('◎','社群 / 官網', `
${socialHtml}
`)}
會員資料
🧾

發票資訊

${memberProfileRow('▣','發票類型', escHtml(invoiceType))} ${memberProfileRow('▤','統編', escHtml(taxId))} ${memberProfileRow('▥','抬頭', escHtml(company))} ${memberProfileRow('✉','載具 / Email', escHtml(carrier !== '—' ? carrier : invoiceEmail))}
發票資訊

會員資料只需填寫一次,
下次報名將自動帶入,省時又方便!

資料如有異動,請記得更新唷~

`; } function syncMemberIntoCurrentRegForm(member){ const m=member||ST.member||{}; const setText=(id,val)=>{const el=$(id); if(el) el.textContent=val||'';}; const setVal=(id,val)=>{const el=$(id); if(el) el.value=val||'';}; const brand=memberVal(m,'brand','brandName','brand_name'); const name=memberVal(m,'name','displayName'); const phone=memberVal(m,'phone'); const fb=memberVal(m,'fb','fb_url'); const ig=memberVal(m,'ig','ig_url'); setText('disp_brand',brand); setText('disp_name',name); setText('disp_phone',phone); setVal('f_brand',brand); setVal('f_name',name); setVal('f_phone',phone); setVal('f_fb',fb); setVal('f_ig',ig); setVal('f_taxId',memberVal(m,'taxId','tax_id')); setVal('f_company',memberVal(m,'company','invoiceTitle','invoice_title')); setVal('f_invoiceTitle',memberVal(m,'company','invoiceTitle','invoice_title')); setVal('f_brandIntro',memberVal(m,'brandIntro','brand_intro','brandDesc')); setVal('f_sellItem',memberVal(m,'sellItem','sell_items','sell_category','sellCat')); setVal('f_invoiceCarrier',memberVal(m,'invoiceCarrier','invoice_carrier')); setVal('f_invoiceEmail',memberVal(m,'invoiceEmail','invoice_email') || memberVal(m,'email') || ST.memberEmail || ''); } function preFillMemberFields(){ try{ const m=ST.member||{}; syncMemberIntoCurrentRegForm(m); const email=(m.email||ST.memberEmail||memberSafeEmail()||'').trim(); const fEmail=$('f_email'); if(fEmail)fEmail.value=email; const dispEmail=$('disp_email'); if(dispEmail)dispEmail.textContent=email; }catch(e){ console.warn('[preFillMemberFields]',e); } } function fillMemberCard(member){ const m=member||{}; const email=String(m.email||ST.memberEmail||memberSafeEmail()||'').trim(); const name=(m.name||m.realName||email||'會員'); const brand=(m.brand||m.brandName||m.brand_name||''); const phone=(m.phone||''); const fb=(m.fb||m.fb_url||''); const ig=(m.ig||m.ig_url||''); // 更新「我的紀錄」頁的顯示 const nameEl=$('memberName'); if(nameEl) nameEl.textContent=name; const emailEl=$('memberEmail2'); if(emailEl) emailEl.textContent=email; const brandEl=$('memberBrandShow'); if(brandEl) brandEl.textContent=brand?('品牌:'+brand):''; // 同步更新報名表的顯示欄位 const dispBrand=$('disp_brand'); if(dispBrand) dispBrand.textContent=brand; const dispName=$('disp_name'); if(dispName) dispName.textContent=name; const dispPhone=$('disp_phone'); if(dispPhone) dispPhone.textContent=phone; // 同步更新報名表的隱藏欄位(送出時使用) const fBrand=$('f_brand'); if(fBrand) fBrand.value=brand; const fName=$('f_name'); if(fName) fName.value=name; const fPhone=$('f_phone'); if(fPhone) fPhone.value=phone; const fFb=$('f_fb'); if(fFb) fFb.value=fb; const fIg=$('f_ig'); if(fIg) fIg.value=ig; } // ── 會員 email 唯一來源 ── // 過去各處自行拼 ST.memberEmail / ST.member.email / localStorage,順序不一, // 只要其中一個是空的就會誤判成「未登入」(曾造成取消報名後名單整個空白)。 // 一律改用本函式取值,不要再自行組合。 function getMemberEmail(){ const a = (ST && ST.memberEmail) ? String(ST.memberEmail).trim() : ''; if(a) return a; const b = (ST && ST.member && ST.member.email) ? String(ST.member.email).trim() : ''; if(b) return b; try{ return String(localStorage.getItem('doing_market_member_email')||'').trim(); }catch(e){ return ''; } } // ── 會員手機唯一來源 ── // 過去手機同時來自「呼叫端參數 / localStorage / ST.member」,呼叫端各自拼。 // 取消報名、退款申請、付款回報後重新載入名單時漏傳手機 → 後端 hGetMyRegs 直接擋下, // 回「請提供 Email 與手機,才能查詢我的紀錄」,被誤會成報名紀錄不見了。 // 一律改用本函式取值,不要再自行組合。 function getMemberPhone(){ const a = (ST && ST.member && ST.member.phone) ? String(ST.member.phone).trim() : ''; if(a) return a; return memberSafePhone(); } async function safeGetMyRegs(email, phone){ const el=$('myRegsList'); const helper=$('memberRecordHelper'); const token=memberSafeToken(); // 呼叫端沒帶手機就自動補;補不到才顯示「請補上手機」,不讓後端回誤導紅字。 const ph = String(phone || getMemberPhone() || '').trim(); if(helper) helper.classList.remove('show'); if(!token&&!ph){ ST.regs=[]; if(el) el.innerHTML='
請補上手機號碼查詢報名紀錄。
系統需要 Email+手機比對,避免資料被其他人看到。
'; if(helper) helper.classList.add('show'); return []; } if(el) el.innerHTML='
報名紀錄載入中…
'; try{ const regs=token ? await api({action:'getMyRegs',memberToken:token,_t:Date.now()}) : await api({action:'getMyRegs',email,phone:ph,_t:Date.now()}); if(regs && regs.error){ if(token&&String(regs.error).includes('登入憑證已失效')) memberClearToken(); ST.regs=[]; if(el){ el.innerHTML=`
${escHtml(regs.error)}
請確認 Email 與手機是否與報名時一致。
`; } if(helper) helper.classList.add('show'); return []; } const list=Array.isArray(regs)?regs:[]; ST.regs=list; try{ await loadMemberNotifications(); }catch(_e){} renderMyRegs(list); try{ if(Array.isArray(ST.sessions) && ST.sessions.length){ ['renderRecentSessions','renderSessions','renderAllSessions','renderHomeFallbackSessions'].forEach(fn=>{ try{ if(typeof window[fn]==='function') window[fn](); }catch(_e){} }); } }catch(_e){} return list; }catch(e){ console.warn('[getMyRegs failed]',e); if(el) el.innerHTML='
報名紀錄暫時載入失敗,請稍後再試。
資料沒有消失,請截圖給官方 LINE。
'; if(helper) helper.classList.add('show'); return []; } } async function safeLoadMemberPage(email, opts={}){ navTo('member', opts.navOptions||{}); email=String(email||ST.memberEmail||memberSafeEmail()||'').trim(); let lookupPhone=String((opts&&opts.phone)||memberSafePhone(email)||(ST.member&&ST.member.phone)||'').trim(); const savedToken=memberSafeToken(); if(savedToken){ memberShowArea('regs'); try{ const restored=await api({action:'memberSession',memberToken:savedToken,_t:Date.now()}); if(restored&&restored.member&&!restored.error){ ST.member={...(restored.member||{})}; ST.memberEmail=String(ST.member.email||'').trim(); ST._verifiedMemberPhone=String(ST.member.phone||'').trim(); memberSaveEmail(ST.memberEmail); memberSavePhone(ST._verifiedMemberPhone,ST.memberEmail); if($('loginEmail')) $('loginEmail').value=ST.memberEmail; if($('memberLookupEmail')) $('memberLookupEmail').value=ST.memberEmail; if($('memberLookupPhone')) $('memberLookupPhone').value=ST._verifiedMemberPhone; fillMemberCard(ST.member); const hasName=!!String(ST.member.name||ST.member.displayName||'').trim(); const hasBrand=!!String(ST.member.brand||ST.member.brand_name||'').trim(); ST._profileIncomplete=!hasName||!hasBrand; const list=await safeGetMyRegs(ST.memberEmail,ST._verifiedMemberPhone); if(ST._profileIncomplete && !opts.forceRegs){switchMemberMode('profile');toast('請先補齊會員資料');} else if(opts.forceRegs) switchMemberMode('regs'); return list; } const invalidToken=restored&&/登入|憑證|失效|LINE/.test(String(restored.error||'')); if(invalidToken){ memberClearToken();ST.member=null;memberShowLogin(); const err=$('memberLoginErr');if(err){err.style.display='block';err.textContent='登入已失效,請重新使用 LINE 登入。';} }else{ const listEl=$('myRegsList');if(listEl) listEl.innerHTML='
登入狀態暫時無法確認,請檢查網路後再試一次。
憑證仍已保留,不需要重新登入。
'; } return []; }catch(e){ console.warn('[platform member restore failed]',e); const listEl=$('myRegsList');if(listEl) listEl.innerHTML='
登入狀態暫時無法確認,請檢查網路後再試一次。
憑證仍已保留,不需要重新登入。
'; return []; } } if(!email){ memberShowLogin(); const el=$('myRegsList'); if(el) el.innerHTML=''; return; } if(!/\S+@\S+\.\S+/.test(email)){ toast('請輸入有效 Email'); memberShowLogin(); return; } ST.memberEmail=email; memberSaveEmail(email); if($('loginEmail')) $('loginEmail').value=email; if($('memberLookupEmail')) $('memberLookupEmail').value=email; if($('memberLookupPhone')) $('memberLookupPhone').value=lookupPhone||memberSafePhone(email)||''; // 記得 Email 但本次瀏覽尚未完成手機驗證時,回到登入框;不可顯示成空白紀錄。 if(!lookupPhone){ memberShowLogin(); const err=$('memberLoginErr'); if(err){ err.style.display='block'; err.textContent='請輸入手機號碼完成身分驗證,即可查看原有報名紀錄。'; } return []; } memberShowArea('regs'); fillMemberCard(ST.member && (ST.member.email||ST.memberEmail) ? ST.member : {email}); // 會員資料與報名紀錄並行讀取,避免頁面切回「我的紀錄」時再次串行等待。 const regsPromise=safeGetMyRegs(email,lookupPhone); try{ const member=await api({action:'getMember',email,phone:lookupPhone,_t:Date.now()}); if(member && !member.error){ ST.member={...(ST.member||{}),...member,email:member.email||email}; lookupPhone=String(lookupPhone||member.phone||'').trim(); if(lookupPhone) memberSavePhone(lookupPhone,email); if($('memberLookupPhone')) $('memberLookupPhone').value=lookupPhone; fillMemberCard(ST.member); const m = ST.member; const hasName = !!String(m.name||m.displayName||'').trim(); const hasBrand = !!String(m.brand||m.brand_name||'').trim(); const hasSocial = !!String(m.fb||m.fb_url||m.ig||m.ig_url||m.collabUrl||m.collab_url||m.website||m.web||'').trim(); ST._profileIncomplete = !hasName || !hasBrand; if(ST._profileIncomplete && !opts.forceRegs){ const missing=[]; if(!hasName) missing.push('姓名'); if(!hasBrand) missing.push('品牌名稱'); switchMemberMode('profile'); toast('請先補齊會員資料:' + missing.join('、')); } }else{ if(!ST.member) ST.member={email,phone:lookupPhone}; fillMemberCard(ST.member); } }catch(e){ console.warn('[getMember failed]',e); if(!ST.member) ST.member={email,phone:lookupPhone}; fillMemberCard(ST.member); toast('會員資料暫時載入失敗,報名紀錄會繼續載入'); } // 手機為空的情況由 safeGetMyRegs 統一處理(同一提示只留一份)。 const list=await regsPromise; if(opts.forceRegs) switchMemberMode('regs'); return list; } async function memberLogin(){ const btn=$('loginBtn'); const email=String(($('loginEmail')?.value||ST.memberEmail||memberSafeEmail()||'')).trim(); if(btn){btn.disabled=true;btn.textContent='查詢中…';} try{ await safeLoadMemberPage(email,{navOptions:{noPush:true}}); } finally{ if(btn){btn.disabled=false;btn.textContent='登入 / 查詢報名';} } } async function memberEmailPhoneLogin(){ const btn=$('memberLookupBtn'),err=$('memberLoginErr'); if(err){err.style.display='none';err.textContent='';} if(btn){btn.disabled=true;btn.textContent='前往 LINE…';} try{ const u=new URL(API_BASE+'/auth/line/start'); u.searchParams.set('tenant',TENANT_ID); u.searchParams.set('mode','member'); u.searchParams.set('return_url',location.href); location.href=u.toString(); }catch(e){ if(err){err.style.display='block';err.textContent=e.message||'LINE 登入啟動失敗';} if(btn){btn.disabled=false;btn.textContent='使用 LINE 登入';} } } // 查無報名紀錄時的說明:避免使用者以為要重填一次資料而卡在原地 function showNoRecordHelp(email, phone){ const err=$('memberLoginErr'); if(!err) return; err.style.display='block'; err.innerHTML='查無符合的報名紀錄。
' +'請確認 Email 與手機和報名當時填寫的一樣('+esc(String(email||''))+'/'+esc(String(phone||''))+')。
' +'如果您還沒有報名過,請回到「報名」分頁選擇場次報名;這裡只查詢已完成的報名。'; } function goMemberPage(){ return safeLoadMemberPage(ST.memberEmail||memberSafeEmail()); } window.safeLoadMemberPage=safeLoadMemberPage; window.memberLogin=memberLogin; window.goMemberPage=goMemberPage; function isMemberPageActive(){ const p=$('pageMember'); return !!(p && p.classList.contains('active')); } async function syncMemberRecordsFromDb(reason){ const email=String(ST.memberEmail||memberSafeEmail()||'').trim(); if(!email || !isMemberPageActive()) return []; console.log('[AA9 sync myRegs]', reason||'manual'); return safeGetMyRegs(email); } function scheduleMemberRecordsSync(reason, delay=120){ clearTimeout(ST.__memberSyncTimer); ST.__memberSyncTimer=setTimeout(()=>syncMemberRecordsFromDb(reason).catch(e=>console.warn('[member sync failed]',e)), delay); } window.syncMemberRecordsFromDb=syncMemberRecordsFromDb; document.addEventListener('visibilitychange',()=>{ if(!document.hidden) scheduleMemberRecordsSync('visibility'); }); window.addEventListener('focus',()=>scheduleMemberRecordsSync('focus',250)); window.addEventListener('pageshow',()=>scheduleMemberRecordsSync('pageshow',250)); // ── 首頁載入 ── function renderAnnouncements(list){ const el=$('announcementBar'); if(!el) return; if(!list.length){ el.style.display='none'; return; } el.style.display='block'; el.innerHTML = list.map(a=>`
${a.title||''}
${a.content||''}
${a.url?`${a.urlText||'查看詳情'} →`:''}
`).join(''); } // 主題底下的開放場次數(主題是系列,不是單一場次,故不顯示日期時間) function eventSessionCount(eventId){ const all = (ST.allSessionsData||[]).filter(s=>String(s.eventId||'')===String(eventId) && isOpenSession(s)); return all.length; } function isOpenSession(s){ const st=String(s?.status||'').trim(); return !st || st==='報名中' || st==='開放中'; } function mergeSessions(list){ const old=Array.isArray(ST.allSessionsData)?ST.allSessionsData:[]; const combined=[...old, ...(Array.isArray(list)?list:[])].filter(Boolean); ST.allSessionsData=[...new Map(combined.map(x=>[String(x.id),x])).values()]; ST.sessions=ST.allSessionsData.filter(isOpenSession); return ST.sessions; } function sessionStartMs(s){ if(!s) return Number.POSITIVE_INFINITY; let rawDate=''; let rawTime=''; const dates=Array.isArray(s.dates)?s.dates:[]; if(dates.length){ const sorted=dates.slice().sort((a,b)=>String(a.date||'').localeCompare(String(b.date||''))); rawDate=sorted[0]?.date||''; rawTime=sorted[0]?.start||sorted[0]?.startTime||''; } rawDate = rawDate || s.date || s.startDate || s.eventDate || s.sessionDate || ''; rawTime = rawTime || s.startTime || s.time || ''; let date=String(rawDate||'').trim(); if(!date) return Number.POSITIVE_INFINITY; date=date.replace(/[./]/g,'-'); const now=new Date(); if(/^\d{1,2}-\d{1,2}$/.test(date)) date=now.getFullYear()+'-'+date; const m=date.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/); if(!m) { const t=Date.parse(date); return Number.isFinite(t)?t:Number.POSITIVE_INFINITY; } const y=m[1], mo=String(m[2]).padStart(2,'0'), da=String(m[3]).padStart(2,'0'); let time=String(rawTime||'').trim(); const tm=time.match(/(\d{1,2}):(\d{2})/); time=tm?`${String(tm[1]).padStart(2,'0')}:${tm[2]}`:'00:00'; const t=Date.parse(`${y}-${mo}-${da}T${time}:00`); return Number.isFinite(t)?t:Number.POSITIVE_INFINITY; } function sortSessionsByUpcoming(list){ const today=new Date(); today.setHours(0,0,0,0); const base=today.getTime(); return (Array.isArray(list)?list:[]).slice().sort((a,b)=>{ const am=sessionStartMs(a), bm=sessionStartMs(b); const af=am>=base?0:1, bf=bm>=base?0:1; if(af!==bf) return af-bf; if(af===0) return am-bm; return bm-am; }); } // ── 場次列表/日曆模式(只使用場次正式日期,不自行推算)── function activityViewState(){ if(!ST.activityViewData) ST.activityViewData={home:[],portal:[]}; if(!ST.activityCalendarMonth) ST.activityCalendarMonth={home:'',portal:''}; if(!ST.activityCalendarSelected) ST.activityCalendarSelected={home:'',portal:''}; return ST.activityViewData; } function normalizeActivityDate(value){ const raw=String(value||'').trim().replace(/[./]/g,'-'); const m=raw.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/); if(!m) return ''; const mo=String(Number(m[2])).padStart(2,'0'); const da=String(Number(m[3])).padStart(2,'0'); const d=new Date(Number(m[1]),Number(m[2])-1,Number(m[3])); if(d.getFullYear()!==Number(m[1])||d.getMonth()!==Number(m[2])-1||d.getDate()!==Number(m[3])) return ''; return `${m[1]}-${mo}-${da}`; } function sessionActivityDateRows(s){ let rows=Array.isArray(s&&s.dates)?s.dates:null; if(!rows){ const raw=s&&(s.dates_json!=null?s.dates_json:s.datesJson); if(Array.isArray(raw)) rows=raw; else if(typeof raw==='string'&&raw.trim()){ try{ const parsed=JSON.parse(raw); if(Array.isArray(parsed)) rows=parsed; }catch(e){} } } if(!Array.isArray(rows)||!rows.length){ const date=normalizeActivityDate(s&&(s.date||s.startDate||s.eventDate||s.sessionDate)); return date?[{date,time:String((s&&(s.startTime||s.time))||'').trim()}]:[]; } const found=new Map(); rows.forEach(row=>{ const obj=(row&&typeof row==='object')?row:{date:row}; const date=normalizeActivityDate(obj.date||obj.startDate||obj.day); if(!date) return; const time=String(obj.start||obj.startTime||obj.time||s.startTime||s.time||'').trim(); if(!found.has(date)) found.set(date,{date,time}); }); return [...found.values()].sort((a,b)=>a.date.localeCompare(b.date)||a.time.localeCompare(b.time)); } function activityCalendarEntries(sessions){ const entries=[]; const seen=new Set(); (Array.isArray(sessions)?sessions:[]).forEach(s=>{ sessionActivityDateRows(s).forEach(row=>{ const key=String(s.id||s.name||'')+'|'+row.date; if(seen.has(key)) return; seen.add(key); entries.push({date:row.date,time:row.time||'',session:s}); }); }); return entries.sort((a,b)=>a.date.localeCompare(b.date)||a.time.localeCompare(b.time)||String(a.session.name||'').localeCompare(String(b.session.name||''),'zh-Hant')); } function currentLocalDateKey(){ const d=new Date(); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; } function activityMonthKeyFromDate(date){ return String(date||'').slice(0,7); } function activityScopeElements(scope){ return scope==='portal' ? {host:$('portalSessionList'),bar:$('portalActivityViewBar')} : {host:$('eventsList'),bar:$('homeActivityViewBar')}; } function preferredActivityView(){ try{ return localStorage.getItem('2bl_activity_view')==='calendar'?'calendar':'list'; }catch(e){ return 'list'; } } function setActivitySessions(scope,sessions){ const deduped=[...new Map((Array.isArray(sessions)?sessions:[]).filter(Boolean).map((s,i)=>[String(s.id||s.sessionId||s.name||i),s])).values()]; activityViewState()[scope]=sortSessionsByUpcoming(deduped); renderActivitySessions(scope); } function switchActivityView(scope,mode){ const next=mode==='calendar'?'calendar':'list'; try{ localStorage.setItem('2bl_activity_view',next); }catch(e){} renderActivitySessions(scope,next); } function moveActivityCalendarMonth(scope,delta){ activityViewState(); const key=ST.activityCalendarMonth[scope]||activityMonthKeyFromDate(currentLocalDateKey()); const m=key.match(/^(\d{4})-(\d{2})$/); const d=m?new Date(Number(m[1]),Number(m[2])-1+Number(delta||0),1):new Date(); ST.activityCalendarMonth[scope]=`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`; ST.activityCalendarSelected[scope]=''; renderActivitySessions(scope,'calendar'); } function goActivityCalendarToday(scope){ activityViewState(); const today=currentLocalDateKey(); ST.activityCalendarMonth[scope]=activityMonthKeyFromDate(today); ST.activityCalendarSelected[scope]=today; renderActivitySessions(scope,'calendar'); } function selectActivityCalendarDate(scope,date){ activityViewState(); ST.activityCalendarMonth[scope]=activityMonthKeyFromDate(date); ST.activityCalendarSelected[scope]=date; renderActivitySessions(scope,'calendar'); } function activityCalendarEventHtml(entry,detail){ const s=entry.session||{}; const theme=(typeof getFrontSessionThemeInfo==='function'?getFrontSessionThemeInfo(s):{cls:'theme-none'}); const time=String(entry.time||'').match(/\d{1,2}:\d{2}/)?.[0]||''; if(detail){ const location=s.location||s.venue||s.place||''; return ``; } return ``; } function renderActivityCalendar(scope,sessions){ activityViewState(); const entries=activityCalendarEntries(sessions); const today=currentLocalDateKey(); let monthKey=ST.activityCalendarMonth[scope]; if(!/^\d{4}-\d{2}$/.test(monthKey||'')){ const firstUpcoming=entries.find(x=>x.date>=today)||entries[0]; monthKey=activityMonthKeyFromDate(firstUpcoming?firstUpcoming.date:today); ST.activityCalendarMonth[scope]=monthKey; } const [year,month]=monthKey.split('-').map(Number); const first=new Date(year,month-1,1); const start=new Date(year,month-1,1-first.getDay()); const byDate=new Map(); entries.forEach(entry=>{ if(!byDate.has(entry.date)) byDate.set(entry.date,[]); byDate.get(entry.date).push(entry); }); let selected=ST.activityCalendarSelected[scope]; if(!selected||activityMonthKeyFromDate(selected)!==monthKey){ selected=(byDate.has(today)&&activityMonthKeyFromDate(today)===monthKey)?today:((entries.find(x=>activityMonthKeyFromDate(x.date)===monthKey)||{}).date||''); ST.activityCalendarSelected[scope]=selected; } const weekdays=['日','一','二','三','四','五','六'].map(x=>`
週${x}
`).join(''); let days=''; for(let i=0;i<42;i++){ const d=new Date(start.getFullYear(),start.getMonth(),start.getDate()+i); const key=`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; const dayEntries=byDate.get(key)||[]; const cls=['activity-calendar-day',d.getMonth()!==month-1?'is-other':'',key===today?'is-today':'',key===selected?'is-selected':''].filter(Boolean).join(' '); days+=`
${d.getDate()}${dayEntries.length?`${dayEntries.length}場`:''}
${dayEntries.map(x=>activityCalendarEventHtml(x,false)).join('')}
`; } const selectedEntries=selected?(byDate.get(selected)||[]):[]; const detail=selected?`
${selected.replace(/-/g,'/')} · ${selectedEntries.length}場活動
${selectedEntries.length?selectedEntries.map(x=>activityCalendarEventHtml(x,true)).join(''):'
當天沒有開放報名的活動
'}
`:''; return `
${weekdays}${days}
${detail}`; } function renderActivitySessions(scope,forcedMode){ const data=activityViewState()[scope]||[]; const mode=forcedMode||preferredActivityView(); const els=activityScopeElements(scope); if(!els.host||!els.bar) return; els.host.classList.add('activity-list-host'); els.host.classList.toggle('calendar-mode',mode==='calendar'); const calendarHtml=mode==='calendar'?renderActivityCalendar(scope,data):''; const monthKey=ST.activityCalendarMonth[scope]||activityMonthKeyFromDate(currentLocalDateKey()); const monthText=monthKey.replace('-', '年')+'月'; els.bar.innerHTML=`
${mode==='calendar'?`
${monthText}
`:''}`; if(mode==='calendar'){ els.host.innerHTML=calendarHtml; }else{ els.host.innerHTML=data.length?data.map(s=>renderSessionCard(s)).join(''):'
目前尚無開放報名
'; } } window.switchActivityView=switchActivityView; window.moveActivityCalendarMonth=moveActivityCalendarMonth; window.goActivityCalendarToday=goActivityCalendarToday; window.selectActivityCalendarDate=selectActivityCalendarDate; function renderHomeFallbackSessions(){ const el=$('eventsList'); if(!el) return; const open=sortSessionsByUpcoming((ST.allSessionsData||[]).filter(isOpenSession)); setActivitySessions('home',open); } async function filterCat(type, el){ ST.currentCatFilter = type; document.querySelectorAll('.cat-btn').forEach(b=>b.classList.remove('active')); if(el) el.classList.add('active'); const title = $('portalTitle'); if(title) title.textContent = type; const list = $('portalSessionList'); if(list) list.innerHTML = '
載入中…
'; const _catBgMap={'市集報名':'#EEF5F0','體驗活動':'#EEF6FA','通路寄賣':'#F5F4E6','合作洽詢':'#F7F4E8','團購':'#EDF5F3'}; const _catBg=_catBgMap[type]||''; const _portalPageEl=$('pagePortal'); if(_portalPageEl) _portalPageEl.style.background=_catBg; document.body.style.background=_catBg||'var(--wh)'; navTo('portal'); window.scrollTo(0,0); let data=[]; try{ const sessions = await api({action:'getSessions', portal:type, _t:Date.now()}); data = Array.isArray(sessions) ? sessions.filter(isOpenSession) : []; mergeSessions(data); }catch(e){ console.error('filterCat api error', e); } if(!data.length){ data=(ST.allSessionsData||[]).filter(s=>{ const ps=Array.isArray(s.portals)?s.portals:String(s.portals||'').split(',').map(x=>x.trim()).filter(Boolean); return isOpenSession(s) && ps.includes(type); }); } if(!list) return; data=sortSessionsByUpcoming(data); setActivitySessions('portal',data); } async function openEvent(eventId){ const evt=(ST.events||[]).find(e=>String(e.id)===String(eventId)); ST.currentEvent=evt||null; const title=$('portalTitle'); if(title) title.textContent=evt?evt.title:'場次列表'; const list=$('portalSessionList'); if(list) list.innerHTML='
載入中…
'; const _evtBgMap={'耶市集':'#E6F1ED','幻日祭':'#EBE2E5'}; const _evtBg=evt?Object.entries(_evtBgMap).find(([k])=>(evt.title||'').includes(k)):null; const _bg=_evtBg?_evtBg[1]:''; const _portalPage=$('pagePortal'); if(_portalPage) _portalPage.style.background=_bg; document.body.style.background=_bg||'var(--wh)'; navTo('portal'); let sessions=[]; try{ const res=await api({action:'getSessions',eventId,_t:Date.now()}); sessions=Array.isArray(res)?res.filter(isOpenSession):[]; mergeSessions(sessions); }catch(e){ console.error('openEvent error',e); } if(!sessions.length){ sessions=(ST.allSessionsData||[]).filter(s=>isOpenSession(s) && String(s.eventId||'')===String(eventId)); } ST.sessions=sortSessionsByUpcoming(sessions); sessions=ST.sessions; if(!list) return; setActivitySessions('portal',sessions); } function myRegForSession(s){ if(!ST.memberEmail || !Array.isArray(ST.regs) || !ST.regs.length) return null; const sid=String(s && s.id || ''); const sname=String(s && s.name || ''); const list=ST.regs.filter(r=>{ const rid=String(r.sessionId || r.session_id || ''); const rname=String(r.sessionName || r.sesName || ''); return (sid && rid===sid) || (sname && rname===sname); }); if(!list.length) return null; // 優先顯示仍有效的報名;若全都取消/退費,就顯示最新那筆(讓你知道曾報過、可重報) const active=list.filter(r=> !(typeof isCancelledReg==='function' && isCancelledReg(r))); return active.length ? active[active.length-1] : list[list.length-1]; } function myRegBadge(s){ const r=myRegForSession(s); if(!r) return ''; const p=mReviewPill(r); const t=String(p.text||''); let label; if(t==='已取消') label='已取消·可重報'; else if(t==='未錄取') label='未錄取'; else label='你已報名·'+t; return `${p.icon?p.icon+' ':''}${esc(label)}`; } /* 20260705 前台主題色:同一主題活動固定同色,不用場次名稱判斷 */ function getFrontSessionThemeInfo(s){ try{ const eventId=String((s&&s.eventId) || '').trim(); const evt=(ST.events||[]).find(e=>String(e.id)===eventId); const rawTitle=String((evt&&(evt.title||evt.name)) || (s&&(s.eventTitle||s.event_title||s.themeTitle||s.theme_title)) || '').trim(); const title=rawTitle || (eventId ? '主題活動' : '無主題'); let cls='theme-none'; if(rawTitle.indexOf('幻日祭')>-1) cls='theme-huanri'; else if(rawTitle.indexOf('耶市集')>-1) cls='theme-yes'; else if(rawTitle.indexOf('兔彼樂的市集小旅行')>-1) cls='theme-travel'; else if(rawTitle.indexOf('翻轉市集')>-1) cls='theme-flip'; else if(!eventId) cls='theme-none'; else cls='theme-none'; return {cls,title}; }catch(_e){ return {cls:'theme-none',title:'無主題'}; } } // ── BUNDLE_ACTIVE_GUARD_20260809 ─────────────────────────────── function frontDateKeyTaipei(){ try{return new Intl.DateTimeFormat('en-CA',{timeZone:'Asia/Taipei',year:'numeric',month:'2-digit',day:'2-digit'}).format(new Date());} catch(_e){return new Date().toISOString().slice(0,10);} } function frontBundleSessionAvailable(bs){ if(!bs)return false; if(bs.available===false)return false; const status=String(bs.status||'').trim(); if(['關閉','已關閉','停用','封存','已封存','取消','已取消'].includes(status))return false; const ds=Array.isArray(bs.dates)?bs.dates:[]; const keys=ds.map(d=>String((d&&typeof d==='object')?(d.date||d.day||''):d).slice(0,10)).filter(Boolean).sort(); if(keys.length&&keys[keys.length-1]{ const list=Array.isArray(b&&b.sessions)?b.sessions:[]; return list.length>=2 && list.some(x=>String((x&&x.id)||'')===sid) && list.every(frontBundleSessionAvailable); }); } function frontBundleBadgeHtml(s){ try{ const sid=String((s&&s.id)||'').trim(); if(!sid) return ''; const bundles=activeBundlesForSession(s); let addCount=0; bundles.forEach(b=>{ const list=Array.isArray(b&&b.sessions)?b.sessions:[]; const hasCurrent=list.some(x=>String((x&&x.id)||'')===sid); if(!hasCurrent) return; const otherCount=list.filter(x=>String((x&&x.id)||'')!==sid).length; if(otherCount>addCount) addCount=otherCount; }); if(addCount<=0) return ''; return `🎫 可加報 ${addCount} 場|組合優惠`; }catch(_e){ return ''; } } function renderSessionCard(s){ const themeInfo=getFrontSessionThemeInfo(s); const themeClass=themeInfo.cls||'theme-none'; const full = Number(s.limit)>0 && Number(s.count)>=Number(s.limit); const scheduleUnavailable = s.registrationOpen===false; const closed = s.status==='關閉' || s.status==='已關閉' || full || scheduleUnavailable; const dates=Array.isArray(s.dates)?s.dates:[]; let dateStr=''; if(dates.length>1){ const fmt=d=>d?String(d).slice(5).replace('-','/'):''; dateStr=fmt(dates[0].date)+'~'+fmt(dates[dates.length-1].date); }else if(dates.length===1){ const d=dates[0]||{}; dateStr=(d.date||'').slice(5).replace('-','/')+(d.start?' '+d.start:'')+(d.end?'~'+d.end:''); }else if(s.date){ dateStr=String(s.date).slice(5).replace('-','/'); } const placeStr=s.venue||s.location||s.place||''; const eventId=String(s.eventId||''); const evt=(ST.events||[]).find(e=>String(e.id)===eventId); const cover=s.cover||evt?.cover||''; const bundleBadge=frontBundleBadgeHtml(s); const myBadge=myRegBadge(s); const coverEl=cover ? `` : ``; const unavailableLabel=full?'名額已滿':(scheduleUnavailable?({upcoming:'尚未開放',paused:'暫停報名',ended:'報名已截止'}[s.registrationState]||'目前未開放'):'已關閉'); const scheduleNote=scheduleUnavailable&&s.registrationMessage?`
⏱ ${esc(s.registrationMessage)}
`:''; const actionHtml=closed ? `${unavailableLabel}` : ``; return `
${coverEl}
${esc(themeInfo.title||'無主題')}
${esc(s.name||'')}
${dateStr?'📅 '+esc(dateStr):''}${placeStr?'
📍 '+esc(placeStr):''}
${scheduleNote} ${myBadge?`
${myBadge}
`:''} ${bundleBadge?`
${bundleBadge}
`:''}
${actionHtml}
`; } function renderAllSessions(){ const el=$('allSessionsList'); if(!el) return; const list=(ST.allSessionsData||[]).filter(isOpenSession); if(!list.length){ el.innerHTML='
目前無開放場次
'; return; } el.innerHTML=list.map(s=>renderSessionCard(s)).join(''); } // ── 會員資料查找步驟(報名前先帶入會員資料)── // 已報過同名場次時,在報名表最上方顯示明顯提醒(不阻擋,但一定看得到) function showSameNameRegWarning(reg){ try{ const dynEl=$('dynForm'); if(!dynEl) return; if(document.getElementById('sameNameRegWarn')) return; const d=document.createElement('div'); d.id='sameNameRegWarn'; d.style.cssText='background:#FAEEC7;border:2px solid #e0c56a;border-radius:14px;padding:14px;margin-bottom:14px;font-size:16px;line-height:1.7;color:#6b4e00'; d.innerHTML='⚠️ 您已經報名過「'+esc(String(reg.sessionName||reg.sesName||''))+'」
' +'報名編號:'+esc(String(reg.id||''))+' 狀態:'+esc(String(reg.status||'')) +'
若只是要查看或繳費,請到「我的紀錄」,不需要再報一次。' +'
'; dynEl.prepend(d); try{ d.scrollIntoView({behavior:'smooth',block:'center'}); }catch(e){} }catch(e){ console.warn('[showSameNameRegWarning]',e); } } function showEmailStep(s){ const dynEl=$('dynForm'); const submitBtn=$('submitBtn'); if(submitBtn) submitBtn.style.display='none'; const feeBox=$('feeBox'); if(feeBox) feeBox.style.display='none'; if(!dynEl) return; dynEl.innerHTML=`
🔍 先查找會員資料
輸入 Email+手機 查找會員資料。找到資料會自動帶入;第一次使用則進入會員資料填寫。
`; const btn=$('memberLookupContinueBtn'); if(btn) btn.addEventListener('click', checkMemberStep); ['step1Email','step1Phone'].forEach(id=>{ const el=$(id); if(el) el.addEventListener('keydown',e=>{ if(e.key==='Enter'){ e.preventDefault(); btn&&btn.click(); } }); }); } // 新會員專用:先把會員資料填完整,再回到報名表繼續選擇內容 // Email 已有會員但手機不一致:直接講清楚怎麼處理,並可當場改手機重試 function showPhoneMismatchStep(email, phone){ ST._phoneMismatchEmail=String(email||'').trim(); const dynEl=$('dynForm'); if(!dynEl) return; const submitBtn=$('submitBtn'); if(submitBtn) submitBtn.style.display='none'; const feeBox=$('feeBox'); if(feeBox) feeBox.style.display='none'; dynEl.innerHTML=`
手機號碼與資料庫不一致
這個 Email(${esc(email)}已經有會員資料
但您輸入的手機 ${esc(phone)} 與當初留的號碼不同,因此無法直接報名。
請擇一處理:
1. 改用當初報名時留的手機號碼再試一次
2. 手機號碼換了 → 請聯繫主辦協助更新,之後就能正常報名
`; try{ dynEl.scrollIntoView({behavior:'smooth',block:'start'}); }catch(e){} } function retryPhoneMismatch(){ const v=String(($('pm_phone')||{}).value||'').trim(); const err=$('pm_err'); if(!v){ if(err) err.textContent='請輸入手機號碼'; return; } const email=String(ST._phoneMismatchEmail||ST.memberEmail||memberSafeEmail()||'').trim(); memberSavePhone(v); showEmailStep(ST.currentSession); const pIn=$('step1Phone'), eIn=$('step1Email'); if(pIn) pIn.value=v; if(eIn) eIn.value=email; setTimeout(()=>{ const btn=$('memberLookupContinueBtn'); if(btn) btn.click(); else checkMemberStep(); },0); } function goSupportFromForm(){ try{ closeRegFormModal({force:true}); }catch(e){} try{ if(typeof navToTab==='function') navToTab('support'); }catch(e){} } function showNewMemberProfileStep(s){ const dynEl=$('dynForm'); const submitBtn=$('submitBtn'); if(submitBtn) submitBtn.style.display='none'; const feeBox=$('feeBox'); if(feeBox) feeBox.style.display='none'; if(!dynEl) return; const V=ST.member||{}; const email=ST.memberEmail||V.email||''; dynEl.innerHTML=`
會員資料
資料會直接儲存到系統,下次可用相同 Email+手機繼續補填。
FB、IG 或官網至少建議填寫一項方便主辦審核品牌與商品內容。
若完全未填寫,審核將不會通過。
資料未完成也可以先存進資料庫,必填資料完整後,才可以進入正式報名表。
`; const btn=$('saveMemberProfileBtn'); if(btn) btn.addEventListener('click',()=>saveMemberProfileToDb({silent:false})); showMemberSocialReviewNoticeOnce(); try{ dynEl.scrollIntoView({behavior:'smooth',block:'start'}); }catch(e){} } const DOING_VENDOR_CATEGORIES=new Set(['餐飲美食','手作設計','文創選物','服飾配件','生活用品','親子兒童','寵物相關','收藏娛樂','美類','美業服務','體驗/服務','其他']); function doingVendorCategory(value){const v=String(value||'').trim();return DOING_VENDOR_CATEGORIES.has(v)?v:'其他';} function doingProfileMemberPayload(profile){return {...(profile||{}),brand:(profile&&profile.brand)||'',brand_name:(profile&&profile.brand_name)||(profile&&profile.brand)||'',brandIntro:(profile&&profile.brandIntro)||'',sellCat:(profile&&profile.sellCat)||'',sellItem:(profile&&profile.sellItem)||'',fb:(profile&&profile.fb)||'',ig:(profile&&profile.ig)||'',photo:(profile&&profile.photo)||'',company:(profile&&profile.company)||'',taxId:(profile&&profile.taxId)||''};} async function saveDoingPlatformMemberProfile(values){ const email=String(values.email||ST.memberEmail||memberSafeEmail()||'').trim(); const authPhone=String(ST._verifiedMemberPhone||ST.member?.phone||memberSafePhone(email)||values.phone||'').trim(); if(!email||!authPhone) return {error:'請先完成 LINE 會員登入'}; const payload={ action:'saveMember',email,authPhone,phone:String(values.phone||authPhone).trim(), name:String(values.name||'').trim(),brand:String(values.brand||'').trim(),brandIntro:String(values.brandIntro||'').trim(), sellItem:String(values.sellItem||values.sellCat||'').trim(),sellCat:doingVendorCategory(values.category||values.sellItem||values.sellCat||''), fb:String(values.fb||'').trim(),ig:String(values.ig||'').trim(),collabUrl:String(values.web||values.collabUrl||'').trim(), photo:String(values.photo||'').trim(),company:String(values.company||'').trim(),taxId:String(values.taxId||'').trim(),lineId:String(values.lineId||'').trim(),city:String(values.city||'').trim() }; const saved=await apiPost(payload,20000); if(saved&&saved.error) return saved; const phone=String(payload.phone||authPhone).trim(); const fresh=await api({action:'getMember',email,phone,_t:Date.now()}); if(fresh&&!fresh.error){ const member=doingProfileMemberPayload(fresh); const complete=!!String(member.name||'').trim()&&!!String(member.brand||member.brand_name||'').trim(); return {success:true,profileComplete:complete,member}; } return {success:true,profileComplete:!!(saved&&saved.profileComplete),member:doingProfileMemberPayload((saved&&saved.member)||values)}; } async function saveMemberProfileToDb({silent=false}={}){ const g=id=>String(($(id)||{}).value||'').trim(); const err=$('np_err'); const status=$('np_save_status'); const btn=$('saveMemberProfileBtn'); const email=g('np_email'), requestedPhone=g('np_phone'); const verifiedPhone=String(ST._verifiedMemberPhone||ST.member?.phone||memberSafePhone()||'').trim(); // 背景儲存不得改登入手機;只有使用者明確儲存並確認後才採用畫面中的新手機。 const phone=(silent&&verifiedPhone)?verifiedPhone:requestedPhone; if(!silent && verifiedPhone && requestedPhone && requestedPhone!==verifiedPhone){ if(!await uiConfirm('你正在修改登入用手機:\n'+verifiedPhone+' → '+requestedPhone+'\n\n修改後,下次登入要使用新手機。確定要修改嗎?')){if(btn){btn.disabled=false;btn.textContent='儲存資料';}return;} } const showError=(msg,focusId)=>{ if(!silent && err) err.textContent=msg; const el=$(focusId); if(!silent && el){ try{ el.scrollIntoView({behavior:'smooth',block:'center'}); el.focus(); }catch(e){} } }; if(!email || !/\S+@\S+\.\S+/.test(email)){ showError('請先填寫正確 Email,才能儲存資料','np_email'); return {success:false}; } if(!phone){ showError('請先填寫手機號碼,才能儲存資料','np_phone'); return {success:false}; } if(!silent && !g('np_fb')&&!g('np_ig')&&!g('np_web')) await showMemberSocialReviewNoticeOnce(); if(err) err.textContent=''; if(!silent && btn){ btn.disabled=true; btn.textContent='儲存中…'; } const authPhone=String(ST._verifiedMemberPhone||ST.member?.phone||memberSafePhone()||phone).trim(); try{ const res=await saveDoingPlatformMemberProfile({ email,phone,name:g('np_name'),brand:g('np_brand'),sellItem:g('np_sellItem'),brandIntro:g('np_brandIntro'), fb:g('np_fb'),ig:g('np_ig'),company:ST.member?.company||'',taxId:ST.member?.taxId||ST.member?.tax_id||'',photo:ST.member?.photo||ST.member?.photo_url||'',lineId:ST.member?.lineId||ST.member?.line_id||'',city:ST.member?.city||'' }); if(!res || res.error){ if(!silent && err) err.textContent=(res&&res.error)||'儲存失敗,請稍後再試'; return {success:false,error:(res&&res.error)||'儲存失敗'}; } const member=res.member||{}; ST.member={...(ST.member||{}),...member}; ST.memberEmail=String(member.email||email).trim(); ST._verifiedMemberPhone=String(member.phone||phone).trim(); memberSaveEmail(ST.memberEmail); memberSavePhone(ST._verifiedMemberPhone); if(res.profileComplete){ if(!silent){ if(status){ status.style.display='block'; status.style.background='#DDEAD1'; status.style.color='#2a4d18'; status.textContent='✅ 會員資料已完整儲存,正在進入報名表'; } toast('會員資料已儲存'); renderDynFormFull(ST.currentSession,false); setTimeout(()=>{ const d=$('dynForm'); if(d){ try{ d.scrollIntoView({behavior:'smooth',block:'start'}); }catch(e){} } },120); } }else if(!silent){ const missing=Array.isArray(res.missingFields)?res.missingFields:[]; if(status){ status.style.display='block'; status.style.background='#FAEEC7'; status.style.color='#6b4e00'; status.textContent='✅ 已儲存到資料庫。尚未完成:'+(missing.join('、')||'請補齊必填資料'); } toast('資料已儲存,可稍後再回來補填'); } return {success:true,profileComplete:!!res.profileComplete,missingFields:res.missingFields||[]}; }catch(e){ console.warn('[saveMemberProfileToDb failed]',e); if(!silent && err) err.textContent='儲存失敗,請檢查網路後再試'; return {success:false,error:e&&e.message?e.message:'儲存失敗'}; }finally{ if(!silent && btn && document.body.contains(btn)){ btn.disabled=false; btn.textContent='儲存'; } } } // 統一處理「下一步」的畫面切換:任何錯誤都要看得到,不能靜靜停在原地 function goRegStep(fn){ try{ if(!ST.currentSession || !ST.currentSession.id){ const st=$('step1Status'); if(st){ st.style.display='block'; st.style.background='#FAE1DD'; st.style.color='#8b1c1c'; st.textContent='場次資料已失效,請關閉本頁後重新點選場次報名。'; } return; } fn(); }catch(e){ console.warn('[goRegStep failed]', e); const st=$('step1Status'); if(st){ st.style.display='block'; st.style.background='#FAE1DD'; st.style.color='#8b1c1c'; st.textContent='畫面載入失敗:'+((e&&e.message)?e.message:'請重新整理後再試一次'); } toast&&toast('畫面載入失敗,請重新整理再試'); } } async function checkMemberStep(){ const emailEl=$('step1Email'); const phoneEl=$('step1Phone'); const errEl=$('step1Err'); const statusEl=$('step1Status'); const email=(emailEl?.value||'').trim(); const phone=(phoneEl?.value||'').trim(); if(!email || !/\S+@\S+\.\S+/.test(email)){ if(errEl) errEl.textContent='請填寫正確 Email'; return; } if(!phone){ if(errEl) errEl.textContent='請填寫手機號碼'; return; } if(errEl) errEl.textContent=''; if(emailEl) emailEl.disabled=true; if(phoneEl) phoneEl.disabled=true; const checkBtn=$('memberLookupContinueBtn'); if(checkBtn){ checkBtn.disabled=true; checkBtn.textContent='查詢中…'; } try{ // 查詢會員資料走 GET;Email 是否存在與手機是否相符走 POST。兩者同時執行,且各自只有一套 handler。 const [member, chk] = await Promise.all([ api({action:'getMember',email,phone,_t:Date.now(),__timeout:12000}), apiPost({action:'checkMemberEmailPhone',email,phone},12000).catch(()=>null) ]); if((member && member.error) || (chk && chk.error)){ throw new Error((member&&member.error)||(chk&&chk.error)||'會員查詢失敗'); } if(chk && chk.exists && !chk.match){ showPhoneMismatchStep(email,phone); return; } ST.memberEmail=email; memberSaveEmail(email); memberSavePhone(phone); ST._verifiedMemberPhone=phone; if(member && !member.error){ ST.member={...member}; ST.memberEmail=String(member.email||email).trim(); ST._verifiedMemberPhone=String(member.phone||phone).trim(); memberSaveEmail(ST.memberEmail); memberSavePhone(ST._verifiedMemberPhone); const fromMembers=String(member.source||'')==='members'; if(member.profileComplete && fromMembers){ if(statusEl){ statusEl.style.display='block'; statusEl.style.background='#DDEAD1'; statusEl.style.color='#2a4d18'; statusEl.textContent='✅ 已找到完整會員資料'; } goRegStep(()=>renderDynFormFull(ST.currentSession,false)); }else{ if(statusEl){ statusEl.style.display='block'; statusEl.style.background='#FAEEC7'; statusEl.style.color='#6b4e00'; statusEl.textContent='會員資料尚未完成,已帶入資料庫內容,請補齊後按「儲存」。'; } goRegStep(()=>showNewMemberProfileStep(ST.currentSession)); } }else{ ST.member={email,phone}; if(statusEl){ statusEl.style.display='block'; statusEl.style.background='#DCD7ED'; statusEl.style.color='#4a3c75'; statusEl.textContent='第一次使用,請填寫會員資料。資料未完整也可以先儲存。'; } goRegStep(()=>showNewMemberProfileStep(ST.currentSession)); } }catch(e){ console.warn('[checkMemberStep failed]',e); if(statusEl){ statusEl.style.display='block'; statusEl.style.background='#FAE1DD'; statusEl.style.color='#8b1c1c'; statusEl.textContent='查詢失敗,請稍後再試。'; } }finally{ if(emailEl) emailEl.disabled=false; if(phoneEl) phoneEl.disabled=false; if(checkBtn && document.body.contains(checkBtn)){ checkBtn.disabled=false; checkBtn.textContent='查詢 / 繼續 →'; } } } // 手機切到 FB/IG/LINE 複製網址時,若會員表正在畫面上,背景儲存目前欄位。 // 正式來源仍是資料庫;返回後不以 localStorage 假資料覆蓋。 let _memberProfileBackgroundSaving=false; async function saveVisibleMemberProfileOnBackground(){ if(_memberProfileBackgroundSaving || !$('np_email') || !$('np_phone')) return; const email=String(($('np_email')||{}).value||'').trim(); // 背景儲存只能沿用已驗證手機,禁止把尚未正式送出的輸入值變成登入手機。 const phone=String(ST._verifiedMemberPhone||ST.member?.phone||memberSafePhone()||'').trim(); if(!email || !/\S+@\S+\.\S+/.test(email) || !phone) return; _memberProfileBackgroundSaving=true; try{ await saveMemberProfileToDb({silent:true}); }catch(e){} finally{ _memberProfileBackgroundSaving=false; } } document.addEventListener('visibilitychange',()=>{ if(document.hidden) saveVisibleMemberProfileOnBackground(); }); window.addEventListener('pagehide',()=>{ saveVisibleMemberProfileOnBackground(); }); window.checkMemberStep=checkMemberStep; window.saveMemberProfileToDb=saveMemberProfileToDb; function onBundleAddonChange(cb){ document.querySelectorAll('.bundleAddonChk').forEach(x=>{ if(x!==cb) x.checked=false; }); ST.bundleAddon = cb.checked ? cb.value : ''; if(typeof updateFee==='function') updateFee(); } function renderDynFormFull(s, isNew=false){ ST.seatChoiceIntent='auto'; const V=ST.member||{}; const email=ST.memberEmail||V.email||''; const vFb=V.fb||V.fb_url||''; const vIg=V.ig||V.ig_url||''; const vWeb=V.website||V.web||''; const vCarrier=V.invoiceCarrier||V.invoice_carrier||''; const vTaxId=V.taxId||V.tax_id||''; const vCompany=V.company||''; const vInvEmail=V.invoiceEmail||V.invoice_email||''; const isCompany=!!vTaxId; ST.equipQty={};ST.addonQty={};ST.stallCount=1; const cover=s.cover||''; buildFormHero(s,cover); // 取得日期字串 const dates=s.dates||[]; let dateStr=''; if(dates.length>1){ const fmt=d=>d?String(d).slice(5).replace('-','/'):''; dateStr=fmt(dates[0].date)+'~'+fmt(dates[dates.length-1].date); } else if(dates.length===1){ const d=dates[0]; dateStr=(d.date||'').slice(5).replace('-','/')+(d.start?' '+d.start:'')+(d.end?'~'+d.end:''); } const sType=s.type||'市集場次'; const isCollab=sType==='合作申請'; const isMkt=sType==='市集場次'||sType==='通路寄賣'; const equipDefs=Object.entries(s.equip||{}).filter(([k,v])=>v&&(v.open===true||v.open==='true')); const addons=(s.addons||[]).map((a,i)=>({...a,_idx:i})).filter(a=>a.open===true||a.open==='true'); let html=''; // ── 已報名狀態提示(非同步查詢,不阻塞報名表渲染)── const _email = getMemberEmail(); if (_email && s && s.id) { api({ action:'getMyRegs', email:_email, phone:memberSafePhone()||'', _t:Date.now() }) .then(regs => { const list = Array.isArray(regs) ? regs : []; const existing = list.find(r => String(r.sessionId||r.session_id||'') === String(s.id)); const banner = document.getElementById('regStatusBanner'); if (!banner) return; if (!existing) { banner.style.display='none'; return; } const status = existing.status || existing.reviewStatus || ''; const payStatus = existing.payStatus || existing.payment_status || ''; const transfer = existing.transferStatus || existing.transfer_status || ''; let msg='', bg='', icon=''; if (transfer==='已退費'||transfer==='已退款') { msg=`你曾報名此場次,但已退費,可重新報名。`; bg='#FAEEC7'; icon='⚠️'; } else if (status==='已取消'||status==='不錄取') { msg=`你曾報名此場次(${status}),可重新報名。`; bg='#FAEEC7'; icon='⚠️'; } else if (status==='已錄取' && (payStatus==='已繳費'||payStatus==='免費')) { msg=`✅ 你已完成報名並繳費,請等待通知。`; bg='#DDEAD1'; icon=''; } else if (status==='已錄取') { msg=`✅ 你已報名此場次,目前狀態:${status}(${payStatus})。`; bg='#DDEAD1'; icon=''; } else if (status==='待審核') { msg=`🕐 你已報名此場次,目前狀態:待審核,請等待主辦審核。`; bg='#D9E5EE'; icon=''; } else { msg=`你已報名此場次,目前狀態:${status}。`; bg='#DCD7ED'; icon=''; } banner.style.cssText=`display:block;background:${bg};border-radius:12px;padding:14px 16px;margin:0 16px 12px;font-size:16px;font-weight:700;color:#2C2C2C;line-height:1.6`; banner.textContent=msg; }) .catch(()=>{}); // 查詢失敗不影響報名表 } // ── ① 活動資訊 ── html+=`
1活動資訊
${esc(s.name||'')}
${dateStr?`
📅${esc(dateStr)}
`:''} ${s.venue?`
📍${esc(s.venue)+(s.region&&s.region!==s.venue?' · '+s.region:'')}
`:''} ${s.description?`
${esc(s.description)}
`:''}
`; // ── 多日選擇(如有)── if(dates.length>1){ html+=`
+選擇參加日期
`; dates.forEach((d,i)=>{ html+=``; }); const multiDayTiers=Array.isArray(s.multiDayTiers)?s.multiDayTiers:(Array.isArray(dates[0]&&dates[0].multiDayTiers)?dates[0].multiDayTiers:[]); if(s.fee>0||multiDayTiers.length){ html+=``; } if(multiDayTiers.length){ html+=`
🎉 多日報名優惠
${multiDayTiers.map(t=>Number(t.maxDays)>0?`選 ${Number(t.minDays)||0}~${Number(t.maxDays)} 天:每攤每日 NT$${Number(t.dailyFee||0).toLocaleString()}`:`選 ${Number(t.minDays)||0} 天以上:每攤每日 NT$${Number(t.dailyFee||0).toLocaleString()}`).join('
')}
`; } html+=`
`; } // ── ② 設備確認 ── if(!isCollab){ html+=`
2設備確認
`; // 報名數量 html+=`
🏪報名數量
1
(NT$${Number(s.fee)||0})
`; // 設備品項 const equipIcons={'桌':'🪑','椅':'🪑','延長線':'🔌','燈':'💡','燈具':'💡','帳篷':'⛺','電':'⚡'}; equipDefs.forEach(([k,v])=>{ ST.equipQty[k]=frontEquipFreeTotal(k,v,s); const icon=Object.entries(equipIcons).find(([key])=>k.includes(key))?.[1]||'📦'; const hasIncl=Number(v.incl)>0; const inclNote=hasIncl?`(每攤含${v.incl})`:''; const priceTxt=Number(v.price)>0?` NT$${v.price}`:''; html+=`
${icon}${esc(k)}${priceTxt}${inclNote}
${ST.equipQty[k]||0}
(NT$0)
`; }); // 加購品項 addons.forEach((a)=>{ ST.addonQty[a._idx]=0; html+=`
${esc(a.name||'')}${Number(a.price)>0?' NT$'+a.price:''}
0
(NT$0)
`; }); html+=`
`; // 加價選位告知(正式規則:報名階段只告知,不選位、不加價、不卡位) try{ const _myB=activeBundlesForSession(s); if(_myB.length){ ST.bundleAddon=''; html+=`
加報其他場次(組合優惠)
⚠️ 加報屬「組合報名,同進退」:退場即整組退出、組合優惠失效;若只需單場,請取消後重新報名單場。
`; _myB.forEach(b=>{ const _o=(b.sessions||[]).filter(x=>String(x.id)!==String(s.id)); const _nm=_o.map(x=>esc(x.name)).join('、'); html+=``; }); html+=`
`; } }catch(e){} const seatEnabled = !!(s.seatPricingEnabled || s.seat_pricing_enabled || (s.modules&&s.modules.seatPricing)); if(seatEnabled){ ST.seatChoiceIntent = 'auto'; html+=`
加價選位告知
本場次有開放加價選位。
錄取後 24 小時內,可至「我的紀錄」選擇加價位置並完成付款,即可保留位置。
超過期限未完成選位與付款,位置將釋出,且同場次不可再加購選位。
不選位則依付款確認順序排位。
此處僅供報名前查看位置配置;正式選位在錄取後的「我的紀錄」完成。
`; } // ── ③ 總計費用 ── html+=`
3總計費用
攤位小計NT$0
保證金NT$${s.deposit||0}
總計費用 NT$${Number(s.fee)||0}
`; } const cardNum=isCollab?2:4; // ── ④ 會員資料 ── const socialHtml=(vFb||vIg||vWeb)?`
${vFb?``:''} ${vIg?``:''} ${vWeb?``:''}
`:''; if(isNew){ html+=`
${cardNum}建立會員資料
Email 會綁定您的會員 ID;下次可用 Email+手機查找資料,不用重填。
FB、IG 或官網至少建議填寫一項方便主辦審核品牌與商品內容。
若完全未填寫,審核將不會通過。
`; } else { html+=`
${cardNum}會員資料
攤位名稱${esc(V.brand||V.brand_name||'')} 姓名${esc(V.name||'')} 電話${esc(V.phone||'')} Email${esc(email)}
${socialHtml}
`; html+=``; html+=``; html+=``; html+=``; html+=``; html+=``; html+=``; html+=``; } html+=``; // ── ⑤ 發票資訊 ── const invTypeStr=isCompany?'公司戶(三聯式)':'個人(二聯式)'; html+=`
${cardNum+1}發票資訊
發票類型${invTypeStr} ${vTaxId?`統編${esc(vTaxId)}`:''} ${vCompany?`抬頭${esc(vCompany)}`:''} 載具/Email${esc(vCarrier||vInvEmail||email)}
`; // 發票隱藏欄位 html+=``; html+=``; html+=``; html+=``; html+=``; // ── 自訂欄位(如有)── const customFields=(s.customFields||s.modules?.customFields||[]).filter(f=>f&&f.label); if(customFields.length){ customFields.forEach((f,i)=>{ html+=`
${esc(f.label)}${f.required?` 必填`:''}
`; if(f.type==='select'&&f.options){ html+=``; } else if(f.type==='textarea'){ html+=``; } else { html+=``; } html+=`
`; }); } // ── ⑥ 合約 ── html+=`
${cardNum+2}合約
✅ 已完成合約同意

請先查看合約內容,完成同意後才能送出報名。

`; // ── ⑦ 備註留言 ── html+=`
${cardNum+3} 備註留言 選填
0 / 200
`; const dynEl=$('dynForm'); // banner 放在 dynForm 前面(form-hero 和 dynForm 之間) let bannerEl=document.getElementById('regStatusBanner'); if(!bannerEl){ bannerEl=document.createElement('div'); bannerEl.id='regStatusBanner'; bannerEl.style.display='none'; const dynElParent=dynEl?dynEl.parentNode:null; if(dynElParent) dynElParent.insertBefore(bannerEl, dynEl); } if(dynEl) dynEl.innerHTML=html; syncMemberIntoCurrentRegForm(ST.member||{}); // 表單欄位變動時自動儲存(刷新還原用) setTimeout(()=>{ const saveFields = ['f_brand','f_name','f_phone','f_sellItem','f_brandIntro','f_note', 'f_fb','f_ig','f_web','f_taxId','f_company','f_invoiceCarrier','f_invoiceEmail']; saveFields.forEach(id=>{ const el=$(id); if(!el) return; el.addEventListener('input', ()=>{ try{ const existing = JSON.parse(localStorage.getItem('doing_market_page_state')||'{}'); if(existing.tab==='regForm'){ if(!existing.formFields) existing.formFields={}; existing.formFields[id] = el.value; localStorage.setItem('doing_market_page_state', JSON.stringify(existing)); } }catch(e){} }); }); }, 200); // 合約只保留 dynForm 內的 contractCard,不再顯示第二個合約按鈕。 // 送出按鈕初始 disabled;正式啟用交給 initAgreementArea / onAgreementRead 控制 const subBtn=$('submitBtn'); if(subBtn){ subBtn.style.display='block'; subBtn.disabled=true; subBtn.classList.add('submit-btn-new'); subBtn.textContent='送出報名'; } // 阻斷性修正:每次重繪報名表後,都必須重新初始化合約狀態。 // 避免「合約讀完但送出仍 disabled」或沿用上一場次合約狀態。 initAgreementArea(s); updateFee(); setTimeout(()=>{ preFillMemberFields(); }, 100); } // 直接輸入攤數 // 直接輸入設備數 // 直接輸入加購數 // 更新設備列小計 // 發票修改彈窗 function openInvoiceEditModal(){ // 使用現有的 member edit modal,切換到發票頁籤 openMemberEditModal(); setTimeout(()=>{ const tabs=document.querySelectorAll('[data-edit-tab]'); tabs.forEach(t=>{ if(t.dataset.editTab==='invoice') t.click(); }); },200); } function adjStall(delta){ const s=ST.currentSession||{}; const sType=s.type||'市集場次'; const isMkt=sType==='市集場次'||sType==='通路寄賣'; // 上限從後台場次設定讀取,不硬編碼 const maxN=Number(s.maxStalls)>0?Number(s.maxStalls):999; const oldCount=Math.max(Number(ST.stallCount)||1,1); const nextCount=Math.max(1,Math.min(maxN,oldCount+delta)); if(nextCount===oldCount) return; // B-04:ST.equipQty 是「總量」。改攤數時若只重算內含量,攤友原本額外加租的數量會被吃掉 // (1攤含1桌+加租1桌=2;改成2攤時應為3桌,不是2桌)。先記下加租量,換算完再加回去。 const extras={}; Object.entries(s.equip||{}).forEach(([k,def])=>{ if(!def || def.open===false || def.open==='false') return; const oldBase=frontEquipFreeTotal(k,def,s,oldCount); const oldSelected=Number((ST.equipQty||{})[k]||0); extras[k]=Math.max(0, oldSelected-oldBase); }); ST.stallCount=nextCount; if(!ST.equipQty) ST.equipQty={}; Object.entries(s.equip||{}).forEach(([k,def])=>{ if(!def || def.open===false || def.open==='false') return; ST.equipQty[k]=frontEquipFreeTotal(k,def,s,nextCount)+(extras[k]||0); }); // 數字欄位只放純數字,不接「攤/份」單位(單位已寫在標題「報名數量」旁)。 const el=$('stallDisplay'); if(el) el.textContent=String(ST.stallCount); ensureIncludedEquipQty(); updateFee(); } function buildFormHero(s, cover){ const dateStr=(s.dates&&s.dates.length)?s.dates.map(d=>d.date+(d.start?' '+d.start:'')+(d.end?'–'+d.end:'')).join(' / '):(s.date||''); const meta=[dateStr,s.venue].filter(Boolean).join(' '); if(cover){ $('formHero').innerHTML=`
${s.name}
${meta}
`; } else { $('formHero').innerHTML=`
🎪
${s.name}
${meta}
`; } } // ── 報名表 Modal 開關 ── function resetRegFormState(options={}){ const keepContent=!!options.keepContent; const dynEl=$('dynForm'); if(dynEl){ dynEl.style.display='block'; if(!keepContent) dynEl.innerHTML=''; } // 外層 feeBox 只保留給舊流程,A/B 卡片內的 feeBox 由 renderDynFormFull 自己建立 const outerFeeBox=document.querySelector('#regFormModal .form-wrap > #feeBox'); if(outerFeeBox) outerFeeBox.style.display='none'; const submitBtn=$('submitBtn'); if(submitBtn){ submitBtn.disabled=false; submitBtn.textContent='送出報名'; submitBtn.style.opacity='1'; submitBtn.style.display='none'; } // 重置合約同意狀態 ST._agreementRequired = true; ST._agreementViewed = false; ST._agreementAccepted = false; ST._agreementData = null; const badge = $('contractDoneBadge'); if (badge) badge.style.display = 'none'; const modalBox=document.querySelector('#regFormModal .modal-box'); if(modalBox){ modalBox.querySelectorAll('.success-screen').forEach(el=>el.remove()); } const formErr=$('formError'); if(formErr){ formErr.style.display='none'; formErr.innerHTML=''; } ST.__submitting=false; } function openRegFormModal(){ resetRegFormState({keepContent:false}); if(ST.currentSession){ if(ST.member && ST.memberEmail){ renderDynFormFull(ST.currentSession, false); } else { showEmailStep(ST.currentSession); } } // 記住開啟報名表前所在的列表頁,送出成功後回到場次列表,不再停留報名表。 const activePageBeforeReg = document.querySelector('.page.active'); ST._regFormReturnState = { pageId: activePageBeforeReg ? activePageBeforeReg.id : '', portalTitle: ($('portalTitle')?.textContent || '').trim(), catFilter: ST.currentCatFilter || '', eventId: ST.currentEvent ? (ST.currentEvent.id || '') : (ST.currentSession ? (ST.currentSession.eventId || '') : '') }; // 切換到報名表頁面 document.querySelectorAll('.page').forEach(p=>p.classList.remove('active')); const page=$('regFormModal'); if(page) page.classList.add('active'); window.scrollTo(0,0); history.pushState({regFormOpen:true}, '', location.href); // 儲存報名表狀態(刷新還原用) try{ const state = { tab: 'regForm', sessionId: ST.currentSession ? ST.currentSession.id : null, stallCount: ST.stallCount || 1, equipQty: ST.equipQty || {}, addonQty: ST.addonQty || {}, }; localStorage.setItem('doing_market_current_tab', 'regForm'); localStorage.setItem('doing_market_page_state', JSON.stringify(state)); }catch(e){} // 底部無 active ['botHome','botMember','botSupport'].forEach(id=>{ const el=document.getElementById(id); if(el) el.classList.remove('active'); }); } function setEditValue(id, value){ const el=$(id); if(el) el.value = value == null ? '' : String(value); } async function openMemberEditModal(forceMsg){ const email = String(ST.memberEmail || memberSafeEmail() || '').trim(); if(!email){ toast('請先登入會員'); return; } const modal=$('memberEditModal'); if(!modal){ toast('找不到會員編輯視窗'); return; } modal.classList.add('show'); const btn=$('saveMemberBtn'); if(btn){ btn.disabled=false; btn.textContent='儲存資料'; } const msg=$('saveSuccessMsg'); if(msg) msg.style.display='none'; // 強制填寫提示 const hintEl=$('memberEditHint'); const closeBtn=modal.querySelector('button[onclick*="closeMemberEditModal"]')||modal.querySelector('button[onclick*="closeModal(\'memberEditModal\')"]'); if(forceMsg){ if(hintEl){ hintEl.textContent=forceMsg; hintEl.style.display='block'; } if(closeBtn) closeBtn.style.display='none'; // 攔截點擊背景關閉 modal._forceOpen=true; modal.onclick=function(e){ if(e.target===modal) toast('請先填寫完整資料並儲存'); }; } else { if(hintEl){ hintEl.style.display='none'; hintEl.textContent=''; } if(closeBtn) closeBtn.style.display=''; modal._forceOpen=false; modal.onclick=function(e){ if(e.target===modal) closeMemberEditModal(); }; } let m = ST.member || {email}; try{ const fresh = await api({action:'getMember', email, phone:getMemberPhone(), _t:Date.now()}); if(fresh && !fresh.error){ m = {...m, ...fresh, email:fresh.email||email}; ST.member = m; fillMemberCard(m); } }catch(e){ console.warn('[openMemberEditModal getMember failed]', e); } setEditValue('editName', m.name || ''); setEditValue('editPhone', m.phone || ''); setEditValue('editBrand', m.brand || m.brandName || m.brand_name || ''); setEditValue('editBrandIntro', m.brandIntro || m.brand_intro || ''); setEditValue('editFb', m.fb || m.fb_url || ''); setEditValue('editIg', m.ig || m.ig_url || ''); setEditValue('editCollabUrl', m.collabUrl || m.collab_url || ''); setEditValue('editPhoto', m.photo || m.photo_url || ''); setEditValue('editLineId', m.lineId || m.line_id || ''); setEditValue('editInvoiceCarrier', m.invoiceCarrier || m.invoice_carrier || ''); setEditValue('editTaxId', m.taxId || m.tax_id || ''); setEditValue('editCompany', m.company || m.invoiceTitle || m.invoice_title || ''); setEditValue('editInvoiceEmail', m.invoiceEmail || m.invoice_email || email); } function closeMemberEditModal(){ const modal=$('memberEditModal'); if(modal) modal.classList.remove('show'); } async function saveMemberEdit(){ const btn=$('saveMemberBtn'); if(btn){ btn.disabled=true; btn.textContent='儲存中…'; } const restore=()=>{ if(btn){ btn.disabled=false; btn.textContent='儲存資料'; } }; const email = String(ST.memberEmail||memberSafeEmail()||'').trim(); if(!email){ toast('請先登入'); restore(); return; } const requestedPhone=String(($('editPhone')?.value||'').trim()); const verifiedPhone=String(ST._verifiedMemberPhone||ST.member?.phone||memberSafePhone()||'').trim(); if(verifiedPhone && requestedPhone && requestedPhone!==verifiedPhone){if(!await uiConfirm('你正在修改登入用手機:\n'+verifiedPhone+' → '+requestedPhone+'\n\n修改後,下次登入要使用新手機。確定要修改嗎?')){restore();return;}} const body = { email, name: ($('editName')?.value||'').trim(), phone: ($('editPhone')?.value||'').trim(), brand: ($('editBrand')?.value||'').trim(), brandIntro: ($('editBrandIntro')?.value||'').trim(), fb: ($('editFb')?.value||'').trim(), ig: ($('editIg')?.value||'').trim(), collabUrl: ($('editCollabUrl')?.value||'').trim(), photo: ($('editPhoto')?.value||'').trim(), lineId: ($('editLineId')?.value||'').trim(), invoiceCarrier:($('editInvoiceCarrier')?.value||'').trim(), taxId: ($('editTaxId')?.value||'').trim(), company: ($('editCompany')?.value||'').trim(), invoiceEmail:($('editInvoiceEmail')?.value||'').trim(), sellItem: ST.member?.sellItem||ST.member?.sell_items||ST.member?.sellCat||ST.member?.sell_category||'' }; if(!body.fb&&!body.ig&&!body.collabUrl){ toast('請填寫 FB、IG 或官網其中一項'); const first=$('editFb'); if(first){try{first.scrollIntoView({behavior:'smooth',block:'center'});first.focus();}catch(e){}} restore(); return; } try{ const res=await saveDoingPlatformMemberProfile(body); if(!res || res.error){ toast('❌ 儲存失敗:'+(res?.error||'請稍後再試')); restore(); return; } const savedPhone = body.phone || getMemberPhone(); memberSavePhone(savedPhone,email); ST._verifiedMemberPhone=savedPhone; ST.member={...(ST.member||{}),...(res.member||body),email:(res.member&&res.member.email)||email,phone:(res.member&&res.member.phone)||savedPhone,collabUrl:body.collabUrl,invoiceCarrier:body.invoiceCarrier,invoiceEmail:body.invoiceEmail}; fillMemberCard(ST.member); syncMemberIntoCurrentRegForm(ST.member); const msg=$('saveSuccessMsg'); if(msg){ msg.style.display='block'; setTimeout(()=>msg.style.display='none',1800); } toast('✅ 會員資料已更新'); // 重新檢查完整性 const m2 = ST.member; const hasName2 = !!String(m2.name||m2.displayName||'').trim(); const hasBrand2 = !!String(m2.brand||m2.brand_name||'').trim(); const hasSocial2 = !!String(m2.fb||m2.fb_url||m2.ig||m2.ig_url||m2.collabUrl||m2.collab_url||m2.website||m2.web||'').trim(); if(!hasName2 || !hasBrand2 || !hasSocial2){ const missing2=[]; if(!hasName2) missing2.push('姓名'); if(!hasBrand2) missing2.push('品牌名稱'); if(!hasSocial2) missing2.push('FB、IG 或官網(至少一項)'); openMemberEditModal('仍缺少以下資料:' + missing2.join('、')); } else { ST._profileIncomplete = false; setTimeout(()=>closeMemberEditModal(), 500); } }catch(e){ console.warn('[saveMemberEdit failed]',e); toast('❌ 網路錯誤,請稍後再試'); }finally{ restore(); } } function regFormHasContent(){ try{ const ids=['f_name','f_email','f_phone','f_brand','f_brandIntro','f_taxId','f_invoiceTitle']; return ids.some(function(id){ const el=$(id); return el && String(el.value||'').trim().length>0; }); }catch(e){ return false; } } function closeRegFormModal(opts){ const force=!!(opts&&opts.force); // 防呆:返回鍵/關閉時,若已填但尚未送出,先確認,避免白填又誤以為已報名 if(!force && !ST.__submitting && regFormHasContent()){ return false; // 交由呼叫端(attemptCloseRegForm / popstate)用自製確認框詢問 } const page=$('regFormModal'); if(page) page.classList.remove('active'); ST.currentSession=null; resetRegFormState({keepContent:false}); // 回到上一頁(首頁或場次列表) const lastTab=localStorage.getItem('doing_market_current_tab')||'home'; if(lastTab==='support'){ document.querySelectorAll('.page').forEach(p=>p.classList.remove('active')); const pg=$('pageSupport'); if(pg) pg.classList.add('active'); const btn=$('botSupport'); if(btn) btn.classList.add('active'); } else if(lastTab==='member'){ document.querySelectorAll('.page').forEach(p=>p.classList.remove('active')); const pg=$('pageMember'); if(pg) pg.classList.add('active'); const btn=$('botMember'); if(btn) btn.classList.add('active'); } else { navTo('home'); const btn=$('botHome'); if(btn){ ['botHome','botMember','botSupport'].forEach(id=>{const e=$(''+id);if(e)e.classList.remove('active');}); btn.classList.add('active'); } } window.scrollTo(0,0); return true; } function showRegisterSuccess(res, ses, email){ try{ const regNo = String((res&&(res.regId||res.registrationId||res.id))||'').trim(); const sesName = String((ses&&(ses.name||ses.title))||'').trim(); let d=document.getElementById('regSuccessModal'); if(d) d.remove(); d=document.createElement('div'); d.id='regSuccessModal'; d.style.cssText='position:fixed;inset:0;z-index:9200;background:rgba(20,20,20,.55);display:flex;align-items:center;justify-content:center;padding:16px'; d.innerHTML='
' +'
' +'
報名已送出成功
' +(sesName?'
'+esc(sesName)+'
':'') +(regNo?'
報名編號:'+esc(regNo)+'
':'') +'
' +'接下來:
1. 主辦會進行審核,結果將寄到您的 Email
2. 錄取後會收到繳費通知
3. 隨時可到「我的紀錄」查看進度' +'
' +(email?'
通知信會寄到:'+esc(email)+'
':'') +'' +'' +'
'; document.body.appendChild(d); document.body.style.overflow='hidden'; }catch(e){ console.warn('[showRegisterSuccess]',e); } } function closeRegSuccessAndGoRecords(){ const m=document.getElementById('regSuccessModal'); if(m) m.remove(); document.body.style.overflow=''; try{ if(typeof navToTab==='function') navToTab('member'); }catch(e){} } function closeRegFormToSessionListAfterSubmit(sessionSnapshot, email){ hideSubmitOverlay(); const s = sessionSnapshot || ST.currentSession || {}; const returnState = ST._regFormReturnState || {}; const page = $('regFormModal'); if(page){ page.classList.remove('active'); page.classList.remove('show'); } resetRegFormState({keepContent:false}); document.querySelectorAll('.page').forEach(p=>p.classList.remove('active')); const portal = $('pagePortal'); if(portal) portal.classList.add('active'); const title = $('portalTitle'); const fallbackTitle = returnState.portalTitle || returnState.catFilter || ST.currentCatFilter || '場次列表'; if(title) title.textContent = fallbackTitle && fallbackTitle !== '報名表' ? fallbackTitle : '場次列表'; const listEl = $('portalSessionList'); const allRaw = [ ...(Array.isArray(ST.sessions) ? ST.sessions : []), ...(Array.isArray(ST.allSessionsData) ? ST.allSessionsData : []) ]; const seen = new Set(); const all = allRaw.filter(item=>{ if(!item || !item.id) return false; const key = String(item.id); if(seen.has(key)) return false; seen.add(key); return typeof isOpenSession === 'function' ? isOpenSession(item) : true; }); const eventId = String(s.eventId || returnState.eventId || ''); let sessions = []; if(eventId){ sessions = all.filter(item=>String(item.eventId || '') === eventId); } if(!sessions.length && (returnState.catFilter || ST.currentCatFilter)){ const cat = returnState.catFilter || ST.currentCatFilter; const typeMap = {'市集報名':['市集場次'],'體驗活動':['活動報名','體驗活動'],'通路寄賣':['通路寄賣'],'合作洽詢':['合作申請']}; const allow = typeMap[cat] || []; sessions = all.filter(item=>{ const portals = Array.isArray(item.portals) ? item.portals : String(item.portals || '').split(',').map(x=>x.trim()).filter(Boolean); return portals.includes(cat) || allow.includes(item.type); }); } if(!sessions.length){ sessions = (Array.isArray(ST.sessions) && ST.sessions.length ? ST.sessions : all).filter(item=>typeof isOpenSession === 'function' ? isOpenSession(item) : true); } if(typeof sortSessionsByUpcoming === 'function') sessions = sortSessionsByUpcoming(sessions); if(listEl) setActivitySessions('portal',sessions); ST.currentSession = null; ST._regFormReturnState = null; try{ localStorage.setItem('doing_market_current_tab', 'portal'); localStorage.setItem('doing_market_page_state', JSON.stringify({tab:'portal', catFilter:returnState.catFilter || ST.currentCatFilter || '', eventId:eventId || ''})); }catch(e){} ['botHome','botMember','botSupport'].forEach(id=>{ const el=$(id); if(el) el.classList.remove('active'); }); try{ history.replaceState({page:'portal', afterRegister:true}, '', location.href); }catch(e){} window.scrollTo(0,0); toast('✅ 報名已送出'); } // 返回鍵攔截:如果報名 modal 是開著的,關掉它;否則讓瀏覽器正常返回 window.addEventListener('popstate', function(e){ // Layer 3:報名詳情 Modal const regDM=document.getElementById('regDetailModal'); if(regDM){ regDM.remove(); return; } // Layer 2:報名表 Modal 開著 → 關閉 Modal const modal=$('regFormModal'); if(modal && (modal.classList.contains('show')||modal.classList.contains('active'))){ const closed=closeRegFormModal(); if(closed===false){ try{ history.pushState({page:'regForm'},'',location.href); }catch(e){} uiConfirm('報名尚未送出,確定要離開嗎?\n離開後已填寫的資料會清除。').then(function(ok){ if(ok) closeRegFormModal({force:true}); }); } return; } // Layer 1:在非首頁 → 回首頁(不讓 browser 離開 app) const isHome=$('pageHome')?.classList.contains('active'); if(!isHome){ document.querySelectorAll('.page').forEach(p=>p.classList.remove('active')); $('pageHome').classList.add('active'); const pp=$('pagePortal'); if(pp) pp.style.background=''; document.body.style.background='var(--wh)'; window.scrollTo(0,0); // 重新 push home state 避免再按返回直接離開 history.pushState({page:'home'}, '', location.href); return; } // 已在首頁 → 讓 browser 正常處理(回上一頁或離開) }); // 分享當前場次 function shareCurrentSession(){ const s=ST.currentSession; if(!s) return; openSharePanel('session', s.id, s.name||'報名表'); } async function openSession(sessionId){ ST.currentBundle=null; ST.bundleAddon=''; if(!Array.isArray(ST.bundles)||!ST.bundles.length){ try{ const _bl=await api({action:'getBundlesPublic'}); ST.bundles=Array.isArray(_bl)?_bl:[]; }catch(_e){} } // 點場次必須立即有反應:先用列表快取打開報名表,再背景抓最新資料。 const cached = (ST.allSessionsData||[]).find(x=>String(x.id)===String(sessionId)) || (ST.sessions||[]).find(x=>String(x.id)===String(sessionId)); const openWithSession = async (s, fromFresh=false)=>{ if(!s || s.error){ toast('場次資料讀取失敗,請重試'); return; } ST.currentSession=s; ST.equipQty={}; ST.addonQty={}; ST.stallCount=1; const evt=(ST.events||[]).find(e=>String(e.id)===String(s.eventId)); const cover=s.cover||evt?.cover||''; buildFormHero(s,cover); const titleEl=$('formBackTitle'); if(titleEl) titleEl.textContent=s.name||'報名表'; const modal=$('regFormModal'); if(!modal || !modal.classList.contains('show')) openRegFormModal(); if(s.registrationOpen===false){ showRegistrationUnavailable(s); return; } if(ST.member && ST.memberEmail){ try{ const _ph=String((ST.member&&ST.member.phone)||memberSafePhone()||'').trim(); const myRegs=await api({action:'getMyRegs',email:ST.memberEmail,phone:_ph,_t:Date.now()}); const list=Array.isArray(myRegs)?myRegs:[]; ST.regs=list; const alive=rg=>String(rg.status||'')!=='不錄取'&&String(rg.status||'')!=='已取消'&&String(rg.transferStatus||'')!=='已退費'; const dup=list.find(rg=>String(rg.sessionId)===String(s.id)&&alive(rg)); if(dup){ showAlreadyRegistered(s,dup); } else { const sameName=list.find(rg=>alive(rg)&&String(rg.sessionName||rg.sesName||'').trim()===String(s.name||'').trim()); renderDynFormFull(s, false); if(sameName) showSameNameRegWarning(sameName); } }catch(e){ renderDynFormFull(s, false); } } else { showEmailStep(s); } updateFee(); }; if(cached){ await openWithSession(cached, false); // 背景更新最新場次資料,不讓使用者覺得點了沒反應。 api({action:'getSession',id:sessionId,_t:Date.now()}).then(fresh=>{ if(!fresh || fresh.error) return; ST.allSessionsData=[...new Map([...(ST.allSessionsData||[]), fresh].map(x=>[x.id,x])).values()]; if(ST.currentSession && String(ST.currentSession.id)===String(sessionId)){ ST.currentSession=fresh; // 只在表單還沒進入填寫成功畫面時刷新內容。 const success=document.querySelector('#regFormModal .success-screen'); if(!success){ buildFormHero(fresh, fresh.cover||((ST.events||[]).find(e=>String(e.id)===String(fresh.eventId))||{}).cover||''); if(fresh.registrationOpen===false) showRegistrationUnavailable(fresh); else { if(ST.member && ST.memberEmail) renderDynFormFull(fresh, false); else showEmailStep(fresh); updateFee(); } } } }).catch(()=>{}); return; } // 沒快取時,顯示明確載入畫面,不要無感等待。 const formHero=$('formHero'); if(formHero) formHero.innerHTML='
🎪
場次資料載入中…
'; const titleEl=$('formBackTitle'); if(titleEl) titleEl.textContent='報名表'; ST.currentSession=null; openRegFormModal(); const dyn=$('dynForm'); if(dyn) dyn.innerHTML='
正在讀取場次資料…
'; const s = await api({action:'getSession',id:sessionId,_t:Date.now()}); await openWithSession(s, true); } function showRegistrationUnavailable(s){ const dynEl=$('dynForm'); const submitBtn=$('submitBtn');if(submitBtn)submitBtn.style.display='none'; const feeBox=$('feeBox');if(feeBox)feeBox.style.display='none'; if(!dynEl)return; const title={upcoming:'報名尚未開放',paused:'目前暫停報名',ended:'本場報名已截止',manual_closed:'本場報名已關閉'}[s.registrationState]||'目前未開放報名'; dynEl.innerHTML=`
⏱ ${esc(title)}
${esc(s.registrationMessage||title)}
開放時間由主辦依活動排程設定;時間到會自動開放,不需要重新取得連結。
`; } function showAlreadyRegistered(s,reg){ const dynEl=$('dynForm'); const submitBtn=$('submitBtn'); if(submitBtn) submitBtn.style.display='none'; const feeBox=$('feeBox'); if(feeBox) feeBox.style.display='none'; if(!dynEl) return; const statusColor={待審核:'#856404',已錄取:'#155724',不錄取:'#721c24',已取消:'#aaa'}[reg.status]||'#555'; dynEl.innerHTML=`
✋ 您已報名此場次
場次:${esc(s.name||'')}
狀態 ${reg.status||'待審核'}
如需修改品牌介紹等資料,請至「我的紀錄」查看。
如需取消,請聯繫主辦單位。
`; } function getParticipantCountValue(id, fallback){ const el=$(id); const n=parseInt(el?.value ?? fallback ?? 0,10); return Number.isFinite(n) && n>0 ? n : 0; } function collectParticipantsForSubmit(showFormError, restoreBtn){ const hasAdult=!!$('f_adult'); const hasChild=!!$('f_child'); if(!hasAdult && !hasChild) return {adultCount:0, childCount:0, childAges:[], totalCount:0}; const adultCount=hasAdult ? getParticipantCountValue('f_adult',1) : 0; const childCount=hasChild ? getParticipantCountValue('f_child',0) : 0; const childAges=[]; for(let i=0;i18){ const msg=`請填寫小孩 ${i+1} 的年齡`; toast(msg); if(showFormError) showFormError(msg); $('f_child_age_'+i)?.focus(); if(restoreBtn) restoreBtn(); return null; } childAges.push(ageNum); } if((adultCount+childCount)<=0){ const msg='請至少填寫 1 位參加者'; toast(msg); if(showFormError) showFormError(msg); if(restoreBtn) restoreBtn(); return null; } return {adultCount, childCount, childAges, totalCount:adultCount+childCount}; } // [adjStall 統一由上方新版定義] function adjAddon(i,delta){ if(!ST.addonQty)ST.addonQty={}; const cur=ST.addonQty[i]||0; const next=Math.max(0,cur+delta); ST.addonQty[i]=next; const el=document.getElementById('addon_num_'+i); if(el)el.textContent=next; updateFee(); } // B-04:內含總量 = 每攤內含數 × 攤位數。內含量的唯一計算入口, // 其他地方一律呼叫本函式,不可自己再寫一次(過去 adjEquip / updateFee 各算各的,才會少乘攤數)。 function frontEquipFreeTotal(k, def, session, stallCount){ const stalls=Math.max(Number(stallCount ?? ST.stallCount ?? 1)||1, 1); const inclPerStall=Math.max(0, frontEffectiveEquipIncl(k, def, session?.basicEquip||session?.basic_equip||'')); return inclPerStall*stalls; } function ensureIncludedEquipQty(){ const s=ST.currentSession||{}; if(!ST.equipQty) ST.equipQty={}; Object.entries(s.equip||{}).forEach(([k,v])=>{ if(!v || v.open===false || v.open==='false') return; const free=frontEquipFreeTotal(k,v,s); const cur=Number(ST.equipQty[k]||0); if(cur < free) ST.equipQty[k]=free; const elNum=document.getElementById('eq_num_'+k); if(elNum) elNum.textContent=ST.equipQty[k]||0; const elInput=document.getElementById('eq_'+k); if(elInput) elInput.value=ST.equipQty[k]||0; }); } function frontEffectiveEquipIncl(name, def, basicEquipText){ // A→Z 阻斷修正:後台設備設定只要有 incl,就代表該設備「本次內含」; // 前台顯示「本次含 N」時,不可因 basicEquip 文字沒有寫到該品項而又把內含數量算錢。 // 若未來某場次不含,後台應把該設備 incl 設為 0,而不是依文字猜。 const n=Number(def&&def.incl)||0; return n>0 ? n : 0; } function adjEquip(k,delta){ const cur=ST.equipQty[k]||0; const s=ST.currentSession; const equipDef=s?.equip?.[k]||{}; const stalls=ST.stallCount||1; // 每攤上限(0=不限) const maxPerStall=equipDef.maxPerStall||0; const maxTotal=maxPerStall>0?maxPerStall*stalls:50; const next=Math.max(0,Math.min(maxTotal,cur+delta)); if(next===cur && delta>0 && maxPerStall>0){ toast('每攤最多加購 '+maxPerStall+' 件,超出請租多攤!'); return; } ST.equipQty[k]=next; // 相容 input(id=eq_${k})和 span(id=eq_num_${k})兩種格式 const elInput=document.getElementById('eq_'+k); if(elInput) elInput.value=next; const elNum=document.getElementById('eq_num_'+k); if(elNum) elNum.textContent=next; // 動態更新說明:已含幾件、額外幾件 const _ed=(s?.equip||{})[k]||{}; const inclFree=frontEquipFreeTotal(k,_ed,s,stalls); // 內含總量=每攤內含 × 攤位數 const extra=Math.max(0,next-inclFree); const price=Number(_ed.price)||0; const hintId='eq_hint_'+k.replace(/[^a-zA-Z0-9]/g,'_'); const hintEl=document.getElementById(hintId); if(hintEl){ if(inclFree>0){ if(next<=inclFree) hintEl.textContent=`✓ 目前含 ${inclFree} 件免費,已含足夠`; else hintEl.textContent=`✓ 含 ${inclFree} 件免費 + 額外 ${extra} 件 = NT$${extra*price}`; } } updateFee(); } function updateFee(){ const s=ST.currentSession; if(!s)return; ensureIncludedEquipQty(); const stalls=ST.stallCount||1; const dep=(s.deposit||0); // 保證金固定,不乘攤數 // 同步儲存最新狀態(刷新還原用) try{ const existing = JSON.parse(localStorage.getItem('doing_market_page_state')||'{}'); if(existing.tab==='regForm'){ existing.stallCount = ST.stallCount||1; existing.equipQty = {...(ST.equipQty||{})}; existing.addonQty = {...(ST.addonQty||{})}; localStorage.setItem('doing_market_page_state', JSON.stringify(existing)); } }catch(e){} // 計算日期費用 let fee=0; const dates=s.dates||[]; if(dates.length>1){ // 多日場次:依勾選日期計算 const selectedDates=dates.filter((_,i)=>document.getElementById('dp_'+i)?.checked); const tiers=Array.isArray(s.multiDayTiers)?s.multiDayTiers:(Array.isArray(dates[0]&&dates[0].multiDayTiers)?dates[0].multiDayTiers:[]),dayCount=selectedDates.length; const tier=tiers.filter(t=>t&&t.enabled!==false&&Number(t.minDays)>0&&dayCount>=Number(t.minDays)&&(Number(t.maxDays)<=0||dayCount<=Number(t.maxDays))).sort((a,b)=>Number(b.minDays)-Number(a.minDays))[0]; const allSelected=selectedDates.length===dates.length; if(tier){ fee=Number(tier.dailyFee||0)*dayCount*stalls; const disc=document.getElementById('fullPkgHint');if(disc){disc.style.display='block';disc.textContent=`🎉 已套用 ${dayCount} 天優惠:每攤每日 NT$${Number(tier.dailyFee||0).toLocaleString()}`;} } else if(allSelected && Number(s.fee)>0){ fee=Number(s.fee)*stalls; // 顯示全報優惠提示 const disc=document.getElementById('fullPkgHint'); if(disc){disc.style.display='block';disc.textContent=`🎉 全程參加優惠 NT$${Number(s.fee).toLocaleString()}/攤`;} } else { fee=selectedDates.reduce((sum,d)=>sum+(Number(d.fee)||0),0)*stalls; const disc=document.getElementById('fullPkgHint'); if(disc) disc.style.display='none'; } } else if(dates.length===1){ // 單日場次:用 dates[0].fee,沒有才用 s.fee(與 worker calcFee 一致) fee=(Number(dates[0].fee)>0 ? Number(dates[0].fee) : Number(s.fee)||0)*stalls; } else { fee=(Number(s.fee)||0)*stalls; } let equipTotal=0; Object.entries(s.equip||{}).forEach(([k,v])=>{ if(v&&v.open){ const inclTotal=frontEquipFreeTotal(k,v,s,stalls); // 內含總量=每攤內含 × 攤位數 const want=ST.equipQty[k]||0; const extra=Math.max(0, want-inclTotal); // 只算超過內含總量的部分 equipTotal+=(v.price||0)*extra; } }); let addonTotal=0; (s.addons||[]).forEach((a,i)=>{ if(a && a.open===true) addonTotal+=(Number(a.price)||0)*(ST.addonQty&&ST.addonQty[i]||0); }); let _bfee=fee, _bdep=dep; if(ST.bundleAddon){ const _bd=(ST.bundles||[]).find(b=>String(b.id)===String(ST.bundleAddon)); if(_bd){ const _n=(_bd.sessions||[]).length||1; _bfee=Number(_bd.bundlePrice)||0; _bdep=(Number(s.deposit)||0)*_n; } } const total=_bfee+_bdep+equipTotal+addonTotal; // 只顯示有值的費用行 // 費用行顯隱(直接用 id) if($('feeBase')) $('feeBase').textContent='NT$'+_bfee; const fbRow=$('feeBaseRow'); if(fbRow) fbRow.style.display=_bfee>0?'':'none'; if($('feeEquip')) $('feeEquip').textContent='NT$'+equipTotal; const feRow=$('feeEquipRow'); if(feRow) feRow.style.display=equipTotal>0?'':'none'; if($('feeAddon')) $('feeAddon').textContent='NT$'+addonTotal; const faRow=$('feeAddonRow'); if(faRow) faRow.style.display=addonTotal>0?'':'none'; if($('feeDeposit')) $('feeDeposit').textContent='NT$'+_bdep; const fdRow=$('feeDepRow'); if(fdRow) fdRow.style.display=_bdep>0?'':'none'; if($('feeTotal')) $('feeTotal').textContent='NT$'+total; // feeBox 在卡片內時永遠顯示(不需要切換) const feeBoxEl=$('feeBox'); if(feeBoxEl && !feeBoxEl.closest('#dynForm')) feeBoxEl.style.display=total>0?'block':'none'; // 更新設備各列小計(相容 qty-row 與 table 兩種UI格式) const _s=ST.currentSession||{}; // 攤位小計 const stEl=$('stallTotal'); if(stEl){ const isQtyRow=stEl.classList.contains('qty-total'); stEl.textContent=isQtyRow?'(NT$'+_bfee+')':'NT$'+_bfee; } // 設備各列小計 Object.entries(_s.equip||{}).forEach(([k,v])=>{ if(!v||!v.open) return; const _stalls=ST.stallCount||1; const _inclTotal=frontEquipFreeTotal(k,v,_s,_stalls); const _want=ST.equipQty[k]||0; const _extra=Math.max(0,_want-_inclTotal); const _sub=(Number(v.price)||0)*_extra; const _el=$('eqTotal_'+k); if(_el){ const _isQty=_el.classList.contains('qty-total'); _el.textContent=_isQty?'(NT$'+_sub+')':'NT$'+_sub; } }); // 加購各列小計 (_s.addons||[]).forEach((a,i)=>{ if(!a||!(a.open===true||a.open==='true')) return; const _sub2=(Number(a.price)||0)*(ST.addonQty&&ST.addonQty[i]||0); const _el2=$('addonTotal_'+i); if(_el2){ const _isQty2=_el2.classList.contains('qty-total'); _el2.textContent=_isQty2?'(NT$'+_sub2+')':'NT$'+_sub2; } }); } // ── 送出報名 ── // ── 合約同意機制 ───────────────────────────────────────────── // 在 renderDynFormFull 完成後由外部呼叫,初始化合約區 async function initAgreementArea(session) { // 合約 UI 只保留 dynForm 內的 contractCard;此函式保留為相容入口,不再產生第二個合約區。 const submitBtn = $('submitBtn'); const badge = $('contractDoneBadge'); const setUnlocked = () => { ST._agreementRequired = false; ST._agreementViewed = true; ST._agreementAccepted = true; if (badge) badge.style.display = 'none'; if (submitBtn) { submitBtn.style.display = 'block'; submitBtn.disabled = false; submitBtn.textContent = '送出報名'; submitBtn.style.opacity = '1'; } }; if (!agreementRequiredOn(session && session.agreementRequired)) { setUnlocked(); return; } ST._agreementRequired = true; ST._agreementViewed = false; ST._agreementAccepted = false; ST._agreementData = { title: (session && session.agreementTitle) || '報名合約', content: (session && session.agreementContent) || '', version: (session && session.agreementVersion) || '' }; if (submitBtn) { submitBtn.style.display = 'block'; submitBtn.disabled = true; submitBtn.textContent = '送出報名'; } if (badge) badge.style.display = 'none'; if (session && session.id) { try { const ag = await api({ action:'getSessionAgreement', id: session.id }); if (ag && !ag.error) { if (!agreementRequiredOn(ag.agreementRequired)) { setUnlocked(); return; } ST._agreementData = ag; const titleEl = $('agreementModalTitle'); if (titleEl) titleEl.textContent = '📋 ' + (ag.title || '報名合約'); } } catch(e) { console.warn('[initAgreementArea] 合約載入失敗', e); } } } function openAgreementModal() { const modal = $('agreementModal'); if (!modal) return; const body = $('agreementModalBody'); if (body) { const ag = ST._agreementData; if (ag && ag.content) { // 原文完整顯示,不改寫、不摘要 body.textContent = ag.content; } else { body.innerHTML = '
合約內容載入中…
'; // 嘗試即時取得 const ses = ST.currentSession; if (ses && ses.id) { api({ action:'getSessionAgreement', id: ses.id }).then(ag => { if (ag && ag.content) { ST._agreementData = ag; body.textContent = ag.content; const titleEl = $('agreementModalTitle'); if (titleEl) titleEl.textContent = '📋 ' + (ag.title || '報名合約'); } else { body.innerHTML = '
無法載入合約內容,請聯繫主辦。
'; } }).catch(() => { body.innerHTML = '
合約載入失敗,請重試。
'; }); } } } modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; // 標記已查看。正式同意由 modal 內「我同意合約,關閉」完成。 ST._agreementViewed = true; } function closeAgreementModal() { const modal = $('agreementModal'); if (modal) modal.style.display = 'none'; document.body.style.overflow = ''; } function onAgreementRead() { const body = $('agreementModalBody'); const txt = (body && body.textContent || '').trim(); // 阻斷性防呆:合約仍在載入或載入失敗時,不可直接放行送出。 if (ST._agreementRequired !== false) { if (!txt || txt.includes('載入中') || txt.includes('無法載入') || txt.includes('合約載入失敗')) { toast('合約內容尚未成功載入,請稍後再試或聯繫主辦。', 5000); return; } } closeAgreementModal(); ST._agreementViewed = true; ST._agreementAccepted = true; const badge = $('contractDoneBadge'); if (badge) badge.style.display = 'inline-block'; const card = $('contractCard'); if (card) { card.style.background = '#e8f5ec'; card.style.border = '2px solid #9fb59c'; } const submitBtn = $('submitBtn'); if (submitBtn) { submitBtn.style.display = 'block'; submitBtn.disabled = false; submitBtn.textContent = '送出報名'; submitBtn.style.opacity = '1'; } } function showSubmitOverlay(msg){ let ov=document.getElementById('submitOverlay'); if(!ov){ ov=document.createElement('div'); ov.id='submitOverlay'; ov.style.cssText='position:fixed;inset:0;background:rgba(255,255,255,.88);z-index:999999;display:flex;align-items:center;justify-content:center;padding:24px;text-align:center'; ov.innerHTML='
請不要重複點擊或關閉頁面
'; document.body.appendChild(ov); } const t=document.getElementById('submitOverlayText'); if(t) t.textContent=msg||'報名送出中…'; ov.style.display='flex'; } function hideSubmitOverlay(){ const ov=document.getElementById('submitOverlay'); if(ov) ov.style.display='none'; } async function submitReg(ev){ if(ev && ev.preventDefault) ev.preventDefault(); const btn=$('submitBtn'); if(ST.__submitting){ toast('報名送出中,請稍候…'); return false; } const showFormError=(msg)=>{const el=$('formError');if(el){el.style.display='block';el.innerHTML='❌ 錯誤:'+msg+'
(截圖此錯誤訊息,方便排查)'; try{el.scrollIntoView({behavior:'smooth',block:'center'});}catch(e){}}}; const hideFormError=()=>{const el=$('formError');if(el)el.style.display='none';}; const restoreBtn=()=>{ ST.__submitting=false; hideSubmitOverlay(); if(btn){ btn.disabled=false; btn.textContent='送出報名'; btn.style.opacity='1'; } }; // 按下按鈕立刻給回饋,確認 onclick 有觸發 if(btn){ btn.textContent='確認中…'; btn.style.opacity='.7'; } try{ const s=ST.currentSession; if(!s || !s.id){ showFormError('找不到場次資料,請關閉後重新點選場次'); toast('❌ 找不到場次資料'); restoreBtn(); return false; } if(s.registrationOpen===false){ showFormError(s.registrationMessage||'本場目前未開放報名'); toast('⏱ '+(s.registrationMessage||'本場目前未開放報名'),6000); restoreBtn(); return false; } const email=($('f_email')?.value||ST.memberEmail||'').trim(); const name=($('f_name')?.value||'').trim(); const phone=($('f_phone')?.value||'').trim(); let ok=true; if(!name){ const e=$('e_name'); if(e)e.textContent='請填寫姓名'; ok=false; } else { const e=$('e_name'); if(e)e.textContent=''; } if(!phone){ const e=$('e_phone'); if(e)e.textContent='請填寫手機'; ok=false; } else { const e=$('e_phone'); if(e)e.textContent=''; } if(!email||!/\S+@\S+\.\S+/.test(email)){ const e=$('e_email'); if(e)e.textContent='請填寫正確 Email'; ok=false; } else { const e=$('e_email'); if(e)e.textContent=''; } // 會員主資料必填:報名前必須完成會員資料,才可送出本次報名。 const brandVal=($('f_brand')?.value||'').trim(); if(!brandVal){ toast('請填寫攤位/品牌名稱'); showFormError('請填寫攤位/品牌名稱'); $('f_brand')?.focus(); restoreBtn(); return false; } if(!ok){ toast('請確認必填欄位'); showFormError('請確認上方必填欄位(姓名 / 手機 / Email)'); restoreBtn(); return false; } let selectedDates=[]; if(Array.isArray(s.dates) && s.dates.length>1){ selectedDates=s.dates.filter((_,i)=>document.getElementById('dp_'+i)?.checked).map(d=>d.date).filter(Boolean); if(!selectedDates.length){ toast('請至少選擇一個參加日期'); showFormError('請至少勾選一個參加日期'); restoreBtn(); return false; } }else if(Array.isArray(s.dates) && s.dates.length===1 && s.dates[0]?.date){ selectedDates=[s.dates[0].date]; } const invoiceType=document.querySelector('input[name="f_invoiceType"]:checked')?.value||'個人'; if(invoiceType==='公司/機關'){ const tax=($('f_taxId')?.value||'').trim(); const title=(($('f_invoiceTitle')?.value||'')||($('f_company')?.value||'')).trim(); if(!/^\d{8}$/.test(tax)){ toast('請填寫正確 8 碼統一編號'); showFormError('請填寫正確 8 碼統一編號'); restoreBtn(); return false; } if(!title){ toast('請填寫公司抬頭'); showFormError('請填寫公司抬頭'); restoreBtn(); return false; } } const participants = collectParticipantsForSubmit(showFormError, restoreBtn); if(!participants) return false; // ── 合約同意最終前端防線(後端亦會驗證)── if (ST._agreementRequired !== false) { if (!ST._agreementViewed) { toast('請先點開並閱讀報名合約,勾選同意後才能送出報名。', 6000); showFormError('請先點開並閱讀報名合約,勾選同意後才能送出報名。'); restoreBtn(); return false; } if (!ST._agreementAccepted) { toast('請勾選同意報名合約後才能送出報名。', 6000); showFormError('請勾選同意報名合約後才能送出報名。'); restoreBtn(); return false; } } ST.__submitting=true; if(btn){ btn.disabled=true; btn.textContent='送出中…'; } showSubmitOverlay('報名送出中…'); const M=ST.member||{}; const body={ action:'register',sessionId:s.id,email,name,phone,stallCount:ST.stallCount||1,invoiceType, invoiceCarrier:($('f_invoiceCarrier')?.value||'').trim(),taxId:($('f_taxId')?.value||'').trim(),invoiceTitle:(($('f_invoiceTitle')?.value||'')||($('f_company')?.value||'')).trim(),invoiceEmail:(($('f_invoiceEmail')?.value||'').trim()||($('f_personalInvoiceEmail')?.value||'').trim()||email),selectedDates, brand:($('f_brand')?.value||M.brand||M.brand_name||'').trim(),sellCat:(M.sellCat||M.sell_category||'').trim(),brandIntro:($('f_brandIntro')?.value||M.brandIntro||M.brand_intro||'').trim(),sellItem:($('f_sellItem')?.value||'').trim(),sellLink:M.sellLink||M.sell_link||'',photo:M.photo||M.photo_url||'',equip:{...(ST.equipQty||{})},addonQty:{...(ST.addonQty||{})},fb:($('f_fb')?.value||M.fb||M.fb_url||'').trim(),ig:($('f_ig')?.value||M.ig||M.ig_url||'').trim(),collabUrl:(M.collabUrl||M.collab_url||M.website||M.web||'').trim(),remark:(($('f_note')?.value||'')||($('f_remark')?.value||'')).trim(),stallNumber:'',seatChoiceIntent:(ST.seatChoiceIntent==='paid'?'paid':'auto'),participantsJson:participants,adultCount:participants.adultCount,childCount:participants.childCount,childAges:participants.childAges,customFields:(s.customFields||[]).map((cf,i)=>({label:cf.label,value:document.getElementById('cf_'+i)?.value||''})), // ── 合約同意(後端必須收到這兩個欄位才建立報名)── agreementViewed: ST._agreementViewed !== false, agreementAccepted: ST._agreementAccepted !== false, agreementTitle: ST._agreementData?.title || '', agreementVersion: ST._agreementData?.version || '', }; if(ST.bundleAddon){ body.action='registerBundle'; body.bundleId=ST.bundleAddon; } // 送出前最後防線:同一場次已有有效報名就擋下,避免卡住兩個名額 try{ const _ph2=String(phone||(ST.member&&ST.member.phone)||memberSafePhone()||'').trim(); if(_ph2){ const nowRegs=await api({action:'getMyRegs',email,phone:_ph2,_t:Date.now()}); if(Array.isArray(nowRegs)){ const exist=nowRegs.find(rg=>String(rg.sessionId)===String(s.id) &&String(rg.status||'')!=='不錄取'&&String(rg.status||'')!=='已取消'&&String(rg.transferStatus||'')!=='已退費'); if(exist){ showFormError('您已經報名過這個場次(報名編號 '+String(exist.id||'')+'),請到「我的紀錄」查看或繳費,不需要重複報名。'); toast('您已報名過此場次',6000); restoreBtn(); return false; } } } }catch(_dupErr){ console.warn('[dup precheck skipped]', _dupErr); } let res=await apiPost(body); if(!res){ toast('❌ 後端沒有回傳資料',8000); showFormError('後端沒有回傳資料,請稍後到我的紀錄確認是否已建立報名。'); restoreBtn(); return false; } if(res.error && isAmbiguousRegisterTransportError(res.error)){ const committed=await recoverCommittedRegistration(s.id,email,phone); if(committed){ res={success:true,ok:true,id:committed.id||committed.regId||'',status:committed.status||committed.reviewStatus||'待審核',total:committed.totalAmount||committed.amount||0,recovered:true}; } } if(res.error){ toast('❌ '+res.error, 8000); showFormError(res.error); restoreBtn(); return false; } hideFormError(); ST.memberEmail=email; ST.currentBundle=null; try{ localStorage.setItem('doing_market_member_email',email||''); }catch(e){} // 手機正式來源為 members;成功報名後只在本次瀏覽階段保留已驗證身分。 if(phone) memberSavePhone(phone,email); scheduleMemberRecordsSync('after-register',500); // 更新本地 member 資料(包含發票資訊,下次報名自動帶入) const _brand=($('f_brand')?.value||'').trim(); const _taxId=($('f_taxId')?.value||'').trim(); const _company=(($('f_invoiceTitle')?.value||'')||($('f_company')?.value||'')).trim(); const _carrier=($('f_invoiceCarrier')?.value||'').trim(); const _invEmail=($('f_invoiceEmail')?.value||$('f_personalInvoiceEmail')?.value||'').trim(); if(_brand||_taxId||_company){ ST.member={...(ST.member||{}),brand:_brand||ST.member?.brand,sellCat:(ST.member?.sellCat||ST.member?.sell_category||''),company:_company||ST.member?.company,taxId:_taxId||ST.member?.taxId,invoiceCarrier:_carrier||ST.member?.invoiceCarrier,invoiceEmail:_invEmail||ST.member?.invoiceEmail}; try{ localStorage.setItem('doing_market_member_email',email||''); }catch(e){} } // 會員資料改成背景更新,不讓 getMember 卡住成功畫面 setTimeout(()=>{ api({action:'getMember',email,phone}).then(updatedMember=>{ if(updatedMember&&!updatedMember.error){ ST.member=updatedMember; try{localStorage.setItem('doing_market_member_email',email||'');}catch(e){} } }).catch(e=>console.warn('[background getMember failed]', e)); },0); const sessionSnapshot = {...s}; if(btn){ btn.disabled=true; btn.textContent='報名成功'; btn.style.display='none'; } closeRegFormToSessionListAfterSubmit(sessionSnapshot, email); try{ if(typeof navToTab==='function') navToTab('member'); }catch(_e){} showRegisterSuccess(res, sessionSnapshot, email); ST.__submitting=false; scheduleMemberRecordsSync('after-register-success',150); return false; }catch(e){ console.error('[submitReg fatal]', e); const errMsg='報名送出失敗:'+(e && e.message ? e.message : String(e)); showFormError(errMsg); toast('❌ '+errMsg, 8000); restoreBtn(); return false; } } // ── 會員登出 ── function memberLogout(){ // 清除所有前台會員 token(報名者登出) try { localStorage.removeItem('doing_market_member_token'); sessionStorage.removeItem('doing_line_pending'); localStorage.removeItem('2bl_display_name'); localStorage.removeItem('2bl_display_name_pending'); localStorage.removeItem('tb_member'); localStorage.removeItem('doing_market_member_email'); localStorage.removeItem('doing_market_member_email_v2'); } catch(e){} memberClearToken(); ST.memberEmail = ''; ST.memberToken = ''; ST.member = null; // 回到登入畫面 memberShowLogin(); // 更新底部標籤 const lbl = $('botMemberLabel'); if(lbl) lbl.textContent = '📋 我的紀錄'; // 更新 active ['botHome','botMember','botSupport'].forEach(id=>{ const el=document.getElementById(id); if(el) el.classList.remove('active'); }); const home=document.getElementById('botHome'); if(home) home.classList.add('active'); toast('已登出'); } function isPaidText(v){ const s=String(v||'').trim(); return ['已繳費','已付款','付款完成','付款成功','paid','confirmed_paid','payment_confirmed'].includes(s) || s.indexOf('已繳費')>=0 || s.indexOf('已付款')>=0 || s==='免費'; } function isPendingPayText(v){ const s=String(v||'').trim(); return ['待確認','付款待確認','已回報'].includes(s); } function isCancelledReg(reg){ const st=String(reg?.status||reg?.reviewStatus||'').trim(); const ps=String(reg?.payStatus||'').trim(); return st==='已取消'||ps==='已取消'; } // 組合套組(bundle_group_id 相同)為綁定優惠:必須「一起繳費」,不可單獨繳其中一場。 // 否則等於用組合價只買單場(與退費同進退規則一致)。 // 組合卡標題:取各場共同的前綴(例如「幻日祭8月美島 特別場」「幻日祭8月高火北」→「幻日祭8月」) function bundleTitleOf(legs){ const names=(legs||[]).map(x=>String(x.sessionName||x.sesName||'').trim()).filter(Boolean); if(!names.length) return '組合報名'; let pre=names[0]; for(const n of names.slice(1)){ let i=0; while(i=2 ? pre+' 組合' : '組合報名'; } // 組合卡內容:逐場列出日期/地點/設備,並顯示合計金額 function bundleLegsHtml(legs, totalAll){ const rows=(legs||[]).map(x=>{ const nm=escHtml(x.sessionName||x.sesName||'場次'); const date=escHtml(mShortDateRange(mRecordDateText(x))||'—'); const loc=escHtml(mRecordLocationText(x)||'—'); const equip=escHtml(mPreNoticeEquipDisplayText(x)); const stalls=escHtml(mRecordStallText(x)); return `
${nm}
📅 ${date}|${stalls}
◎ ${loc}
設備:${equip}
`; }).join(''); // 應收(total) 是主辦拆帳看的;已繳(paid) 是實際收到的錢;待補 = 應收 − 已繳。 // 只有「已單場報過、後來併入套組」的人才會出現落差,一般報名 paid=0 或已全繳,不顯示這兩行。 const paidAll=legs.reduce((n,x)=>n+Number(x.paid||0),0); const dueAll=legs.reduce((n,x)=>n+Number(x.due||0),0); const extra = paidAll>0 && dueAll>0 ? `
已繳:NT$ ${paidAll.toLocaleString()} 待補繳:NT$ ${dueAll.toLocaleString()}
` : ''; return `
${rows}
繳費(含押金):NT$ ${Number(totalAll||0).toLocaleString()}(組合合計)
${extra}
`; } function bundleGroupOf(reg){ return String(reg && reg.bundleGroupId || '').trim(); } function sameBundleRegs(reg){ const g = bundleGroupOf(reg); if(!g) return [reg]; return (ST.regs||[]).filter(r=>bundleGroupOf(r)===g); } function bundleUnpaidRegs(reg){ return sameBundleRegs(reg).filter(r=>canPayReg(r)); } function regOutstandingAmount(reg){ if(!reg) return 0; const total=Number(reg.total||reg.totalAmount||reg.amount||0)||0; const paid=Number(reg.paid||reg.paidAmount||0)||0; return Number.isFinite(Number(reg.due)) ? Math.max(0,Number(reg.due)) : Math.max(0,total-paid); } function canPayReg(reg){ if(!reg || isCancelledReg(reg) || String(reg.status||'')!=='已錄取') return false; if(isPendingPayText(reg.payStatus)) return false; if(['申請退費','退費中','已退費','refund_pending','refunded'].includes(String(reg.transferStatus||''))) return false; const due=regOutstandingAmount(reg); if(due<=0) return false; if(String(reg.seatChoiceIntent||'')==='paid' && !['reserved','locked'].includes(String(reg.seatChoiceStatus||''))) return false; return true; } function isRefundingReg(reg){ return !!reg && ['申請退費','退費中','refund_pending'].includes(String(reg.transferStatus||'')); } function canRefundReg(reg){ if(!reg || isCancelledReg(reg)) return false; const paidMoney=Number(reg.paid||0)>0 || (isPaidText(reg.payStatus) && String(reg.payStatus||'')!=='免費'); if(!paidMoney) return false; if(isRefundingReg(reg)) return false; if(['已退費','refunded'].includes(String(reg.transferStatus||''))) return false; return true; } function canDirectCancelReg(reg){ // 未曾實收才可直接取消;只要已有實收,就必須走退款流程,避免「補選位後待補款」誤出現取消按鈕。 if(!reg || isCancelledReg(reg)) return false; const paidMoney=Number(reg.paid||0)>0 || (isPaidText(reg.payStatus) && String(reg.payStatus||'')!=='免費'); if(paidMoney) return false; const st=String(reg.status||''); if(['不錄取','已取消','已退費'].includes(st)) return false; const ts=String(reg.transferStatus||''); if(['申請退費','退費中','已退費'].includes(ts)) return false; return true; } function isExpiredIso(v){ return !!v && Date.parse(v) <= Date.now(); } function canSelectStallReg(reg){ if(!reg || isCancelledReg(reg)) return false; if(String(reg.status||'')!=='已錄取') return false; if(!(reg.seatPricingEnabled===true || reg.seatPricingEnabled==='true')) return false; if(String(reg.seatChoiceIntent||'auto')!=='paid') return false; // 付款待確認時禁止變更;已繳費但尚未選位者可選,選位後只補繳加價差額。 if(isPendingPayText(reg.payStatus) || String(reg.payStatus||'')==='免費') return false; if(reg.stallNumber) return false; if(['申請退費','退費中','已退費','refund_pending','refunded'].includes(String(reg.transferStatus||''))) return false; if(String(reg.seatChoiceStatus||'')==='released') return false; if(reg.seatSelectionDeadline && isExpiredIso(reg.seatSelectionDeadline)) return false; return true; } function stallSelectionHtml(reg){ if(!reg) return ''; const transferStatus=String(reg.transferStatus||''); const reviewStatus=String(reg.status||''); // 已取消/退費流程/未錄取/已結案都不再顯示任何選位或自動排位提示。 if(isCancelledReg(reg) || ['申請退費','退費中','已退費','refund_pending','refunded'].includes(transferStatus) || ['不錄取','已取消','已退費'].includes(reviewStatus) || reviewStatus!=='已錄取' || !(reg.seatPricingEnabled===true || reg.seatPricingEnabled==='true')) return ''; const deadline = reg.seatHoldExpiresAt || reg.seatSelectionDeadline || ''; const holdHours=Math.max(1,Number(reg.seatHoldHours||24)||24); const deadlineText = deadline ? new Date(deadline).toLocaleString('zh-TW',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}) : `選位完成後 ${holdHours} 小時內`; const paid=Number(reg.paid||0)||0; const due=Number.isFinite(Number(reg.due))?Math.max(0,Number(reg.due)):Math.max(0,(Number(reg.total||0)||0)-paid); if(reg.stallNumber && String(reg.seatChoiceType||'')==='paid'){ const locked=String(reg.seatChoiceStatus||'')==='locked'; const line=locked ? '付款已完成,位置已正式鎖定。' : (paid>0 ? `請於 ${escHtml(deadlineText)} 前補繳加價差額 NT$ ${due.toLocaleString()}。` : `請於 ${escHtml(deadlineText)} 前完成付款回報。`); return `
✅ 已選加價位置:${escHtml(reg.stallNumber)}
${line}
`; } if(String(reg.seatChoiceStatus||'')==='released' || (reg.seatSelectionDeadline && isExpiredIso(reg.seatSelectionDeadline))){ return `
加價選位期限已過,系統將依付款確認順序安排位置。
`; } if(canSelectStallReg(reg)){ const lead=paid>0 ? '您已完成原費用付款,可再選加價位置;選位後只需補繳差額。' : `本場可加價選位。選位後請於 ${escHtml(deadlineText)} 完成付款。`; return `
${lead}
`; } return `
不加價選位將依付款確認順序安排位置。
`; } function publishedSeatHtml(reg){ const days=Array.isArray(reg&®.dayPositions)?reg.dayPositions:[]; if(days.length)return `
您的攤位:${days.map(x=>`${escHtml(String(x.date||'').replace(/^\d{4}-/,''))}|${escHtml(x.stallNumber||'')}`).join('、')}
`; if(reg&®.stallNumber)return `
您的攤位:${escHtml(reg.stallNumber)}
`; return ''; } function payBadge(reg){ if(isCancelledReg(reg)) return {t:'已取消',bg:'#eeeeee',c:'#777777'}; const ts=String(reg.transferStatus||''); if(['已退費','refunded'].includes(ts)) return {t:'已退費',bg:'#eeeeee',c:'#777777'}; if(['申請退費','退費中','refund_pending'].includes(ts)) return {t:'退費中',bg:'#fff0f0',c:'#c0392b'}; const ps=String(reg.payStatus||'未繳費'); const total=Number(reg.total||0)||0, paid=Number(reg.paid||0)||0; const due=Number.isFinite(Number(reg.due))?Math.max(0,Number(reg.due)):Math.max(0,total-paid); if(ps==='免費') return {t:'免費',bg:'#e8f7ee',c:'#2a6a10'}; if(paid>0 && due>0) return {t:'待補款',bg:'#fff8ed',c:'#b45309'}; if(isPaidText(ps) && due<=0) return {t:'已繳費',bg:'#e8f7ee',c:'#2a6a10'}; if(isPendingPayText(ps)) return {t:'待確認',bg:'#fff8ed',c:'#b45309'}; return {t:'未繳費',bg:'#fdecea',c:'#c0392b'}; } function escHtml(v){return String(v??'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');} function mFirstVal(obj, keys, fallback=''){ for(const k of keys){ const v=obj && obj[k]; if(v!==undefined && v!==null && String(v).trim()!=='') return v; } return fallback; } function mFormatDateTime(v){ if(!v) return ''; const s=String(v); const d=new Date(s); if(Number.isNaN(d.getTime())) return s; return d.toLocaleString('zh-TW',{year:'numeric',month:'2-digit',day:'2-digit',weekday:'short',hour:'2-digit',minute:'2-digit',hour12:false}).replace(/\//g,'/'); } function mSessionForReg(r){ const sid=String(r?.sessionId||r?.session_id||''); const list=[...(Array.isArray(ST.allSessionsData)?ST.allSessionsData:[]), ...(Array.isArray(ST.sessions)?ST.sessions:[])]; return list.find(s=>String(s.id||s.sessionId||'')===sid) || {}; } function mSessionDateLine(d){ if(!d) return ''; const date=String(d.date||d.day||'').replace(/-/g,'/'); const start=d.start||d.startTime||''; const end=d.end||d.endTime||''; return [date, (start||end)?`${start}${end?' - '+end:''}`:''].filter(Boolean).join(' '); } function mRecordDateText(r){ const s=mSessionForReg(r); const dates=Array.isArray(s.dates)&&s.dates.length?s.dates:(Array.isArray(r.sessionDates)?r.sessionDates:[]); const selected=Array.isArray(r.selectedDates)?r.selectedDates.map(String).filter(Boolean):[]; if(dates.length){ const chosen=selected.length ? dates.filter(d=>selected.includes(String(d.date||d.day||''))) : dates; const lines=(chosen.length?chosen:dates).map(mSessionDateLine).filter(Boolean); if(lines.length) return lines.join('、'); } const sessionRaw=s.date||s.startDate||s.eventDate||s.sessionDate||''; const sessionTime=s.startTime||s.time||''; if(sessionRaw) return [String(sessionRaw).replace(/-/g,'/'), sessionTime].filter(Boolean).join(' '); const selectedText=selected.length ? selected.join('、') : ''; return selectedText || mFirstVal(r,['dateText','sessionDateText','timeText','eventTime','date','sessionDate'], '') || (r.createdAt ? '報名日期:'+mFormatDateTime(r.createdAt) : '時間請見活動公告'); } function mRecordLocationText(r){ const s=mSessionForReg(r); return mFirstVal(r,['location','sessionLocation','venue','sessionVenue','place','address'], '') || mFirstVal(s,['location','sessionLocation','venue','place','address'], '活動地點請見公告'); } function mRecordCover(r){ const s=mSessionForReg(r); const skin = window.activeBrandSkin || defaultBrandSkin || {}; return mFirstVal(r,['coverUrl','cover','image','imageUrl','sessionCover','eventCover'], '') || mFirstVal(s,['coverUrl','cover','imageUrl','imgUrl','sessionCover','eventCover'], '') || skin.mascotPortal || 'IP指引.png'; } function mShortDateRange(v){ const s=String(v===undefined||v===null?'':v).trim(); if(!s) return '未設定'; const dates=[]; const re=/(20\d{2})[\/\-\.年](\d{1,2})[\/\-\.月](\d{1,2})|(^|[^0-9])(\d{1,2})[\/\-\.月](\d{1,2})(?=[^0-9]|$)/g; let m; while((m=re.exec(s))){ const mo=m[2]||m[5]; const da=m[3]||m[6]; if(mo&&da) dates.push(Number(mo)+'/'+Number(da)); } const uniq=[]; dates.forEach(d=>{ if(!uniq.includes(d)) uniq.push(d); }); if(uniq.length>1) return uniq[0]+'-'+uniq[uniq.length-1]; if(uniq.length===1) return uniq[0]; return s.replace(/20\d{2}[\/\-\.年]/g,'').replace(/月/g,'/').replace(/日/g,'').replace(/\s+/g,' ').trim() || '未設定'; } function mEquipTextIsUseful(v){ const s=String(v===undefined||v===null?'':v).trim(); return !!s && !/^(無|沒有|未加購|不需|none)$/i.test(s); } function mRecordIncludedEquipText(r){ return String(mFirstVal(r,['preNoticeIncludedEquipmentText','includedEquipmentText','included_equipment_text','baseEquipmentText','base_equipment_text'],'')||'').trim(); } function mRecordExtraEquipText(r){ return String(mFirstVal(r,['preNoticeExtraEquipmentText','extraEquipmentText','extra_equipment_text'],'')||'').trim(); } function mRecordEquipText(r){ // 正式設備顯示優先使用 Worker 從資料庫整理好的總設備:內含數量×攤位數+加購設備。 const direct=String(mFirstVal(r,['preNoticeEquipmentText','totalEquipmentText','equipmentText','wholeEquipmentText','dailyEquipmentText','equipText','equipment_text','daily_equipment_text'],'')||'').trim(); if(mEquipTextIsUseful(direct)) return direct; // 總設備一律以 Worker 從資料庫算好的為準(內含設備 × 攤位數 + 加租設備)。 // 不可用前端自行推算的加購清單頂替,否則會漏掉場次內含的桌椅(曾造成「只顯示椅子」)。 const included=mRecordIncludedEquipText(r); const extra=mRecordExtraEquipText(r); const hasIncluded=mEquipTextIsUseful(included); const hasExtra=mEquipTextIsUseful(extra); if(hasIncluded && hasExtra) return `${included},另加 ${extra}`; if(hasIncluded) return included; if(hasExtra) return `加購:${extra}`; return '本場不含設備(請自備)'; } function mCleanEquipDisplayText(txt){ let s=String(txt||'').trim(); s=s.replace(/^總設備\s*[::]\s*/,'').replace(/^加購\s*[::]\s*/,'').trim(); // 後端算出「無」=該場內含設備為 0 且攤友未加租 → 就是不含設備,不是載入失敗 if(s==='無' || s==='依本場設定') return '本場不含設備(請自備)'; if(!s) return '本場不含設備(請自備)'; return s; } function mPreNoticeEquipDisplayText(r){ const raw = mRecordEquipText(r); return mCleanEquipDisplayText(raw); } function mRecordAddonText(r){ return String(mFirstVal(r,['addonText','addonsText','selectedAddonsText','addonQtyText','addon_text','addons_text','addon_qty_text'],'無')||'無'); } function mRecordStallText(r){ const v=mFirstVal(r,['stallCount','stall_count','stalls','boothCount','booth_count'],''); return v!=='' ? (String(v).replace(/攤$/,'')+'攤') : '未設定'; } function mRecordPayText(r){ const v=mFirstVal(r,['total','totalAmount','amount','receivableAmount','registration_total_amount'],''); if(v!=='' && v!==undefined && v!==null && !Number.isNaN(Number(v))){ let t='NT$ '+Number(v).toLocaleString('zh-TW'); // 已繳與應收有落差(併入套組補差額)時要講清楚,否則攤友會以為要再付一次全額。 const paid=Number(r.paid||0), due=Number(r.due||0); if(paid>0 && due>0) t+='(已繳 NT$ '+paid.toLocaleString('zh-TW')+',待補繳 NT$ '+due.toLocaleString('zh-TW')+')'; return t; } return String(r.payStatus||r.paymentStatus||'請聯繫客服'); } function canViewPreNoticeReg(reg){ if(!reg || isCancelledReg(reg)) return false; const ps=String(reg.payStatus||reg.paymentStatus||'').trim(); return isPaidText(ps) || ps==='免費'; } function officialGroupForReg(regOrId){ const reg=typeof regOrId==='object'&®OrId ? regOrId : findMyRegById(regOrId); const group=reg&®.officialGroup&&typeof reg.officialGroup==='object' ? reg.officialGroup : null; if(!group || group.enabled!==true || !/^https:\/\//i.test(String(group.url||''))) return null; return group; } // 保留既有公開函式與 fallback;新增的已繳費卡片只會優先使用該筆報名的受保護設定。 function officialGroupUrl(regOrId){ const protectedGroup=officialGroupForReg(regOrId); if(protectedGroup&&protectedGroup.url) return protectedGroup.url; const t=ST.tenant||{}; const fromDb=t.officialGroupUrl || t.official_group_url || t.bigGroupUrl || t.big_group_url || t.groupUrl || t.group_url || ''; if(fromDb) return fromDb; if(String(TENANT_ID||'')==='tuibile') return 'https://line.me/ti/g2/cp-K_Los4J2zBc6rGcRA14TJCx3e99v0i4p-hQ?utm_source=invitation&utm_medium=link_copy&utm_campaign=default'; return LINE_URL || ''; } function officialGroupName(regOrId){ const protectedGroup=officialGroupForReg(regOrId); if(protectedGroup&&protectedGroup.name) return protectedGroup.name; const t=ST.tenant||{}; return t.officialGroupName || t.official_group_name || '全台市集藝文資訊中心 大群組'; } function openOfficialGroup(regId){ const group=officialGroupForReg(regId); const url=(group&&group.url)||officialGroupUrl(); if(!url){ toast('尚未取得已繳費的大群組邀請,請重新整理我的紀錄'); return; } window.open(url,'_blank','noopener'); } function officialGroupCardHtml(reg){ const group=officialGroupForReg(reg); if(!group) return ''; return `
📣 ${escHtml(group.name||'活動大群組')}
${escHtml(group.inviteText||'請點選以下按鈕加入社群!')}
${group.password?`
加入密碼:${escHtml(group.password)}
`:''}
`; } function openPreNoticeSupport(id){ // 直接把說明疊在「行前通知」上面(不關閉行前通知,其 z-index 8000 > 5000)。 // 看完關閉說明即回到原本三顆按鈕,可接著點下一個,不再繞回我的紀錄。 try{ if(typeof openSupportFaqCard === 'function' && document.getElementById(id)){ openSupportFaqCard(id); }else{ toast('說明開啟失敗,請至客服頁查看'); } }catch(e){ console.warn('[openPreNoticeSupport]', e); toast('說明開啟失敗,請至客服頁查看'); } } async function openPreEventNotice(regId){ const r=findMyRegById(regId); if(!r){ toast('找不到報名資料,請重新整理我的紀錄'); return; } const body=$('preNoticeBody'); if(!body) return; const group=officialGroupForReg(r); const groupBtn=group?``:''; body.innerHTML=`
場次${escHtml(r.sessionName||r.sesName||'場次')}
日期${escHtml(mRecordDateText(r))}
地點${escHtml(mRecordLocationText(r))}
報到時間活動前 60 分鐘開始報到
攤位${escHtml(mRecordStallText(r))}
設備${escHtml(mPreNoticeEquipDisplayText(r))}
已完成繳費,請務必查看下方行前規範。後續提醒與現場異動,均以官方大群組統一公告為主。
${groupBtn}
`; openModal('preNoticeModal'); const mapBox=$('preNoticeSeatMap'); if(!mapBox||!r.seatMapUrl||(!r.stallNumber&&!(Array.isArray(r.dayPositions)&&r.dayPositions.length)))return; mapBox.innerHTML='
🗺️ 位置圖(金色標記是您的位置)
正在載入正式排位…
'; try{ const seatData=await api({action:'getSeatMap',sessionId:r.sessionId,regId:r.id,email:getMemberEmail(),phone:getMemberPhone(),_t:Date.now()}); if(seatData&&seatData.error)throw new Error(seatData.error); const seats=Array.isArray(seatData&&seatData.seats)?seatData.seats:[]; // 手機位置圖:依「第幾天」分開顯示。每天只把「你那天的攤位」標成金色,其餘為小灰點。 // 兩天不同位置時不會再同時冒出兩個金點;切換日期只換金點。報兩攤則兩格同時金色、緊鄰=連攤。 const splitCodes=v=>String(v==null?'':v).split(/[,,、\s]+/).map(x=>x.trim()).filter(Boolean); const days=Array.isArray(r.dayPositions)&&r.dayPositions.length ? r.dayPositions.map(d=>({date:String(d.date||''),codes:new Set(splitCodes(d.stallNumber))})) : [{date:'',codes:new Set(splitCodes(r.stallNumber))}]; const fmtDate=d=>String(d||'').replace(/^\d{4}-/,'').replace(/-/g,'/'); const drawMarkers=codeSet=>seats.filter(s=>Number(s.x)>0||Number(s.y)>0).map(s=>{ const mine=codeSet.has(String(s.code||'')); const size=mine?'min-width:46px;height:34px;padding:3px 6px;font-size:12px;':'width:14px;height:10px;padding:0;font-size:0;line-height:0;'; return `${mine?escHtml(s.code||''):''}`; }).join(''); const tabsHtml=days.length>1 ? '
' + days.map((d,i)=>``).join('') + '
' : ''; const stageHtml=codeSet=>`
場地位置圖${drawMarkers(codeSet)}
`; mapBox.innerHTML=`
🗺️ 位置圖(金色是您的攤位${days.length>1?',可切換日期':''})
${tabsHtml}${stageHtml(days[0].codes)}`; if(days.length>1){ mapBox.querySelectorAll('#preNoticeSeatTabs button').forEach(btn=>{ btn.addEventListener('click',()=>{ const sel=Number(btn.getAttribute('data-day'))||0; mapBox.querySelectorAll('#preNoticeSeatTabs button').forEach((b,i)=>{b.style.background=(i===sel?'#ffd76a':'#fff');}); const stage=mapBox.querySelector('#preNoticeSeatStage'); if(stage)stage.innerHTML=`場地位置圖${drawMarkers(days[sel].codes)}`; }); }); } }catch(e){ mapBox.innerHTML=`
🗺️ 位置圖
場地位置圖
目前僅顯示正式底圖,請以上方攤位編號為準。
`; } } function mRecordFixedBlockHtml(r){ const date=mShortDateRange(mRecordDateText(r)); const stalls=mRecordStallText(r); const equip=mRecordEquipText(r); const addon=mRecordAddonText(r); const pay=mRecordPayText(r); return `
${escHtml(date)}${escHtml(stalls)}
設備:${escHtml(mPreNoticeEquipDisplayText(r))}
繳費(含押金):${escHtml(pay)}
`; } // ── MEMBER_NOTIFICATIONS_20260809 ─────────────────────────────── let MemberNotifications=[]; function memberRecordUnreadCount(regId){ return (MemberNotifications||[]).filter(n=>!n.isRead&&String(n.registrationId||'')===String(regId||'')).length; } function memberRecordUnreadHtml(regId){ const n=memberRecordUnreadCount(regId); return n?`
🔔 ${n} 則新通知
`:''; } function memberNotifyCredentials(){ return {email:String(ST.memberEmail||memberSafeEmail()||'').trim(),phone:String(getMemberPhone()||'').trim()}; } async function loadMemberNotifications(){ const c=memberNotifyCredentials(); if(!c.email||!c.phone){MemberNotifications=[];renderMemberNotifications();return;} try{ const r=await api({action:'memberNotifications',email:c.email,phone:c.phone,_t:Date.now()}); MemberNotifications=Array.isArray(r)?r:[]; }catch(e){MemberNotifications=[];} renderMemberNotifications(); } function renderMemberNotifications(){ const unread=(MemberNotifications||[]).filter(n=>!n.isRead).length; const badge=$('memberNotifyCount'); if(badge){badge.textContent=String(unread);badge.classList.toggle('hidden',unread<=0);} const box=$('memberNotifyList'); if(box){ box.innerHTML=MemberNotifications.length ? MemberNotifications.map(n=>`
${escHtml(n.title||'通知')}
${escHtml(n.message||'')}
`).join('') : '
目前沒有通知
'; } if(Array.isArray(ST.regs)&&ST.regs.length) renderMyRegs(ST.regs); } function toggleMemberNotifications(){ const p=$('memberNotifyPanel'); if(p)p.classList.toggle('hidden'); } async function openMemberNotification(id,regId){ const c=memberNotifyCredentials(); try{await apiPost({action:'markMemberNotificationRead',id,email:c.email,phone:c.phone});}catch(e){} const n=MemberNotifications.find(x=>String(x.id)===String(id)); if(n)n.isRead=true; renderMemberNotifications(); const p=$('memberNotifyPanel');if(p)p.classList.add('hidden'); if(regId){ setTimeout(()=>{const el=document.querySelector('[data-registration-id="'+CSS.escape(String(regId))+'"]');if(el)el.scrollIntoView({behavior:'smooth',block:'center'});},50); } } async function markAllMemberNotificationsRead(){ const c=memberNotifyCredentials(); try{await apiPost({action:'markMemberNotificationRead',all:true,email:c.email,phone:c.phone});}catch(e){} MemberNotifications.forEach(n=>n.isRead=true);renderMemberNotifications(); } function mReviewPill(r){ const st=String(r.status||r.reviewStatus||'待審核').trim(); const ps=String(r.payStatus||'').trim(); const ts=String(r.transferStatus||'').trim(); if(ts==='已轉場') return {text:'已轉場',icon:'→',bg:'#e8f7ee',color:'#2a6a10',border:'#b9d3a5',note:'原場紀錄已保留,請查看新的場次報名'}; if(ts==='已轉活動金') return {text:'已轉活動金',icon:'$',bg:'#eef6ff',color:'#24638a',border:'#b7d8ea',note:'資料庫已確認收款總額(包含押金)已轉入活動金'}; if(['已退費','refunded'].includes(ts) || ['已退費','已退款'].includes(ps)) return {text:'已退費',icon:'✓',bg:'#f3f4f6',color:'#777',border:'#e5e7eb',note:'本筆退費已完成'}; if(['申請退費','退費中','refund_pending'].includes(ts)) return {text:'退費中',icon:'⏱',bg:'#fdecea',color:'#c0392b',border:'#f3b5b5',note:'主辦正在處理退費'}; if(isCancelledReg(r)) return {text:'已取消', icon:'✕', bg:'#f3f4f6', color:'#777', border:'#e5e7eb', note:'此筆報名已取消'}; const total=Number(r.total||0)||0, paid=Number(r.paid||0)||0; const due=Number.isFinite(Number(r.due))?Math.max(0,Number(r.due)):Math.max(0,total-paid); if(paid>0 && due>0) return {text:'待補款',icon:'$',bg:'#fff8ed',color:'#d97706',border:'#f5d7a1',note:'尚需補繳 NT$ '+due.toLocaleString()}; if(isPaidText(ps) || ps==='免費') return {text:ps==='免費'?'免費':'已完成', icon:'✓', bg:'#e8f7ee', color:'#2a6a10', border:'#b9d3a5', note:'感謝參與,期待下次見面!'}; if(isPendingPayText(ps)) return {text:'待確認', icon:'⏱', bg:'#fff8ed', color:'#b45309', border:'#f5d7a1', note:'已回報,等待主辦確認'}; if(st==='已錄取') return {text:'待付款', icon:'$', bg:'#fff8ed', color:'#d97706', border:'#f5d7a1', note:r.paymentDeadline?('請於 '+escHtml(r.paymentDeadline)+' 前完成付款'):'請依錄取通知完成付款'}; if(st==='待審核') return {text:'審核中', icon:'⏱', bg:'#eef6e6', color:'#2a6a10', border:'#b9d3a5', note:'主辦單位審核中,請耐心等候'}; if(st==='不錄取') return {text:'未錄取', icon:'–', bg:'#fdecea', color:'#c0392b', border:'#f3b5b5', note:'可提前報名其他場次'}; return {text:st||'處理中', icon:'•', bg:'#eef2ff', color:'#4f46e5', border:'#c7d2fe', note:''}; } function renderMyRegs(regs){ ST.regs = Array.isArray(regs) ? regs : []; const el=$('myRegsList'); const helper=$('memberRecordHelper'); if(!el) return; if(!ST.regs.length){ el.innerHTML='
目前沒有報名紀錄。
若剛完成報名,請稍候幾秒後再點一次「我的紀錄」。
'; if(helper) helper.classList.add('show'); handlePendingPayLink(ST.regs); return; } if(helper) helper.classList.add('show'); const creditBalance=Math.max(0,Number(ST.regs[0]&&ST.regs[0].activityCreditBalance||0)||0); const creditLedger=Array.isArray(ST.regs[0]&&ST.regs[0].activityCreditLedger)?ST.regs[0].activityCreditLedger:[]; const creditSummary=`
會員活動金
可用餘額 NT$ ${creditBalance.toLocaleString()}
${creditLedger.length?`
查看活動金明細${creditLedger.map(x=>`
${escHtml(x.note||(x.direction==='debit'?'活動金扣抵':'活動金轉入'))}
${escHtml(String(x.createdAt||'').replace('T',' ').slice(0,16))}|${escHtml(x.status||'')}
${x.direction==='debit'?'-':'+'} NT$ ${Number(x.amount||0).toLocaleString()}
`).join('')}
`:'
目前沒有活動金異動紀錄
'}
`; // 組合套組(bundle_group_id 相同)在攤友端合併為「一張卡片」,卡內列出各場日期/地點/設備。 // 拆帳、分場次名單、分別行前通知都是後台的事;攤友看到兩張卡會誤以為可以只退一場來省錢。 const _shown = new Set(); const _units = []; for(const r of (ST.regs||[])){ const g = bundleGroupOf(r); if(g){ if(_shown.has(g)) continue; _shown.add(g); const legs = (ST.regs||[]).filter(x=>bundleGroupOf(x)===g); _units.push(legs.length>1 ? {bundle:true, legs, head:legs[0]} : {bundle:false, head:r}); } else { _units.push({bundle:false, head:r}); } } el.innerHTML=creditSummary+_units.map(u=>{ const r=u.head; const legs=u.bundle?u.legs:[r]; const pill=mReviewPill(r); const title=u.bundle ? `🎫 ${escHtml(bundleTitleOf(legs))}(${legs.length} 場)` : escHtml(r.sessionName||r.sesName||'場次'); const locationText=escHtml(mRecordLocationText(r)); const cover=escHtml(mRecordCover(r)); const note=escHtml(pill.note||''); const totalAll=legs.reduce((s,x)=>s+Number(x.total||0),0); const canPay=legs.some(x=>canPayReg(x)); const canCancel=legs.every(x=>canDirectCancelReg(x)); const refundTarget=legs.find(x=>canRefundReg(x))||r; const canRefund=legs.some(x=>canRefundReg(x)), canPreNotice=legs.some(x=>canViewPreNoticeReg(x)); const groupReg=legs.find(x=>officialGroupForReg(x))||null; const resolution=r.resolution||null; const resolutionMsg=resolution?`
${String(resolution.mode||'')==='transfer'?'已轉移至其他場次':'已轉為活動金'}
${Number(resolution.credit_created||0)>0?'活動金增加 NT$ '+Number(resolution.credit_created).toLocaleString()+'
':''}${Number(resolution.due_amount||0)>0?'新場待補 NT$ '+Number(resolution.due_amount).toLocaleString()+'
':''}${Number(resolution.deposit_refund_due||0)>0?'待退押金 NT$ '+Number(resolution.deposit_refund_due).toLocaleString()+'
':''}目前活動金餘額:NT$ ${Number(r.activityCreditBalance||0).toLocaleString()}
`:''; const statusMsg = resolutionMsg || (isCancelledReg(r) ? `
已取消,報名紀錄保留
` : (isRefundingReg(r) ? `
退費處理中,等待主辦確認退款${legs.length>1?'(組合套組整組一起退)':''}。名額已釋出,無需再操作。
` : (isPendingPayText(r.payStatus) ? `
已回報,等待主辦確認
` : ''))); let actions=''; if(canPay){ actions+= u.bundle ? `` : ``; } // 組合為綁定優惠:整組一起繳、一起退,故不提供單筆勾選 const cartBox = (canPay && !u.bundle) ? `` : ''; if(canPreNotice){ actions+= u.bundle ? legs.filter(x=>canViewPreNoticeReg(x)).map(x=>``).join('') : ``; } if(canCancel) actions+=``; if(canRefund) actions+=``; const bodyBlock = u.bundle ? bundleLegsHtml(legs, totalAll) : mRecordFixedBlockHtml(r); const seatBlock = u.bundle ? legs.map(x=>stallSelectionHtml(x)).join('') : stallSelectionHtml(r); const idLine = u.bundle ? `
報名編號:${legs.map(x=>escHtml(x.id)).join('、')}
` : `
報名編號:${escHtml(r.id)}
`; return `
${memberRecordUnreadHtml(r.id)}
${title}
${u.bundle?'
組合優惠:一起繳費、一起退費
':`
◎ ${locationText}
`}
${pill.icon} ${pill.text} ${note}
${bodyBlock} ${statusMsg} ${groupReg?officialGroupCardHtml(groupReg):''} ${seatBlock} ${cartBox} ${actions?`
${actions}
`:''} ${idLine}
`; }).join(''); el.className='m-record-list m-record-grid'; el.style.cssText=''; handlePendingPayLink(ST.regs); updatePayCart(); } // ── 合併繳費(購物車):勾選多筆一起繳,省手續費 ── function removePayCartBar(){ const b=document.getElementById('payCartBar'); if(b) b.remove(); } function updatePayCart(){ // 僅在「我的紀錄」頁顯示;其他頁一律移除,避免長駐畫面底部 const cbs=[...document.querySelectorAll('.m-cart-cb')]; if(!cbs.length){ removePayCartBar(); return; } const picked=cbs.filter(c=>c.checked).map(c=>c.value); let bar=$('payCartBar'); if(picked.length<2){ if(bar) bar.remove(); return; } const total=picked.reduce((s,id)=>{ const r=(ST.regs||[]).find(x=>String(x.id)===String(id)); return s+regOutstandingAmount(r); },0); if(!bar){ bar=document.createElement('div'); bar.id='payCartBar'; bar.style.cssText='position:fixed;left:0;right:0;bottom:0;z-index:900;background:#fff;border-top:2px solid var(--g);box-shadow:0 -4px 20px rgba(0,0,0,.12);padding:12px 16px calc(12px + env(safe-area-inset-bottom));display:flex;align-items:center;gap:12px;justify-content:space-between;flex-wrap:wrap'; document.body.appendChild(bar); } bar.innerHTML=`
已選 ${picked.length} 場|合計 NT$${total.toLocaleString()}
`; } // 組合付款:自動把同組未繳的場次一起帶入合併繳費 function startBundlePay(regId){ const reg=(ST.regs||[]).find(x=>String(x.id)===String(regId)); if(!reg){ toast('找不到報名'); return; } const group=bundleUnpaidRegs(reg); if(group.length<=1){ showPayPage(reg.id, reg.sessionName||'', regOutstandingAmount(reg)); return; } const total=group.reduce((s,r)=>s+regOutstandingAmount(r),0); ST_mergedPayIds=group.map(r=>String(r.id)); showPayPage(group[0].id, `組合繳費(共 ${group.length} 場)`, total, {merged:true, reg:group[0]}); } function getPayCartIds(){ return [...document.querySelectorAll('.m-cart-cb')].filter(c=>c.checked).map(c=>c.value); } function startMergedPay(){ const ids=getPayCartIds(); if(ids.length<2){ toast('請至少勾選兩場'); return; } const total=ids.reduce((s,id)=>{ const r=(ST.regs||[]).find(x=>String(x.id)===String(id)); return s+regOutstandingAmount(r); },0); const first=(ST.regs||[]).find(x=>String(x.id)===String(ids[0])); ST_mergedPayIds=ids; showPayPage(ids[0], `合併繳費(共 ${ids.length} 場)`, total, {merged:true, reg:first}); } function handlePendingPayLink(regs){ const regId = window._pendingPayRegId || ''; if(!regId) return; const reg = (Array.isArray(regs)?regs:[]).find(r=>String(r.id)===String(regId)); if(!reg) return; window._pendingPayRegId = ''; setTimeout(()=>{ if(canPayReg(reg)) showPayPage(reg.id, reg.sessionName||reg.sesName||'場次', regOutstandingAmount(reg)); else openRegDetail(reg.id); }, 300); } function openSeatMapZoom(kind){ const src = kind==='picker' ? (ST._stallPickerMapUrl||'') : (ST._seatPreviewMapUrl||''); if(!src){ toast('目前沒有可放大的場地圖'); return; } ST._seatMapZoomScale=1.8; const img=$('seatMapZoomImg'); if(img){ img.src=src; img.style.width=(ST._seatMapZoomScale*100)+'%'; } const title=$('seatMapZoomTitle'); if(title) title.textContent='放大查看場地圖'; openModal('seatMapZoomModal'); setTimeout(()=>{ const sc=$('seatMapZoomScroller'); if(sc){ sc.scrollLeft=Math.max(0,(sc.scrollWidth-sc.clientWidth)/2); sc.scrollTop=0; } },80); } function adjustSeatMapZoom(delta){ ST._seatMapZoomScale=Math.max(1, Math.min(4, Number(ST._seatMapZoomScale||1.8)+Number(delta||0))); const img=$('seatMapZoomImg'); if(img) img.style.width=(ST._seatMapZoomScale*100)+'%'; } async function previewSeatMap(sessionId){ openModal('seatPreviewModal'); const body=$('seatPreviewBody'); ST._seatPreviewMapUrl=''; if(body) body.innerHTML='
載入場地圖…
'; try{ const res=await api({action:'getSeatMap',sessionId,_t:Date.now()}); if(res.error){ body.innerHTML='
'+escHtml(res.error)+'
'; return; } const seats=Array.isArray(res.seats)?res.seats:[]; const map=res.mapUrl||''; ST._seatPreviewMapUrl=map; const stats={auto:0,paid:0,service:0,closed:0}; seats.forEach(s=>{ const t=s.type||'auto'; if(stats[t]!=null) stats[t]++; }); const paidSeats=seats.filter(s=>s.type==='paid'&&s.active&&!s.occupied); const paidList=paidSeats.map(s=>{ const code=String(s.code||s.stallNo||'').trim(); const price=Number(s.price||0); return `
${escHtml(code)}
錄取後可選${price>0?`
+NT$${price}`:''}
`; }).join(''); const occupiedPaid=seats.filter(s=>s.type==='paid'&&s.active&&s.occupied).length; const mapBlock=map ? `
` : `
此場次尚未上傳圖片;下方攤位號碼仍由資料庫載入並正常顯示。
`; body.innerHTML=mapBlock +`
號碼狀態:自動 ${stats.auto}|加價可選 ${paidSeats.length}|加價已占用 ${occupiedPaid}|不開放 ${stats.closed}|服務台 ${stats.service}
本畫面僅供報名前查看位置配置;正式加價選位請依「我的紀錄」顯示的狀態操作。
可選的加價位置
${paidList||'
此場次目前沒有可選的加價位置。
'}
自動排位號碼只供查看,不開放指定;若不加價,系統將依付款確認順序安排位置。
`; }catch(e){ body.innerHTML='
場地圖載入失敗
'; } } function normalizeStallStatus(s){ const v=String(s||'').trim(); if(v==='paid') return '已選走'; if(v==='reserved') return '預留中'; return v||'可選'; } async function openStallPickerForReg(regId){ const reg=(ST.regs||[]).find(r=>String(r.id)===String(regId)); if(!reg){ toast('找不到報名紀錄'); return; } if(!canSelectStallReg(reg)){ toast('目前狀態不可選位'); return; } if(reg.stallNumber){ toast('已完成選位,如需換位請聯繫主辦'); return; } ST._stallPick={reg, selected:[]}; $('stallPickerTitle').textContent='加價選位|'+(reg.sessionName||reg.sesName||'場次'); $('stallGrid').innerHTML='
載入攤位中…
'; if($('stallPickerMap')) $('stallPickerMap').innerHTML=''; $('stallConfirmBtn').disabled=true; $('stallConfirmBtn').style.opacity='.5'; openModal('stallPickerModal'); try{ const res=await api({action:'getSeatMap',sessionId:reg.sessionId,regId:reg.id,email:getMemberEmail(),phone:getMemberPhone(),_t:Date.now()}); if(res.error){ $('stallGrid').innerHTML='
'+escHtml(res.error)+'
'; return; } const paidSeats=(Array.isArray(res.seats)?res.seats:[]).filter(s=>s.type==='paid'&&s.active); const list=paidSeats.map(s=>s.code); // B-02:公開 API 不再回傳 regId/email;「這格是不是我的」改由後端驗證後給 mine 旗標。 const taken={}; paidSeats.forEach(s=>{ if(s.occupied) taken[s.code]={status:s.status==='預留'?'reserved':'paid',mine:!!s.mine}; }); ST._stallPick.seatMap=res; ST._stallPick.seatMeta=Object.fromEntries(paidSeats.map(s=>[s.code,s])); ST._stallPickerMapUrl=res.mapUrl||''; if($('stallPickerMap')) $('stallPickerMap').innerHTML=res.mapUrl?`
`:`
此場次尚未上傳已編號場地圖,請依號碼清單選擇。
`; if(!list.length){ $('stallGrid').innerHTML='
此場次尚未建立可選攤位
'; return; } // SEATFULL_20260725:有加價位、但全部已被占用(不是自己的)→ 依規則顯示額滿,不給選。 if(!paidSeats.some(s=>!s.occupied || s.mine)){ $('stallGrid').innerHTML='
本場自由選位已額滿。
'; $('stallConfirmBtn').disabled=true; $('stallConfirmBtn').style.opacity='.5'; return; } ST._stallPick.stallList=list; ST._stallPick.taken=taken; renderStallPickerGrid(); }catch(e){ $('stallGrid').innerHTML='
攤位載入失敗
'; } } function renderStallPickerGrid(){ const pick=ST._stallPick||{}; const reg=pick.reg||{}; const max=Math.max(1,Number(reg.stallCount||1)); const taken=pick.taken||{}; const selected=pick.selected||[]; $('stallPickerHint').textContent=`請選擇 ${max} 個加價位置。只有加價位置可選;逾時未付款會自動釋出。`; const summary=$('stallSelectedSummary'); if(summary){ if(selected.length){ const fee=selected.reduce((sum,code)=>sum+Number((pick.seatMeta&&pick.seatMeta[code]&&pick.seatMeta[code].price)||0),0); summary.style.background='#e8f5ec'; summary.style.borderColor='#9fb59c'; summary.style.color='#1b4332'; summary.innerHTML=`✅ 目前已選:${selected.map(escHtml).join('、')}${fee>0?`|加價 NT$${fee}`:''}`; }else{ summary.style.background='#fff8ed'; summary.style.borderColor='#f59e0b'; summary.style.color='#7c2d12'; summary.textContent='尚未選擇位置,請點下方號碼。'; } } $('stallGrid').innerHTML=(pick.stallList||[]).map(num=>{ const info=taken[num]; const mine=!!(info && info.mine); const blocked=info && !mine; const on=selected.includes(num); const price=(pick.seatMeta&&pick.seatMeta[num]&&Number(pick.seatMeta[num].price)>0)?Number(pick.seatMeta[num].price):0; const bg=blocked?(info.status==='reserved'?'#fff8ed':'#fdecea'):(on?'#9fb59c':'#fff'); const color=blocked?(info.status==='reserved'?'#b45309':'#c0392b'):(on?'#fff':'var(--dk)'); const border=on?'#1b4332':(blocked?'#f3b5ad':'#cfe5d6'); const label=blocked?normalizeStallStatus(info.status):(on?'✅ 已選':'可選'); const shadow=on?'box-shadow:0 0 0 3px rgba(45,106,79,.18);':''; return ``; }).join(''); const btn=$('stallConfirmBtn'); btn.disabled=selected.length!==max; btn.style.opacity=btn.disabled?'.55':'1'; btn.textContent=selected.length===max?`確認選擇:${selected.join('、')}`:'已選 '+selected.length+'/'+max; } function toggleStallPick(num){ const pick=ST._stallPick||{}; const reg=pick.reg||{}; const max=Math.max(1,Number(reg.stallCount||1)); const arr=pick.selected||[]; const idx=arr.indexOf(num); if(idx>=0) arr.splice(idx,1); else{ if(arr.length>=max){ toast('選位數量不可超過報名數量'); return; } arr.push(num); } pick.selected=arr; ST._stallPick=pick; renderStallPickerGrid(); } async function submitSelectedStalls(){ const pick=ST._stallPick||{}; const reg=pick.reg||{}; const selected=pick.selected||[]; const max=Math.max(1,Number(reg.stallCount||1)); if(selected.length!==max){ toast('請選滿 '+max+' 個攤位'); return; } const btn=$('stallConfirmBtn'); if(btn){ btn.disabled=true; btn.textContent='送出中…'; } try{ const res=await apiPost({action:'claimPaidSeat',regId:reg.id,sessionId:reg.sessionId,seats:selected,email:getMemberEmail(),phone:getMemberPhone()}); if(res.error){ toast('❌ '+res.error); return; } closeModal('stallPickerModal'); toast('✅ 已完成加價選位'); if(res.message) alert(res.message); if(ST.memberEmail) await safeGetMyRegs(ST.memberEmail); }finally{ if(btn){ btn.disabled=false; btn.textContent='確認選位'; } } } function openRegDetail(regId){ const reg=(ST.regs||[]).find(r=>r.id===regId); if(!reg) return; const pb=payBadge(reg); const canPay = canPayReg(reg); const canCancel = canDirectCancelReg(reg); const canRefund = canRefundReg(reg); const canPreNotice = canViewPreNoticeReg(reg); const modal=document.createElement('div'); modal.id='regDetailModal'; modal.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:9000;display:flex;align-items:flex-end;justify-content:center'; modal.innerHTML=`
報名詳情
${escHtml(reg.sessionName||reg.sesName||'場次')}
繳費${pb.t}
審核:${escHtml(reg.status||'')}
攤位數:${reg.stallCount||1} 攤
${reg.seatPricingEnabled ? ( ((Array.isArray(reg.dayPositions)&®.dayPositions.length)||reg.stallNumber) ? publishedSeatHtml(reg)+(reg.seatMapUrl?`
場地圖
場地圖
`:'') : ((canViewPreNoticeReg(reg) && String(reg.seatChoiceIntent||'auto')!=='paid') ? `
攤位號碼:將於活動前公布
排定後會在此顯示,並隨行前通知一起寄給您
` : '') ) : ''} ${reg.equipmentText?`
設備:${escHtml(reg.equipmentText)}
`:''} ${reg.total>0?`
應繳金額:NT$ ${Number(reg.total||0).toLocaleString()}
`:''} ${reg.transferStatus?`
退款狀態:${escHtml(reg.transferStatus)}
`:''}
報名編號:${reg.id}
${canPay?``:''} ${isCancelledReg(reg)?`
已取消,報名紀錄保留
`:(isPendingPayText(reg.payStatus)?`
已回報,等待主辦確認
`:'')} ${officialGroupCardHtml(reg)} ${stallSelectionHtml(reg)} ${canPreNotice?``:''} ${canCancel?``:''} ${canRefund?``:''}
`; modal.addEventListener('click', function(e){ if(e.target===modal) modal.remove(); }); history.pushState({regDetail:true},'',location.href); document.body.appendChild(modal); } async function cancelMyReg(regId, sesName, isRefund){ const cur=(ST.regs||[]).find(r=>String(r.id)===String(regId)); const gid=cur&&cur.bundleGroupId?String(cur.bundleGroupId):''; const groupRegs=gid?(ST.regs||[]).filter(r=>String(r.bundleGroupId||'')===gid):[]; const isBundle=groupRegs.length>1; $('cancelModalSesName').textContent=(isRefund?'申請取消/退款:':'取消報名:')+'「'+sesName+'」'; $('cancelConfirmCheck').checked=false; $('cancelSubmitBtn').disabled=true; $('cancelSubmitBtn').textContent=isBundle?(isRefund?'確認申請整組退款':'確認取消整組報名'):(isRefund?'確認申請退費':'確認取消報名'); const refundPolicy=$('cancelRefundPolicy'); const directPolicy=$('cancelDirectPolicy'); const confirmLabel=$('cancelConfirmLabel'); const bundleNotice=$('cancelBundleNotice'); if(refundPolicy) refundPolicy.style.display=isRefund?'block':'none'; if(directPolicy) directPolicy.style.display=isRefund?'none':'block'; if(bundleNotice){ if(isBundle){ const names=groupRegs.map(r=>escHtml(r.sessionName||r.sesName||'場次')).filter(Boolean); bundleNotice.style.display='block'; bundleNotice.innerHTML=`
此筆為組合報名。送出後,系統會由資料庫判斷同組報名並同步處理:
    ${names.map(n=>`
  • ${n}
  • `).join('')}
`; }else{ bundleNotice.style.display='none'; bundleNotice.innerHTML=''; } } if(confirmLabel) confirmLabel.textContent=isBundle ? (isRefund?'我確認要申請整組報名退款':'我確認要取消整組報名') : (isRefund?'我已了解取消與退款規則,仍要送出退款申請':'我確認要取消此報名'); ST._pendingCancelRegId=regId; ST._pendingCancelSesName=sesName; ST._pendingCancelIsRefund=!!isRefund; openModal('cancelModal'); } function onCancelCheckChange(){ const checked=$('cancelConfirmCheck').checked; const btn=$('cancelSubmitBtn'); btn.disabled=!checked; btn.style.opacity=checked?'1':'0.5'; btn.style.cursor=checked?'pointer':'not-allowed'; } async function doSubmitCancel(){ const regId=ST._pendingCancelRegId; if(!regId)return; const btn=$('cancelSubmitBtn'); if(btn){btn.disabled=true;btn.textContent='處理中…';} try{ const action=ST._pendingCancelIsRefund?'applyRefund':'cancelReg'; const em=getMemberEmail(); const res=await apiPost({action,regId,email:em,phone:getMemberPhone()}); if(res&&res.error){toast('❌ '+res.error,7000);return;} closeModal('cancelModal'); const n=Number(res&&(res.affectedCount||res.bundleCount)?(res.affectedCount||res.bundleCount):1); const groupMsg=n>1?`(同組 ${n} 筆)`:''; toast(ST._pendingCancelIsRefund?'✅ 已送出退款申請'+groupMsg:'✅ 已取消報名'+groupMsg); const refreshEmail = getMemberEmail(); if(refreshEmail){ ST.memberEmail = refreshEmail; await safeGetMyRegs(refreshEmail); } ST._pendingCancelRegId=''; ST._pendingCancelSesName=''; ST._pendingCancelIsRefund=false; }catch(e){ toast('❌ 取消失敗:'+(e&&e.message?e.message:e),6000); }finally{ if(btn){ const cur=(ST.regs||[]).find(r=>String(r.id)===String(ST._pendingCancelRegId)); const gid=cur&&cur.bundleGroupId?String(cur.bundleGroupId):''; const isBundle=gid&&(ST.regs||[]).filter(r=>String(r.bundleGroupId||'')===gid).length>1; btn.disabled=false; btn.textContent=isBundle?(ST._pendingCancelIsRefund?'確認申請整組退款':'確認取消整組報名'):(ST._pendingCancelIsRefund?'確認申請退費':'確認取消報名'); } } } // ── 繳費流程 ── let ST_currentPayRegId = ''; let ST_currentPayAmount = 0; let ST_mergedPayIds = []; let ST_currentPaymentCard = ''; let ST_currentPaymentInfo = null; function findMyRegById(regId){ return (ST.regs||[]).find(r=>String(r.id)===String(regId)) || null; } function paymentAllowedMethodsFromReg(reg){ // 以後端「可用付款方式」勾選旗標為唯一真實來源(與 worker.js _paymentMethodsAllowed 對齊)。 // 不可用「有沒有連結」推斷是否開放,否則主辦填了連結卻沒勾,攤友會選到被後端打回的方式。 const p=(reg&®.paymentProfile)||{}; const a=(reg&®.allowedPaymentMethods) || p.allowedMethods || {}; return {bank:a.bank!==false, linepay:!!a.linepay, card:!!a.card}; } function bankInfoHtmlFromPayment(reg){ const p=(reg&®.paymentProfile)||{}; const b=(reg&®.bankAccount)||p.bankAccount||{}; const lines=[]; if(p.paymentProfileName) lines.push('本場收款:'+p.paymentProfileName); if(b.bankName) lines.push('銀行:'+b.bankName); if(b.branchName) lines.push('分行:'+b.branchName); if(b.accountName) lines.push('戶名:'+b.accountName); if(b.accountNumber) lines.push('帳號:'+b.accountNumber); return lines.length?lines.map(escHtml).join('
'):'此報名尚未取得收款快照,請聯繫主辦'; } function paymentMethodOptionsForReg(reg){ const a=paymentAllowedMethodsFromReg(reg); let opts=''; if(a.bank) opts+=''; if(a.linepay) opts+=''; if(a.card) opts+=''; return opts || ''; } function showPayPage(regId, sesName, amount, opts){ const merged = !!(opts && opts.merged); if(!merged) ST_mergedPayIds = []; // 單筆繳費:清除合併狀態,避免殘留造成誤判 const reg=findMyRegById(regId) || {}; ST_currentPayRegId = regId; // 正式回報金額由 Worker 再從資料庫核算;畫面也顯示同一筆未付差額,避免已繳後加價選位卻提示重繳全額。 const outstanding=regOutstandingAmount(reg); ST_currentPayAmount = merged ? (Number(amount)||0) : (outstanding>0?outstanding:(Number(amount)||0)); ST_currentPaymentInfo = reg.paymentProfile || null; $('payModalTitle').textContent = merged ? `合併繳費(共 ${ST_mergedPayIds.length} 場)` : '前往繳費'; const creditBalance=Math.max(0,Number(reg.activityCreditBalance||0)||0); const creditApplied=Math.max(0,Number(reg.activityCreditApplied||0)||0); const creditCanUse=!merged&&creditBalance>0&&ST_currentPayAmount>0; $('payModalInfo').innerHTML = `
${escHtml(sesName)}
原應繳:NT$ ${Number(reg.total||reg.totalAmount||0).toLocaleString()}${creditApplied>0?`|已扣活動金:NT$ ${creditApplied.toLocaleString()}`:''}
${creditApplied>0?'扣抵後待繳餘額':'目前待繳'}:NT$ ${ST_currentPayAmount.toLocaleString()}
${creditCanUse?`
可用活動金:NT$ ${creditBalance.toLocaleString()}
系統會先扣活動金,再只收剩餘金額。
`:''}`; const bankEl=$('bankInfoDisplay'); if(bankEl) bankEl.innerHTML=bankInfoHtmlFromPayment(reg); const payLastEl = $('payLastFive'); const methodEl = $('payMethod'); if(payLastEl) payLastEl.value = ''; if(methodEl){ methodEl.innerHTML=paymentMethodOptionsForReg(reg); methodEl.value = methodEl.querySelector('option')?.value || ''; } $('payFormArea').style.display='block'; $('paySuccessArea').style.display='none'; if(typeof clearPayFormError==='function') clearPayFormError(); onPayMethodChange(); $('payModal').classList.add('show'); } async function applyActivityCreditForCurrent(btn){ const reg=findMyRegById(ST_currentPayRegId);if(!reg){toast('找不到報名資料');return;} if(btn){btn.disabled=true;btn.textContent='活動金扣抵中…';} try{ const preview=await apiPost({action:'activityCreditCheckout',regId:reg.id,email:getMemberEmail(),phone:getMemberPhone(),preview:true}); if(!(Number(preview.canApply||0)>0)){toast('目前沒有可扣抵的活動金');return;} if(!await uiConfirm('本次使用活動金 '+Number(preview.canApply).toLocaleString()+' 元\n扣抵後待繳 '+Number(preview.remaining||0).toLocaleString()+' 元\n\n確定使用?'))return; const r=await apiPost({action:'activityCreditCheckout',regId:reg.id,email:getMemberEmail(),phone:getMemberPhone()}); toast('已扣抵活動金 NT$ '+Number(r.applied||0).toLocaleString(),6000); await safeGetMyRegs(getMemberEmail(),getMemberPhone()); const updated=findMyRegById(reg.id); if(r.fullyPaid||!updated||regOutstandingAmount(updated)<=0){$('payModal').classList.remove('show');toast('活動金已全額付清,本筆不需再匯款',7000);} else showPayPage(updated.id,updated.sessionName||updated.sesName||'場次',regOutstandingAmount(updated)); }catch(e){showPayFormError('活動金扣抵失敗:'+(e.message||e));} finally{if(btn){btn.disabled=false;btn.textContent='先使用活動金扣抵';}} } function getPayLink(method){ const reg=findMyRegById(ST_currentPayRegId)||{}; const p=reg.paymentProfile||{}; if(method==='LINE Pay'){ const x=p.linepay||{}; return x.url||x.linepayUrl||x.linepay_url||x.link||''; } if(method==='信用卡付款'){ const x=p.card||{}; return x.url||x.cardUrl||x.card_url||x.ecpayUrl||x.link||''; } const hit=(PAY_METHODS||[]).find(x=>x && x.name===method); return (hit&&hit.url)||''; } function onPayMethodChange(){ const method = $('payMethod')?.value || 'ATM 轉帳'; const isBank = method.includes('ATM') || method.includes('銀行') || method.includes('轉帳') || method.includes('匯款'); const bankBox=$('bankPayBox'); if(bankBox) bankBox.style.display=isBank?'block':'none'; const lastWrap=$('payLastFiveWrap'); if(lastWrap) lastWrap.style.display=isBank?'block':'none'; const btn=$('immediatePayBtn'); if(btn){ btn.style.display = isBank ? 'none' : 'block'; btn.textContent = '立即付款'; } } function openExternalPay(){ const method = $('payMethod')?.value || 'ATM 轉帳'; const isBank = method.includes('ATM') || method.includes('銀行') || method.includes('轉帳') || method.includes('匯款'); if(isBank){ toast('請依上方 ATM 資訊完成轉帳,付款後回來按「我已完成付款,回報付款」',5000); return; } const url = getPayLink(method); if(!url){ toast('此報名的收款設定尚未提供此付款方式連結,請改用已開放方式或聯繫主辦'); return; } toast('付款前提醒:完成後請務必截圖付款證明,再回「我的紀錄」按「我已完成付款,回報付款」,並連同截圖傳到官方 LINE。',7000); window.open(url,'_blank'); } function showPayFormError(msg){ const el=$('payFormError'); if(el){ el.textContent=msg; el.style.display='block'; try{ el.scrollIntoView({behavior:'smooth',block:'center'}); }catch(e){} } else { toast('❌ '+msg); } } function clearPayFormError(){ const el=$('payFormError'); if(el){ el.style.display='none'; el.textContent=''; } } async function submitPaymentForm(){ clearPayFormError(); const method = $('payMethod')?.value || 'ATM 轉帳'; const isBank = method.includes('ATM') || method.includes('銀行') || method.includes('轉帳') || method.includes('匯款'); const lastFive = isBank ? ($('payLastFive')?.value.trim() || '') : ''; const amount = ST_currentPayAmount; if(isBank && !lastFive){ showPayFormError('請填寫帳號末五碼'); return; } if(!amount || amount<=0){ showPayFormError('金額異常,請聯繫該場次官方 LINE 客服'); return; } const btn = $('reportPaymentBtn'); if(btn){ btn.disabled=true; btn.textContent='回報中…'; } try{ const merged = Array.isArray(ST_mergedPayIds) && ST_mergedPayIds.length>1; const res = merged // B-06:正式金額只由後端從 DB 取,前端 amount 只能拿來顯示,不再送出。 ? await apiPost({action:'submitPaymentBatch',regIds:ST_mergedPayIds,method,lastFive,email:getMemberEmail(),phone:getMemberPhone()}) : await apiPost({action:'submitPayment',regId:ST_currentPayRegId,method,lastFive,email:getMemberEmail(),phone:getMemberPhone()}); if(res.error){ showPayFormError(res.error); return; } const cardText = res.lineCardText || res.paymentLineCardText || ''; if(!cardText){ showPayFormError('付款卡片產生失敗,請聯繫該場次官方 LINE 客服'); return; } ST_currentPaymentCard = cardText; ST_mergedPayIds = []; showPaymentSuccess(cardText); const refreshEmail2 = getMemberEmail(); if(refreshEmail2){ ST.memberEmail = refreshEmail2; await safeGetMyRegs(refreshEmail2); } }catch(e){ // 原本沒有 catch:一旦發生非預期錯誤,畫面會完全沒反應, // 攤友以為已經回報成功,主辦端卻仍是「未繳費」。 console.error('[submitPaymentForm]', e); showPayFormError('回報失敗:'+((e&&e.message)?e.message:'請重新整理後再試一次,或截圖此畫面聯繫官方 LINE')+'(狀態仍為未繳費,請務必重新回報)'); }finally{ if(btn){ btn.disabled=false; btn.textContent='我已完成付款,回報付款'; } } } function showPaymentSuccess(cardText){ $('payModalTitle').textContent='付款回報完成'; $('payFormArea').style.display='none'; $('paySuccessArea').style.display='block'; const card=$('paymentCopyCard'); if(card) card.textContent=cardText||''; } function copyPaymentCard(){ const text = ST_currentPaymentCard || $('paymentCopyCard')?.textContent || ''; if(!text){ toast('沒有可複製的付款卡片'); return; } const done=()=>{ toast('已複製付款卡片,請貼到官方 LINE,並一起傳付款截圖'); const b=document.querySelector('.payment-copy-btn'); if(b){ const o=b.getAttribute('data-label')||b.textContent; b.setAttribute('data-label',o); b.textContent='✓ 已複製!請貼到官方 LINE'; b.disabled=true; setTimeout(()=>{ b.textContent=o; b.disabled=false; },2200); } }; const fallback=()=>{ try{ const ta=document.createElement('textarea'); ta.value=text; ta.setAttribute('readonly','readonly'); ta.style.position='fixed'; ta.style.left='-9999px'; ta.style.top='0'; document.body.appendChild(ta); ta.focus(); ta.select(); try{ ta.setSelectionRange(0,text.length); }catch(_e){} let ok=false; try{ ok=document.execCommand('copy'); }catch(_e){} document.body.removeChild(ta); if(ok) done(); else prompt('請長按下方文字全選、複製,貼到官方 LINE:', text); }catch(_e){ prompt('請長按下方文字全選、複製,貼到官方 LINE:', text); } }; try{ if(navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(text).then(done).catch(fallback); } else { fallback(); } }catch(_e){ fallback(); } } // ── URL 深連結:信件按鈕 / 查看報名紀錄 / 前往繳費 ── (async()=>{ const _p = new URLSearchParams(window.location.search); const isMemberLink = _p.get('member')==='1' || _p.get('page')==='member' || _p.get('view')==='records'; const _payRegId = _p.get('pay') || _p.get('reg') || _p.get('regId') || ''; const _email = (_p.get('email') || _p.get('mail') || '').trim(); if(_payRegId) window._pendingPayRegId = _payRegId; if(_email){ ST.memberEmail=_email; memberSaveEmail(_email); if($('loginEmail')) $('loginEmail').value=_email; } if(isMemberLink || _payRegId){ setTimeout(async()=>{ // B-02:原本會用 regId 向公開 API 反查 Email(等於任何人拿到 regId 就能拿到 Email)。 // 已移除。舊信件裡的 ?pay=REG_ID 仍可用:regId 先記著,等使用者用 Email+手機登入後, // 由 handlePendingPayLink(regs) 在自己的名單裡比對出這筆,再開啟付款/詳情。 let email = _email || ST.memberEmail || memberSafeEmail(); if(email){ await safeLoadMemberPage(email,{navOptions:{noPush:true}}); }else{ navTo('member',{noPush:true}); memberShowLogin(); const el=$('myRegsList'); if(el) el.innerHTML='
找不到這筆報名對應的 Email。
請輸入報名 Email 查詢,或截圖聯繫官方 LINE。
'; } const ml=$('memberLoginArea'); if(ml && ml.style.display!=='none') ml.scrollIntoView({behavior:'smooth',block:'start'}); }, 800); } })(); // ── 電腦版滑鼠拖曳橫向滑動 ── function enableDragScroll(){ const el = document.querySelector('.events-scroll'); if(!el || el.__dragBound) return; el.__dragBound = true; let isDown=false, startX, scrollLeft; el.addEventListener('mousedown', e=>{ isDown=true; startX=e.pageX-el.offsetLeft; scrollLeft=el.scrollLeft; }); el.addEventListener('mouseleave', ()=>{ isDown=false; }); el.addEventListener('mouseup', ()=>{ isDown=false; }); el.addEventListener('mousemove', e=>{ if(!isDown) return; e.preventDefault(); const x=e.pageX-el.offsetLeft; el.scrollLeft = scrollLeft - (x-startX)*1.5; }); } // ── 前台啟動:一定要自動載入首頁資料 ── function initFrontApp(){ if(window.__tbFrontInited) return; window.__tbFrontInited = true; try{ history.replaceState({page:'home'}, '', location.href); }catch(e){} clearFrontPageStateForDeepLink(); try{ const saved=null; // V5: 只從 DB 回抓,不從 localStorage 讀取 member 資料 if(saved){ const d=JSON.parse(saved); if(d&&d.email){ ST.memberEmail=d.email; ST.member=null; api({action:'getMember',email:d.email,phone:getMemberPhone(),_t:Date.now()}).then(m=>{ if(m&&!m.error) ST.member=m; }).catch(()=>{}); } } }catch(e){} try{ applySiteConfig(); }catch(e){} // 深層分享與頁面還原統一交給 bootDbFront,避免同一連結被開兩次或被上次報名頁覆蓋 if(!window.__tbBootDbFrontAutoScheduled && typeof bootDbFront === 'function') { bootDbFront().catch(e=>{ console.error('bootDbFront error:',e); loadHome(); }); } } if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', initFrontApp); }else{ initFrontApp(); } // ──────────────────────────────────────────────────────────────── // DB-DRIVEN CORE PATCH 20260618:資料庫主導前台,不再靠零散前台狀態 // ──────────────────────────────────────────────────────────────── (function(){ const portalTypeMap = {'市集報名':['市集場次'],'體驗活動':['活動報名','體驗活動'],'通路寄賣':['通路寄賣'],'合作洽詢':['合作申請']}; // 開放狀態判斷唯一來源(與上方 isOpenSession 同一份規則:未設定或報名中/開放中皆為開放)。 // 注意:這裡不可呼叫 isOpenSession,因為下方 window.isOpenSession = openSess 會覆蓋它, // 互相呼叫會造成無限遞迴、前台卡在載入中。 function openSess(s){ const st=String(s?.status||'').trim(); return !st || st==='報名中' || st==='開放中'; } function asArr(v){ return Array.isArray(v)?v:[]; } function uniqueSessions(list){ return [...new Map(asArr(list).filter(Boolean).map(s=>[String(s.id),s])).values()]; } function saveEmail(email){ try{ localStorage.setItem('doing_market_member_email', email||''); localStorage.removeItem('tb_member'); }catch(e){} } function readEmail(){ try{ return localStorage.getItem('doing_market_member_email')||''; }catch(e){ return ''; } } window.isOpenSession = openSess; window.loadHome = async function(){ const evEl=$('eventsList'); if(evEl) evEl.innerHTML='
載入中…
'; try{ const boot = await api({action:'frontBootstrap', tenant:TENANT_ID, _t:Date.now()}); ST.events=asArr(boot.events); ST.sessions=uniqueSessions(boot.sessions).filter(openSess); ST.allSessionsData=ST.sessions; try{ const publicBundles=await api({action:'getBundlesPublic', tenant:TENANT_ID, _t:Date.now()}); ST.bundles=Array.isArray(publicBundles)?publicBundles:[]; }catch(_bundleErr){ ST.bundles=[]; } if(boot.tenant){ ST.tenant = boot.tenant; // 動態更新 LINE_URL if(boot.tenant.name) document.title = boot.tenant.name + '|報名系統'; if(boot.tenant.lineUrl) LINE_URL = boot.tenant.lineUrl; // 動態更新銀行帳號與線上付款連結顯示(缺設定不 fallback,顯示未設定) const bankEl = document.getElementById('bankInfoDisplay'); if(boot.tenant.bankInfo){ const bi = boot.tenant.bankInfo; if(bankEl){ bankEl.innerHTML = bi.replace(/\n/g,'
'); } } else if(bankEl){ bankEl.innerHTML = '此主辦尚未設定付款資訊,請聯繫主辦'; } const payCfg = boot.tenant.paymentConfig || boot.tenant.paymentLinks || {}; const tenantLinePay = payCfg.linePayUrl || payCfg.linePay || payCfg.linepay || payCfg.line_pay_url || ''; const tenantCardPay = payCfg.cardPayUrl || payCfg.creditCardUrl || payCfg.ecpayUrl || payCfg.card || payCfg.card_pay_url || ''; if(tenantLinePay) LINEPAY_LINK = tenantLinePay; if(tenantCardPay) CARDPAY_LINK = tenantCardPay; PAY_METHODS = Array.isArray(payCfg.payMethods) ? payCfg.payMethods.filter(m=>m && m.name) : []; // 動態更新品牌顯示 const logoText = document.querySelector('.logo-text'); if(logoText && boot.tenant.name) logoText.childNodes[0].textContent = boot.tenant.name; const logoImg = document.querySelector('.logo-img'); if(logoImg && boot.tenant.logoUrl) logoImg.src = boot.tenant.logoUrl; // infoText / heroImg const band=$('siteInfoBand'); const strong=band&&band.querySelector('strong'); if(strong && boot.tenant.infoText) strong.textContent=boot.tenant.infoText; const hero=$('siteHero'); const tenantCover = boot.tenant.coverUrl || boot.tenant.heroImg || ''; if(hero && tenantCover){ hero.style.backgroundImage='url('+tenantCover+')'; hero.style.backgroundSize='cover'; hero.style.backgroundPosition='center'; } } renderAnnouncements(asArr(boot.announcements)); renderEvents(); return boot; }catch(e){ console.error('[frontBootstrap failed]', e); const sessions=await api({action:'getSessions', _t:Date.now()}); ST.allSessionsData=uniqueSessions(sessions).filter(openSess); ST.sessions=ST.allSessionsData; ST.events=[]; ST.bundles=[]; renderEvents(); } }; window.renderEvents = function(){ // ── 精選主題活動(ST.events 活動層)── const fl=$('featuredEventsList'); if(fl){ const events=asArr(ST.events); if(events.length){ fl.innerHTML=events.map(e=>{ const coverHtml=e.cover ?`` :``; const placeholderHtml=`
`; return `
${coverHtml}${placeholderHtml}
${esc(e.title||'')}
${e.location?`
📍 ${esc(e.location)}
`:''} ${e.desc?`
${esc(e.desc)}
`:''}
🎪 ${eventSessionCount(e.id)} 個場次
`; }).join(''); } else { fl.innerHTML='
目前無主題活動
'; } } // ── 近期場次(ST.allSessionsData 場次層)── const el=$('eventsList'); if(el){ const sessions=sortSessionsByUpcoming(uniqueSessions(ST.allSessionsData).filter(openSess)); setActivitySessions('home',sessions); } setTimeout(enableDragScroll,100); }; window.openSessionFromCard=function(ev,sessionId){ if(ev){ev.preventDefault();ev.stopPropagation();} openSession(sessionId); }; window.handleSessionTap=window.openSessionFromCard; window.openEvent=async function(eventId){ const evt=asArr(ST.events).find(e=>String(e.id)===String(eventId)); const list=$('portalSessionList'); const title=$('portalTitle'); if(title) title.textContent=evt?evt.title:'場次列表'; navTo('portal'); if(list) list.innerHTML='
載入中…
'; let data=uniqueSessions(ST.allSessionsData).filter(s=>String(s.eventId||'')===String(eventId)&&openSess(s)); if(!data.length){ data=uniqueSessions(await api({action:'getSessions',eventId,_t:Date.now()})).filter(openSess); ST.allSessionsData=uniqueSessions([...(ST.allSessionsData||[]),...data]); } setActivitySessions('portal',data); }; window.filterCat=async function(type,el){ ST.currentCatFilter=type; document.querySelectorAll('.cat-btn').forEach(b=>b.classList.remove('active')); if(el) el.classList.add('active'); const title=$('portalTitle'); if(title) title.textContent=type; const list=$('portalSessionList'); if(list) list.innerHTML='
載入中…
'; navTo('portal'); const allow=portalTypeMap[type]||[]; let data=uniqueSessions(ST.allSessionsData).filter(s=>openSess(s)&&((asArr(s.portals).includes(type))||allow.includes(s.type))); if(!data.length){ data=uniqueSessions(await api({action:'getSessions',portal:type,_t:Date.now()})).filter(openSess); ST.allSessionsData=uniqueSessions([...(ST.allSessionsData||[]),...data]); } setActivitySessions('portal',data); }; // 會員登入與我的紀錄統一由 safeLoadMemberPage 管理;此處不再覆蓋 memberLogin/goMemberPage。 async function bootDbFront(){ // 前台使用固定 30 天的安全登入憑證;憑證不保存手機,頁面載入後向 Worker 重新核對會員。 const savedEmail = readEmail(); const savedMemberToken = memberSafeToken(); if (savedEmail) ST.memberEmail = savedEmail; if (savedMemberToken) ST.memberToken = savedMemberToken; // 讀取分享連結;若網址有指定場次/主題,必須優先於上次停留頁面 const deepLink = readFrontDeepLink(); if(deepLink.hasDeepLink) clearFrontPageStateForDeepLink(); // 讀取上次儲存的完整狀態 let savedState = null; try { savedState = deepLink.hasDeepLink ? null : JSON.parse(localStorage.getItem('doing_market_page_state')||'null'); } catch(e){} const rawTab = (savedState && savedState.tab) || localStorage.getItem('doing_market_current_tab') || 'home'; const lastTab = deepLink.hasDeepLink ? 'home' : ((rawTab === 'myregs') ? 'member' : rawTab); // 分享連結:先蓋一層載入畫面,避免使用者看到首頁閃一下才跳到場次 if(deepLink.hasDeepLink) showDeepLinkLoading(); try{ await loadPhotoFrames(); }catch(e){} try{ await openPhotoFrameByLink(); }catch(e){} await loadHome(); if(deepLink.sessionId){ await openSession(deepLink.sessionId); hideDeepLinkLoading(); return; } if(deepLink.eventId){ await openEvent(deepLink.eventId); hideDeepLinkLoading(); return; } if(deepLink.isMember){ await navToTab('member'); hideDeepLinkLoading(); return; } hideDeepLinkLoading(); // 還原頁面:非分享連結一律回到單一首頁;避免上次停留的報名表或舊場次列表蓋回首頁。 if(lastTab === 'member' && (savedEmail||savedMemberToken)){ setTimeout(()=>{ navToTab('member'); }, 200); } else if(lastTab === 'support'){ setTimeout(()=>{ navToTab('support'); }, 200); } else { navToTab('home'); } } handleDoingLineCallback(); window.__tbBootDbFrontAutoScheduled = true; if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',bootDbFront,{once:true}); else setTimeout(bootDbFront,0); })();