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:
+96
-25
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user