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
This commit is contained in:
@@ -307,6 +307,9 @@ export function handleFeatureLocalClear(onLocalClear) {
|
||||
|
||||
if (typeof onLocalClear === "function") {
|
||||
onLocalClear();
|
||||
} else if (typeof window.localClearScreen === "function") {
|
||||
// 默认调用聊天室清屏函数,将当前可见消息全部移除。
|
||||
window.localClearScreen();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,3 +466,390 @@ export function shouldMigrateLocalChatPreferences(serverPreferences, localBlocke
|
||||
|
||||
return !hasServerPreferences && (localBlockedSenders.length > 0 || localMuted);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据消息内容识别其对应的屏蔽规则键。
|
||||
*
|
||||
* @param {Record<string, unknown>} msg 消息对象
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function resolveBlockedSystemSenderKey(msg) {
|
||||
const fromUser = String(msg?.from_user || "");
|
||||
const content = String(msg?.content || "");
|
||||
|
||||
if (fromUser === "钓鱼播报") {
|
||||
return "钓鱼播报";
|
||||
}
|
||||
|
||||
if (fromUser === "神秘箱子") {
|
||||
return "神秘箱子";
|
||||
}
|
||||
|
||||
if (fromUser === "星海小博士") {
|
||||
return "星海小博士";
|
||||
}
|
||||
|
||||
// 兼容旧版自动钓鱼卡购买通知:历史上该消息曾以"系统传音"发送,但正文里带有"钓鱼播报"字样。
|
||||
if ((fromUser === "系统传音" || fromUser === "系统") && (content.includes("钓鱼播报") || content.includes("自动钓鱼模式"))) {
|
||||
return "钓鱼播报";
|
||||
}
|
||||
|
||||
if ((fromUser === "系统传音" || fromUser === "系统") && content.includes("神秘箱子")) {
|
||||
return "神秘箱子";
|
||||
}
|
||||
|
||||
if ((fromUser === "系统传音" || fromUser === "系统") && content.includes("百家乐")) {
|
||||
return "百家乐";
|
||||
}
|
||||
|
||||
if ((fromUser === "系统传音" || fromUser === "系统") && (content.includes("赛马") || content.includes("跑马"))) {
|
||||
return "跑马";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── 偏好持久化 ──
|
||||
|
||||
/**
|
||||
* 构建当前聊天室偏好快照。
|
||||
*
|
||||
* @returns {{blocked_system_senders:string[],sound_muted:boolean}}
|
||||
*/
|
||||
export function buildChatPreferencesPayload() {
|
||||
const state = window.chatState;
|
||||
return {
|
||||
blocked_system_senders: state ? Array.from(state.blockedSystemSenders) : [],
|
||||
sound_muted: isSoundMuted(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将聊天室偏好写入本地缓存,供刷新前快速恢复与迁移兜底。
|
||||
*/
|
||||
export function persistChatPreferencesToLocal() {
|
||||
const state = window.chatState;
|
||||
if (state) {
|
||||
persistBlockedSystemSenders(state.blockedSystemSenders);
|
||||
}
|
||||
setSoundMuted(isSoundMuted());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前聊天室偏好保存到当前登录账号。
|
||||
*/
|
||||
export async function saveChatPreferences() {
|
||||
const payload = buildChatPreferencesPayload();
|
||||
persistChatPreferencesToLocal();
|
||||
|
||||
if (!window.chatContext?.chatPreferencesUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(window.chatContext.chatPreferencesUrl, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]')?.content ?? "",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("save chat preferences failed");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data?.status === "success") {
|
||||
window.chatContext.chatPreferences = normalizeChatPreferences(data.data || payload);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("聊天室偏好保存失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 屏蔽 UI 同步 ──
|
||||
|
||||
/**
|
||||
* 同步屏蔽菜单中的复选框状态。
|
||||
*/
|
||||
export function syncBlockedSystemSenderCheckboxes() {
|
||||
const state = window.chatState;
|
||||
const blockedSet = state ? state.blockedSystemSenders : new Set();
|
||||
|
||||
const checkboxMap = {
|
||||
"block-sender-fishing": "钓鱼播报",
|
||||
"block-sender-doctor": "星海小博士",
|
||||
"block-sender-baccarat": "百家乐",
|
||||
"block-sender-horse-race": "跑马",
|
||||
"block-sender-mystery-box": "神秘箱子",
|
||||
};
|
||||
|
||||
Object.entries(checkboxMap).forEach(([id, sender]) => {
|
||||
const checkbox = document.getElementById(id);
|
||||
if (checkbox) {
|
||||
checkbox.checked = blockedSet.has(sender);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量切换当前已渲染消息的显示状态。
|
||||
*
|
||||
* @param {string} blockKey 屏蔽规则键
|
||||
* @param {boolean} hidden true = 隐藏,false = 恢复显示
|
||||
*/
|
||||
export function setRenderedMessagesVisibilityBySender(blockKey, hidden) {
|
||||
const state = window.chatState;
|
||||
[state?.container, state?.container2].forEach(targetContainer => {
|
||||
if (!targetContainer) return;
|
||||
|
||||
targetContainer.querySelectorAll("[data-block-key]").forEach(node => {
|
||||
if (node.dataset.blockKey === blockKey) {
|
||||
if (hidden) {
|
||||
node.dataset.blockHidden = "1";
|
||||
node.style.display = "none";
|
||||
} else if (node.dataset.blockHidden === "1") {
|
||||
node.removeAttribute("data-block-hidden");
|
||||
node.style.display = "";
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!hidden && state?.autoScroll) {
|
||||
const container = state.container;
|
||||
const container2 = state.container2;
|
||||
if (container) container.scrollTop = container.scrollHeight;
|
||||
if (container2) container2.scrollTop = container2.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定系统播报项的屏蔽状态,并在勾选后立即清理当前窗口。
|
||||
*
|
||||
* @param {string} sender 系统播报发送者/规则键
|
||||
* @param {boolean} blocked 是否屏蔽
|
||||
*/
|
||||
export function toggleBlockedSystemSender(sender, blocked) {
|
||||
const state = window.chatState;
|
||||
if (!state) return;
|
||||
|
||||
if (!BLOCKABLE_SYSTEM_SENDERS.includes(sender)) return;
|
||||
|
||||
if (blocked) {
|
||||
state.blockedSystemSenders.add(sender);
|
||||
setRenderedMessagesVisibilityBySender(sender, true);
|
||||
} else {
|
||||
state.blockedSystemSenders.delete(sender);
|
||||
setRenderedMessagesVisibilityBySender(sender, false);
|
||||
}
|
||||
|
||||
persistBlockedSystemSenders(state.blockedSystemSenders);
|
||||
syncBlockedSystemSenderCheckboxes();
|
||||
void saveChatPreferences();
|
||||
}
|
||||
|
||||
// ── 挂载到 window:偏好持久化 ──
|
||||
window.saveChatPreferences = saveChatPreferences;
|
||||
window.syncBlockedSystemSenderCheckboxes = syncBlockedSystemSenderCheckboxes;
|
||||
window.setRenderedMessagesVisibilityBySender = setRenderedMessagesVisibilityBySender;
|
||||
window.toggleBlockedSystemSender = toggleBlockedSystemSender;
|
||||
window.persistChatPreferencesToLocal = persistChatPreferencesToLocal;
|
||||
window.buildChatPreferencesPayload = buildChatPreferencesPayload;
|
||||
|
||||
// ── 挂载到 window:菜单/浮层控制(供 bindBlockMenuControls 事件代理调用)──
|
||||
window.toggleBlockMenu = toggleBlockMenu;
|
||||
window.toggleFeatureMenu = toggleFeatureMenu;
|
||||
window.closeFeatureMenu = closeFeatureMenu;
|
||||
window.openDailyStatusEditor = openDailyStatusEditor;
|
||||
window.closeDailyStatusEditor = closeDailyStatusEditor;
|
||||
window.handleFeatureLocalClear = handleFeatureLocalClear;
|
||||
|
||||
// ── 每日状态 UI 同步 ──
|
||||
|
||||
/**
|
||||
* 获取当前登录用户仍然有效的每日状态。
|
||||
*
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
export function getCurrentUserDailyStatus() {
|
||||
return normalizeDailyStatus(window.chatContext?.currentDailyStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除用户在线载荷中的状态字段,避免合并时残留旧状态。
|
||||
*
|
||||
* @param {Record<string, unknown>} payload 用户在线载荷
|
||||
*/
|
||||
export function removeDailyStatusFields(payload) {
|
||||
if (!payload || typeof payload !== "object") return;
|
||||
delete payload.daily_status_key;
|
||||
delete payload.daily_status_label;
|
||||
delete payload.daily_status_icon;
|
||||
delete payload.daily_status_group;
|
||||
delete payload.daily_status_expires_at;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将状态写回指定用户的在线载荷。
|
||||
*
|
||||
* @param {string} username 用户名
|
||||
* @param {Object|null} status 标准化后的状态对象
|
||||
*/
|
||||
export function setOnlineUserDailyStatus(username, status) {
|
||||
const onlineUsers = window.chatState?.onlineUsers || window.onlineUsers || {};
|
||||
if (!username || !onlineUsers[username]) return;
|
||||
|
||||
removeDailyStatusFields(onlineUsers[username]);
|
||||
if (!status) return;
|
||||
|
||||
onlineUsers[username].daily_status_key = status.key;
|
||||
onlineUsers[username].daily_status_label = status.label;
|
||||
onlineUsers[username].daily_status_icon = status.icon;
|
||||
onlineUsers[username].daily_status_group = status.group;
|
||||
onlineUsers[username].daily_status_expires_at = status.expires_at;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步状态按钮文字与图标。
|
||||
*/
|
||||
function syncDailyStatusTrigger() {
|
||||
const shortcutIcon = document.getElementById("daily-status-shortcut-icon");
|
||||
const shortcutLabel = document.getElementById("daily-status-shortcut-label");
|
||||
const activeStatus = getCurrentUserDailyStatus();
|
||||
|
||||
if (shortcutIcon) shortcutIcon.textContent = activeStatus?.icon || "🙂";
|
||||
if (shortcutLabel) shortcutLabel.textContent = activeStatus?.label || "状态";
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步状态面板中当前选中项的高亮样式。
|
||||
*/
|
||||
function syncDailyStatusMenuSelection() {
|
||||
const activeKey = getCurrentUserDailyStatus()?.key || "";
|
||||
|
||||
document.querySelectorAll("#daily-status-editor-overlay .daily-status-item").forEach((button) => {
|
||||
const selected = button.dataset.statusKey === activeKey;
|
||||
button.style.borderColor = selected ? "#6366f1" : "#e5e7eb";
|
||||
button.style.background = selected ? "linear-gradient(180deg,#eef2ff 0%,#e0e7ff 100%)" : "#ffffffcc";
|
||||
button.style.color = selected ? "#312e81" : "#334155";
|
||||
button.style.boxShadow = selected ? "0 8px 18px rgba(99,102,241,.18)" : "none";
|
||||
button.style.transform = selected ? "translateY(-1px)" : "translateY(0)";
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步聊天室状态相关 UI(按钮、面板高亮、聊天上下文)。
|
||||
*/
|
||||
export function syncDailyStatusUi() {
|
||||
const activeStatus = getCurrentUserDailyStatus();
|
||||
if (window.chatContext) window.chatContext.currentDailyStatus = activeStatus;
|
||||
|
||||
syncDailyStatusTrigger();
|
||||
syncDailyStatusMenuSelection();
|
||||
}
|
||||
|
||||
// ── 每日状态更新与清除 ──
|
||||
|
||||
/**
|
||||
* 向服务端发送每日状态更新请求。
|
||||
*
|
||||
* @param {string} statusKey 状态键值
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function updateDailyStatus(statusKey) {
|
||||
const url = window.chatContext?.dailyStatusUpdateUrl;
|
||||
if (!url || !statusKey) return;
|
||||
|
||||
const csrf = document.querySelector('meta[name="csrf-token"]')?.content ?? "";
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": csrf,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ daily_status_key: statusKey }),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("update daily status failed");
|
||||
|
||||
const data = await response.json();
|
||||
if (data?.status === "success" && window.chatContext) {
|
||||
window.chatContext.currentDailyStatus = data.data ?? null;
|
||||
}
|
||||
|
||||
closeDailyStatusEditor();
|
||||
syncDailyStatusUi();
|
||||
|
||||
// 让在线用户列表同步当前用户的最新状态
|
||||
const username = window.chatContext?.username;
|
||||
if (username) {
|
||||
setOnlineUserDailyStatus(username, getCurrentUserDailyStatus());
|
||||
}
|
||||
|
||||
if (typeof window.renderUserList === "function") {
|
||||
window.renderUserList();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("每日状态更新失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前登录用户的每日状态。
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function clearDailyStatus() {
|
||||
const url = window.chatContext?.dailyStatusUpdateUrl;
|
||||
if (!url) return;
|
||||
|
||||
const csrf = document.querySelector('meta[name="csrf-token"]')?.content ?? "";
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": csrf,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ daily_status_key: null }),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("clear daily status failed");
|
||||
|
||||
const data = await response.json();
|
||||
if (data?.status === "success" && window.chatContext) {
|
||||
window.chatContext.currentDailyStatus = null;
|
||||
}
|
||||
|
||||
closeDailyStatusEditor();
|
||||
syncDailyStatusUi();
|
||||
|
||||
// 移除当前用户在线载荷中的状态字段
|
||||
const username = window.chatContext?.username;
|
||||
if (username) {
|
||||
setOnlineUserDailyStatus(username, null);
|
||||
}
|
||||
|
||||
if (typeof window.renderUserList === "function") {
|
||||
window.renderUserList();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("每日状态清除失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 挂载到 window:每日状态 ──
|
||||
window.getCurrentUserDailyStatus = getCurrentUserDailyStatus;
|
||||
window.setOnlineUserDailyStatus = setOnlineUserDailyStatus;
|
||||
window.syncDailyStatusUi = syncDailyStatusUi;
|
||||
window.updateDailyStatus = updateDailyStatus;
|
||||
window.clearDailyStatus = clearDailyStatus;
|
||||
|
||||
Reference in New Issue
Block a user