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 命名空间
This commit is contained in:
li
2026-03-27 18:41:06 +08:00
parent 48cfe4be04
commit 4a94fe36cf
87 changed files with 8681 additions and 646 deletions
+96 -25
View File
@@ -17,6 +17,94 @@
* @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' };
@@ -31,7 +119,7 @@
note.style.transform = 'translateX(calc(100% + 20px))';
note.className = '';
note.classList.add(...baseClasses, styles[type]);
note.innerHTML = `<i class="fas ${icons[type]}"></i><span style="word-wrap: break-word; flex: 1; max-width: calc(100% - 24px);">${message}</span>`;
setNotificationContent(note, icons[type], message);
setTimeout(() => {
note.style.transform = 'translateX(0)';
}, 10);
@@ -49,22 +137,9 @@
return new Promise(resolve => {
const mask = document.createElement("div");
mask.className = "fixed inset-0 bg-black/50 flex items-center justify-center z-50";
mask.innerHTML = `
<div class="bg-white rounded-lg shadow-lg w-64 text-center overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-800">${title}</h3>
</div>
<div class="px-4 py-5">
<p class="text-gray-600">${msg}</p>
</div>
<div class="px-4 py-3 flex justify-center gap-3">
<button class="px-4 py-1.5 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"> 取消 </button>
<button class="px-4 py-1.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors"> 确定 </button>
</div>
</div>
`;
const { panel, cancelBtn, okBtn } = buildConfirmDialog(title, msg);
mask.appendChild(panel);
document.body.appendChild(mask);
const [cancelBtn, okBtn] = mask.querySelectorAll("button");
cancelBtn.onclick = () => { mask.remove(); resolve(false); };
okBtn.onclick = () => { mask.remove(); resolve(true); };
});
@@ -221,7 +296,7 @@
.then(html => {
const mainContent = document.getElementById('main-content');
if (mainContent) {
mainContent.innerHTML = html;
safelyReplaceHtml(mainContent, html);
highlightActiveMenu();
// 触发子页面加载完成事件
@@ -318,11 +393,7 @@
: 'bg-gray-100 text-gray-800';
// 渲染状态标签
statusDisplay.innerHTML = `
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusClass}">
${statusText}
</span>
`;
renderStatusBadge(statusDisplay, statusText, statusClass);
}
}
@@ -362,10 +433,10 @@
const submitBtn = profileForm.querySelector('button[type="submit"]');
if (!submitBtn) return;
const originalText = submitBtn.innerHTML;
const originalNodes = captureButtonContent(submitBtn);
// 禁用按钮并显示加载状态
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 保存中...';
setSubmitButtonLoading(submitBtn, true, originalNodes);
try {
const formData = new FormData(profileForm);
@@ -403,7 +474,7 @@
} finally {
// 恢复按钮状态
submitBtn.disabled = false;
submitBtn.innerHTML = originalText;
setSubmitButtonLoading(submitBtn, false, originalNodes);
}
};