- 拆分 HomeController → TransactionController, ReportWebController, FollowPlanController - 新增 Service 层: TransactionService, ReportService, FollowPlanService - pk10.php JS 抽离为 4 个独立文件 (sound/race/bet/poll) - 前台新增报表查询页面 (/report) + 跟单计划页面 (/follow-plan) - 后台新增跟单计划管理 + 用户输赢明细统计 - 封盘状态显示倒计时 (x:xx) - 音效仅在开奖弹窗打开时播放 - 路由按模块分组整理 - autoload 支持 App\Services 命名空间
552 lines
22 KiB
JavaScript
Executable File
552 lines
22 KiB
JavaScript
Executable File
document.addEventListener('DOMContentLoaded', () => {
|
||
// 初始化各功能模块
|
||
initMobileSidebar();
|
||
initSubmenuNavigation();
|
||
highlightActiveMenu();
|
||
initAjaxContentLoader();
|
||
initProfileEditModal();
|
||
|
||
// 触发一次resize事件用于响应式布局初始化
|
||
window.dispatchEvent(new Event('resize'));
|
||
});
|
||
|
||
|
||
/**
|
||
* 显示消息提示
|
||
* @param {string} text - 提示内容
|
||
* @param {string} type - 提示类型(success/error/warning/info)
|
||
*/
|
||
|
||
function createIconElement(iconClass) {
|
||
const icon = document.createElement('i');
|
||
icon.className = `fas ${iconClass}`;
|
||
return icon;
|
||
}
|
||
|
||
function setNotificationContent(container, iconClass, message) {
|
||
container.replaceChildren();
|
||
const icon = createIconElement(iconClass);
|
||
const text = document.createElement('span');
|
||
text.style.wordWrap = 'break-word';
|
||
text.style.flex = '1';
|
||
text.style.maxWidth = 'calc(100% - 24px)';
|
||
text.textContent = String(message ?? '');
|
||
container.append(icon, text);
|
||
}
|
||
|
||
function buildConfirmDialog(title, msg) {
|
||
const panel = document.createElement('div');
|
||
panel.className = 'bg-white rounded-lg shadow-lg w-64 text-center overflow-hidden';
|
||
|
||
const header = document.createElement('div');
|
||
header.className = 'px-4 py-3 border-b border-gray-200';
|
||
const heading = document.createElement('h3');
|
||
heading.className = 'text-lg font-semibold text-gray-800';
|
||
heading.textContent = String(title ?? '');
|
||
header.appendChild(heading);
|
||
|
||
const body = document.createElement('div');
|
||
body.className = 'px-4 py-5';
|
||
const text = document.createElement('p');
|
||
text.className = 'text-gray-600';
|
||
text.textContent = String(msg ?? '');
|
||
body.appendChild(text);
|
||
|
||
const footer = document.createElement('div');
|
||
footer.className = 'px-4 py-3 flex justify-center gap-3';
|
||
|
||
const cancelBtn = document.createElement('button');
|
||
cancelBtn.className = 'px-4 py-1.5 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors';
|
||
cancelBtn.textContent = '取消';
|
||
|
||
const okBtn = document.createElement('button');
|
||
okBtn.className = 'px-4 py-1.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors';
|
||
okBtn.textContent = '确定';
|
||
|
||
footer.append(cancelBtn, okBtn);
|
||
panel.append(header, body, footer);
|
||
|
||
return { panel, cancelBtn, okBtn };
|
||
}
|
||
|
||
function safelyReplaceHtml(target, html) {
|
||
const parser = new DOMParser();
|
||
const doc = parser.parseFromString(String(html || ''), 'text/html');
|
||
doc.querySelectorAll('script').forEach(node => node.remove());
|
||
target.replaceChildren(...Array.from(doc.body.childNodes));
|
||
}
|
||
|
||
function renderStatusBadge(target, text, className) {
|
||
target.replaceChildren();
|
||
const badge = document.createElement('span');
|
||
badge.className = `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${className}`;
|
||
badge.textContent = text;
|
||
target.appendChild(badge);
|
||
}
|
||
|
||
function captureButtonContent(button) {
|
||
return Array.from(button.childNodes).map(node => node.cloneNode(true));
|
||
}
|
||
|
||
function restoreButtonContent(button, nodes) {
|
||
button.replaceChildren(...nodes.map(node => node.cloneNode(true)));
|
||
}
|
||
|
||
function setSubmitButtonLoading(button, isLoading, originalNodes = []) {
|
||
if (isLoading) {
|
||
button.replaceChildren();
|
||
const icon = document.createElement('i');
|
||
icon.className = 'fa fa-spinner fa-spin mr-2';
|
||
const text = document.createTextNode(' 保存中...');
|
||
button.append(icon, text);
|
||
return;
|
||
}
|
||
|
||
restoreButtonContent(button, originalNodes);
|
||
}
|
||
|
||
function showMessage(message, type = 'success') {
|
||
const styles = {success: 'bg-green-500', error: 'bg-red-500', warning: 'bg-yellow-500', info: 'bg-blue-500' };
|
||
const icons = {success: 'fa-check-circle', error: 'fa-exclamation-circle', warning: 'fa-exclamation-triangle', info: 'fa-info-circle' };
|
||
const baseClasses = ['fixed', 'top-4', 'right-4', 'z-50', 'px-4', 'py-3', 'rounded-lg', 'shadow-lg', 'text-white', 'flex', 'items-center', 'gap-2', 'max-w-sm', 'transition-transform', 'duration-300' ];
|
||
let note = document.getElementById('notification');
|
||
if (!note) {
|
||
note = document.createElement('div');
|
||
note.id = 'notification';
|
||
note.classList.add(...baseClasses);
|
||
document.body.appendChild(note);
|
||
}
|
||
note.style.transform = 'translateX(calc(100% + 20px))';
|
||
note.className = '';
|
||
note.classList.add(...baseClasses, styles[type]);
|
||
setNotificationContent(note, icons[type], message);
|
||
setTimeout(() => {
|
||
note.style.transform = 'translateX(0)';
|
||
}, 10);
|
||
clearTimeout(note.timer);
|
||
note.timer = setTimeout(() => {
|
||
note.style.transform = 'translateX(calc(100% + 20px))';
|
||
}, 2000);
|
||
}
|
||
|
||
/**
|
||
* 显示确认弹窗
|
||
* @param {string} text - 提示内容
|
||
*/
|
||
function showConfirm(title, msg) {
|
||
return new Promise(resolve => {
|
||
const mask = document.createElement("div");
|
||
mask.className = "fixed inset-0 bg-black/50 flex items-center justify-center z-50";
|
||
const { panel, cancelBtn, okBtn } = buildConfirmDialog(title, msg);
|
||
mask.appendChild(panel);
|
||
document.body.appendChild(mask);
|
||
cancelBtn.onclick = () => { mask.remove(); resolve(false); };
|
||
okBtn.onclick = () => { mask.remove(); resolve(true); };
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 复制内容
|
||
* @param {string} text - 复制的内容
|
||
* @param {string} type - 内容类型
|
||
*/
|
||
function copyToClipboard(text, typeName = '内容') {
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
showMessage(`${typeName} 复制成功`, 'success');
|
||
}).catch(err => {
|
||
console.error(`${typeName} 复制失败:`, err);
|
||
showMessage(`${typeName} 复制失败,请手动复制`, 'error');
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 移动端侧边栏菜单切换功能
|
||
*/
|
||
function initMobileSidebar() {
|
||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||
const sidebar = document.getElementById('sidebar');
|
||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||
|
||
// 元素不存在则不初始化
|
||
if (!mobileMenuButton || !sidebar || !sidebarOverlay) return;
|
||
|
||
// 打开侧边栏
|
||
const openSidebar = () => {
|
||
sidebar.classList.remove('-translate-x-full');
|
||
sidebar.classList.add('translate-x-0');
|
||
sidebarOverlay.classList.remove('hidden');
|
||
document.body.classList.add('overflow-hidden');
|
||
};
|
||
|
||
// 关闭侧边栏
|
||
const closeSidebar = () => {
|
||
sidebar.classList.add('-translate-x-full');
|
||
sidebar.classList.remove('translate-x-0');
|
||
sidebarOverlay.classList.add('hidden');
|
||
document.body.classList.remove('overflow-hidden');
|
||
};
|
||
|
||
// 切换侧边栏状态
|
||
const toggleSidebar = () => {
|
||
sidebar.classList.contains('translate-x-0') ? closeSidebar() : openSidebar();
|
||
};
|
||
|
||
// 绑定事件
|
||
mobileMenuButton.addEventListener('click', toggleSidebar);
|
||
sidebarOverlay.addEventListener('click', closeSidebar);
|
||
}
|
||
|
||
/**
|
||
* 子菜单折叠/展开功能
|
||
*/
|
||
function initSubmenuNavigation() {
|
||
// 关闭所有子菜单
|
||
const closeAllSubmenus = () => {
|
||
document.querySelectorAll('.submenu').forEach(submenu => {
|
||
submenu.classList.add('hidden');
|
||
});
|
||
document.querySelectorAll('.menu-parent .fa-chevron-down').forEach(icon => {
|
||
icon.classList.remove('rotate-180');
|
||
});
|
||
};
|
||
|
||
// 切换单个子菜单
|
||
const toggleSubmenu = (parentElement) => {
|
||
const submenu = parentElement.nextElementSibling;
|
||
const icon = parentElement.querySelector('.fa-chevron-down');
|
||
const isCurrentlyOpen = submenu && !submenu.classList.contains('hidden');
|
||
|
||
closeAllSubmenus();
|
||
|
||
if (!isCurrentlyOpen && submenu) {
|
||
submenu.classList.remove('hidden');
|
||
icon?.classList.add('rotate-180');
|
||
}
|
||
};
|
||
|
||
// 绑定父菜单点击事件
|
||
document.querySelectorAll('.menu-parent').forEach(parent => {
|
||
parent.addEventListener('click', (e) => {
|
||
// 仅处理菜单本身的点击(不影响内部链接)
|
||
if (e.target.tagName.toLowerCase() === 'a' || e.target.closest('a') === parent) {
|
||
e.preventDefault();
|
||
toggleSubmenu(parent);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 高亮当前页面对应的菜单
|
||
*/
|
||
function highlightActiveMenu() {
|
||
const currentUrl = window.location.pathname;
|
||
|
||
// 移除所有菜单的高亮状态
|
||
document.querySelectorAll('.main-menu-item, .submenu-item').forEach(item => {
|
||
item.classList.remove('bg-primary/10', 'text-primary', 'font-medium');
|
||
});
|
||
|
||
// 优先高亮子菜单
|
||
let submenuHighlighted = false;
|
||
document.querySelectorAll('.submenu-item').forEach(item => {
|
||
if (item.getAttribute('href') === currentUrl) {
|
||
item.classList.add('bg-primary/10', 'text-primary', 'font-medium');
|
||
submenuHighlighted = true;
|
||
|
||
// 高亮父菜单并展开子菜单
|
||
const parentItem = item.closest('.menu-group').querySelector('.menu-parent');
|
||
if (parentItem) {
|
||
parentItem.classList.add('bg-primary/10', 'text-primary', 'font-medium');
|
||
const submenu = parentItem.nextElementSibling;
|
||
if (submenu?.classList.contains('submenu')) {
|
||
submenu.classList.remove('hidden');
|
||
parentItem.querySelector('.fa-chevron-down')?.classList.add('rotate-180');
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 子菜单未匹配时,尝试高亮主菜单
|
||
if (!submenuHighlighted) {
|
||
document.querySelectorAll('.main-menu-item').forEach(item => {
|
||
if (item.getAttribute('href') === currentUrl) {
|
||
item.classList.add('bg-primary/10', 'text-primary', 'font-medium');
|
||
const submenu = item.nextElementSibling;
|
||
if (submenu?.classList.contains('submenu')) {
|
||
submenu.classList.remove('hidden');
|
||
item.querySelector('.fa-chevron-down')?.classList.add('rotate-180');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化插件内容的Ajax加载功能
|
||
*/
|
||
function initAjaxContentLoader() {
|
||
// 通过Ajax加载内容
|
||
const loadContent = (url) => {
|
||
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||
.then(response => {
|
||
if (!response.ok) throw new Error(`加载失败: HTTP ${response.status}`);
|
||
return response.text();
|
||
})
|
||
.then(html => {
|
||
const mainContent = document.getElementById('main-content');
|
||
if (mainContent) {
|
||
safelyReplaceHtml(mainContent, html);
|
||
highlightActiveMenu();
|
||
|
||
// 触发子页面加载完成事件
|
||
window.dispatchEvent(new CustomEvent('subpageLoaded', { detail: { url } }));
|
||
|
||
// 移动端自动关闭侧边栏
|
||
if (window.innerWidth < 768) {
|
||
document.getElementById('mobile-menu-button')?.click();
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('内容加载失败:', error);
|
||
showMessage('页面加载失败,请重试', 'error');
|
||
});
|
||
};
|
||
|
||
// 绑定菜单点击事件(通过Ajax加载内容)
|
||
document.querySelectorAll('.plugin-menu-link, .plugin-submenu-link').forEach(link => {
|
||
link.addEventListener('click', (e) => {
|
||
// 有子菜单的父项不加载内容
|
||
if (link.classList.contains('menu-parent') && link.nextElementSibling?.classList.contains('submenu')) {
|
||
return;
|
||
}
|
||
e.preventDefault();
|
||
const url = link.getAttribute('href');
|
||
if (url) {
|
||
loadContent(url);
|
||
history.pushState(null, '', url); // 更新浏览器地址栏
|
||
}
|
||
});
|
||
});
|
||
|
||
// 监听浏览器前进/后退事件
|
||
window.addEventListener('popstate', () => {
|
||
loadContent(window.location.pathname);
|
||
});
|
||
|
||
// 监听子页面加载完成事件
|
||
window.addEventListener('subpageLoaded', (e) => {
|
||
console.log('子页面加载完成:', e.detail.url);
|
||
// 此处注释掉了自动刷新,避免循环刷新
|
||
location.reload();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 初始化个人资料编辑弹窗
|
||
*/
|
||
function initProfileEditModal() {
|
||
// 获取DOM元素
|
||
const profileModal = document.getElementById('profileModal');
|
||
const profileModalBackdrop = document.getElementById('profileModalBackdrop');
|
||
const openProfileBtn = document.getElementById('profile');
|
||
const closeProfileModal = document.getElementById('closeProfileModal');
|
||
const cancelProfileBtn = document.getElementById('cancelProfileBtn');
|
||
const profileForm = document.getElementById('profileForm');
|
||
const userIdInput = document.getElementById('profileUserId');
|
||
|
||
// 元素不完整则不初始化
|
||
if (!profileModal || !profileForm || !userIdInput) return;
|
||
|
||
const userId = userIdInput.value;
|
||
|
||
// 从服务器加载用户信息
|
||
const loadUserProfile = async () => {
|
||
try {
|
||
const response = await fetch(`/admin/users/${userId}`);
|
||
if (!response.ok) throw new Error('获取用户信息失败');
|
||
const data = await response.json();
|
||
|
||
if (data.success && data.data) {
|
||
// 填充邮箱字段
|
||
const emailInput = document.getElementById('profileEmail');
|
||
if (emailInput) emailInput.value = data.data.email || '';
|
||
const roleDisplay = document.getElementById('userRoleDisplay');
|
||
if (roleDisplay) {
|
||
// 从接口数据中获取角色,转换为中文显示
|
||
const role = data.data.role || 'user';
|
||
roleDisplay.textContent = role === 'admin' ? '管理员' : '普通用户';
|
||
}
|
||
document.getElementById('userRoleInput').value = data.data.role;
|
||
|
||
// 3. 填充状态(新增逻辑)
|
||
const statusDisplay = document.getElementById('userStatusDisplay');
|
||
if (statusDisplay) {
|
||
const rawStatus = data.data.status;
|
||
const isEnabled = rawStatus == 1 || rawStatus == '1' || rawStatus == true;
|
||
document.getElementById('userStatusInput').value = data.data.status;
|
||
// 根据判断结果设置文本和样式
|
||
const statusText = isEnabled ? '已启用' : '已禁用';
|
||
const statusClass = isEnabled
|
||
? 'bg-green-100 text-green-800'
|
||
: 'bg-gray-100 text-gray-800';
|
||
|
||
// 渲染状态标签
|
||
renderStatusBadge(statusDisplay, statusText, statusClass);
|
||
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载个人资料失败:', error);
|
||
showMessage('加载个人资料失败', 'error');
|
||
}
|
||
};
|
||
|
||
// 打开弹窗
|
||
const openProfileModal = () => {
|
||
profileModal.classList.remove('invisible', 'opacity-0');
|
||
const modalContent = profileModal.querySelector('div');
|
||
if (modalContent) {
|
||
modalContent.classList.remove('scale-95');
|
||
modalContent.classList.add('scale-100');
|
||
}
|
||
document.body.style.overflow = 'hidden';
|
||
loadUserProfile(); // 加载用户数据
|
||
};
|
||
|
||
// 关闭弹窗
|
||
const closeProfileModalFunc = () => {
|
||
profileModal.classList.add('invisible', 'opacity-0');
|
||
const modalContent = profileModal.querySelector('div');
|
||
if (modalContent) {
|
||
modalContent.classList.remove('scale-100');
|
||
modalContent.classList.add('scale-95');
|
||
}
|
||
document.body.style.overflow = '';
|
||
};
|
||
|
||
// 提交表单处理
|
||
const submitProfileForm = async (e) => {
|
||
e.preventDefault();
|
||
|
||
const submitBtn = profileForm.querySelector('button[type="submit"]');
|
||
if (!submitBtn) return;
|
||
|
||
const originalNodes = captureButtonContent(submitBtn);
|
||
// 禁用按钮并显示加载状态
|
||
submitBtn.disabled = true;
|
||
setSubmitButtonLoading(submitBtn, true, originalNodes);
|
||
|
||
try {
|
||
const formData = new FormData(profileForm);
|
||
const response = await fetch('/admin/users/update', {
|
||
method: 'POST',
|
||
body: formData,
|
||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showMessage('个人资料更新成功');
|
||
closeProfileModalFunc();
|
||
|
||
// 更新页面显示的用户名
|
||
const newUsername = document.getElementById('profileUsername')?.value;
|
||
if (newUsername) {
|
||
|
||
if (targetUsername) {
|
||
const usernameElements = Array.from(document.querySelectorAll('span'))
|
||
.filter(span => {
|
||
return span.textContent.trim().includes(targetUsername.trim());
|
||
});
|
||
|
||
usernameElements.forEach(el => {
|
||
el.textContent = newUsername;
|
||
});
|
||
}
|
||
}
|
||
} else {
|
||
throw new Error(data.message || '更新失败');
|
||
}
|
||
} catch (error) {
|
||
showMessage(error.message, 'error');
|
||
} finally {
|
||
// 恢复按钮状态
|
||
submitBtn.disabled = false;
|
||
setSubmitButtonLoading(submitBtn, false, originalNodes);
|
||
}
|
||
};
|
||
|
||
// 绑定事件
|
||
if (openProfileBtn) {
|
||
openProfileBtn.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
openProfileModal();
|
||
});
|
||
}
|
||
|
||
if (closeProfileModal) closeProfileModal.addEventListener('click', closeProfileModalFunc);
|
||
if (cancelProfileBtn) cancelProfileBtn.addEventListener('click', closeProfileModalFunc);
|
||
if (profileModalBackdrop) profileModalBackdrop.addEventListener('click', closeProfileModalFunc);
|
||
|
||
profileForm.addEventListener('submit', submitProfileForm);
|
||
|
||
// ESC键关闭弹窗
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape' && !profileModal.classList.contains('invisible')) {
|
||
closeProfileModalFunc();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 获取元素(仅在元素存在时绑定事件)
|
||
const contactLink = document.getElementById('contactSupport');
|
||
const popup = document.getElementById('supportPopup');
|
||
const closeBtn = document.getElementById('closePopup');
|
||
|
||
// 打开弹出层(仅在元素存在时)
|
||
if (contactLink && popup) {
|
||
contactLink.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
popup.classList.remove('hidden');
|
||
// 添加淡入动画效果
|
||
setTimeout(() => {
|
||
const popupDiv = popup.querySelector('div');
|
||
if (popupDiv) {
|
||
popupDiv.classList.add('scale-100');
|
||
popupDiv.classList.remove('scale-95');
|
||
}
|
||
}, 10);
|
||
});
|
||
}
|
||
|
||
// 关闭弹出层
|
||
function closePopup() {
|
||
if (popup) {
|
||
const popupDiv = popup.querySelector('div');
|
||
if (popupDiv) {
|
||
popupDiv.classList.add('scale-95');
|
||
popupDiv.classList.remove('scale-100');
|
||
}
|
||
setTimeout(() => {
|
||
popup.classList.add('hidden');
|
||
}, 300);
|
||
}
|
||
}
|
||
|
||
// 绑定关闭按钮(仅在元素存在时)
|
||
if (closeBtn) {
|
||
closeBtn.addEventListener('click', closePopup);
|
||
}
|
||
|
||
// 点击外部关闭(仅在元素存在时)
|
||
if (popup) {
|
||
popup.addEventListener('click', function(e) {
|
||
if (e.target === popup) {
|
||
closePopup();
|
||
}
|
||
});
|
||
}
|
||
|