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
+333 -143
View File
@@ -1,13 +1,5 @@
(function() {
// 常量定义 - 类名移除avif并添加ja-前缀
const CLOSE_ICON = ` <img src="https://yanxuan.nosdn.127.net/0c64ce12c71cf276504cb2e15164d4ff.png"width="25px"> `;
const RESET_ICON = ` <img src="https://yanxuan.nosdn.127.net/8296ea39d8c548ed320f79a483756cd9.jpg"width="25px" > `;
const PREVIEW_ICON = ` <img class="ja-action-icon" src="https://yanxuan.nosdn.127.net/520b793c329df6349a25404efefbd0f4.png"/> `;
const COPY_ICON = ` <img class="ja-action-icon" src="https://yanxuan.nosdn.127.net/0732b6e6d1f249dad11ac7d004af5964.png"/> `;
const UP_ICON = ` <img class="ja-button-icon" src="https://yanxuan.nosdn.127.net/94494a4dc561ae21157f50764ddf035e.png"/> `;
const ERRO_ICON = ` <img class="ja-erro-icon" src="https://yanxuan.nosdn.127.net/74777259ff515f056e8fc340df28fcd7.png"/> `;
// 基础配置
const scriptBaseUrl = getScriptBaseUrl();
const API_CONFIG = {
@@ -76,11 +68,265 @@ function getScriptBaseUrl() {
window.addEventListener('resize', setVh);
window.addEventListener('orientationchange', setVh);
function createIconWrapper(className, iconType) {
const wrapper = document.createElement('div');
wrapper.className = className;
wrapper.replaceChildren(createIconNode(iconType));
return wrapper;
}
function createImageElement(src, alt = '', className = '', width = '') {
const image = document.createElement('img');
image.src = src;
image.alt = alt;
if (width) {
image.width = Number(width);
}
return image;
}
function createIconNode(type) {
switch (type) {
case 'close':
return createImageElement('https://yanxuan.nosdn.127.net/0c64ce12c71cf276504cb2e15164d4ff.png', '', '', '25');
case 'reset':
return createImageElement('https://yanxuan.nosdn.127.net/8296ea39d8c548ed320f79a483756cd9.jpg', '', '', '25');
case 'preview':
return createImageElement('https://yanxuan.nosdn.127.net/520b793c329df6349a25404efefbd0f4.png', '', 'ja-action-icon');
case 'copy':
return createImageElement('https://yanxuan.nosdn.127.net/0732b6e6d1f249dad11ac7d004af5964.png', '', 'ja-action-icon');
case 'upload':
return createImageElement('https://yanxuan.nosdn.127.net/94494a4dc561ae21157f50764ddf035e.png', '', 'ja-button-icon');
case 'error':
return createImageElement('https://yanxuan.nosdn.127.net/74777259ff515f056e8fc340df28fcd7.png', '', 'ja-erro-icon');
default:
return document.createTextNode('');
}
}
function createUploadingIcon() {
return createFontIcon('uploading-icon');
}
function createLoadingContainer(message = '加载中...') {
const container = document.createElement('div');
container.className = 'ja-loading-container';
const icon = document.createElement('i');
icon.className = 'loading-icon';
const text = document.createElement('p');
text.className = 'ja-loading-text';
text.textContent = message;
container.append(icon, text);
return container;
}
function createStatusContainer(type, message) {
const container = document.createElement('div');
container.className = `ja-${type}-container`;
container.appendChild(createIconNode('error'));
const text = document.createElement('p');
text.className = `ja-${type}-text`;
text.textContent = String(message || '');
container.appendChild(text);
return container;
}
function createMediaCard(item) {
const itemElement = document.createElement('div');
itemElement.className = 'ja-media-item';
const thumbnail = document.createElement('div');
thumbnail.className = 'ja-item-thumbnail';
const image = document.createElement('img');
image.src = item.url;
image.alt = item.name;
image.className = 'ja-thumbnail-image';
image.loading = 'lazy';
const overlay = document.createElement('div');
overlay.className = 'ja-item-overlay';
const copyButton = document.createElement('button');
copyButton.className = 'ja-copy-button';
copyButton.dataset.url = item.url;
copyButton.appendChild(createIconNode('copy'));
const previewButton = document.createElement('button');
previewButton.className = 'ja-preview-button';
previewButton.dataset.url = item.url;
previewButton.appendChild(createIconNode('preview'));
overlay.append(copyButton, previewButton);
thumbnail.append(image, overlay);
const info = document.createElement('div');
info.className = 'ja-item-info';
const name = document.createElement('div');
name.className = 'ja-item-name';
name.textContent = item.name;
const details = document.createElement('div');
details.className = 'ja-item-details';
const size = document.createElement('span');
size.className = 'ja-item-size';
size.textContent = formatFileSize(item.size);
const dimensions = document.createElement('span');
dimensions.className = 'ja-item-dimensions';
dimensions.textContent = `${item.width}*${item.height}`;
details.append(size, dimensions);
info.append(name, details);
itemElement.append(thumbnail, info);
return itemElement;
}
function createUploadErrorItem(fileName, reason) {
const errorItem = document.createElement('div');
errorItem.className = 'ja-upload-error';
const header = document.createElement('div');
header.className = 'ja-error-header';
const name = document.createElement('span');
name.className = 'ja-error-filename';
name.textContent = fileName;
const status = document.createElement('span');
status.className = 'ja-error-status';
status.textContent = '错误';
header.append(name, status);
const message = document.createElement('div');
message.className = 'ja-error-message';
message.textContent = reason;
errorItem.append(header, message);
return errorItem;
}
function createUploadSuccessItem(data) {
const wrapper = document.createElement('div');
wrapper.className = 'ja-media-item';
const thumbnail = document.createElement('div');
thumbnail.className = 'ja-item-thumbnail relative';
const image = document.createElement('img');
image.src = data.url;
image.alt = data.name;
image.className = 'ja-thumbnail-image';
const overlay = document.createElement('div');
overlay.className = 'ja-item-overlay';
const copyButton = document.createElement('button');
copyButton.className = 'ja-copy-button';
copyButton.dataset.url = data.url;
copyButton.appendChild(createIconNode('copy'));
const previewButton = document.createElement('button');
previewButton.className = 'ja-preview-button';
previewButton.dataset.url = data.url;
previewButton.appendChild(createIconNode('preview'));
overlay.append(copyButton, previewButton);
thumbnail.append(image, overlay);
const info = document.createElement('div');
info.className = 'ja-item-info';
const detailTop = document.createElement('div');
detailTop.className = 'ja-item-details';
const name = document.createElement('span');
name.className = 'ja-item-name';
name.textContent = data.name;
const success = document.createElement('span');
success.className = 'ja-upload-success';
success.textContent = '已完成';
detailTop.append(name, success);
const detailBottom = document.createElement('div');
detailBottom.className = 'ja-item-details';
const size = document.createElement('span');
size.className = 'ja-item-size';
size.textContent = formatFileSize(data.size || 0);
const dimensions = document.createElement('span');
dimensions.className = 'ja-item-dimensions';
dimensions.textContent = `${data.width}*${data.height}`;
detailBottom.append(size, dimensions);
info.append(detailTop, detailBottom);
wrapper.append(thumbnail, info);
return wrapper;
}
function createUploadFailureContent(message) {
const fragment = document.createDocumentFragment();
const header = document.createElement('div');
header.className = 'ja-upload-header';
const fileName = document.createElement('span');
fileName.className = 'ja-upload-filename';
const failure = document.createElement('span');
failure.className = 'ja-upload-failure';
header.append(fileName, failure);
const detail = document.createElement('div');
detail.className = 'ja-upload-error-message';
detail.textContent = message;
fragment.append(header, detail);
return fragment;
}
function createPreviewModal(url) {
const modal = document.createElement('div');
modal.className = 'ja-image-preview-modal';
const content = document.createElement('div');
content.className = 'ja-modal-content';
const image = document.createElement('img');
image.src = url;
image.alt = '预览图片';
image.className = 'ja-modal-image';
const button = document.createElement('button');
button.className = 'ja-close-button';
button.appendChild(createIconNode('close'));
content.append(image, button);
modal.appendChild(content);
return { modal, content, image, button };
}
// UI 创建函数 - 所有ID和类名添加ja-前缀并移除avif
function createUI() {
// 动态插入 CSS,禁止顶部下拉刷新
const style = document.createElement('style');
style.innerHTML = `
style.textContent = `
html, body {
overscroll-behavior-y: contain;
}
@@ -100,14 +346,14 @@ function getScriptBaseUrl() {
// 创建关闭按钮
closeBtn = document.createElement('button');
closeBtn.className = 'ja-modal-close';
closeBtn.innerHTML = `${CLOSE_ICON}`;
closeBtn.appendChild(createIconNode('close'));
// 创建重置按钮
resetBtn = document.createElement('button');
resetBtn.className = 'ja-img-close';
resetBtn.setAttribute('data-action', 'reset');
resetBtn.innerHTML = `${RESET_ICON}`;
resetBtn.appendChild(createIconNode('reset'));
// 标签页导航
const tabs = document.createElement('div');
@@ -143,27 +389,56 @@ function getScriptBaseUrl() {
const qualityLabel = document.createElement('label');
qualityLabel.className = 'ja-param-label';
qualityLabel.innerHTML = `
<span class="ja-param-title">质量设置 (1-100)</span>
<div class="ja-range-wrapper">
<input type="range" id="ja-quality" min="1" max="100" value="60" class="ja-range-input">
<span id="ja-quality-value" class="ja-range-span">60</span>
</div>
`;
const qualityTitle = document.createElement('span');
qualityTitle.className = 'ja-param-title';
qualityTitle.textContent = '质量设置 (1-100)';
const qualityWrapper = document.createElement('div');
qualityWrapper.className = 'ja-range-wrapper';
const qualityRange = document.createElement('input');
qualityRange.type = 'range';
qualityRange.id = 'ja-quality';
qualityRange.min = '1';
qualityRange.max = '100';
qualityRange.value = '60';
qualityRange.className = 'ja-range-input';
const qualityValue = document.createElement('span');
qualityValue.id = 'ja-quality-value';
qualityValue.className = 'ja-range-span';
qualityValue.textContent = '60';
qualityWrapper.append(qualityRange, qualityValue);
qualityLabel.append(qualityTitle, qualityWrapper);
const widthLabel = document.createElement('label');
widthLabel.className = 'ja-param-label';
widthLabel.innerHTML = `
<span class="ja-param-title">转换宽度</span>
<input type="number" id="ja-width" placeholder="留空为自动" class="ja-text-input">
`;
const widthTitle = document.createElement('span');
widthTitle.className = 'ja-param-title';
widthTitle.textContent = '转换宽度';
const widthInput = document.createElement('input');
widthInput.type = 'number';
widthInput.id = 'ja-width';
widthInput.placeholder = '留空为自动';
widthInput.className = 'ja-text-input';
widthLabel.append(widthTitle, widthInput);
const heightLabel = document.createElement('label');
heightLabel.className = 'ja-param-label';
heightLabel.innerHTML = `
<span class="ja-param-title">转换高度</span>
<input type="number" id="ja-height" placeholder="留空为自动" class="ja-text-input">
`;
const heightTitle = document.createElement('span');
heightTitle.className = 'ja-param-title';
heightTitle.textContent = '转换高度';
const heightInput = document.createElement('input');
heightInput.type = 'number';
heightInput.id = 'ja-height';
heightInput.placeholder = '留空为自动';
heightInput.className = 'ja-text-input';
heightLabel.append(heightTitle, heightInput);
// 上传控件
const uploadInput = document.createElement('input');
@@ -176,10 +451,12 @@ function getScriptBaseUrl() {
const uploadBtn = document.createElement('button');
uploadBtn.id = 'ja-upload-btn';
uploadBtn.className = 'ja-button ja-button-primary';
uploadBtn.innerHTML = `
<div id="ja-upload-svg"> ${UP_ICON} </div>
<div id="ja-upload-status">开始上传</div>
`;
const uploadSvg = createIconWrapper('ja-upload-svg', 'upload');
uploadSvg.id = 'ja-upload-svg';
const uploadStatus = document.createElement('div');
uploadStatus.id = 'ja-upload-status';
uploadStatus.textContent = '开始上传';
uploadBtn.append(uploadSvg, uploadStatus);
const uploadResults = document.createElement('div');
uploadResults.id = 'ja-upload-results';
@@ -189,9 +466,9 @@ function getScriptBaseUrl() {
const loadMoreBtn = document.createElement('button');
loadMoreBtn.id = 'ja-load-more';
loadMoreBtn.className = 'ja-load-more ja-button ja-button-secondary mt-4';
loadMoreBtn.innerHTML = `
<i class="fa fa-refresh mr-2"></i> 加载更多
`;
const loadMoreIcon = document.createElement('i');
loadMoreIcon.className = 'fa fa-refresh mr-2';
loadMoreBtn.append(loadMoreIcon, document.createTextNode(' 加载更多'));
document.head.appendChild(style);
@@ -343,7 +620,7 @@ function getScriptBaseUrl() {
// 清空上传结果
const uploadResults = document.getElementById('ja-upload-results');
if (uploadResults) uploadResults.innerHTML = '';
if (uploadResults) uploadResults.replaceChildren();
// 清空文件选择
const uploadInput = document.getElementById('ja-upload-input');
@@ -384,22 +661,12 @@ function getScriptBaseUrl() {
// 首次加载显示加载状态
if (isInitialLoad) {
grid.innerHTML = `
<div class="ja-loading-container">
<i class="loading-icon"></i>
<p class="ja-loading-text">加载中...</p>
</div>
`;
grid.replaceChildren(createLoadingContainer());
// 初始加载时隐藏加载更多按钮
loadMoreBtn.style.display = 'none';
} else {
// 非首次加载时显示加载状态
const loadingIndicator = document.createElement('div');
loadingIndicator.className = 'ja-loading-container';
loadingIndicator.innerHTML = `
<i class="loading-icon"></i>
<p class="ja-loading-text">加载中...</p>
`;
const loadingIndicator = createLoadingContainer();
grid.appendChild(loadingIndicator);
}
@@ -430,15 +697,17 @@ function getScriptBaseUrl() {
// 控制加载更多按钮显示
if (state.currentPage >= state.totalPages) {
loadMoreBtn.innerHTML = `
<i class="fa fa-check mr-2"></i> 没有更多图片了
`;
loadMoreBtn.replaceChildren();
const doneIcon = document.createElement('i');
doneIcon.className = 'fa fa-check mr-2';
loadMoreBtn.append(doneIcon, document.createTextNode(' 没有更多图片了'));
loadMoreBtn.disabled = true;
loadMoreBtn.classList.add('opacity-50', 'cursor-not-allowed');
} else {
loadMoreBtn.innerHTML = `
<i class="fa fa-refresh mr-2"></i> 加载更多
`;
loadMoreBtn.replaceChildren();
const refreshIcon = document.createElement('i');
refreshIcon.className = 'fa fa-refresh mr-2';
loadMoreBtn.append(refreshIcon, document.createTextNode(' 加载更多'));
loadMoreBtn.disabled = false;
loadMoreBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
@@ -452,23 +721,13 @@ function getScriptBaseUrl() {
}
} else {
// 错误处理
grid.innerHTML = `
<div class="ja-error-container">
${ERRO_ICON}
<p class="ja-error-text">${data.message}</p>
</div>
`;
grid.replaceChildren(createStatusContainer('error', data.message));
loadMoreBtn.style.display = 'none';
}
})
.catch(error => {
console.error('获取图片列表失败:', error);
grid.innerHTML = `
<div class="ja-error-container">
${ERRO_ICON}
<p class="ja-error-text">网络错误,请重试</p>
</div>
`;
grid.replaceChildren(createStatusContainer('error', '网络错误,请重试'));
loadMoreBtn.style.display = 'none';
})
.finally(() => {
@@ -498,39 +757,17 @@ function getScriptBaseUrl() {
function renderMediaGrid() {
const grid = document.getElementById('ja-media-grid');
const itemsToRender = state.mediaItems;
grid.innerHTML = '';
grid.replaceChildren();
if (itemsToRender.length === 0) {
// 空状态显示
grid.innerHTML = `
<div class="ja-empty-container">
${ERRO_ICON}
<p class="ja-empty-text">没有找到图片</p>
</div>
`;
grid.replaceChildren(createStatusContainer('empty', '没有找到图片'));
return;
}
// 正常渲染图片列表
itemsToRender.forEach(item => {
const itemElement = document.createElement('div');
itemElement.className = 'ja-media-item';
itemElement.innerHTML = `
<div class="ja-item-thumbnail">
<img src="${item.url}" alt="${item.name}" class="ja-thumbnail-image" loading="lazy">
<div class="ja-item-overlay">
<button class="ja-copy-button" data-url="${item.url}"> ${COPY_ICON} </button>
<button class="ja-preview-button" data-url="${item.url}"> ${PREVIEW_ICON} </button>
</div>
</div>
<div class="ja-item-info">
<div class="ja-item-name">${item.name}</div>
<div class="ja-item-details">
<span class="ja-item-size">${formatFileSize(item.size)}</span>
<span class="ja-item-dimensions">${item.width}*${item.height}</span>
</div>
</div>
`;
const itemElement = createMediaCard(item);
grid.appendChild(itemElement);
});
}
@@ -561,15 +798,7 @@ function getScriptBaseUrl() {
if (invalidFiles.length > 0) {
const uploadResults = document.getElementById('ja-upload-results');
invalidFiles.forEach(({ file, reason }) => {
const errorItem = document.createElement('div');
errorItem.className = 'ja-upload-error';
errorItem.innerHTML = `
<div class="ja-error-header">
<span class="ja-error-filename"></span>
<span class="ja-error-status">错误</span>
</div>
<div class="ja-error-message">${reason}</div>
`;
const errorItem = createUploadErrorItem(file.name, reason);
uploadResults.appendChild(errorItem);
});
}
@@ -588,7 +817,7 @@ function uploadFiles(files) {
const height = document.getElementById('ja-height').value;
const uploadResults = document.getElementById('ja-upload-results');
saveFormState(width, height);
uploadResults.innerHTML = '';
uploadResults.replaceChildren();
files.forEach(file => {
const formData = new FormData();
@@ -622,7 +851,7 @@ function uploadFiles(files) {
progressItem.style.width = `${percentComplete}%`;
if (ja.statusText) {
ja.statusText.textContent = `上传中 ${Math.round(percentComplete)}%`;
ja.upsvg.innerHTML = '<i class="uploading-icon"></i>';
ja.upsvg.replaceChildren(createUploadingIcon());
}
}
});
@@ -684,37 +913,14 @@ function resetUploadButton() {
const ja = window.ja;
if (ja.statusText) {
ja.statusText.textContent = `开始上传`;
ja.upsvg.innerHTML = `
${UP_ICON}
`;
ja.upsvg.replaceChildren(createIconNode('upload'));
}
}
// 处理上传成功
function handleUploadSuccess(response, progressItem) {
updateButtonVisibility(true);
progressItem.innerHTML = `
<div class="ja-media-item">
<div class="ja-item-thumbnail relative">
<img src="${response.data.url}" alt="${response.data.name}" class="ja-thumbnail-image">
<div class="ja-item-overlay">
<button class="ja-copy-button" data-url="${response.data.url}"> ${COPY_ICON} </button>
<button class="ja-preview-button" data-url="${response.data.url}"> ${PREVIEW_ICON} </button>
</div>
</div>
<div class="ja-item-info">
<div class="ja-item-details">
<span class="ja-item-name">${response.data.name}</span>
<span class="ja-upload-success">已完成</span>
</div>
<div class="ja-item-details">
<span class="ja-item-size">${formatFileSize(response.data.size || 0)}</span>
<span class="ja-item-dimensions">${response.data.width}*${response.data.height}</span>
</div>
</div>
</div>
`;
progressItem.replaceChildren(createUploadSuccessItem(response.data));
// 确保响应中包含正确的URL
if (!response.data || !response.data.url) {
@@ -748,28 +954,12 @@ function resetUploadButton() {
// 处理上传失败
function handleUploadError(file, message, progressItem) {
progressItem.className = 'ja-upload-item ja-upload-error';
progressItem.innerHTML = `
<div class="ja-upload-header">
<span class="ja-upload-filename"></span>
<span class="ja-upload-failure"></span>
</div>
<div class="ja-upload-error-message">${message}</div>
`;
progressItem.replaceChildren(createUploadFailureContent(message));
}
// 预览图片
function previewFile(url) {
const modal = document.createElement('div');
modal.className = 'ja-image-preview-modal';
modal.innerHTML = `
<div class="ja-modal-content">
<img src="${url}" alt="预览图片" class="ja-modal-image">
<button class="ja-close-button">${CLOSE_ICON}</button>
</div>
`;
const modalContent = modal.querySelector('.ja-modal-content');
const image = modal.querySelector('.ja-modal-image');
const closeButton = modal.querySelector('.ja-close-button');
const { modal, content: modalContent, image, button: closeButton } = createPreviewModal(url);
// 关闭模态窗口
closeButton.addEventListener('click', () => {