新增每日签到与补签卡功能
This commit is contained in:
@@ -423,6 +423,544 @@
|
||||
renderUserList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从服务端响应中提取最新金币余额。
|
||||
*
|
||||
* @param {Record<string, any>} data 接口响应数据
|
||||
* @returns {number|null}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从签到响应中提取当前用户最新在线载荷。
|
||||
*
|
||||
* @param {Record<string, any>} data 接口响应数据
|
||||
* @returns {Record<string, any>|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(payload => payload && typeof payload === 'object') || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从签到响应中提取签到身份字段。
|
||||
*
|
||||
* @param {Record<string, any>} data 接口响应数据
|
||||
* @returns {Record<string, any>}
|
||||
*/
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将签到成功结果同步到金币余额与在线名单。
|
||||
*
|
||||
* @param {Record<string, any>} data 接口响应数据
|
||||
*/
|
||||
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(username, {
|
||||
...(payload || {}),
|
||||
...identityPayload,
|
||||
username,
|
||||
});
|
||||
}
|
||||
|
||||
renderUserList();
|
||||
}
|
||||
|
||||
window.dailySignInState = {
|
||||
month: null,
|
||||
prevMonth: null,
|
||||
nextMonth: null,
|
||||
repairCardItem: null,
|
||||
repairCardCount: 0,
|
||||
rewardRules: [],
|
||||
status: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开每日签到日历弹窗。
|
||||
*/
|
||||
window.openDailySignInModal = 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(window.dailySignInState.month),
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭每日签到日历弹窗。
|
||||
*/
|
||||
window.closeDailySignInModal = function closeDailySignInModal() {
|
||||
const modal = document.getElementById('daily-sign-modal');
|
||||
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 快速签到入口:打开日历,让用户能看到当月签到和补签状态。
|
||||
*/
|
||||
window.quickDailySignIn = async function quickDailySignIn() {
|
||||
await window.openDailySignInModal();
|
||||
};
|
||||
|
||||
/**
|
||||
* 拉取今日签到状态。
|
||||
*/
|
||||
async function loadDailySignInStatus() {
|
||||
const statusUrl = window.chatContext?.dailySignInStatusUrl;
|
||||
|
||||
if (!statusUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(statusUrl, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || data?.status === 'error') {
|
||||
throw new Error(data?.message || '签到状态加载失败');
|
||||
}
|
||||
|
||||
window.dailySignInState.status = data.data || {};
|
||||
renderDailySignInStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取并渲染指定月份签到日历。
|
||||
*
|
||||
* @param {string|null|undefined} month 月份 YYYY-MM
|
||||
*/
|
||||
window.loadDailySignInCalendar = 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': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || data?.status === 'error') {
|
||||
throw new Error(data?.message || '签到日历加载失败');
|
||||
}
|
||||
|
||||
const payload = data.data || {};
|
||||
window.dailySignInState.month = payload.month || month || null;
|
||||
window.dailySignInState.prevMonth = payload.prev_month || null;
|
||||
window.dailySignInState.nextMonth = payload.next_month || null;
|
||||
window.dailySignInState.repairCardItem = payload.sign_repair_card_item || null;
|
||||
window.dailySignInState.repairCardCount = Number(payload.makeup_card_count || 0);
|
||||
window.dailySignInState.rewardRules = Array.isArray(payload.reward_rules) ? payload.reward_rules : [];
|
||||
renderDailySignInCalendar(payload);
|
||||
renderDailySignInStatus();
|
||||
renderDailySignInRewardRules();
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染签到状态摘要。
|
||||
*/
|
||||
function renderDailySignInStatus() {
|
||||
const status = window.dailySignInState.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 = window.dailySignInState.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 = `补签卡 ${window.dailySignInState.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';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染签到月历。
|
||||
*
|
||||
* @param {Record<string, any>} payload 日历响应数据
|
||||
*/
|
||||
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.onclick = () => makeupDailySignIn(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(window.dailySignInState.status?.current_streak_days || 0);
|
||||
const rules = window.dailySignInState.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('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 在日历弹窗中领取今日签到。
|
||||
*/
|
||||
window.claimDailySignInFromModal = 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': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
|
||||
'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(window.dailySignInState.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');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用补签卡补签指定日期。
|
||||
*
|
||||
* @param {string} targetDate 目标日期 YYYY-MM-DD
|
||||
*/
|
||||
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': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
|
||||
'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(window.dailySignInState.month),
|
||||
]);
|
||||
renderDailySignInRewardRules();
|
||||
window.chatToast?.show({
|
||||
title: '补签成功',
|
||||
message: data?.message || '补签已完成。',
|
||||
icon: '🗓️',
|
||||
color: '#0f766e',
|
||||
duration: 3200,
|
||||
});
|
||||
} catch (error) {
|
||||
window.chatDialog?.alert(error.message || '补签失败,请稍后重试。', '补签失败', '#cc4444');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 询问补签卡购买数量。
|
||||
*
|
||||
* @param {Record<string, any>} item 补签卡商品
|
||||
* @returns {Promise<number|null>}
|
||||
*/
|
||||
window.promptSignRepairQuantity = 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 在签到弹窗内快速购买补签卡。
|
||||
*/
|
||||
window.buyDailySignRepairCard = async function buyDailySignRepairCard() {
|
||||
const item = window.dailySignInState.repairCardItem;
|
||||
|
||||
if (!item?.id) {
|
||||
window.chatDialog?.alert('补签卡暂未上架。', '提示', '#f59e0b');
|
||||
return;
|
||||
}
|
||||
|
||||
const quantity = await window.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(window.dailySignInState.month);
|
||||
loadDailySignInStatus();
|
||||
}, 900);
|
||||
return;
|
||||
}
|
||||
|
||||
window.openShopModal?.();
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置新的当日状态。
|
||||
*
|
||||
@@ -1512,7 +2050,7 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建原有徽标(职务 / 管理员 / VIP)。
|
||||
* 构建职务 / 管理员徽标。
|
||||
*
|
||||
* @param {Record<string, any>} user 用户在线载荷
|
||||
* @param {string} username 用户名
|
||||
@@ -1531,15 +2069,25 @@
|
||||
return `<span class="user-badge-icon" style="font-size:12px; margin-left:2px;" data-instant-tooltip="最高统帅">🎖️</span>`;
|
||||
}
|
||||
|
||||
if (user.vip_icon) {
|
||||
const vipColor = user.vip_color || '#f59e0b';
|
||||
const safeVipTitle = escapeHtml(String(user.vip_name || 'VIP'));
|
||||
const safeVipIcon = escapeHtml(String(user.vip_icon || '👑'));
|
||||
return '';
|
||||
}
|
||||
|
||||
return `<span class="user-badge-icon" style="font-size:12px; margin-left:2px; color:${vipColor};" data-instant-tooltip="${safeVipTitle}">${safeVipIcon}</span>`;
|
||||
/**
|
||||
* 构建用户 VIP 徽标。
|
||||
*
|
||||
* @param {Record<string, any>} user 用户在线载荷
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildUserVipBadgeHtml(user) {
|
||||
if (!user.vip_icon) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '';
|
||||
const vipColor = user.vip_color || '#f59e0b';
|
||||
const safeVipTitle = escapeHtml(String(user.vip_name || 'VIP'));
|
||||
const safeVipIcon = escapeHtml(String(user.vip_icon || '👑'));
|
||||
|
||||
return `<span class="user-badge-icon" style="font-size:12px; margin-left:2px; color:${vipColor};" data-instant-tooltip="${safeVipTitle}">${safeVipIcon}</span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1559,30 +2107,65 @@
|
||||
const safeTooltip = escapeHtml(`${status.group} · ${status.label}`);
|
||||
|
||||
return `
|
||||
<span style="display:inline-flex;align-items:center;gap:3px;margin-left:4px;padding:0 6px;height:18px;border-radius:999px;background:#eef2ff;border:1px solid #c7d2fe;color:#4338ca;font-size:11px;line-height:18px;vertical-align:middle;"
|
||||
<span class="user-badge-icon" style="display:inline-flex;align-items:center;gap:3px;margin-left:4px;padding:0 6px;height:18px;max-width:78px;overflow:hidden;white-space:nowrap;border-radius:999px;background:#eef2ff;border:1px solid #c7d2fe;color:#4338ca;font-size:11px;line-height:18px;vertical-align:middle;"
|
||||
data-instant-tooltip="${safeTooltip}">
|
||||
<span style="font-size:11px;line-height:1;">${safeIcon}</span>
|
||||
<span style="line-height:1;">${safeLabel}</span>
|
||||
<span style="font-size:11px;line-height:1;flex-shrink:0;">${safeIcon}</span>
|
||||
<span style="line-height:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${safeLabel}</span>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 3 秒节奏在原有徽标与状态徽标之间切换。
|
||||
* 构建签到身份徽标。
|
||||
*
|
||||
* @param {Record<string, any>} user 用户在线载荷
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildUserSignIdentityBadgeHtml(user) {
|
||||
const identityKey = String(user.sign_identity_key ?? user.sign_identity ?? '');
|
||||
const identityLabel = String(user.sign_identity_label ?? user.sign_identity_name ?? '');
|
||||
const identityIcon = String(user.sign_identity_icon ?? '');
|
||||
|
||||
if (!identityKey || !identityLabel || !identityIcon) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const color = String(user.sign_identity_color || '#0f766e');
|
||||
const bgColor = String(user.sign_identity_bg_color || '#ccfbf1');
|
||||
const borderColor = String(user.sign_identity_border_color || '#5eead4');
|
||||
const safeIcon = escapeHtml(identityIcon);
|
||||
const safeLabel = escapeHtml(identityLabel);
|
||||
const safeTooltip = escapeHtml(`签到 · ${identityLabel}`);
|
||||
|
||||
return `
|
||||
<span class="user-badge-icon" style="display:inline-flex;align-items:center;gap:3px;margin-left:4px;padding:0 6px;height:18px;max-width:78px;overflow:hidden;white-space:nowrap;border-radius:999px;background:${bgColor};border:1px solid ${borderColor};color:${color};font-size:11px;line-height:18px;vertical-align:middle;"
|
||||
data-instant-tooltip="${safeTooltip}">
|
||||
<span style="font-size:11px;line-height:1;flex-shrink:0;">${safeIcon}</span>
|
||||
<span style="line-height:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${safeLabel}</span>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 3 秒节奏在签到身份、状态、职务/管理、VIP 徽标之间轮换。
|
||||
*
|
||||
* @param {Record<string, any>} user 用户在线载荷
|
||||
* @param {string} username 用户名
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildUserBadgeHtml(user, username) {
|
||||
const statusBadge = buildUserStatusBadgeHtml(user);
|
||||
const primaryBadge = buildUserPrimaryBadgeHtml(user, username);
|
||||
const badges = [
|
||||
buildUserSignIdentityBadgeHtml(user),
|
||||
buildUserStatusBadgeHtml(user),
|
||||
buildUserPrimaryBadgeHtml(user, username),
|
||||
buildUserVipBadgeHtml(user),
|
||||
].filter(Boolean);
|
||||
|
||||
if (statusBadge && primaryBadge) {
|
||||
return userBadgeRotationTick % 2 === 0 ? statusBadge : primaryBadge;
|
||||
if (badges.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return statusBadge || primaryBadge;
|
||||
return badges[userBadgeRotationTick % badges.length];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1603,9 +2186,9 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 名单中“状态 / 原徽标”双轨展示时,每 3 秒只刷新徽标槽位,不重建头像行。
|
||||
// 名单中多种徽标轮换展示时,每 3 秒只刷新徽标槽位,不重建头像行。
|
||||
window.setInterval(() => {
|
||||
userBadgeRotationTick = (userBadgeRotationTick + 1) % 2;
|
||||
userBadgeRotationTick = (userBadgeRotationTick + 1) % 4;
|
||||
refreshRenderedUserBadges();
|
||||
syncDailyStatusUi();
|
||||
}, 3000);
|
||||
|
||||
Reference in New Issue
Block a user