Files
chatroom/resources/js/chat-room/daily-sign-in.js
T
pllx f17f171f4b fix: 修复迁移遗留的按钮无响应、头像框层级及构建错误
迁移收尾修复:
- heartbeat.js: 移除 export { } 中重复的 startHeartbeat/stopHeartbeat(已通过 export function 导出)
- scripts.blade.php: 移除 JS 注释中的 {{ }} 避免 Blade 编译为 e() 导致 PHP 解析错误
- preferences-status.js: 补全 6 个缺失的 window.* 赋值(toggleBlockMenu/toggleFeatureMenu 等),
  实现迁移中丢失的 updateDailyStatus/clearDailyStatus,修复 handleFeatureLocalClear 清屏回调
- toolbar.js: 补全 window.runFeatureShortcut 赋值

头像框样式修复(chat-decorations.css):
- z-index 互换:头像降至 1,框升至 3,使框边缘可遮挡头像外围
- 使用 CSS mask(radial-gradient)挖环形替代旧 ::before 实心圆遮挡方案
- clip-path: circle(50%) 硬裁剪确保圆形,不受 chat.css border-radius: 2px 覆盖
- 特异性提升至 .user-item .avatar-frame-wrapper .user-head

新 Vite 模块(从 Blade 迁移):
- chat-state.js / message-renderer.js / user-list.js / chat-events.js
- composer.js(重写)/ heartbeat.js / admin-commands.js
- vip-presence.js / chat-decorations.css
2026-04-27 09:19:49 +00:00

532 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 每日签到完整模块:事件代理、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 = `<span class="day-num">${day.day}</span><span class="day-state">${escapeHtml(stateText)}</span>`;
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 = '<div style="font-size:12px;color:#94a3b8;padding:4px;">暂无奖励规则</div>';
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 `
<div class="daily-sign-reward-card${activeClass}" title="${name} · ${rewardText} · ${escapeHtml(distanceText)}">
<div class="daily-sign-reward-title">
<span>第 ${streakDays} 天</span>
<span style="color:${color};">${icon}</span>
</div>
<div class="daily-sign-reward-name">${name}</div>
<div class="daily-sign-reward-desc">${rewardText}</div>
</div>
`;
}).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") || ""));
}
});
}