// 每日签到完整模块:事件代理、API 请求与日历渲染全部由 Vite 管理。 import { escapeHtml } from "./html.js"; let dailySignInEventsBound = false; // ── 状态(全局共享,兼容 Blade 中 window.dailySignInState 引用)── window.dailySignInState = window.dailySignInState || { month: null, prevMonth: null, nextMonth: null, repairCardItem: null, repairCardCount: 0, rewardRules: [], status: null, }; // ── 辅助函数 ────────────────────────────────────────── function csrf() { return document.querySelector('meta[name="csrf-token"]')?.content ?? ""; } /** * 从服务端响应中提取最新金币余额。 */ function resolveDailySignInGoldBalance(data) { const candidates = [ data?.data?.user?.jjb, data?.data?.user?.gold, data?.data?.presence?.jjb, data?.data?.presence?.gold, data?.data?.my_jjb, data?.data?.new_jjb, data?.data?.balance, data?.my_jjb, data?.new_jjb, data?.balance, ]; for (const candidate of candidates) { const amount = Number(candidate); if (Number.isFinite(amount)) return amount; } return null; } /** * 从签到响应中提取当前用户最新在线载荷。 */ function resolveDailySignInPresencePayload(data) { const candidates = [ data?.data?.presence, data?.data?.online_user, data?.data?.onlineUser, data?.data?.user_payload, data?.data?.userPayload, data?.data?.user, data?.presence, data?.online_user, data?.onlineUser, ]; return candidates.find(p => p && typeof p === 'object') || null; } /** * 从签到响应中提取签到身份字段。 */ function resolveDailySignInIdentityPayload(data) { const identity = data?.data?.identity || data?.data?.sign_identity || data?.identity || data?.sign_identity; if (!identity || typeof identity !== 'object') return {}; return { sign_identity_key: identity.key ?? identity.sign_identity_key ?? identity.code ?? '', sign_identity_label: identity.label ?? identity.name ?? identity.sign_identity_label ?? '', sign_identity_icon: identity.icon ?? identity.sign_identity_icon ?? '', sign_identity_color: identity.color ?? identity.sign_identity_color ?? undefined, sign_identity_bg_color: identity.bg_color ?? identity.background_color ?? identity.sign_identity_bg_color ?? undefined, sign_identity_border_color: identity.border_color ?? identity.sign_identity_border_color ?? undefined, }; } /** * 将签到成功结果同步到金币余额与在线名单。 */ function applyDailySignInResult(data) { const balance = resolveDailySignInGoldBalance(data); const payload = resolveDailySignInPresencePayload(data); const identityPayload = resolveDailySignInIdentityPayload(data); const username = window.chatContext?.username; if (balance !== null && window.chatContext) { window.chatContext.userJjb = balance; window.chatContext.myGold = balance; } if (username) { // hydrateOnlineUserPayload 由 Blade 主脚本暴露在 window 上供 Vite 模块桥接调用。 if (typeof window.hydrateOnlineUserPayload === "function") { window.hydrateOnlineUserPayload(username, { ...(payload || {}), ...identityPayload, username, }); } } // 通知 Blade 主脚本刷新在线用户列表。 if (typeof window.renderUserList === "function") { window.renderUserList(); } } // ── 渲染函数 ────────────────────────────────────────── function getState() { return window.dailySignInState; } function renderDailySignInStatus() { const status = getState().status || {}; const streakEl = document.getElementById('daily-sign-streak'); const previewEl = document.getElementById('daily-sign-preview'); const cardCountEl = document.getElementById('daily-sign-card-count'); const cardPriceEl = document.getElementById('daily-sign-card-price'); const claimBtn = document.getElementById('daily-sign-claim-btn'); const buyBtn = document.getElementById('daily-sign-buy-card-btn'); const cardItem = getState().repairCardItem; if (streakEl) streakEl.textContent = `连续 ${Number(status.current_streak_days || 0)} 天`; if (previewEl) { const rule = status.preview_rule || {}; const parts = []; if (Number(rule.gold_reward || 0) > 0) parts.push(`${rule.gold_reward} 金币`); if (Number(rule.exp_reward || 0) > 0) parts.push(`${rule.exp_reward} 经验`); if (Number(rule.charm_reward || 0) > 0) parts.push(`${rule.charm_reward} 魅力`); previewEl.textContent = status.signed_today ? '今日已签到' : `今日可领:${parts.join(' + ') || '签到奖励'}`; } if (cardCountEl) cardCountEl.textContent = `补签卡 ${getState().repairCardCount || 0} 张`; if (cardPriceEl) { cardPriceEl.textContent = cardItem ? `${cardItem.icon || '🗓️'} ${cardItem.name}:${Number(cardItem.price || 0).toLocaleString()} 金币` : '补签卡暂未上架'; } if (claimBtn) { claimBtn.disabled = !!status.signed_today; claimBtn.textContent = status.signed_today ? '今日已签到' : '今日签到'; claimBtn.style.opacity = status.signed_today ? '0.55' : '1'; claimBtn.style.cursor = status.signed_today ? 'not-allowed' : 'pointer'; } if (buyBtn) { buyBtn.disabled = !cardItem?.id; buyBtn.style.opacity = cardItem?.id ? '1' : '0.55'; buyBtn.style.cursor = cardItem?.id ? 'pointer' : 'not-allowed'; } } function renderDailySignInCalendar(payload) { const grid = document.getElementById('daily-sign-calendar-grid'); const label = document.getElementById('daily-sign-month-label'); if (!grid) return; if (label) label.textContent = payload.month_label || payload.month || '本月'; const days = Array.isArray(payload.days) ? payload.days : []; grid.innerHTML = ''; const firstWeekday = Number(days[0]?.weekday || 0); for (let i = 0; i < firstWeekday; i += 1) { const blank = document.createElement('div'); blank.className = 'daily-sign-day blank'; grid.appendChild(blank); } days.forEach(day => { const cell = document.createElement('button'); cell.type = 'button'; cell.className = 'daily-sign-day'; if (day.signed) cell.classList.add('signed'); if (day.can_makeup) cell.classList.add('missed'); if (day.is_today) cell.classList.add('today'); if (day.is_future) cell.classList.add('future'); const stateText = day.signed ? `${day.is_makeup ? '补签' : '已签'} ${day.streak_days || ''}天` : (day.is_future ? '未到' : (day.is_today ? '今天' : '漏签')); cell.innerHTML = `${day.day}${escapeHtml(stateText)}`; cell.title = day.reward_text || stateText; if (day.can_makeup) cell.dataset.dailySignMakeup = day.date; grid.appendChild(cell); }); } function renderDailySignInRewardRules() { const list = document.getElementById('daily-sign-rewards-list'); const progress = document.getElementById('daily-sign-reward-progress'); if (!list) return; const currentDays = Number(getState().status?.current_streak_days || 0); const rules = getState().rewardRules || []; if (progress) progress.textContent = `当前 ${currentDays} 天`; if (!rules.length) { list.innerHTML = '
暂无奖励规则
'; return; } list.innerHTML = rules.map(rule => { const streakDays = Number(rule.streak_days || 0); const parts = []; if (Number(rule.gold_reward || 0) > 0) parts.push(`${rule.gold_reward}金`); if (Number(rule.exp_reward || 0) > 0) parts.push(`${rule.exp_reward}经验`); if (Number(rule.charm_reward || 0) > 0) parts.push(`${rule.charm_reward}魅力`); const icon = escapeHtml(rule.identity_badge_icon || '✅'); const name = escapeHtml(rule.identity_badge_name || '签到奖励'); const color = escapeHtml(rule.identity_badge_color || '#0f766e'); const activeClass = currentDays >= streakDays ? ' active' : ''; const distanceText = currentDays >= streakDays ? '已达成' : `还差 ${Math.max(streakDays - currentDays, 0)} 天`; const rewardText = escapeHtml(parts.join(' + ') || '签到记录'); return `
第 ${streakDays} 天 ${icon}
${name}
${rewardText}
`; }).join(''); } // ── API 请求函数 ────────────────────────────────────── async function loadDailySignInStatus() { const statusUrl = window.chatContext?.dailySignInStatusUrl; if (!statusUrl) return; const response = await fetch(statusUrl, { headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': csrf() }, }); const data = await response.json(); if (!response.ok || data?.status === 'error') { throw new Error(data?.message || '签到状态加载失败'); } getState().status = data.data || {}; renderDailySignInStatus(); } async function loadDailySignInCalendar(month) { const calendarUrl = window.chatContext?.dailySignInCalendarUrl; if (!calendarUrl) return; const url = new URL(calendarUrl, window.location.origin); if (month) url.searchParams.set('month', month); const response = await fetch(url.toString(), { headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': csrf() }, }); const data = await response.json(); if (!response.ok || data?.status === 'error') { throw new Error(data?.message || '签到日历加载失败'); } const payload = data.data || {}; const state = getState(); state.month = payload.month || month || null; state.prevMonth = payload.prev_month || null; state.nextMonth = payload.next_month || null; state.repairCardItem = payload.sign_repair_card_item || null; state.repairCardCount = Number(payload.makeup_card_count || 0); state.rewardRules = Array.isArray(payload.reward_rules) ? payload.reward_rules : []; renderDailySignInCalendar(payload); renderDailySignInStatus(); renderDailySignInRewardRules(); } // ── 公开操作 ────────────────────────────────────────── async function openDailySignInModal() { const modal = document.getElementById('daily-sign-modal'); if (!window.chatContext?.dailySignInCalendarUrl || !modal) { window.chatDialog?.alert('签到入口暂未开放,请稍后再试。', '提示', '#f59e0b'); return; } modal.style.display = 'flex'; await Promise.all([ loadDailySignInStatus(), loadDailySignInCalendar(getState().month), ]); } function closeDailySignInModal() { const modal = document.getElementById('daily-sign-modal'); if (modal) modal.style.display = 'none'; } async function quickDailySignIn() { await openDailySignInModal(); } async function claimDailySignInFromModal() { const claimUrl = window.chatContext?.dailySignInClaimUrl; if (!claimUrl) { window.chatDialog?.alert('签到入口暂未开放,请稍后再试。', '提示', '#f59e0b'); return; } try { const response = await fetch(claimUrl, { method: 'POST', headers: { 'X-CSRF-TOKEN': csrf(), 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify({ room_id: window.chatContext?.roomId ?? null }), }); const data = await response.json(); if (!response.ok || data?.status === 'error' || data?.ok === false) { throw new Error(data?.message || '签到失败'); } applyDailySignInResult(data); await Promise.all([ loadDailySignInStatus(), loadDailySignInCalendar(getState().month), ]); renderDailySignInRewardRules(); window.chatToast?.show({ title: '签到成功', message: data?.message || '今日签到奖励已到账。', icon: data?.data?.sign_identity_icon || data?.data?.identity?.icon || '✅', color: '#16a34a', duration: 3200, }); } catch (error) { window.chatDialog?.alert(error.message || '签到失败,请稍后重试。', '签到失败', '#cc4444'); } } async function makeupDailySignIn(targetDate) { const makeupUrl = window.chatContext?.dailySignInMakeupUrl; if (!makeupUrl) return; const ok = await window.chatDialog?.confirm(`确认使用 1 张补签卡补签 ${targetDate} 吗?`, '确认补签'); if (!ok) return; try { const response = await fetch(makeupUrl, { method: 'POST', headers: { 'X-CSRF-TOKEN': csrf(), 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify({ target_date: targetDate, room_id: window.chatContext?.roomId ?? null }), }); const data = await response.json(); if (!response.ok || data?.status === 'error' || data?.ok === false) { const firstError = data?.errors ? Object.values(data.errors).flat()[0] : null; throw new Error(firstError || data?.message || '补签失败'); } applyDailySignInResult(data); await Promise.all([ loadDailySignInStatus(), loadDailySignInCalendar(getState().month), ]); renderDailySignInRewardRules(); window.chatToast?.show({ title: '补签成功', message: data?.message || '补签已完成。', icon: '🗓️', color: '#0f766e', duration: 3200, }); } catch (error) { window.chatDialog?.alert(error.message || '补签失败,请稍后重试。', '补签失败', '#cc4444'); } } async function promptSignRepairQuantity(item) { const unitPrice = Number(item?.price || 0); const ruleText = item?.description || '补签卡只能补签本月漏掉的未签到日期,不能补签上月或更早日期。'; const promptPromise = window.chatDialog?.prompt( `请输入要购买的补签卡数量(1-99):\n单价 ${unitPrice.toLocaleString()} 金币\n说明:${ruleText}`, '1', '购买补签卡', '#0f766e', ); const inputEl = document.getElementById('global-dialog-input'); const previousInputStyle = inputEl?.getAttribute('style') || ''; if (inputEl) { inputEl.style.minHeight = '40px'; inputEl.style.height = '40px'; inputEl.style.resize = 'none'; inputEl.style.overflow = 'hidden'; } const rawQuantity = await promptPromise; if (inputEl) inputEl.setAttribute('style', previousInputStyle); if (rawQuantity === null || rawQuantity === undefined) return null; const quantity = Number.parseInt(String(rawQuantity).trim(), 10); if (!Number.isInteger(quantity) || quantity < 1 || quantity > 99) { window.chatDialog?.alert('购买数量必须是 1 到 99 之间的整数。', '数量不正确', '#cc4444'); return null; } return quantity; } async function buyDailySignRepairCard() { const item = getState().repairCardItem; if (!item?.id) { window.chatDialog?.alert('补签卡暂未上架。', '提示', '#f59e0b'); return; } const quantity = await promptSignRepairQuantity(item); if (quantity === null) return; const totalPrice = Number(item.price || 0) * quantity; const ok = await window.chatDialog?.confirm( `确认花费 ${totalPrice.toLocaleString()} 金币购买【${item.name}】× ${quantity} 吗?\n说明:补签卡只能补签本月未签到日期。`, '购买补签卡', ); if (!ok) return; if (typeof window.buyItem === 'function') { window.buyItem(item.id, item.name, item.price, 'all', '', quantity); setTimeout(() => { loadDailySignInCalendar(getState().month); loadDailySignInStatus(); }, 900); return; } window.openShopModal?.(); } // ── 暴露到 window(兼容 Blade 存量引用)─────────────── window.openDailySignInModal = openDailySignInModal; window.closeDailySignInModal = closeDailySignInModal; window.quickDailySignIn = quickDailySignIn; window.loadDailySignInCalendar = loadDailySignInCalendar; window.claimDailySignInFromModal = claimDailySignInFromModal; window.makeupDailySignIn = makeupDailySignIn; window.promptSignRepairQuantity = promptSignRepairQuantity; window.buyDailySignRepairCard = buyDailySignRepairCard; // ── 事件绑定 ────────────────────────────────────────── /** * 读取每日签到月份翻页目标。 */ function resolveDailySignInMonth(direction) { if (direction === "prev") return getState().prevMonth || null; if (direction === "next") return getState().nextMonth || null; return null; } /** * 绑定每日签到弹窗遮罩、关闭、签到、补签卡和月份切换事件。 */ export function bindDailySignInControls() { if (dailySignInEventsBound || typeof document === "undefined") return; dailySignInEventsBound = true; document.addEventListener("click", (event) => { if (!(event.target instanceof Element)) return; const overlay = event.target.closest("[data-daily-sign-modal-overlay]"); if (overlay && event.target === overlay) { closeDailySignInModal(); return; } if (event.target.closest("[data-daily-sign-close]")) { event.preventDefault(); closeDailySignInModal(); return; } if (event.target.closest("[data-daily-sign-claim]")) { event.preventDefault(); claimDailySignInFromModal(); return; } if (event.target.closest("[data-daily-sign-buy-repair-card]")) { event.preventDefault(); buyDailySignRepairCard(); return; } const makeupButton = event.target.closest("[data-daily-sign-makeup]"); if (makeupButton) { event.preventDefault(); makeupDailySignIn(makeupButton.getAttribute("data-daily-sign-makeup") || ""); return; } const monthButton = event.target.closest("[data-daily-sign-month]"); if (monthButton) { event.preventDefault(); loadDailySignInCalendar(resolveDailySignInMonth(monthButton.getAttribute("data-daily-sign-month") || "")); } }); }