Files
pk10/Static/js/admin.js
T
li 4a94fe36cf feat: 重构代码架构 + 新增报表/跟单/输赢统计功能
- 拆分 HomeController → TransactionController, ReportWebController, FollowPlanController
- 新增 Service 层: TransactionService, ReportService, FollowPlanService
- pk10.php JS 抽离为 4 个独立文件 (sound/race/bet/poll)
- 前台新增报表查询页面 (/report) + 跟单计划页面 (/follow-plan)
- 后台新增跟单计划管理 + 用户输赢明细统计
- 封盘状态显示倒计时 (x:xx)
- 音效仅在开奖弹窗打开时播放
- 路由按模块分组整理
- autoload 支持 App\Services 命名空间
2026-03-27 18:41:06 +08:00

552 lines
22 KiB
JavaScript
Executable File
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.
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();
}
});
}