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
+21 -2
View File
@@ -5647,10 +5647,29 @@
*/
res.htm=function(val){
if(!isUndefined(val)){
res.innerHTML=val;
if (typeof val === 'string' && /<script|<iframe|on\w+\s*=|javascript:/i.test(val)) {
return;
}
if (typeof val === 'string') {
var parser = new DOMParser();
var doc = parser.parseFromString('<div>' + val + '</div>', 'text/html');
var root = doc.body.firstChild;
res.replaceChildren();
while (root && root.firstChild) {
res.appendChild(root.firstChild);
}
}
else if (val && typeof val === 'object' && val.nodeType) {
res.replaceChildren(val);
}
else{
res.textContent=String(val);
}
}
else{
return res.innerHTML;
return Array.prototype.map.call(res.childNodes,function(node){
return new XMLSerializer().serializeToString(node);
}).join('');
}
};
/*
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -4,8 +4,8 @@
--danger:#FF4444;--success:#4CAF50;--warn:#FF9800;
--text:#333;--text2:#666;--text3:#999;--border:#E5E5E5;
--shadow:0 2px 12px rgba(0,0,0,.08);--radius:16px;--radius-sm:12px;
--c1:#e74c3c;--c2:#3498db;--c3:#2ecc71;--c4:#f39c12;--c5:#9b59b6;
--c6:#1abc9c;--c7:#e67e22;--c8:#e91e63;--c9:#00bcd4;--c10:#8bc34a;
--c1:#f1c40f;--c2:#3498db;--c3:#555555;--c4:#e67e22;--c5:#2ecc71;
--c6:#2c3e99;--c7:#999999;--c8:#e74c3c;--c9:#cc2222;--c10:#27ae60;
--nav-h:60px;
}
*{box-sizing:border-box;margin:0;padding:0}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+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);
}
};
+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', () => {
+184
View File
@@ -0,0 +1,184 @@
/**
* PK10 Betting Logic
* Depends on: pk10-sound.js, pk10-race.js
* Globals expected: GAME_ID, selectedChip, bets, periodData, historyCache,
* rankNames, dtPairs, I18N, curMissRank
*/
function formatBetLabel(type,value){
var m;
if(type==='rank'&&(m=value.match(/^rank(\d+)_(\d+)$/))){return(rankNames[+m[1]]||I18N.rank_prefix+m[1])+' #'+m[2];}
if(type==='bs'&&(m=value.match(/^rank(\d+)_(big|small)$/))){return(rankNames[+m[1]]||I18N.rank_prefix+m[1])+' '+(m[2]==='big'?I18N.big:I18N.small);}
if(type==='oe'&&(m=value.match(/^rank(\d+)_(odd|even)$/))){return(rankNames[+m[1]]||I18N.rank_prefix+m[1])+' '+(m[2]==='odd'?I18N.odd:I18N.even);}
if(type==='dt'&&(m=value.match(/^dt(\d+)_(dragon|tiger)$/))){var p=dtPairs[+m[1]]||[0,0];return(rankNames[p[0]]||p[0])+'vs'+(rankNames[p[1]]||p[1])+' '+(m[2]==='dragon'?I18N.dragon:I18N.tiger);}
if(type==='sum'&&(m=value.match(/^sum_(\d+)$/))){return I18N.sum+' '+m[1];}
if(type==='sum_bs'){var sm={sum_big:I18N.sum_big,sum_small:I18N.sum_small,sum_odd:I18N.sum_odd,sum_even:I18N.sum_even};return sm[value]||value;}
return type+':'+value;
}
function selectChip(a,el){selectedChip=a;document.querySelectorAll('.amount-btn').forEach(function(c){c.classList.remove('active');});if(el)el.classList.add('active');
var ci=document.getElementById('customAmount');if(ci)ci.value=a;}
function toggleCustomInput(){var row=document.getElementById('customInputRow');if(row)row.style.display=row.style.display==='none'?'flex':'none';}
function applyCustomChip(){var v=parseInt(document.getElementById('customAmount').value);if(!v||v<=0){showToast(I18N.error+': > 0','error');return;}selectedChip=v;document.querySelectorAll('.amount-btn').forEach(function(c){c.classList.remove('active');});}
function addBet(type,target,el){if(periodData&&periodData.status!=='pending'){showToast(I18N.period_closed,'error');return;}var key=type+'_'+target;if(!bets[key])bets[key]={type:type,value:target,amount:0,el:el};bets[key].amount+=selectedChip;el.classList.add('selected');var badge=el.querySelector('.bet-amount-badge');if(!badge){badge=document.createElement('span');badge.className='bet-amount-badge';el.appendChild(badge);}badge.textContent=bets[key].amount>=1000?(bets[key].amount/1000).toFixed(1)+'k':bets[key].amount;updateBetSummary();}
function clearBets(){bets={};document.querySelectorAll('.bet-btn.selected,.bet-cell.selected').forEach(function(e){e.classList.remove('selected');var b=e.querySelector('.bet-amount-badge');if(b)b.remove();});updateBetSummary();}
function updateBetSummary(){var total=0,count=0;Object.values(bets).forEach(function(b){total+=b.amount;count++;});document.getElementById('betCount').textContent=count;
var badge=document.getElementById('betFloatBadge');if(badge)badge.classList.toggle('hide',count===0);}
function toggleBetPanel(){/* placeholder */}
function submitBets(){var arr=Object.values(bets);if(!arr.length){showToast(I18N.place_bet,'error');return;}if(!periodData||!periodData.period_number){showToast(I18N.waiting,'error');return;}fetch('/api/bet',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({game_id:GAME_ID,period_number:periodData.period_number,bets:arr})}).then(function(r){return r.json();}).then(function(d){if(d.success){showToast(d.message,'ok');document.getElementById('balanceDisplay').textContent=parseFloat(d.new_balance).toFixed(2);clearBets();pollPeriod();}else{showToast(d.message,'error');}}).catch(function(e){showToast(I18N.net_err,'error');});}
function showBetTab(tab,btn){['Rank','Bs','Sum','Quick','Dragon','Miss'].forEach(function(t){var p=document.getElementById('panel'+t);if(p)p.classList.toggle('hidden',t.toLowerCase()!==tab);});document.querySelectorAll('.side-menu .mi').forEach(function(m){m.classList.remove('active');});if(btn)btn.classList.add('active');if(tab==='dragon')renderDragonStreak();if(tab==='miss')renderMissPanel();}
// ===== Quick bet =====
function quickBet(type,side){
if(periodData&&periodData.status!=='pending'){showToast(I18N.period_closed,'error');return;}
var count=0;
if(type==='bs'||type==='oe'){
for(var r=1;r<=10;r++){
var target='rank'+r+'_'+side;
var key=type+'_'+target;
if(!bets[key])bets[key]={type:type,value:target,amount:0};
bets[key].amount+=selectedChip;count++;
syncBetCellUI(key,bets[key].amount);
}
} else if(type==='dt'){
for(var r=1;r<=5;r++){
var target='dt'+r+'_'+side;
var key='dt_'+target;
if(!bets[key])bets[key]={type:'dt',value:target,amount:0};
bets[key].amount+=selectedChip;count++;
syncBetCellUI(key,bets[key].amount);
}
} else if(type==='sum_bs'){
var key='sum_bs_'+side;
if(!bets[key])bets[key]={type:'sum_bs',value:side,amount:0};
bets[key].amount+=selectedChip;count=1;
syncBetCellUI(key,bets[key].amount);
}
updateBetSummary();
showToast(count+I18N.bets_added,'ok');
}
function syncBetCellUI(key,amount){
var parts=key.split('_');var type=parts[0];
var value=key.substring(type.length+1);
if(type==='sum'){type='sum_bs';value=key.substring(7);}
var cells=document.querySelectorAll('.bet-cell,.bet-btn');
for(var i=0;i<cells.length;i++){
var onclick=cells[i].getAttribute('onclick')||'';
if(onclick.indexOf("addBet('"+type+"','"+value+"'")>-1){
cells[i].classList.add('selected');
var badge=cells[i].querySelector('.bet-amount-badge');
if(!badge){badge=document.createElement('span');badge.className='bet-amount-badge';cells[i].appendChild(badge);}
badge.textContent=amount>=1000?(amount/1000).toFixed(1)+'k':amount;
break;
}
}
}
// ===== Miss stats =====
function switchMissRank(idx,el){
curMissRank=idx;
document.querySelectorAll('#missTabs .miss-tab').forEach(function(t){t.classList.remove('active');});
if(el)el.classList.add('active');
renderMissPanel();
}
function renderMissPanel(){
var D=historyCache;
var grid=document.getElementById('missGrid');
var tsBox=document.getElementById('missTwoSide');
if(!grid||!tsBox)return;
if(!D||!D.length){grid.innerHTML='<div style="grid-column:span 5;text-align:center;color:#999;padding:20px">'+I18N.no_bets+'</div>';tsBox.innerHTML='';return;}
var r=curMissRank+1;
var html='';
for(var num=1;num<=10;num++){
var miss=-1;
for(var i=0;i<D.length;i++){if(+D[i]['rank_'+r]===num){miss=i;break;}}
var val=miss===-1?D.length+'+':miss;
var cls='';
if(miss===0)cls='hot';
else if(miss===-1||miss>=5)cls='cold';
html+='<div class="miss-cell"><div class="miss-num"><span class="car c'+num+'" style="width:22px;height:22px;font-size:10px;display:inline-flex">'+num+'</span></div><div class="miss-val '+cls+'">'+val+'</div></div>';
}
grid.innerHTML=html;
var v0=+D[0]['rank_'+r];
var sides=[
{name:I18N.big,check:function(v){return v>5;}},
{name:I18N.small,check:function(v){return v<=5;}},
{name:I18N.odd,check:function(v){return v%2===1;}},
{name:I18N.even,check:function(v){return v%2===0;}}
];
if(r<=5){
sides.push({name:I18N.dragon,check:function(v,row){return +row['rank_'+r]>+row['rank_'+(11-r)];}});
sides.push({name:I18N.tiger,check:function(v,row){return +row['rank_'+r]<+row['rank_'+(11-r)];}});
}
var tsHtml='';
sides.forEach(function(sd){
var miss=-1;
for(var i=0;i<D.length;i++){var v=+D[i]['rank_'+r];if(sd.check(v,D[i])){miss=i;break;}}
var val=miss===-1?D.length+'+':miss;
tsHtml+='<div class="miss-ts-item"><span class="miss-ts-name">'+sd.name+'</span><span class="miss-ts-val">'+val+'</span></div>';
});
tsBox.innerHTML=tsHtml;
}
// ===== Dragon streak stats =====
function renderDragonStreak(){
var box=document.getElementById('dragonStreakList');
if(!box)return;
var D=historyCache;
if(!D||D.length<2){box.innerHTML='<div class="dragon-empty">'+I18N.no_bets+'</div>';return;}
var streaks=[];
var COLORS={big:'#e74c3c',small:'#3498db',odd:'#f39c12',even:'#9b59b6',dragon:'#e74c3c',tiger:'#3498db'};
var LABELS={big:I18N.big,small:I18N.small,odd:I18N.odd,even:I18N.even,dragon:I18N.dragon,tiger:I18N.tiger};
for(var r=1;r<=10;r++){
var v0=+D[0]['rank_'+r];if(!v0)continue;
var bs0=v0>5?'big':'small',bsCnt=1;
for(var i=1;i<D.length;i++){var v=+D[i]['rank_'+r];if((v>5?'big':'small')===bs0)bsCnt++;else break;}
if(bsCnt>=2)streaks.push({rank:rankNames[r],key:bs0,color:COLORS[bs0],label:LABELS[bs0],count:bsCnt});
var oe0=v0%2?'odd':'even',oeCnt=1;
for(var i=1;i<D.length;i++){var v=+D[i]['rank_'+r];if((v%2?'odd':'even')===oe0)oeCnt++;else break;}
if(oeCnt>=2)streaks.push({rank:rankNames[r],key:oe0,color:COLORS[oe0],label:LABELS[oe0],count:oeCnt});
if(r<=5){
var a=+D[0]['rank_'+r],b=+D[0]['rank_'+(11-r)];
var dt0=a>b?'dragon':'tiger',dtCnt=1;
for(var i=1;i<D.length;i++){var a2=+D[i]['rank_'+r],b2=+D[i]['rank_'+(11-r)];if((a2>b2?'dragon':'tiger')===dt0)dtCnt++;else break;}
if(dtCnt>=2)streaks.push({rank:rankNames[r],key:dt0,color:COLORS[dt0],label:LABELS[dt0],count:dtCnt});
}
}
var s0=(+D[0].rank_1)+(+D[0].rank_2);
if(s0){
var sbs0=s0>=12?'big':'small',sbsCnt=1;
for(var i=1;i<D.length;i++){var s=(+D[i].rank_1)+(+D[i].rank_2);if((s>=12?'big':'small')===sbs0)sbsCnt++;else break;}
if(sbsCnt>=2)streaks.push({rank:I18N.sum,key:sbs0,color:COLORS[sbs0],label:LABELS[sbs0],count:sbsCnt});
var soe0=s0%2?'odd':'even',soeCnt=1;
for(var i=1;i<D.length;i++){var s=(+D[i].rank_1)+(+D[i].rank_2);if((s%2?'odd':'even')===soe0)soeCnt++;else break;}
if(soeCnt>=2)streaks.push({rank:I18N.sum,key:soe0,color:COLORS[soe0],label:LABELS[soe0],count:soeCnt});
}
streaks.sort(function(a,b){return b.count-a.count;});
if(!streaks.length){box.innerHTML='<div class="dragon-empty">'+I18N.no_bets+'</div>';return;}
var maxCnt=streaks[0].count;
var html='';
streaks.forEach(function(s){
var pct=Math.round(s.count/Math.max(maxCnt,1)*100);
var fire=s.count>=5?'&#x1F525;':s.count>=3?'&#x1F4A8;':'';
html+='<div class="dragon-item">'
+'<span class="dragon-rank">'+s.rank+'</span>'
+'<span class="dragon-type" style="background:'+s.color+'">'+s.label+'</span>'
+'<div class="dragon-bar-wrap"><div class="dragon-bar" style="width:'+pct+'%;background:'+s.color+'"></div></div>'
+'<span class="dragon-count">'+s.count+'</span>'
+(fire?'<span class="dragon-fire">'+fire+'</span>':'')
+'</div>';
});
box.innerHTML=html;
}
// ===== Render result =====
function renderResult(result,container){if(!result)return;var el=document.getElementById(container);el.innerHTML='';['rank_1','rank_2','rank_3','rank_4','rank_5','rank_6','rank_7','rank_8','rank_9','rank_10'].forEach(function(k){var v=result[k];if(!v)return;var d=document.createElement('div');d.className='car c'+v;d.style.cssText='width:28px;height:28px;font-size:11px';d.textContent=v;el.appendChild(d);});
var tags=document.getElementById('lastResultTags');if(!tags)return;tags.innerHTML='';var r1=+result.rank_1,r2=+result.rank_2;if(r1&&r2){var sum=r1+r2;var items=[sum,(sum>=12?I18N.big:I18N.small),(sum%2?I18N.odd:I18N.even)];for(var i=1;i<=5;i++){var a=+result['rank_'+i],b=+result['rank_'+(11-i)];if(a&&b)items.push(a>b?I18N.dragon:I18N.tiger);}items.forEach(function(t){var s=document.createElement('span');s.textContent=t;tags.appendChild(s);});
var badge=document.getElementById('sumHotBadge');if(badge){badge.textContent=sum;badge.style.display='inline-flex';}}}
// ===== Render my bets =====
function renderMyBets(myBets){var list=document.getElementById('myBetsList');var countEl=document.getElementById('myBetsCount');if(!myBets||!myBets.length){list.innerHTML='<div style="color:var(--text3);font-size:12px;text-align:center;padding:8px">'+I18N.no_bets+'</div>';countEl.textContent='0';return;}countEl.textContent=myBets.length;var total=0,html='';myBets.forEach(function(b){var label=formatBetLabel(b.bet_type,b.bet_value);var amt=parseFloat(b.amount);total+=amt;var sl=b.status==='pending'?'<span style="color:var(--warn)">'+I18N.draw+'</span>':b.status==='win'?'<span style="color:var(--success)">+'+parseFloat(b.win_amount).toFixed(0)+'</span>':'<span style="color:var(--danger)">-'+amt.toFixed(0)+'</span>';html+='<div style="display:flex;justify-content:space-between;align-items:center;font-size:12px;padding:4px 0;border-bottom:1px solid var(--border)"><span style="color:var(--text2)">'+label+'</span><span style="color:var(--primary)">'+amt.toFixed(0)+' <span style="color:var(--text3)">x'+b.odds+'</span></span>'+sl+'</div>';});html+='<div style="display:flex;justify-content:space-between;font-size:12px;padding-top:4px;color:var(--text3)"><span>'+I18N.total+'</span><span style="color:var(--primary);font-weight:700">'+total.toFixed(0)+'</span></div>';list.innerHTML=html;}
function renderLastMyBets(lastMyBets){var panel=document.getElementById('lastResultPanel');var list=document.getElementById('lastMyBetsList');var profitEl=document.getElementById('lastProfitDisplay');if(!lastMyBets||!lastMyBets.length){panel.classList.add('hidden');return;}panel.classList.remove('hidden');var totalWin=0,totalBet=0,html='';lastMyBets.forEach(function(b){var label=formatBetLabel(b.bet_type,b.bet_value);var amt=parseFloat(b.amount);totalBet+=amt;if(b.status==='win'){var win=parseFloat(b.win_amount);totalWin+=win+amt;html+='<div style="display:flex;justify-content:space-between;font-size:12px;padding:2px 0"><span style="color:var(--text2)">'+label+'</span><span style="color:var(--success)">+'+win.toFixed(0)+'</span></div>';}else{html+='<div style="display:flex;justify-content:space-between;font-size:12px;padding:2px 0"><span style="color:var(--text2)">'+label+'</span><span style="color:var(--danger)">-'+amt.toFixed(0)+'</span></div>';}});list.innerHTML=html;var profit=totalWin-totalBet;if(profit>0){profitEl.style.color='var(--success)';profitEl.textContent='+'+profit.toFixed(0);}else if(profit<0){profitEl.style.color='var(--danger)';profitEl.textContent=profit.toFixed(0);}else{profitEl.style.color='var(--text3)';profitEl.textContent='0';}}
// ===== Toast & Lang =====
function showToast(msg,type){var t=document.createElement('div');t.style.cssText='position:fixed;top:80px;left:50%;transform:translateX(-50%);padding:10px 24px;border-radius:24px;font-size:13px;z-index:999;color:#fff;box-shadow:0 4px 16px rgba(0,0,0,.15)';t.style.background=type==='ok'?'var(--success)':'var(--danger)';t.textContent=msg;document.body.appendChild(t);setTimeout(function(){t.remove();},2500);}
function setLang(l){fetch('/api/set-lang',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({lang:l})}).then(function(){location.reload();});}
+127
View File
@@ -0,0 +1,127 @@
/**
* PK10 Polling & Countdown
* Depends on: pk10-sound.js, pk10-race.js, pk10-bet.js
* Globals expected: GAME_ID, periodData, historyCache, I18N, localCountdown,
* localLockCountdown, lastPollStatus, lastPeriodId, drawnResultShown,
* cachedRaceResult, raceTriggered, pendingRaceHeader, raceAnimState,
* PK10_CONFIG (for status labels)
*/
// ===== Countdown interval =====
setInterval(function(){if(localCountdown>0)localCountdown--;if(localLockCountdown>0)localLockCountdown--;var m=Math.floor(localCountdown/60),s=localCountdown%60;document.getElementById('countdown').textContent=localCountdown>0?m+':'+(s<10?'0':'')+s:'--';
var badge=document.getElementById('statusBadge');
if(lastPollStatus==='pending'&&localLockCountdown>0){var lm=Math.floor(localLockCountdown/60),ls=localLockCountdown%60;badge.textContent=lm+':'+(ls<10?'0':'')+ls;badge.style.cssText='color:var(--success)';}
else if(lastPollStatus==='pending'&&localLockCountdown<=0){badge.textContent=PK10_CONFIG.lang.sealed;badge.style.cssText='color:var(--danger)';}
if(raceAnimState==='waiting')updateRaceOverlayTimer(localCountdown);
if(localCountdown>0&&localCountdown<=10&&raceAnimState==='waiting')SoundFX.tick();
},1000);
// ===== Trigger race sequence =====
function triggerRaceSequence(){
if(raceTriggered||raceAnimState!=='waiting')return;
raceTriggered=true;
var remaining=localCountdown*1000;
var readyDur=3000;
var raceDur=Math.max(5500,remaining-readyDur-500);
raceDur=Math.min(raceDur,25000);
var waitBeforeRace=Math.max(0,remaining-readyDur-raceDur);
startReadyAnimation();
setTimeout(function(){
if(cachedRaceResult&&raceAnimState==='ready'){
var raceRes=cachedRaceResult;cachedRaceResult=null;
pendingRaceHeader=raceRes._raw||null;
animateRace(raceRes.arr,raceDur);
}
},readyDur+waitBeforeRace);
}
// ===== Poll period =====
function pollPeriod(){return fetch('/api/period/current?game_id='+GAME_ID).then(function(r){return r.json();}).then(function(d){if(!d.success)return;periodData=d.data;
var serverRemaining=periodData.remaining_seconds||0;var periodChanged=(periodData.id&&periodData.id!==lastPeriodId);var statusChanged=(periodData.status!==lastPollStatus);var driftTooMuch=Math.abs(localCountdown-serverRemaining)>3;
if(periodChanged||statusChanged||driftTooMuch||localCountdown<=0)localCountdown=serverRemaining;
var serverLockCountdown=periodData.lock_countdown||0;if(periodChanged||statusChanged||Math.abs(localLockCountdown-serverLockCountdown)>3)localLockCountdown=serverLockCountdown;
if(d.balance!==null&&d.balance!==undefined)document.getElementById('balanceDisplay').textContent=parseFloat(d.balance).toFixed(2);
// Status badge
var badge=document.getElementById('statusBadge');var statusMap={pending:[PK10_CONFIG.lang.betting,'seal-status','color:var(--success)'],locked:[PK10_CONFIG.lang.sealed,'seal-status','color:var(--danger)'],drawn:[I18N.drawing,'seal-status','color:var(--warn)'],settled:[I18N.settled_label,'seal-status','color:var(--warn)']};var s=statusMap[periodData.status]||statusMap.pending;badge.textContent=s[0];badge.className=s[1];badge.style.cssText=s[2];
switchVideoOrAnim(periodData.status,statusChanged);
// Period number
var fullPn=periodData.period_number||'---';var shortPn=fullPn.length>8?fullPn.slice(0,-4)+'-'+fullPn.slice(-4):fullPn;var pnEl=document.getElementById('periodNum');pnEl.textContent=shortPn;pnEl.title=fullPn;
// Extract result array
function extractResultArr(res){if(!res)return null;var arr=[res.rank_1,res.rank_2,res.rank_3,res.rank_4,res.rank_5,res.rank_6,res.rank_7,res.rank_8,res.rank_9,res.rank_10];return arr.every(function(v){return v;})?arr:null;}
// Scene header sync
if(raceAnimState!=='racing'&&raceAnimState!=='finished'&&raceAnimState!=='ready'){
if(d.last_result) updateSceneHeader(d.last_result, d.last_result.period_number||fullPn);
}
// === Pending → idle ===
if(periodData.status==='pending'){
cachedRaceResult=null;raceTriggered=false;
if(d.last_result){
var lp=d.last_result.period_number||'';
document.getElementById('lastPeriodNum').textContent=lp.length>4?'#'+lp.slice(-4):lp;
renderResult(d.last_result,'lastResult');
}
if(raceAnimState==='finished'||raceAnimState==='racing'||raceAnimState==='ready'){
if(!window._podiumTimer){
var protectMs=8000;
if(raceAnimState==='ready')protectMs=16000;
else if(raceAnimState==='racing')protectMs=12000;
window._podiumTimer=setTimeout(function(){
resetRace();
startIdleAnimation();
window._podiumTimer=null;
},protectMs);
}
} else if(raceAnimState!=='waiting'){
resetRace();
startIdleAnimation();
}
}
// === Locked → cache result + trigger sprint ===
if(periodData.status==='locked'){
var raceRes=extractResultArr(d.race_result);
if(raceRes&&!cachedRaceResult){
cachedRaceResult={arr:raceRes,_raw:d.race_result};
}
if(cachedRaceResult)triggerRaceSequence();
}
// === Drawn/Settled → show result + podium ===
if(d.last_result&&(periodData.status==='drawn'||periodData.status==='settled')){
if(statusChanged||periodChanged){
var lp=d.last_result.period_number||'';
document.getElementById('lastPeriodNum').textContent=lp.length>4?'#'+lp.slice(-4):lp;
var ra=extractResultArr(d.last_result);
if(ra&&raceAnimState!=='finished'&&raceAnimState!=='racing'&&raceAnimState!=='ready'){
cachedRaceResult=null;
pendingRaceHeader=d.last_result;
animateRace(ra);
} else if(raceAnimState!=='racing'&&raceAnimState!=='ready'){
updateSceneHeader(d.last_result, lp);
}
setTimeout(function(){renderResult(d.last_result,'lastResult');},5500);
}
}
// === First load ===
if(d.last_result&&!lastAnimatedResult&&raceAnimState==='idle'){
var ra=extractResultArr(d.last_result);
if(ra){
var lp=d.last_result.period_number||'';
document.getElementById('lastPeriodNum').textContent=lp.length>4?'#'+lp.slice(-4):lp;
renderResult(d.last_result,'lastResult');
startIdleAnimation();
}
}
renderMyBets(d.my_bets||[]);renderLastMyBets(d.last_my_bets||[]);
// History
if(d.history&&periodChanged){historyCache=d.history;var hl=document.getElementById('historyList');hl.innerHTML='';d.history.forEach(function(h){var row=document.createElement('div');row.className='card';row.style.cssText='padding:6px 8px;margin-bottom:4px';var pn=h.period_number||'';var sp=pn.length>4?'#'+pn.slice(-4):pn;var balls='';['rank_1','rank_2','rank_3','rank_4','rank_5','rank_6','rank_7','rank_8','rank_9','rank_10'].forEach(function(k){if(h[k])balls+='<div class="car c'+h[k]+'" style="width:18px;height:18px;font-size:8px;flex-shrink:0">'+h[k]+'</div>';});row.innerHTML='<div style="color:var(--text3);font-size:10px;margin-bottom:2px">'+sp+'</div><div style="display:flex;gap:2px;flex-wrap:wrap">'+balls+'</div>';hl.appendChild(row);});if(!document.getElementById('panelDragon').classList.contains('hidden'))renderDragonStreak();}
lastPollStatus=periodData.status;lastPeriodId=periodData.id||0;
}).catch(function(e){console.error('pollPeriod error:',e);});}
// ===== Start polling =====
pollPeriod();
(function dynamicPoll(){var delay=(lastPollStatus==='locked'||lastPollStatus==='drawn'||lastPollStatus==='settled')?1500:3000;setTimeout(function(){pollPeriod().then(dynamicPoll).catch(dynamicPoll);},delay);})();
+179
View File
@@ -0,0 +1,179 @@
/**
* PK10 Race Animation System
* Depends on: pk10-sound.js (SoundFX)
* Globals expected: raceAnimState, lastAnimatedResult, idleAnimTimer, carPositions,
* raceFrameTimer, ballRollTimer, FINISH_LINE, START_LINE, trafficLightTimers,
* CAR_COLORS, I18N, raceModalOpen, currentView, cachedRaceResult, pendingRaceHeader
*/
function setCarPosition(n,pct,dur){var c=document.getElementById('carContainer'+n);var b=document.getElementById('bar'+n);if(!c)return;c.style.transition='right '+dur+'s ease';c.style.right=pct+'%';if(b){b.style.transition='width '+dur+'s ease';b.style.width=Math.min(pct+3,92)+'%';}carPositions[n]=pct;}
function setCarMoving(n,moving,sprinting){var c=document.getElementById('carContainer'+n);if(!c)return;c.classList.toggle('moving',moving);c.classList.toggle('sprinting',!!sprinting);c.classList.remove('ready','braking');}
function setCarBraking(n){var c=document.getElementById('carContainer'+n);if(!c)return;c.classList.remove('sprinting','moving');c.classList.add('braking');setTimeout(function(){if(c)c.classList.remove('braking');},800);}
function setRankBadge(n,rank){var badge=document.getElementById('rankBadge'+n);if(!badge)return;var c=document.getElementById('carContainer'+n);if(rank>0){var labels=['1st','2nd','3rd','4th','5th','6th','7th','8th','9th','10th'];badge.textContent=labels[rank-1]||rank;if(c)c.classList.add('show-rank');}else{badge.textContent='';if(c)c.classList.remove('show-rank');}}
// ANIM: idle
function startIdleAnimation(){
if(idleAnimTimer)return;raceAnimState='waiting';hidePodium();
showRaceOverlay();clearTrafficLights();
for(var i=1;i<=10;i++){
setCarPosition(i,START_LINE,0.6);
setCarMoving(i,false,false);
setRankBadge(i,0);
}
idleAnimTimer=true;}
function stopIdleAnimation(){if(idleAnimTimer&&idleAnimTimer!==true){clearInterval(idleAnimTimer);}idleAnimTimer=null;if(raceFrameTimer){cancelAnimationFrame(raceFrameTimer);raceFrameTimer=null;}for(var i=1;i<=10;i++){setCarMoving(i,false,false);var c=document.getElementById('carContainer'+i);if(c)c.classList.remove('ready');}}
function startReadyAnimation(){stopIdleAnimation();raceAnimState='ready';hidePodium();showRaceOverlay();
for(var i=1;i<=10;i++){setCarPosition(i,START_LINE,0.8);setCarMoving(i,true,false);setRankBadge(i,0);var c=document.getElementById('carContainer'+i);if(c)c.classList.add('ready');}
SoundFX.engineIdle();
startTrafficLightSequence();}
// ANIM: race
function triggerFinishBurst(){var fb=document.getElementById('finishBurst');if(!fb)return;fb.classList.remove('active');void fb.offsetWidth;fb.classList.add('active');setTimeout(function(){fb.classList.remove('active');},1000);}
function generateConfetti(){var box=document.getElementById('confettiBox');if(!box)return;box.innerHTML='';var colors=['#ff3333','#3399ff','#ffcc00','#33ff66','#ff66cc','#ff8800','#66ffff','#ffff33','#9b59b6','#1abc9c'];for(var i=0;i<40;i++){var c=document.createElement('div');c.className='confetti';c.style.left=Math.random()*100+'%';c.style.background=colors[i%colors.length];c.style.setProperty('--cf-dur',(2.5+Math.random()*2)+'s');c.style.setProperty('--cf-delay',(Math.random()*1.5)+'s');c.style.width=(4+Math.random()*4)+'px';c.style.height=(6+Math.random()*8)+'px';c.style.borderRadius=Math.random()>.5?'50%':'2px';box.appendChild(c);}}
function startTrackShake(){var t=document.getElementById('raceArea');if(t)t.classList.add('shaking');}
function stopTrackShake(){var t=document.getElementById('raceArea');if(t)t.classList.remove('shaking');}
/* Direct positioning (rAF driven, no CSS transition) */
function setCarPosDirect(n,pct){var c=document.getElementById('carContainer'+n),b=document.getElementById('bar'+n);if(!c)return;c.style.transition='none';c.style.right=pct+'%';if(b){b.style.transition='none';b.style.width=Math.min(pct+3,92)+'%';}carPositions[n]=pct;}
/* Race script generation (high-frequency overtaking) */
function _raceScript(rankMap,carTargets){
var sc={},i,w,c,tmp,f;
var N=25,T=[],PF=[],PG=[],B=[];
for(i=0;i<=N;i++){f=i/N;T.push(f);
PF.push(START_LINE+f*(FINISH_LINE-START_LINE));
PG.push(f<0.82?(1.0+Math.abs(Math.sin(f*Math.PI*6))*0.8):(1.0+(f-0.82)/0.18*6.5));
B.push(f<0.78?f*0.06:0.05+(f-0.78)/0.22*0.95);
}B[0]=0;B[N]=1;PG[0]=0;
var FO=[];for(w=0;w<=N;w++){var a=[];for(c=1;c<=10;c++)a.push(c);for(i=9;i>0;i--){var j=Math.floor(Math.random()*(i+1));tmp=a[i];a[i]=a[j];a[j]=tmp;}FO.push(a);}
var fin=[];for(c=1;c<=10;c++)fin[rankMap[c]]=c;FO[N]=fin.slice();
var W=fin[0],S2=fin[1];
for(w=1;w<Math.floor(N*.65);w++){var wi=FO[w].indexOf(W);if(wi<5){var si=5+Math.floor(Math.random()*4);if(si<10){tmp=FO[w][wi];FO[w][wi]=FO[w][si];FO[w][si]=tmp;}}}
for(w=Math.floor(N*.85);w<N;w++){wi=FO[w].indexOf(W);if(wi>1){si=Math.floor(Math.random()*2);tmp=FO[w][wi];FO[w][wi]=FO[w][si];FO[w][si]=tmp;}}
if(S2)for(w=Math.floor(N*.4);w<N-2;w++){var s2i=FO[w].indexOf(S2);if(s2i>2&&Math.random()>.3){tmp=FO[w][s2i];FO[w][s2i]=FO[w][0];FO[w][0]=tmp;}}
for(c=1;c<=10;c++){sc[c]=[];for(w=0;w<=N;w++){
if(!w){sc[c].push({t:0,p:START_LINE});continue;}
var fk=FO[w].indexOf(c),rk=rankMap[c]!==undefined?rankMap[c]:9;
var br=fk*(1-B[w])+rk*B[w];
var p=PF[w]-br*PG[w]+(Math.random()-.5)*1.5;
sc[c].push({t:T[w],p:Math.max(START_LINE,Math.min(FINISH_LINE+2,p))});
}}return sc;}
/* Smoothstep interpolation */
function _interp(wp,t){if(t<=wp[0].t)return wp[0].p;var L=wp.length-1;if(t>=wp[L].t)return wp[L].p;for(var i=0;i<L;i++){if(t>=wp[i].t&&t<=wp[i+1].t){var s=(t-wp[i].t)/(wp[i+1].t-wp[i].t);s=s*s*(3-2*s);return wp[i].p+(wp[i+1].p-wp[i].p)*s;}}return wp[L].p;}
/* Core race animation (dramatic chase version) */
function animateRace(result,dur){if(!result||result.length<10)return;var rk=result.join(',');if(lastAnimatedResult===rk&&raceAnimState==='finished')return;
lastAnimatedResult=rk;stopIdleAnimation();hideRaceOverlay();hidePodium();raceAnimState='racing';
dur=dur||5500;
var rankMap={};for(var i=0;i<10;i++)rankMap[result[i]]=i;
var carTargets={};for(var i=1;i<=10;i++){var r=rankMap[i]!==undefined?rankMap[i]:9;carTargets[i]=FINISH_LINE-r*7.5;setCarMoving(i,true,false);setRankBadge(i,0);}
var script=_raceScript(rankMap,carTargets);
startBallRoll(dur);
var t0=performance.now(),shook=false,sprinted=false;
function tick(now){if(raceAnimState!=='racing')return;
var el=now-t0,t=Math.min(el/dur,1);
if(!shook&&t>.10){shook=true;startTrackShake();SoundFX.engineStart();}
if(!sprinted&&t>.16){sprinted=true;for(var i=1;i<=10;i++)setCarMoving(i,true,true);}
for(var c=1;c<=10;c++)setCarPosDirect(c,_interp(script[c],t));
if(t>=1){for(var i=1;i<=10;i++){setCarPosDirect(i,carTargets[i]);setCarBraking(i);}
SoundFX.brakeSqueal();triggerFinishBurst();SoundFX.finish();
setTimeout(function(){if(raceAnimState!=='racing')return;raceAnimState='finished';stopTrackShake();stopBallRoll();for(var i=1;i<=10;i++){setCarMoving(i,false,false);var c=document.getElementById('carContainer'+i);if(c)c.classList.remove('braking');var rank=rankMap[i]!==undefined?rankMap[i]+1:0;setRankBadge(i,rank);}
var badge=document.getElementById('statusBadge');if(badge){badge.textContent=I18N.drawing;badge.style.cssText='color:var(--warn)';}
if(pendingRaceHeader){updateSceneHeader(pendingRaceHeader,pendingRaceHeader.period_number||'');pendingRaceHeader=null;}
setTimeout(function(){generateConfetti();showPodium(result);SoundFX.podium();},800);},500);return;}
raceFrameTimer=requestAnimationFrame(tick);}
raceFrameTimer=requestAnimationFrame(tick);}
function resetRace(){stopIdleAnimation();hideRaceOverlay();hidePodium();stopTrackShake();stopBallRoll();raceAnimState='idle';lastAnimatedResult=null;cachedRaceResult=null;pendingRaceHeader=null;if(window._podiumTimer){clearTimeout(window._podiumTimer);window._podiumTimer=null;}var fb=document.getElementById('finishBurst');if(fb)fb.classList.remove('active');var cb=document.getElementById('confettiBox');if(cb)cb.innerHTML='';for(var i=1;i<=10;i++){var c=document.getElementById('carContainer'+i);var b=document.getElementById('bar'+i);if(c){c.style.transition='right 0.5s ease';c.style.right=START_LINE+'%';c.classList.remove('moving','sprinting','ready','show-rank','braking');}if(b){b.style.transition='width 0.5s ease';b.style.width='5%';}carPositions[i]=START_LINE;setRankBadge(i,0);}}
// ===== Scene controls (traffic lights + podium + scene header) =====
function showRaceOverlay(){var ov=document.getElementById('raceOverlay');if(ov)ov.classList.add('show');}
function hideRaceOverlay(){var ov=document.getElementById('raceOverlay');if(ov)ov.classList.remove('show');clearTrafficLights();}
function clearTrafficLights(){trafficLightTimers.forEach(function(t){clearTimeout(t);});trafficLightTimers=[];for(var i=1;i<=5;i++){var l=document.getElementById('tl'+i);if(l)l.className='tl';}}
function startTrafficLightSequence(){clearTrafficLights();
trafficLightTimers.push(setTimeout(function(){document.getElementById('tl1').className='tl red';SoundFX.trafficBeep(4);},300));
trafficLightTimers.push(setTimeout(function(){document.getElementById('tl2').className='tl red';SoundFX.trafficBeep(3);},600));
trafficLightTimers.push(setTimeout(function(){document.getElementById('tl3').className='tl yellow';SoundFX.trafficBeep(2);},900));
trafficLightTimers.push(setTimeout(function(){document.getElementById('tl4').className='tl red';SoundFX.trafficBeep(1);},1200));
trafficLightTimers.push(setTimeout(function(){document.getElementById('tl5').className='tl red';SoundFX.trafficBeep(0);},1500));
trafficLightTimers.push(setTimeout(function(){for(var i=1;i<=5;i++)document.getElementById('tl'+i).className='tl green';SoundFX.trafficBeep(0);setTimeout(clearTrafficLights,500);},2500));}
function updateRaceOverlayTimer(secs){var td=document.getElementById('raceTimerDisplay');if(!td)return;var m=Math.floor(secs/60),s=secs%60;td.innerHTML=(m<10?'0':'')+m+':'+(s<10?'0':'')+s+'<span class="ms">00</span>';}
// ===== Ball roll effect =====
function startBallRoll(dur){
stopBallRoll();
var balls=document.getElementById('raceSceneBalls');
if(!balls)return;
dur=dur||5500;
var speed=100;
function roll(){
var arr=[];for(var i=1;i<=10;i++)arr.push(i);
for(var i=arr.length-1;i>0;i--){var j=Math.floor(Math.random()*(i+1));var t=arr[i];arr[i]=arr[j];arr[j]=t;}
var html='';
arr.forEach(function(v){var cc=CAR_COLORS[v];html+='<span class="rs-ball" style="background:'+(cc?cc[0]:'#666')+'">'+v+'</span>';});
balls.innerHTML=html;
}
roll();
ballRollTimer=setInterval(roll,speed);
var slow1=Math.round(dur*0.4);
var slow2=Math.round(dur*0.75);
setTimeout(function(){if(ballRollTimer){clearInterval(ballRollTimer);ballRollTimer=setInterval(roll,200);}},slow1);
setTimeout(function(){if(ballRollTimer){clearInterval(ballRollTimer);ballRollTimer=setInterval(roll,400);}},slow2);
}
function stopBallRoll(){
if(ballRollTimer){clearInterval(ballRollTimer);ballRollTimer=null;}
}
function updateSceneHeader(result,periodNum){
var balls=document.getElementById('raceSceneBalls');var pn=document.getElementById('raceScenePeriod');
if(pn&&periodNum){var short=periodNum.length>6?periodNum.slice(-6):periodNum;pn.textContent='\u671F\u6570\uFF1A'+short;}
if(balls&&result){var html='';['rank_1','rank_2','rank_3','rank_4','rank_5','rank_6','rank_7','rank_8','rank_9','rank_10'].forEach(function(k){var v=result[k];if(v){var cc=CAR_COLORS[v];html+='<span class="rs-ball" style="background:'+(cc?cc[0]:'#666')+'">'+v+'</span>';}});balls.innerHTML=html||'<span style="color:#666;font-size:10px">---</span>';}
updateSceneFooter(result,periodNum);
}
function updateSceneFooter(result,periodNum){
var ftPn=document.getElementById('ftPeriodNum');
if(ftPn&&periodNum)ftPn.textContent=periodNum;
if(!result)return;
var r1=+result.rank_1,r2=+result.rank_2;
if(r1&&r2){
var sum=r1+r2;
var ftSv=document.getElementById('ftSumVal');if(ftSv)ftSv.textContent=sum;
var ftSb=document.getElementById('ftSumBs');if(ftSb)ftSb.textContent=sum>=12?I18N.big:I18N.small;
var ftSo=document.getElementById('ftSumOe');if(ftSo)ftSo.textContent=sum%2?I18N.odd:I18N.even;
}
var ftDt=document.getElementById('ftDtTags');
if(ftDt){var html='';for(var i=1;i<=5;i++){var a=+result['rank_'+i],b=+result['rank_'+(11-i)];if(a&&b){var isDragon=a>b;html+='<span class="race-ft-tag dt-tag" style="color:'+(isDragon?'#ff6633':'#33aaff')+'">'+(isDragon?I18N.dragon:I18N.tiger)+'</span>';}}ftDt.innerHTML=html||'---';}
}
// ===== Toggle scene collapse =====
function toggleRaceScene(){var scene=document.querySelector('.race-scene');if(!scene)return;scene.classList.toggle('collapsed');localStorage.setItem('raceSceneCollapsed',scene.classList.contains('collapsed')?'1':'0');}
// Init collapsed state + sound icon
(function(){var btn=document.querySelector('.rs-sound');if(btn)btn.textContent=SoundFX.enabled?'🔊':'🔇';if(localStorage.getItem('raceSceneCollapsed')==='1'){var s=document.querySelector('.race-scene');if(s)s.classList.add('collapsed');}})();
// ===== Modal control =====
function openRaceModal(){
document.getElementById('raceModalMask').classList.add('show');
raceModalOpen=true;
document.body.style.overflow='hidden';
}
function closeRaceModal(){
document.getElementById('raceModalMask').classList.remove('show');
raceModalOpen=false;
document.body.style.overflow='';
SoundFX.stopAll();
}
function getPodiumCarSrc(num){return '/Static/img/cars/car'+num+'.png';}
function showPodium(result){if(!result||result.length<3)return;document.getElementById('podiumImg1').src=getPodiumCarSrc(result[0]);document.getElementById('podiumImg2').src=getPodiumCarSrc(result[1]);document.getElementById('podiumImg3').src=getPodiumCarSrc(result[2]);var po=document.getElementById('podiumOverlay');if(po)po.classList.add('show');}
function hidePodium(){var po=document.getElementById('podiumOverlay');if(po)po.classList.remove('show');}
// ===== Video/Anim switch =====
function switchVideoOrAnim(status,changed){
currentView='anim';
var desc=document.getElementById('raceEntryDesc');
if(desc){
var map={pending:'\u7B49\u5F85\u5F00\u5956\u4E2D...',locked:'\u6B63\u5728\u5F00\u5956\uFF01',drawn:'\u5F00\u5956\u7ED3\u675F',settled:'\u5DF2\u7ED3\u7B97'};
desc.textContent=map[status]||'\u70B9\u51FB\u67E5\u770B\u5F00\u5956\u52A8\u753B';
}
}
function tryResumeVideo(){var v=document.querySelector('#pk10VideoPlayer video');if(v){v.muted=true;v.play().catch(function(){});}}
+33
View File
@@ -0,0 +1,33 @@
/**
* PK10 Sound Engine (SoundFX)
* Depends on: (none - first to load)
* Provides: SoundFX, toggleRaceSound
*
* Sound only plays when the race modal is open (raceModalOpen === true).
* betSuccess/error are exempt they are UI feedback, not race sounds.
*/
var SoundFX={
enabled:localStorage.getItem('raceSoundOn')!=='0',
_cache:{},
// Core play — checks both enabled AND raceModalOpen
_play:function(name,vol,startAt){if(!this.enabled||!window.raceModalOpen)return null;var a=new Audio('/Static/sounds/'+name+'.mp3');a.volume=Math.min(vol||0.6,1);if(startAt)a.currentTime=startAt;a.play().catch(function(){});return a;},
_stop:function(key){if(this._cache[key]){try{this._cache[key].pause();this._cache[key].currentTime=0;}catch(e){}delete this._cache[key];}},
engineIdle:function(){this._cache.idle=this._play('engine',0.3);},
trafficBeep:function(n){this._play('beep',n>0?0.4:0.6);},
engineStart:function(){this._stop('idle');this._cache.eng=this._play('engine',0.7);},
accelerate:function(){this._stop('eng');this._cache.race=this._play('racing',0.5);},
gearShift:function(){this._play('engine',0.25,1.5);},
whoosh:function(){this._play('whoosh',0.5);},
brakeSqueal:function(){this._stop('race');this._play('brake',0.6);},
finish:function(){this._play('finish',0.7);},
podium:function(){this._play('finish',0.5);},
// tick — race countdown sound, only when modal open
tick:function(){if(!this.enabled||!window.raceModalOpen)return;try{var ctx=new(window.AudioContext||window.webkitAudioContext)();var o=ctx.createOscillator(),g=ctx.createGain();o.type='sine';o.frequency.value=800;g.gain.setValueAtTime(0.1,ctx.currentTime);g.gain.linearRampToValueAtTime(0,ctx.currentTime+0.05);o.connect(g).connect(ctx.destination);o.start();o.stop(ctx.currentTime+0.05);}catch(e){}},
// betSuccess/error — UI feedback, always plays when enabled (no modal check)
betSuccess:function(){if(!this.enabled)return;try{var ctx=new(window.AudioContext||window.webkitAudioContext)();[880,1175].forEach(function(f,i){var o=ctx.createOscillator(),g=ctx.createGain();o.type='sine';o.frequency.value=f;g.gain.setValueAtTime(0.08,ctx.currentTime+i*0.08);g.gain.linearRampToValueAtTime(0,ctx.currentTime+i*0.08+0.12);o.connect(g).connect(ctx.destination);o.start(ctx.currentTime+i*0.08);o.stop(ctx.currentTime+i*0.08+0.12);});}catch(e){}},
error:function(){if(!this.enabled)return;try{var ctx=new(window.AudioContext||window.webkitAudioContext)();var o=ctx.createOscillator(),g=ctx.createGain();o.type='square';o.frequency.setValueAtTime(200,ctx.currentTime);o.frequency.setValueAtTime(150,ctx.currentTime+0.15);g.gain.setValueAtTime(0.06,ctx.currentTime);g.gain.linearRampToValueAtTime(0,ctx.currentTime+0.3);o.connect(g).connect(ctx.destination);o.start();o.stop(ctx.currentTime+0.3);}catch(e){}},
// stopAll — kill all cached sounds (called when modal closes)
stopAll:function(){for(var k in this._cache){this._stop(k);}},
toggle:function(){this.enabled=!this.enabled;localStorage.setItem('raceSoundOn',this.enabled?'1':'0');return this.enabled;}
};
function toggleRaceSound(){var on=SoundFX.toggle();var btn=document.querySelector('.rs-sound');if(btn)btn.textContent=on?'🔊':'🔇';if(on)SoundFX.tick();}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.