f17f171f4b
迁移收尾修复:
- 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
532 lines
19 KiB
JavaScript
532 lines
19 KiB
JavaScript
// 每日签到完整模块:事件代理、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") || ""));
|
||
}
|
||
});
|
||
}
|