Initial commit: 投注游戏平台初始化

This commit is contained in:
li
2026-02-25 01:26:58 +08:00
commit 77ca2cc8b3
275 changed files with 237479 additions and 0 deletions
Vendored Executable
BIN
View File
Binary file not shown.
+584
View File
@@ -0,0 +1,584 @@
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
<i class="fa fa-user-shield text-primary mr-3"></i>
管理员管理中心
</h1>
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
<!-- 搜索和操作区 -->
<div class="flex justify-between items-center mb-6 gap-4">
<h2 class="text-xl font-semibold text-gray-700 whitespace-nowrap">管理员列表</h2>
<div class="flex gap-3">
<!-- 新增管理员按钮 -->
<button id="openFormBtn" class="bg-primary hover:bg-primary/90 text-white px-5 py-2.5 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center whitespace-nowrap">
<i class="fa fa-plus mr-2"></i>
<span>新增管理员</span>
</button>
</div>
</div>
<!-- 管理员列表表格 -->
<div class="overflow-x-auto">
<table class="w-full bg-white rounded-xl shadow-md overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">管理员信息</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">注册时间</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200" id="adminList">
<?php if (!empty($admins) && is_array($admins)): ?>
<?php foreach ($admins as $admin): ?>
<tr class="hover:bg-gray-50 transition-colors" data-id="<?php echo $admin['id']; ?>">
<!-- 用户名和头像单元格 - 自适应宽度 -->
<td class="px-4 py-4 whitespace-nowrap">
<div class="flex items-center gap-3">
<img src="<?php
if (!empty($admin['avatar'])) {
echo htmlspecialchars($admin['avatar']);
} else {
echo "https://robohash.org/admin" . $admin['id'] . "?size=40x40";
}
?>"
alt="管理员头像" class="w-10 h-10 rounded-full object-cover border border-gray-200 flex-shrink-0">
<div class="min-w-0 flex-1">
<div class="text-sm font-medium text-gray-900 truncate"><?php echo htmlspecialchars($admin['username']); ?></div>
<div class="text-xs text-gray-500 truncate"><?php echo htmlspecialchars($admin['email']); ?></div>
</div>
</div>
</td>
<!-- 创建时间 - 自适应宽度 -->
<td class="px-4 py-4 whitespace-nowrap hidden sm:table-cell">
<div class="text-sm text-gray-500"><?php echo date('Y-m-d H:i', strtotime($admin['created_at'])); ?></div>
</td>
<!-- 状态 - 自适应宽度 -->
<td class="px-4 py-4 whitespace-nowrap">
<?php
$statusClass = $admin['status'] ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
$statusText = $admin['status'] ? '启用' : '停用';
?>
<span class="inline-block px-2 py-1 text-xs rounded-full <?php echo $statusClass; ?>">
<?php echo $statusText; ?>
</span>
</td>
<!-- 操作按钮 - 自适应宽度 -->
<td class="px-4 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex items-center justify-end gap-2">
<button class="view-btn text-gray-500 hover:text-purple-500"
data-id="<?php echo $admin['id']; ?>" title="查看详情">
<i class="fa fa-eye"></i>
</button>
<button class="edit-btn text-gray-500 hover:text-blue-500"
data-id="<?php echo $admin['id']; ?>" title="编辑">
<i class="fa fa-pencil"></i>
</button>
<button class="delete-btn text-gray-500 hover:text-red-500"
data-id="<?php echo $admin['id']; ?>" title="删除">
<i class="fa fa-trash"></i>
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="4" class="px-6 py-12 text-center">
<div class="flex flex-col items-center">
<i class="fa fa-user-shield text-gray-300 text-5xl mb-4"></i>
<h3 class="text-lg font-medium text-gray-900">没有找到管理员</h3>
<p class="mt-1 text-gray-500">尝试调整筛选条件或添加新管理员</p>
<button class="mt-4 bg-primary hover:bg-primary/90 text-white px-5 py-2 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center"
onclick="openFormModal()">
<i class="fa fa-plus mr-2"></i>
<span>添加新管理员</span>
</button>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- 分页控件 -->
<div class="flex justify-between items-center mt-6">
<p class="text-sm text-gray-500">显示 1 至 <?php echo min(10, count($admins ?? [])); ?> 条,共 <?php echo count($admins ?? []); ?> 条</p>
</div>
</div>
<!-- 管理员表单弹窗背景 -->
<div id="formBackdrop" class="fixed inset-0 bg-black/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 z-40"></div>
<!-- 管理员表单弹窗 -->
<div id="formModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-events-none pointer-none transition transition-all duration-300 scale-95">
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-hidden">
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
<h3 id="formTitle" class="text-xl font-bold text-gray-800 flex items-center">
<i class="fa fa-plus-circle text-primary mr-2"></i>
创建新管理员
</h3>
<button id="closeFormBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
<i class="fa fa-times"></i>
</button>
</div>
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-130px)]">
<form id="adminForm" class="space-y-5">
<input type="hidden" id="adminId" name="id">
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">用户名 <span class="text-red-500">*</span></label>
<input type="text" id="username" name="username" required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="请输入用户名">
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">邮箱 <span class="text-red-500">*</span></label>
<input type="email" id="email" name="email" required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="请输入邮箱地址">
</div>
<!-- 管理员表单弹窗中密码字段部分 -->
<div id="passwordField">
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">
密码 <span class="text-red-500">*</span>
</label>
<input type="password" id="password" name="password" required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="请输入密码">
<p class="mt-1 text-xs text-gray-500">密码长度至少8位,包含字母和数字</p>
</div>
<div>
<label class="flex items-center">
<input type="checkbox" id="status" name="status" value="1" checked
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary">
<span class="ml-2 text-sm text-gray-700">启用管理员</span>
</label>
</div>
</form>
</div>
<div class="border-t border-gray-100 px-6 py-4 flex justify-end gap-3">
<button id="cancelBtn" class="px-5 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors">
取消
</button>
<button id="submitBtn" type="button"
class="bg-primary hover:bg-primary/90 text-white px-5 py-2 rounded-lg shadow hover:shadow-md transition-all duration-200">
保存管理员
</button>
</div>
</div>
</div>
<!-- 管理员详情弹窗 -->
<div id="detailModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-none transition-all duration-300 scale-95">
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-hidden">
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
<h3 class="text-xl font-bold text-gray-800 flex items-center">
<i class="fa fa-user-shield text-primary mr-2"></i>
管理员详情
</h3>
<button id="closeDetailBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
<i class="fa fa-times"></i>
</button>
</div>
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-100px)]">
<div class="flex flex-col items-center mb-6">
<img id="detailAvatar" src="https://picsum.photos/seed/admin/100/100" alt="管理员头像" class="w-24 h-24 rounded-full mb-4">
<h4 id="detailUsername" class="text-xl font-bold text-gray-800">管理员名</h4>
<p id="detailRole" class="mt-1 px-3 py-1 text-sm rounded-full bg-red-100 text-red-800">管理员</p>
</div>
<div class="space-y-4">
<div class="grid grid-cols-3 gap-4 items-center">
<span class="text-sm text-gray-500">ID</span>
<span id="detailId" class="col-span-2 text-gray-800">--</span>
</div>
<div class="w-full h-px bg-gray-100"></div>
<div class="grid grid-cols-3 gap-4 items-center">
<span class="text-sm text-gray-500">邮箱</span>
<span id="detailEmail" class="col-span-2 text-gray-800">--</span>
</div>
<div class="w-full h-px bg-gray-100"></div>
<div class="grid grid-cols-3 gap-4 items-center">
<span class="text-sm text-gray-500">状态</span>
<span id="detailStatus" class="col-span-2">
<span class="inline-block px-2 py-1 text-xs rounded-full bg-green-100 text-green-800">启用</span>
</span>
</div>
<div class="w-full h-px bg-gray-100"></div>
<div class="grid grid-cols-3 gap-4 items-center">
<span class="text-sm text-gray-500">创建时间</span>
<span id="detailCreatedAt" class="col-span-2 text-gray-800">--</span>
</div>
<div class="w-full h-px bg-gray-100"></div>
<div class="grid grid-cols-3 gap-4 items-center">
<span class="text-sm text-gray-500">最后登录</span>
<span id="detailLastLogin" class="col-span-2 text-gray-800">--</span>
</div>
</div>
</div>
<div class="border-t border-gray-100 px-6 py-4 flex justify-end">
<button id="closeDetailBtn2" class="px-5 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors">
关闭
</button>
</div>
</div>
</div>
<script>
console.log('管理员管理JS加载完成');
document.addEventListener('DOMContentLoaded', function() {
// 缓存DOM元素
const formModal = document.getElementById('formModal');
const formBackdrop = document.getElementById('formBackdrop');
const detailModal = document.getElementById('detailModal');
const openFormBtn = document.getElementById('openFormBtn');
const closeFormBtn = document.getElementById('closeFormBtn');
const cancelBtn = document.getElementById('cancelBtn');
const submitBtn = document.getElementById('submitBtn');
const formTitle = document.getElementById('formTitle');
const adminForm = document.getElementById('adminForm');
const adminList = document.getElementById('adminList');
const passwordField = document.getElementById('passwordField');
const closeDetailBtn = document.getElementById('closeDetailBtn');
const closeDetailBtn2 = document.getElementById('closeDetailBtn2');
// 检查元素是否存在
function checkElements() {
const elements = [
formModal, formBackdrop, openFormBtn,
closeFormBtn, cancelBtn, submitBtn
];
const missing = elements.filter(el => !el);
if (missing.length > 0) {
console.error('缺少必要的DOM元素,功能无法正常工作');
return false;
}
return true;
}
// 显示表单弹窗
function openFormModal() {
if (!checkElements()) return;
resetForm();
formModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
formModal.classList.add('scale-100');
formBackdrop.classList.remove('opacity-0', 'pointer-events-none');
document.body.style.overflow = 'hidden';
void formModal.offsetWidth; // 强制重绘
}
// 隐藏表单弹窗
function closeFormModal() {
if (!checkElements()) return;
formModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
formModal.classList.remove('scale-100');
formBackdrop.classList.add('opacity-0', 'pointer-events-none');
document.body.style.overflow = '';
}
// 显示详情弹窗
function openDetailModal() {
detailModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
detailModal.classList.add('scale-100');
formBackdrop.classList.remove('opacity-0', 'pointer-events-none');
document.body.style.overflow = 'hidden';
void detailModal.offsetWidth;
}
// 隐藏详情弹窗
function closeDetailModal() {
detailModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
detailModal.classList.remove('scale-100');
formBackdrop.classList.add('opacity-0', 'pointer-events-none');
document.body.style.overflow = '';
}
// 重置表单(新增模式)
function resetForm() {
adminForm.reset();
document.getElementById('adminId').value = '';
formTitle.innerHTML = '<i class="fa fa-plus-circle text-primary mr-2"></i> 创建新管理员';
// 新增模式:密码必填设置
const passwordLabel = document.querySelector('#passwordField label');
const passwordInput = document.getElementById('password');
passwordLabel.innerHTML = '密码 <span class="text-red-500">*</span>';
passwordInput.required = true;
passwordInput.placeholder = '请输入密码';
passwordField.style.display = 'block';
submitBtn.innerHTML = '保存管理员';
submitBtn.disabled = false;
}
// 加载管理员数据(编辑模式)
async function loadAdminData(id) {
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 加载中...';
try {
const response = await fetch(`/admin/admins/${id}`);
if (!response.ok) throw new Error('获取数据失败');
const data = await response.json();
if (data.success && data.data) {
const { id, username, email, status } = data.data;
document.getElementById('adminId').value = id;
document.getElementById('username').value = username || '';
document.getElementById('email').value = email || '';
document.getElementById('status').checked = status == 1;
formTitle.innerHTML = '<i class="fa fa-pencil text-primary mr-2"></i> 编辑管理员';
// 编辑模式:密码可选设置
const passwordLabel = document.querySelector('#passwordField label');
const passwordInput = document.getElementById('password');
passwordLabel.innerHTML = '密码(不填则不修改)';
passwordInput.required = false;
passwordInput.placeholder = '不修改密码请留空';
passwordField.style.display = 'block';
} else {
throw new Error(data.message || '获取数据失败');
}
} catch (e) {
showMessage(e.message, 'error');
closeFormModal();
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = '保存管理员';
}
}
// 加载管理员详情
async function loadAdminDetail(id) {
try {
const response = await fetch(`/admin/admins/${id}`);
if (!response.ok) throw new Error('获取详情失败');
const data = await response.json();
if (data.success && data.data) {
const { id, username, email, status, created_at, last_login, avatar } = data.data;
// 填充详情数据
document.getElementById('detailId').textContent = id;
document.getElementById('detailUsername').textContent = username || '未知管理员';
document.getElementById('detailEmail').textContent = email || '未设置';
document.getElementById('detailCreatedAt').textContent = created_at ? new Date(created_at).toLocaleString() : '未知';
document.getElementById('detailLastLogin').textContent = last_login ? new Date(last_login).toLocaleString() : '从未登录';
document.getElementById('detailAvatar').src = avatar || `https://robohash.org/admin${id}?size=100x100`;
// 设置角色标签样式(管理员固定为管理员)
document.getElementById('detailRole').className = 'mt-1 px-3 py-1 text-sm rounded-full bg-red-100 text-red-800';
document.getElementById('detailRole').textContent = '管理员';
// 设置状态标签样式
const statusClass = status ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
const statusText = status ? '启用' : '禁用';
document.getElementById('detailStatus').innerHTML =
`<span class="inline-block px-2 py-1 text-xs rounded-full ${statusClass}">${statusText}</span>`;
openDetailModal();
} else {
throw new Error(data.message || '获取详情失败');
}
} catch (e) {
showMessage(e.message, 'error');
}
}
// 表单验证
function validateForm() {
const username = document.getElementById('username').value.trim();
const email = document.getElementById('email').value.trim();
const password = document.getElementById('password').value.trim();
const isEditMode = !!document.getElementById('adminId').value;
if (!username) {
showMessage('请输入用户名', 'error');
return false;
}
if (!email) {
showMessage('请输入邮箱地址', 'error');
return false;
}
// 验证邮箱格式
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
showMessage('请输入有效的邮箱地址', 'error');
return false;
}
// 仅在新增或编辑时填写了密码的情况下验证长度
if ((!isEditMode || password) && password.length < 8) {
showMessage('密码长度至少8位', 'error');
return false;
}
return true;
}
// 提交表单(创建/更新)
async function submitFormData() {
if (!validateForm()) return;
const formData = new FormData(adminForm);
const isEditMode = !!document.getElementById('adminId').value;
const statusCheckbox = document.getElementById('status');
formData.delete('status'); // 先删除可能存在的旧值
formData.append('status', statusCheckbox.checked ? '1' : '0');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 保存中...';
try {
const response = await fetch('/admin/admins/update', {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
if (data.success) {
showMessage(isEditMode ? '管理员更新成功' : '管理员创建成功');
closeFormModal();
setTimeout(() => location.reload(), 1000);
} else {
throw new Error(data.message || (isEditMode ? '更新失败' : '创建失败'));
}
} catch (e) {
showMessage(e.message, 'error');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = '保存管理员';
}
}
// 删除管理员
async function deleteAdmin(id) {
if (!confirm('确定要删除该管理员吗?此操作不可恢复!')) return;
try {
const response = await fetch(`/admin/admins/delete/${id}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
showMessage('管理员已删除');
// 移除DOM元素
const row = document.querySelector(`tr[data-id="${id}"]`);
if (row) {
row.remove();
// 检查是否还有数据行
const rows = adminList.querySelectorAll('tr:not(:last-child)');
if (rows.length === 0) {
adminList.innerHTML = `
<tr>
<td colspan="4" class="px-6 py-10 text-center text-gray-500 border border-dashed border-gray-200">
<div>
<i class="fa fa-info-circle text-2xl mb-2 text-gray-300"></i>
<p>暂无管理员数据</p>
</div>
</td>
</tr>`;
}
}
} else {
throw new Error(data.message || '删除失败');
}
} catch (e) {
showMessage(e.message, 'error');
}
}
// 绑定事件
if (checkElements()) {
// 打开表单
openFormBtn.addEventListener('click', openFormModal);
// 关闭表单
closeFormBtn.addEventListener('click', closeFormModal);
cancelBtn.addEventListener('click', closeFormModal);
formBackdrop.addEventListener('click', () => {
if (!formModal.classList.contains('invisible')) closeFormModal();
if (!detailModal.classList.contains('invisible')) closeDetailModal();
});
// 关闭详情
closeDetailBtn.addEventListener('click', closeDetailModal);
closeDetailBtn2.addEventListener('click', closeDetailModal);
// 提交表单
submitBtn.addEventListener('click', submitFormData);
// 编辑、删除、查看事件委托
adminList.addEventListener('click', function(e) {
const editBtn = e.target.closest('.edit-btn');
const deleteBtn = e.target.closest('.delete-btn');
const viewBtn = e.target.closest('.view-btn');
if (editBtn) {
const id = editBtn.getAttribute('data-id');
if (id) {
openFormModal();
setTimeout(() => loadAdminData(id), 300);
}
} else if (deleteBtn) {
const id = deleteBtn.getAttribute('data-id');
if (id) deleteAdmin(id);
} else if (viewBtn) {
const id = viewBtn.getAttribute('data-id');
if (id) loadAdminDetail(id);
}
});
// ESC键关闭弹窗
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
if (!formModal.classList.contains('invisible')) closeFormModal();
if (!detailModal.classList.contains('invisible')) closeDetailModal();
}
});
// 阻止表单默认提交
adminForm.addEventListener('submit', e => {
e.preventDefault();
submitFormData();
});
}
});
</script>
+64
View File
@@ -0,0 +1,64 @@
<div class="p-6">
<h2 class="text-2xl font-bold mb-4">🤝 代理管理</h2>
<button onclick="showAddAgent()" class="mb-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm">+ 添加代理</button>
<div class="bg-white rounded-xl shadow overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50"><tr>
<th class="px-4 py-3 text-left">代理</th><th class="px-4 py-3">级别</th><th class="px-4 py-3">邀请码</th>
<th class="px-4 py-3">佣金%</th><th class="px-4 py-3">反水%</th><th class="px-4 py-3">玩家数</th>
<th class="px-4 py-3">状态</th><th class="px-4 py-3">操作</th>
</tr></thead>
<tbody>
<?php foreach($agents??[] as $a): ?>
<tr class="border-t hover:bg-gray-50">
<td class="px-4 py-2"><?=htmlspecialchars($a['user']['username']??'?')?></td>
<td class="px-4 py-2 text-center"><?=$a['level']==1?'<span class="text-yellow-600 font-bold">总代</span>':'代理'?></td>
<td class="px-4 py-2 font-mono text-xs"><?=$a['agent_code']?></td>
<td class="px-4 py-2 text-center"><?=$a['commission_rate']?>%</td>
<td class="px-4 py-2 text-center"><?=$a['rebate_rate']?>%</td>
<td class="px-4 py-2 text-center"><?=$a['player_count']?></td>
<td class="px-4 py-2 text-center"><?=$a['status']?'<span class="text-green-500">启用</span>':'<span class="text-red-500">禁用</span>'?></td>
<td class="px-4 py-2 text-center">
<button onclick="editAgent(<?=htmlspecialchars(json_encode($a))?>)" class="text-blue-500 hover:underline text-xs">编辑</button>
<button onclick="deleteAgent(<?=$a['id']?>)" class="text-red-500 hover:underline text-xs ml-2">删除</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- 弹窗 -->
<div id="agentModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-xl p-6 w-full max-w-md">
<h3 class="font-bold mb-4" id="agentModalTitle">添加代理</h3>
<input type="hidden" id="agentId" value="0">
<div class="space-y-3">
<div><label class="text-xs text-gray-400">用户ID</label><input type="number" id="agentUserId" class="w-full border rounded px-3 py-2"></div>
<div><label class="text-xs text-gray-400">上级代理ID (留空=总代)</label><input type="number" id="agentParent" class="w-full border rounded px-3 py-2"></div>
<div class="grid grid-cols-2 gap-3">
<div><label class="text-xs text-gray-400">佣金 %</label><input type="number" step="0.1" id="agentComm" class="w-full border rounded px-3 py-2" value="1"></div>
<div><label class="text-xs text-gray-400">反水 %</label><input type="number" step="0.1" id="agentRebate" class="w-full border rounded px-3 py-2" value="0.5"></div>
</div>
<div><label class="text-xs text-gray-400">状态</label><select id="agentStatus" class="w-full border rounded px-3 py-2"><option value="1">启用</option><option value="0">禁用</option></select></div>
</div>
<div class="flex gap-2 mt-4">
<button onclick="saveAgent()" class="flex-1 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">保存</button>
<button onclick="document.getElementById('agentModal').classList.add('hidden')" class="flex-1 py-2 bg-gray-200 rounded hover:bg-gray-300">取消</button>
</div>
</div>
</div>
</div>
<script>
function showAddAgent(){document.getElementById('agentId').value=0;document.getElementById('agentModalTitle').textContent='添加代理';document.getElementById('agentModal').classList.remove('hidden');}
function editAgent(a){document.getElementById('agentId').value=a.id;document.getElementById('agentComm').value=a.commission_rate;document.getElementById('agentRebate').value=a.rebate_rate;document.getElementById('agentStatus').value=a.status;document.getElementById('agentModalTitle').textContent='编辑代理';document.getElementById('agentModal').classList.remove('hidden');}
async function saveAgent(){
const body={id:parseInt(document.getElementById('agentId').value),user_id:parseInt(document.getElementById('agentUserId').value),parent_id:document.getElementById('agentParent').value||null,commission_rate:parseFloat(document.getElementById('agentComm').value),rebate_rate:parseFloat(document.getElementById('agentRebate').value),status:parseInt(document.getElementById('agentStatus').value)};
const r=await fetch('/admin/agents/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const d=await r.json();if(d.status==='success')location.reload();else alert(d.message);
}
async function deleteAgent(id){if(!confirm('确定删除该代理?'))return;await fetch('/admin/agents/delete/'+id,{method:'POST'});location.reload();}
</script>
+169
View File
@@ -0,0 +1,169 @@
<div class="p-6">
<h2 class="text-2xl font-bold mb-4">⏱️ 自动开期设置</h2>
<!-- 宝塔配置提示 -->
<div class="bg-blue-50 border border-blue-200 rounded-xl p-4 mb-6">
<h3 class="font-bold text-blue-700 mb-2">📋 宝塔面板定时任务配置</h3>
<div class="text-sm text-blue-600 space-y-1">
<p>1. 登录宝塔面板 <b>计划任务</b></p>
<p>2. 任务类型: <b>Shell脚本</b></p>
<p>3. 任务名称: <b>PK10自动开期</b></p>
<p>4. 执行周期: <b>每N分钟 1分钟</b></p>
<p>5. 脚本内容:</p>
<pre class="bg-blue-100 p-2 rounded mt-1 text-xs overflow-x-auto select-all" id="cronCmd">cd <?=ROOT_PATH?> && /usr/bin/php cron/auto_period_task.php >> Storage/log/auto_period.log 2>&1</pre>
<button onclick="copyCmd()" class="mt-2 px-3 py-1 bg-blue-500 text-white rounded text-xs hover:bg-blue-600">📋 复制命令</button>
</div>
</div>
<!-- 全局控制 -->
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-4">
<span class="text-gray-500">全局控制:</span>
<button onclick="toggleAll(1)" class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 text-sm">✅ 全部启用</button>
<button onclick="toggleAll(0)" class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 text-sm">🚫 全部关闭</button>
</div>
<button onclick="refreshStatus()" class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300 text-sm">🔄 刷新状态</button>
</div>
<!-- 游戏列表 -->
<div class="space-y-4 mb-8">
<?php foreach($games ?? [] as $game): ?>
<div class="bg-white rounded-xl p-5 shadow" id="game-<?=$game['id']?>">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<span class="text-lg font-bold"><?=$game['name']?></span>
<span class="px-2 py-1 rounded text-xs bg-gray-100 text-gray-500"><?=$game['type']?></span>
<span class="px-2 py-1 rounded text-xs <?=($game['auto_period_enabled']??0)?'bg-green-100 text-green-700':'bg-red-100 text-red-700'?>" id="status-<?=$game['id']?>">
<?=($game['auto_period_enabled']??0)?'✅ 自动开期中':'🚫 已关闭'?>
</span>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" class="sr-only peer" <?=($game['auto_period_enabled']??0)?'checked':''?> onchange="toggleGame(<?=$game['id']?>, this.checked)">
<div class="w-11 h-6 bg-gray-200 rounded-full peer peer-checked:after:translate-x-full peer-checked:bg-green-500 after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all"></div>
</label>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 每期时长 -->
<div>
<label class="block text-sm text-gray-500 mb-1">每期时长</label>
<div class="flex items-center gap-2">
<input type="number" min="60" max="3600" step="30" value="<?=$game['period_duration']??300?>"
id="duration-<?=$game['id']?>" class="w-24 border rounded px-3 py-2 text-center">
<span class="text-gray-400 text-sm">秒</span>
<span class="text-gray-300 text-xs">( = <span id="durationMin-<?=$game['id']?>"><?=round(($game['period_duration']??300)/60, 1)?></span> 分钟)</span>
</div>
</div>
<!-- 封盘提前时间 -->
<div>
<label class="block text-sm text-gray-500 mb-1">结束前提前封盘</label>
<div class="flex items-center gap-2">
<input type="number" min="5" max="120" step="5" value="<?=$game['lock_before_end']??30?>"
id="lock-<?=$game['id']?>" class="w-24 border rounded px-3 py-2 text-center">
<span class="text-gray-400 text-sm">秒</span>
</div>
</div>
</div>
<div class="mt-4 flex justify-end">
<button onclick="saveGame(<?=$game['id']?>)" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm">💾 保存设置</button>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- 运行日志 -->
<div class="bg-white rounded-xl shadow overflow-hidden">
<h3 class="font-bold px-4 py-3 bg-gray-50 border-b">📜 最近自动开期日志</h3>
<div class="max-h-96 overflow-y-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 sticky top-0"><tr>
<th class="px-4 py-2 text-left">时间</th>
<th class="px-4 py-2 text-left">游戏</th>
<th class="px-4 py-2 text-left">动作</th>
<th class="px-4 py-2 text-left">详情</th>
</tr></thead>
<tbody>
<?php foreach($logs ?? [] as $log): ?>
<tr class="border-t hover:bg-gray-50">
<td class="px-4 py-2 text-gray-400 text-xs whitespace-nowrap"><?=$log['created_at']??''?></td>
<td class="px-4 py-2">G<?=$log['game_id']??0?></td>
<td class="px-4 py-2">
<span class="px-2 py-1 rounded text-xs
<?php
$act = $log['action'] ?? '';
echo match($act) {
'start' => 'bg-green-100 text-green-700',
'lock' => 'bg-orange-100 text-orange-700',
'draw' => 'bg-blue-100 text-blue-700',
'settle' => 'bg-purple-100 text-purple-700',
'error' => 'bg-red-100 text-red-700',
default => 'bg-gray-100 text-gray-700',
};
?>">
<?=match($act) { 'start'=>'开期', 'lock'=>'封盘', 'draw'=>'开奖', 'settle'=>'结算', 'error'=>'错误', default=>$act }?>
</span>
</td>
<td class="px-4 py-2 text-gray-500 text-xs"><?=htmlspecialchars($log['message']??'')?></td>
</tr>
<?php endforeach; ?>
<?php if(empty($logs)): ?>
<tr><td colspan="4" class="px-4 py-8 text-center text-gray-400">暂无日志记录。请先执行数据库迁移并配置宝塔定时任务。</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<script>
function copyCmd(){
const t=document.getElementById('cronCmd').textContent;
navigator.clipboard.writeText(t).then(()=>alert('已复制到剪贴板')).catch(()=>{
const ta=document.createElement('textarea');ta.value=t;document.body.appendChild(ta);ta.select();document.execCommand('copy');ta.remove();alert('已复制');
});
}
async function toggleGame(gameId, enabled){
const r=await fetch('/admin/auto-period/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({game_id:gameId,auto_period_enabled:enabled?1:0})});
const d=await r.json();
if(d.success){
const el=document.getElementById('status-'+gameId);
el.textContent=enabled?'✅ 自动开期中':'🚫 已关闭';
el.className='px-2 py-1 rounded text-xs '+(enabled?'bg-green-100 text-green-700':'bg-red-100 text-red-700');
}else{alert(d.message||'操作失败');}
}
async function saveGame(gameId){
const duration=parseInt(document.getElementById('duration-'+gameId).value)||300;
const lock=parseInt(document.getElementById('lock-'+gameId).value)||30;
const r=await fetch('/admin/auto-period/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({game_id:gameId,period_duration:duration,lock_before_end:lock})});
const d=await r.json();
if(d.success){
document.getElementById('durationMin-'+gameId).textContent=(duration/60).toFixed(1);
alert('保存成功');
}else{alert(d.message||'保存失败');}
}
async function toggleAll(enabled){
if(!confirm(enabled?'确认启用所有游戏的自动开期?':'确认关闭所有游戏的自动开期?'))return;
const r=await fetch('/admin/auto-period/toggle-all',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled})});
const d=await r.json();
if(d.success){location.reload();}else{alert(d.message||'操作失败');}
}
async function refreshStatus(){
const r=await fetch('/admin/auto-period/status');
const d=await r.json();
if(d.success){location.reload();}
}
// 每期时长实时更新分钟显示
document.querySelectorAll('input[id^="duration-"]').forEach(el=>{
el.addEventListener('input',function(){
const gid=this.id.split('-')[1];
document.getElementById('durationMin-'+gid).textContent=(parseInt(this.value||300)/60).toFixed(1);
});
});
</script>
+214
View File
@@ -0,0 +1,214 @@
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
<i class="fas fa-list-alt text-primary mr-3"></i>
投注记录
</h1>
<!-- 筛选工具栏 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
<form class="flex flex-col md:flex-row gap-4" method="GET" action="/admin/bets">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 mb-1">用户ID</label>
<input type="text" name="user_id" value="<?= htmlspecialchars($_GET['user_id'] ?? '') ?>" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm" placeholder="输入用户ID">
</div>
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 mb-1">期号</label>
<input type="text" name="period_number" value="<?= htmlspecialchars($_GET['period_number'] ?? '') ?>" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm" placeholder="输入期号">
</div>
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 mb-1">状态</label>
<select name="status" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm">
<option value="">全部状态</option>
<option value="pending" <?= ($_GET['status'] ?? '') === 'pending' ? 'selected' : '' ?>>待结算</option>
<option value="win" <?= ($_GET['status'] ?? '') === 'win' ? 'selected' : '' ?>>已中奖</option>
<option value="lose" <?= ($_GET['status'] ?? '') === 'lose' ? 'selected' : '' ?>>未中奖</option>
<option value="settled" <?= ($_GET['status'] ?? '') === 'settled' ? 'selected' : '' ?>>已结算</option>
</select>
</div>
<div class="flex items-end">
<button type="submit" class="bg-primary hover:bg-primary/90 text-white px-6 py-2 rounded-lg text-sm transition-colors h-[38px]">
<i class="fas fa-search mr-2"></i>查询
</button>
<a href="/admin/bets" class="ml-2 bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-2 rounded-lg text-sm transition-colors h-[38px] flex items-center">
重置
</a>
</div>
</form>
</div>
<!-- 数据列表 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
<div class="overflow-x-auto">
<table class="w-full bg-white rounded-xl overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用户</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">游戏房</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">期号</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">下注内容</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">金额</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">中奖/盈利</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">下注时间</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<?php if (!empty($bets) && is_array($bets)): ?>
<?php foreach ($bets as $bet): ?>
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-4 py-4 text-sm text-gray-500">
#<?= $bet['id'] ?>
</td>
<td class="px-4 py-4">
<div class="text-sm font-medium text-gray-900"><?= htmlspecialchars($bet['username'] ?? '未知用户') ?></div>
<div class="text-xs text-gray-500">ID: <?= $bet['user_id'] ?></div>
<div class="text-xs text-gray-500">余额: <?= number_format($bet['balance'] ?? 0) ?></div>
</td>
<td class="px-4 py-4 text-sm text-gray-700">
<?= htmlspecialchars($bet['game_name'] ?? '未关联') ?>
</td>
<td class="px-4 py-4 text-sm text-gray-700">
<?= htmlspecialchars($bet['period_number']) ?>
</td>
<td class="px-4 py-4">
<?php
// ========== 投注类型中文映射(完整版)==========
$bt = $bet['bet_type'];
$bv = $bet['bet_value'];
$displayType = '';
$displayValue = '';
// PK10 名次投注: rank + rank1_5
if ($bt === 'rank' && preg_match('/^rank(\d+)_(\d+)$/', $bv, $m)) {
$rn = (int)$m[1];
$displayType = $rn === 1 ? '冠军' : ($rn === 2 ? '亚军' : '第'.$rn.'名');
$displayValue = $m[2] . '号车';
}
// PK10 大小: bs + rank1_big
elseif ($bt === 'bs' && preg_match('/^rank(\d+)_(big|small)$/', $bv, $m)) {
$rn = (int)$m[1];
$displayType = ($rn === 1 ? '冠军' : ($rn === 2 ? '亚军' : '第'.$rn.'名')) . ' 大小';
$displayValue = $m[2] === 'big' ? '大' : '小';
}
// PK10 单双: oe + rank1_odd
elseif ($bt === 'oe' && preg_match('/^rank(\d+)_(odd|even)$/', $bv, $m)) {
$rn = (int)$m[1];
$displayType = ($rn === 1 ? '冠军' : ($rn === 2 ? '亚军' : '第'.$rn.'名')) . ' 单双';
$displayValue = $m[2] === 'odd' ? '单' : '双';
}
// PK10 龙虎: dt + dt1_dragon
elseif ($bt === 'dt' && preg_match('/^dt(\d+)_(dragon|tiger)$/', $bv, $m)) {
$pairs = [1=>[1,10],2=>[2,9],3=>[3,8],4=>[4,7],5=>[5,6]];
$p = $pairs[(int)$m[1]] ?? [(int)$m[1], 11-(int)$m[1]];
$displayType = '龙虎 '.$p[0].'vs'.$p[1];
$displayValue = $m[2] === 'dragon' ? '龙' : '虎';
}
// PK10 冠亚和值: sum + sum_11
elseif ($bt === 'sum' && preg_match('/^sum_(\d+)$/', $bv, $m)) {
$displayType = '冠亚和';
$displayValue = $m[1];
}
// PK10 冠亚和大小: sum_bs + sum_big
elseif ($bt === 'sum_bs') {
$displayType = '冠亚和';
$sbMap = ['sum_big'=>'大','sum_small'=>'小','sum_odd'=>'单','sum_even'=>'双'];
$displayValue = $sbMap[$bv] ?? $bv;
}
// 骰子 大小/单双/点数/单骰/豹子
else {
$typeMap = [
'xiu' => '小', 'tai' => '大', 'chan' => '双', 'le' => '单',
'number' => '点数', 'dice' => '单骰', 'combo' => '豹子',
'big_small' => '大小', 'odd_even' => '单双',
];
$displayType = $typeMap[$bt] ?? $bt;
$valueMap = ['big'=>'大','small'=>'小','odd'=>'单','even'=>'双',
'4red'=>'4红','4white'=>'4白','3red1white'=>'3红1白','1red3white'=>'1红3白'];
$displayValue = $valueMap[$bv] ?? $bv;
}
?>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
<?= htmlspecialchars($displayType) ?>
</span>
<span class="text-sm font-bold ml-1 text-gray-700">
<?= htmlspecialchars($displayValue) ?>
</span>
<div class="text-xs text-gray-500 mt-0.5">赔率: <?= $bet['odds'] ?></div>
</td>
<td class="px-4 py-4 text-sm font-bold text-gray-900">
<?= number_format($bet['amount']) ?>
</td>
<td class="px-4 py-4">
<?php
$status = $bet['status'];
$statusClassMap = [
'pending' => 'bg-yellow-100 text-yellow-800',
'win' => 'bg-green-100 text-green-800',
'lose' => 'bg-gray-100 text-gray-800',
'settled' => 'bg-blue-100 text-blue-800'
];
$statusClass = $statusClassMap[$status] ?? 'bg-gray-100 text-gray-800';
$statusTextMap = [
'pending' => '待结算',
'win' => '已中奖',
'lose' => '未中奖',
'settled' => '已结算'
];
$statusText = $statusTextMap[$status] ?? $status;
?>
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full <?= $statusClass ?>">
<?= $statusText ?>
</span>
</td>
<td class="px-4 py-4">
<?php if ($status === 'win'): ?>
<div class="text-sm font-bold text-green-600">+<?= number_format($bet['win_amount']) ?></div>
<div class="text-xs text-gray-500">盈利: <?= number_format($bet['win_amount'] - $bet['amount']) ?></div>
<?php elseif ($status === 'lose'): ?>
<div class="text-sm font-bold text-red-500">-<?= number_format($bet['amount']) ?></div>
<?php else: ?>
<span class="text-gray-400">-</span>
<?php endif; ?>
</td>
<td class="px-4 py-4 text-sm text-gray-500 hidden md:table-cell">
<?= $bet['created_at'] ?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="8" class="px-6 py-12 text-center text-gray-500">
暂无投注记录
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- 分页 -->
<?php if ($totalPages > 1): ?>
<div class="mt-4 flex justify-between items-center">
<div class="text-sm text-gray-500">
共 <?= $totalPages ?> 页
</div>
<div class="flex gap-2">
<?php if ($currentPage > 1): ?>
<a href="?page=<?= $currentPage - 1 ?><?= http_build_query(array_diff_key($_GET, ['page' => ''])) ? '&' . http_build_query(array_diff_key($_GET, ['page' => ''])) : '' ?>" class="px-3 py-1 border rounded hover:bg-gray-50">上一页</a>
<?php endif; ?>
<?php for ($i = max(1, $currentPage - 2); $i <= min($totalPages, $currentPage + 2); $i++): ?>
<a href="?page=<?= $i ?><?= http_build_query(array_diff_key($_GET, ['page' => ''])) ? '&' . http_build_query(array_diff_key($_GET, ['page' => ''])) : '' ?>" class="px-3 py-1 border rounded <?= $i == $currentPage ? 'bg-primary text-white border-primary' : 'hover:bg-gray-50' ?>">
<?= $i ?>
</a>
<?php endfor; ?>
<?php if ($currentPage < $totalPages): ?>
<a href="?page=<?= $currentPage + 1 ?><?= http_build_query(array_diff_key($_GET, ['page' => ''])) ? '&' . http_build_query(array_diff_key($_GET, ['page' => ''])) : '' ?>" class="px-3 py-1 border rounded hover:bg-gray-50">下一页</a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</div>
+268
View File
@@ -0,0 +1,268 @@
<!-- 页面标题 -->
<div class="mb-6">
<h3 class="text-2xl font-bold text-dark">
控制台
</h3>
<p class="text-gray-500 mt-1">
<?=date('Y年m月d日')?> · 今天是星期<?=['日','一','二','三','四','五','六'][date('w')]?>
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-xl p-6 card-shadow hover-lift">
<div class="flex justify-between items-start">
<div>
<p class="text-gray-500 text-sm">今日投注总额</p>
<h3 class="text-2xl font-bold mt-1">
<?= isset($stats['today_bet_amount']) ? number_format($stats['today_bet_amount'], 0) : '0' ?>
</h3>
<p class="text-gray-500 text-xs mt-2">
单位:USDT,含所有有效期号
</p>
</div>
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<i class="fas fa-coins text-primary"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-6 card-shadow hover-lift">
<div class="flex justify-between items-start">
<div>
<p class="text-gray-500 text-sm">今日已派彩金额</p>
<h3 class="text-2xl font-bold mt-1">
<?= isset($stats['today_payout_amount']) ? number_format($stats['today_payout_amount'], 0) : '0' ?>
</h3>
<p class="text-gray-500 text-xs mt-2">
已结算期号产生的实际派彩
</p>
</div>
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
<i class="fas fa-hand-holding-usd text-success"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-6 card-shadow hover-lift">
<div class="flex justify-between items-start">
<div>
<p class="text-gray-500 text-sm">待处理开奖</p>
<h3 class="text-2xl font-bold mt-1">
<?= isset($stats['pending_draw_count']) ? (int)$stats['pending_draw_count'] : 0 ?>
</h3>
<p class="text-gray-500 text-xs mt-2">
含待录入结果与待审核期号
</p>
</div>
<div class="w-10 h-10 rounded-full bg-warning/10 flex items-center justify-center">
<i class="fas fa-trophy text-warning"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-6 card-shadow hover-lift">
<div class="flex justify-between items-start">
<div>
<p class="text-gray-500 text-sm">待处理提现</p>
<h3 class="text-2xl font-bold mt-1">
<?= isset($stats['pending_withdraw_count']) ? (int)$stats['pending_withdraw_count'] : 0 ?>
</h3>
<p class="text-gray-500 text-xs mt-2">
仅统计处于待审核状态的提现申请
</p>
</div>
<div class="w-10 h-10 rounded-full bg-danger/10 flex items-center justify-center">
<i class="fas fa-file-invoice-dollar text-danger"></i>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<div class="bg-white rounded-xl p-6 card-shadow lg:col-span-2">
<h2 class="text-lg font-semibold mb-4">业务流程概览</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-600">
<div class="p-4 border border-gray-100 rounded-lg">
<div class="flex items-center justify-between mb-2">
<span class="font-medium">用户与资金</span>
<span class="inline-flex items-center px-2 py-0.5 text-xs rounded-full bg-primary/10 text-primary">
<i class="fas fa-user-shield mr-1"></i> 账户安全
</span>
</div>
<p class="leading-relaxed">
管理玩家账号、登录状态与风险标记,掌握充值、余额、提现等资金流向,
为后续投注与开奖提供可靠资金基础。
</p>
</div>
<div class="p-4 border border-gray-100 rounded-lg">
<div class="flex items-center justify-between mb-2">
<span class="font-medium">游戏与期号</span>
<span class="inline-flex items-center px-2 py-0.5 text-xs rounded-full bg-warning/10 text-warning">
<i class="fas fa-dice mr-1"></i> 核心玩法
</span>
</div>
<p class="leading-relaxed">
维护骰子游戏配置与赔率,按业务策略生成期号,控制期号生命周期
(下注、封盘、开奖、结算)并处理异常期号。
</p>
</div>
<div class="p-4 border border-gray-100 rounded-lg">
<div class="flex items-center justify-between mb-2">
<span class="font-medium">开奖与派彩</span>
<span class="inline-flex items-center px-2 py-0.5 text-xs rounded-full bg-success/10 text-success">
<i class="fas fa-trophy mr-1"></i> 结果可信
</span>
</div>
<p class="leading-relaxed">
根据直播画面录入开奖结果并上传截图,完成结果审核与锁定,
自动触发派彩结算并与资金流水进行对账。
</p>
</div>
<div class="p-4 border border-gray-100 rounded-lg">
<div class="flex items-center justify-between mb-2">
<span class="font-medium">风控与监控</span>
<span class="inline-flex items-center px-2 py-0.5 text-xs rounded-full bg-danger/10 text-danger">
<i class="fas fa-exclamation-triangle mr-1"></i> 风险预警
</span>
</div>
<p class="leading-relaxed">
依托后台数据监控高额投注、异常盈利、频繁提现等行为,
为人工复核和规则优化提供依据,保障平台资金安全。
</p>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-6 card-shadow">
<h2 class="text-lg font-semibold mb-4">运营快捷入口</h2>
<div class="space-y-3">
<a href="/admin/users" class="flex items-center justify-between px-4 py-2 rounded-lg border border-gray-200 hover:border-primary hover:bg-primary/5 transition-colors text-sm">
<span class="flex items-center">
<i class="fas fa-user-cog w-5 text-center mr-2 text-primary"></i>
<span>用户管理</span>
</span>
<i class="fas fa-chevron-right text-xs text-gray-400"></i>
</a>
<a href="/admin/games" class="flex items-center justify-between px-4 py-2 rounded-lg border border-gray-200 hover:border-primary hover:bg-primary/5 transition-colors text-sm">
<span class="flex items-center">
<i class="fas fa-dice w-5 text-center mr-2 text-primary"></i>
<span>游戏管理</span>
</span>
<i class="fas fa-chevron-right text-xs text-gray-400"></i>
</a>
<a href="/admin/periods" class="flex items-center justify-between px-4 py-2 rounded-lg border border-gray-200 hover:border-primary hover:bg-primary/5 transition-colors text-sm">
<span class="flex items-center">
<i class="fas fa-list-ol w-5 text-center mr-2 text-primary"></i>
<span>期号管理</span>
</span>
<i class="fas fa-chevron-right text-xs text-gray-400"></i>
</a>
<a href="/admin/draws" class="flex items-center justify-between px-4 py-2 rounded-lg border border-gray-200 hover:border-primary hover:bg-primary/5 transition-colors text-sm">
<span class="flex items-center">
<i class="fas fa-trophy w-5 text-center mr-2 text-primary"></i>
<span>开奖管理</span>
</span>
<i class="fas fa-chevron-right text-xs text-gray-400"></i>
</a>
<a href="/admin/finance" class="flex items-center justify-between px-4 py-2 rounded-lg border border-gray-200 hover:border-primary hover:bg-primary/5 transition-colors text-sm">
<span class="flex items-center">
<i class="fas fa-yen-sign w-5 text-center mr-2 text-primary"></i>
<span>财务管理</span>
</span>
<i class="fas fa-chevron-right text-xs text-gray-400"></i>
</a>
</div>
<div class="mt-4 pt-4 border-t border-dashed border-gray-200 text-xs text-gray-600 space-y-2">
<div class="flex items-center justify-between">
<span class="flex items-center">
<span class="w-2 h-2 rounded-full bg-warning mr-2"></span>
<span>待处理开奖</span>
</span>
<span class="font-semibold text-warning">
<?= isset($stats['pending_draw_count']) ? (int)$stats['pending_draw_count'] : 0 ?> 期
</span>
</div>
<div class="flex items-center justify-between">
<span class="flex items-center">
<span class="w-2 h-2 rounded-full bg-danger mr-2"></span>
<span>待处理提现</span>
</span>
<span class="font-semibold text-danger">
<?= isset($stats['pending_withdraw_count']) ? (int)$stats['pending_withdraw_count'] : 0 ?> 笔
</span>
</div>
</div>
</div>
</div>
<div class="mt-8 grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-white rounded-xl p-6 card-shadow">
<h2 class="text-lg font-semibold mb-4">后台运营模块概览</h2>
<div id="game-management" class="mb-4">
<h3 class="text-sm font-medium mb-1 flex items-center">
<i class="fas fa-dice text-primary mr-2"></i> 游戏管理
</h3>
<p class="text-xs text-gray-500 leading-relaxed">
维护游戏平台中的骰子游戏配置,包括游戏列表、赔率设置、直播间绑定和启用状态,为前端提供可用游戏和玩法数据。
</p>
</div>
<div id="period-management" class="mb-4">
<h3 class="text-sm font-medium mb-1 flex items-center">
<i class="fas fa-list-ol text-primary mr-2"></i> 期号管理
</h3>
<p class="text-xs text-gray-500 leading-relaxed">
管理每天的开奖期号,配置期数、生成规则及时间区间,维护期号状态(未开始、下注中、封盘、已开奖、作废),并支持异常期号处理。
</p>
</div>
<div id="draw-management" class="mb-4">
<h3 class="text-sm font-medium mb-1 flex items-center">
<i class="fas fa-trophy text-primary mr-2"></i> 开奖管理
</h3>
<p class="text-xs text-gray-500 leading-relaxed">
开奖工作人员在后台对照直播流为指定期号录入开奖结果,上传开奖截图,完成审核与结果锁定,必要时进行重开或作废处理并记录日志。
</p>
</div>
<div id="finance-management">
<h3 class="text-sm font-medium mb-1 flex items-center">
<i class="fas fa-yen-sign text-primary mr-2"></i> 财务管理
</h3>
<p class="text-xs text-gray-500 leading-relaxed">
处理充值、提现和资金流水统计,核对用户余额变动情况,配合开奖结算结果进行对账,为风控和运营提供数据支持。
</p>
</div>
</div>
<div class="bg-white rounded-xl p-6 card-shadow">
<h2 class="text-lg font-semibold mb-4">操作指引</h2>
<div class="space-y-3 text-sm text-gray-600">
<p>
登录管理后台后,可通过左侧菜单快速进入用户管理、游戏管理、期号管理、开奖管理、财务管理和系统管理等模块,完成日常运营工作。
</p>
<p>
在正式接入真实数据前,建议优先配置游戏基本信息和直播间绑定,然后按业务流程逐步完善期号生成规则和开奖操作流程。
</p>
<p>
系统管理模块用于维护管理员账号和权限,以及后续扩展的系统配置与操作日志,确保平台运行安全可控。
</p>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 页面初始化操作
console.log('Project features page loaded successfully');
// 为功能卡片添加悬停动画效果
const featureCards = document.querySelectorAll('.hover\\:border-primary\\/30');
featureCards.forEach(card => {
card.addEventListener('mouseenter', function() {
this.classList.add('transform', 'translate-y-[-5px]', 'shadow-md');
});
card.addEventListener('mouseleave', function() {
this.classList.remove('transform', 'translate-y-[-5px]', 'shadow-md');
});
});
});
</script>
+778
View File
@@ -0,0 +1,778 @@
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
<i class="fas fa-dice text-primary mr-3"></i>
骰子游戏期号管理
</h1>
<!-- 游戏期号管理区域 -->
<?php if (!empty($gamesList) && is_array($gamesList)): ?>
<?php foreach ($gamesList as $game): ?>
<?php
$gameId = $game['id'];
$gameName = $game['name'];
$currentPeriod = isset($currentPeriods[$gameId]) ? $currentPeriods[$gameId] : null;
?>
<!-- 单个游戏的期号卡片 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-6 border-l-4 border-primary">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-800 flex items-center">
<i class="fas fa-gamepad text-primary mr-2"></i>
<?= htmlspecialchars($gameName) ?>
</h2>
<?php if ($currentPeriod): ?>
<!-- 有当前期号 -->
<div class="flex gap-2">
<?php if ($currentPeriod['status'] === 'pending'): ?>
<button
type="button"
class="period-lock-btn inline-block bg-warning hover:bg-warning/90 text-white px-4 py-2 rounded-lg text-sm"
data-id="<?= $currentPeriod['id'] ?>"
>
<i class="fas fa-lock mr-2"></i>封盘
</button>
<?php endif; ?>
<?php if ($currentPeriod['status'] === 'locked'): ?>
<button
type="button"
class="period-draw-btn inline-block bg-success hover:bg-success/90 text-white px-4 py-2 rounded-lg text-sm"
data-id="<?= $currentPeriod['id'] ?>"
>
<i class="fas fa-dice mr-2"></i>开奖
</button>
<?php endif; ?>
<?php if ($currentPeriod['status'] === 'drawn'): ?>
<button
type="button"
class="period-settle-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
data-id="<?= $currentPeriod['id'] ?>"
>
<i class="fas fa-coins mr-2"></i>结算
</button>
<?php endif; ?>
<?php if ($currentPeriod['status'] === 'settled'): ?>
<button
type="button"
class="period-start-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
data-game-id="<?= $gameId ?>"
>
<i class="fas fa-play mr-2"></i>开始新一期
</button>
<?php endif; ?>
</div>
<?php else: ?>
<button
type="button"
class="period-start-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
data-game-id="<?= $gameId ?>"
>
<i class="fas fa-play mr-2"></i>开始新一期
</button>
<?php endif; ?>
</div>
<?php if ($currentPeriod): ?>
<!-- 当前期号信息 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<p class="text-sm text-gray-500 mb-1">期号</p>
<p class="text-lg font-bold text-gray-900"><?= htmlspecialchars($currentPeriod['period_number']) ?></p>
</div>
<div>
<p class="text-sm text-gray-500 mb-1">状态</p>
<?php
$status = $currentPeriod['status'];
$statusMap = [
'pending' => ['text' => '待开奖', 'class' => 'bg-warning/10 text-warning'],
'locked' => ['text' => '已封盘', 'class' => 'bg-danger/10 text-danger'],
'drawn' => ['text' => '已开奖', 'class' => 'bg-primary/10 text-primary'],
'settled' => ['text' => '已结算', 'class' => 'bg-success/10 text-success']
];
$statusInfo = $statusMap[$status] ?? $statusMap['pending'];
?>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm <?= $statusInfo['class'] ?>">
<?= $statusInfo['text'] ?>
</span>
</div>
<div>
<p class="text-sm text-gray-500 mb-1">开始时间</p>
<p class="text-sm text-gray-900"><?= htmlspecialchars($currentPeriod['start_time'] ?? '-') ?></p>
</div>
<div>
<p class="text-sm text-gray-500 mb-1">开奖结果</p>
<?php if (!empty($currentPeriod['dice1']) && !empty($currentPeriod['dice2']) && !empty($currentPeriod['dice3'])): ?>
<p class="text-lg font-bold text-gray-900">
<?= $currentPeriod['dice1'] ?>.<?= $currentPeriod['dice2'] ?>.<?= $currentPeriod['dice3'] ?>
<span class="text-sm text-gray-500">(<?= $currentPeriod['total'] ?>)</span>
</p>
<?php else: ?>
<p class="text-sm text-gray-400">未开奖</p>
<?php endif; ?>
</div>
</div>
<?php else: ?>
<div class="text-center py-4">
<p class="text-sm text-gray-500">暂无进行中的期号,点击"开始新一期"按钮启动</p>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
<!-- 统计卡片 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">期号总数</p>
<h3 class="text-2xl font-bold mt-1">
<?= isset($periods) && is_array($periods) ? count($periods) : 0 ?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<i class="fas fa-list text-primary"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">待开奖</p>
<h3 class="text-2xl font-bold mt-1 text-warning">
<?php
$pendingCount = 0;
if (isset($periods) && is_array($periods)) {
foreach ($periods as $p) {
if (isset($p['status']) && $p['status'] === 'pending') {
$pendingCount++;
}
}
}
echo $pendingCount;
?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-warning/10 flex items-center justify-center">
<i class="fas fa-clock text-warning"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">已开奖</p>
<h3 class="text-2xl font-bold mt-1 text-success">
<?php
$drawnCount = 0;
if (isset($periods) && is_array($periods)) {
foreach ($periods as $p) {
if (isset($p['status']) && ($p['status'] === 'drawn' || $p['status'] === 'settled')) {
$drawnCount++;
}
}
}
echo $drawnCount;
?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
<i class="fas fa-check-circle text-success"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">已结算</p>
<h3 class="text-2xl font-bold mt-1 text-primary">
<?php
$settledCount = 0;
if (isset($periods) && is_array($periods)) {
foreach ($periods as $p) {
if (isset($p['status']) && $p['status'] === 'settled') {
$settledCount++;
}
}
}
echo $settledCount;
?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<i class="fas fa-coins text-primary"></i>
</div>
</div>
</div>
</div>
<!-- 期号列表 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
<div>
<h2 class="text-xl font-semibold text-gray-800">期号列表</h2>
<p class="text-sm text-gray-500 mt-1">管理骰子游戏期号和开奖结果</p>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full bg-white rounded-xl overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">期号</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">关联游戏</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">开奖结果</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">创建时间</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200" id="periodList">
<?php if (!empty($periods) && is_array($periods)): ?>
<?php foreach ($periods as $period): ?>
<tr class="hover:bg-gray-50 transition-colors" data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>">
<td class="px-4 py-4">
<div class="text-sm font-semibold text-gray-900">
<?= htmlspecialchars((string)($period['period_number'] ?? '')) ?>
</div>
<?php if (!empty($period['auto_generated'])): ?>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] bg-gray-100 text-gray-600 mt-1">
自动生成
</span>
<?php endif; ?>
</td>
<td class="px-4 py-4">
<?php
$gameId = $period['game_id'] ?? null;
$gameName = $gameId && isset($games[$gameId]) ? $games[$gameId] : '未关联';
?>
<span class="text-sm text-gray-600"><?= htmlspecialchars($gameName) ?></span>
</td>
<td class="px-4 py-4">
<?php
$status = $period['status'] ?? 'pending';
$statusMap = [
'pending' => ['text' => '待开奖', 'class' => 'bg-warning/10 text-warning'],
'locked' => ['text' => '已封盘', 'class' => 'bg-danger/10 text-danger'],
'drawn' => ['text' => '已开奖', 'class' => 'bg-primary/10 text-primary'],
'settled' => ['text' => '已结算', 'class' => 'bg-success/10 text-success']
];
$statusInfo = $statusMap[$status] ?? $statusMap['pending'];
?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $statusInfo['class'] ?>">
<span class="w-2 h-2 rounded-full mr-1 <?= str_replace('/10', '', $statusInfo['class']) ?>"></span>
<?= $statusInfo['text'] ?>
</span>
</td>
<td class="px-4 py-4">
<?php if (!empty($period['dice1']) && !empty($period['dice2']) && !empty($period['dice3'])): ?>
<div class="text-sm text-gray-900">
<span class="font-semibold">
<?= htmlspecialchars((string)$period['dice1']) ?>.
<?= htmlspecialchars((string)$period['dice2']) ?>.
<?= htmlspecialchars((string)$period['dice3']) ?>
</span>
<span class="text-gray-500 ml-1">
(<?= htmlspecialchars((string)($period['total'] ?? '')) ?>)
</span>
<?php if (!empty($period['result'])): ?>
<span class="ml-1 px-2 py-0.5 rounded text-[11px] <?= $period['result'] === 'Tài' ? 'bg-red-100 text-red-700' : 'bg-blue-100 text-blue-700' ?>">
<?= htmlspecialchars((string)$period['result']) ?>
</span>
<?php endif; ?>
</div>
<?php else: ?>
<span class="text-sm text-gray-400">未开奖</span>
<?php endif; ?>
</td>
<td class="px-4 py-4 hidden md:table-cell">
<?php if (!empty($period['created_at'])): ?>
<div class="text-xs text-gray-500">
<?= date('Y-m-d H:i', strtotime((string)$period['created_at'])) ?>
</div>
<?php else: ?>
<span class="text-xs text-gray-400">时间未知</span>
<?php endif; ?>
</td>
<td class="px-4 py-4 text-right text-sm font-medium">
<div class="flex items-center justify-end gap-2">
<?php if (($period['status'] ?? '') === 'pending'): ?>
<button
type="button"
class="period-lock-btn text-gray-500 hover:text-warning"
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
title="封盘"
>
<i class="fas fa-lock"></i>
</button>
<?php endif; ?>
<?php if (($period['status'] ?? '') === 'locked' || ($period['status'] ?? '') === 'pending'): ?>
<button
type="button"
class="period-draw-btn text-gray-500 hover:text-primary"
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
title="录入开奖"
>
<i class="fas fa-dice"></i>
</button>
<?php endif; ?>
<?php if (($period['status'] ?? '') === 'drawn'): ?>
<button
type="button"
class="period-draw-btn text-gray-500 hover:text-warning"
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
title="修改结果"
>
<i class="fas fa-edit"></i>
</button>
<button
type="button"
class="period-settle-btn text-gray-500 hover:text-success"
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
title="确认结算"
>
<i class="fas fa-check-circle"></i>
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="6" class="px-6 py-12 text-center">
<div class="flex flex-col items-center">
<i class="fas fa-dice text-gray-300 text-5xl mb-4"></i>
<h3 class="text-lg font-medium text-gray-900">暂无期号</h3>
<p class="mt-1 text-gray-500 text-sm">
当前还没有任何骰子游戏期号。
</p>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- 录入开奖结果模态框 -->
<div
id="drawPeriodBackdrop"
class="fixed inset-0 bg-black/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 z-40"
></div>
<div
id="drawPeriodModal"
class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-none transition-all duration-300 scale-95"
>
<div class="bg-white rounded-xl shadow-xl w-full max-w-md max-h-[90vh] overflow-hidden">
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
<h3 class="text-xl font-bold text-gray-800 flex items-center">
<i class="fas fa-dice text-primary mr-2"></i>
录入开奖结果
</h3>
<button id="closeDrawPeriodBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
<i class="fas fa-times"></i>
</button>
</div>
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-130px)]">
<form id="drawPeriodForm" class="space-y-4">
<input type="hidden" id="drawPeriodId" name="id">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
期号
</label>
<p id="drawPeriodNumber" class="text-lg font-bold text-gray-900"></p>
</div>
<!-- 骰子游戏输入 -->
<div id="diceInputSection" class="grid grid-cols-3 gap-4">
<div>
<label for="drawDice1" class="block text-sm font-medium text-gray-700 mb-1">
骰子1 <span class="text-red-500">*</span>
</label>
<input
type="number"
id="drawDice1"
name="dice1"
min="1"
max="6"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm text-center text-lg font-bold"
placeholder="1-6"
>
</div>
<div>
<label for="drawDice2" class="block text-sm font-medium text-gray-700 mb-1">
骰子2 <span class="text-red-500">*</span>
</label>
<input
type="number"
id="drawDice2"
name="dice2"
min="1"
max="6"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm text-center text-lg font-bold"
placeholder="1-6"
>
</div>
<div>
<label for="drawDice3" class="block text-sm font-medium text-gray-700 mb-1">
骰子3 <span class="text-red-500">*</span>
</label>
<input
type="number"
id="drawDice3"
name="dice3"
min="1"
max="6"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm text-center text-lg font-bold"
placeholder="1-6"
>
</div>
</div>
<div id="drawResultPreview" class="hidden p-4 bg-gray-50 rounded-lg">
<p class="text-sm text-gray-600 mb-1">开奖结果预览:</p>
<p class="text-lg font-bold">
<span id="drawResultText"></span>
<span id="drawResultTotal" class="ml-2 text-gray-500"></span>
<span id="drawResultType" class="ml-2"></span>
</p>
</div>
<div class="pt-4 border-t border-gray-100">
<button
type="button"
id="submitDrawPeriodBtn"
class="w-full bg-primary hover:bg-primary/90 text-white px-4 py-2.5 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center justify-center"
>
<i class="fas fa-check mr-2"></i>
确认录入
</button>
</div>
</form>
</div>
</div>
</div>
<script src="/Static/js/admin.js"></script>
<script>
layui.use(['layer'], function() {
var layer = layui.layer;
// 获取元素
const closeDrawPeriodBtn = document.getElementById('closeDrawPeriodBtn');
const drawPeriodBackdrop = document.getElementById('drawPeriodBackdrop');
const drawPeriodModal = document.getElementById('drawPeriodModal');
const drawPeriodForm = document.getElementById('drawPeriodForm');
const submitDrawPeriodBtn = document.getElementById('submitDrawPeriodBtn');
const drawDice1 = document.getElementById('drawDice1');
const drawDice2 = document.getElementById('drawDice2');
const drawDice3 = document.getElementById('drawDice3');
const drawResultPreview = document.getElementById('drawResultPreview');
const drawResultText = document.getElementById('drawResultText');
const drawResultTotal = document.getElementById('drawResultTotal');
const drawResultType = document.getElementById('drawResultType');
// 打开录入开奖模态框
function openDrawPeriodModal(periodId) {
fetch(`/admin/dice-periods/${periodId}`)
.then(res => res.json())
.then(data => {
if (data.success) {
const period = data.data;
document.getElementById('drawPeriodId').value = period.id;
document.getElementById('drawPeriodNumber').textContent = period.period_number;
// 填充已有的骰子数据
drawDice1.value = period.dice1 || '';
drawDice2.value = period.dice2 || '';
drawDice3.value = period.dice3 || '';
updateDrawPreview();
drawPeriodBackdrop.classList.remove('opacity-0', 'pointer-events-none');
drawPeriodModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
drawPeriodModal.classList.add('scale-100');
} else {
layer.msg(data.message || '获取期号信息失败', {icon: 2});
}
})
.catch(e => {
layer.msg('获取期号信息失败:' + e.message, {icon: 2});
});
}
// 关闭录入开奖模态框
function closeDrawPeriodModal() {
drawPeriodBackdrop.classList.add('opacity-0', 'pointer-events-none');
drawPeriodModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
drawPeriodModal.classList.remove('scale-100');
drawPeriodForm.reset();
drawResultPreview.classList.add('hidden');
}
// 更新开奖结果预览
function updateDrawPreview() {
const d1 = parseInt(drawDice1.value) || 0;
const d2 = parseInt(drawDice2.value) || 0;
const d3 = parseInt(drawDice3.value) || 0;
if (d1 >= 1 && d1 <= 6 && d2 >= 1 && d2 <= 6 && d3 >= 1 && d3 <= 6) {
const total = d1 + d2 + d3;
let result = '';
let resultClass = '';
// 检查是否为爆子
if (d1 === d2 && d2 === d3) {
if (d1 <= 3) {
result = 'Xỉu';
resultClass = 'bg-blue-100 text-blue-700 px-2 py-1 rounded text-sm';
} else {
result = 'Tài';
resultClass = 'bg-red-100 text-red-700 px-2 py-1 rounded text-sm';
}
} else if (total >= 4 && total <= 10) {
result = 'Xỉu';
resultClass = 'bg-blue-100 text-blue-700 px-2 py-1 rounded text-sm';
} else {
result = 'Tài';
resultClass = 'bg-red-100 text-red-700 px-2 py-1 rounded text-sm';
}
drawResultText.textContent = `${d1}.${d2}.${d3}`;
drawResultTotal.textContent = `(总和: ${total})`;
drawResultType.innerHTML = `<span class="${resultClass}">${result}</span>`;
drawResultPreview.classList.remove('hidden');
} else {
drawResultPreview.classList.add('hidden');
}
}
// 提交录入开奖
async function submitDrawPeriod() {
const dice1 = parseInt(drawDice1.value);
const dice2 = parseInt(drawDice2.value);
const dice3 = parseInt(drawDice3.value);
if (!dice1 || !dice2 || !dice3 || dice1 < 1 || dice1 > 6 || dice2 < 1 || dice2 > 6 || dice3 < 1 || dice3 > 6) {
layer.msg('请输入有效的骰子点数(1-6', {icon: 2});
return;
}
const data = {
id: document.getElementById('drawPeriodId').value,
auto: false,
dice1: dice1,
dice2: dice2,
dice3: dice3
};
submitDrawPeriodBtn.disabled = true;
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>录入中...';
try {
const response = await fetch('/admin/dice-periods/draw', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
layer.msg(result.message || '开奖结果录入成功', {icon: 1}, function() {
location.reload();
});
} else {
layer.msg(result.message || '录入失败', {icon: 2});
submitDrawPeriodBtn.disabled = false;
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-check mr-2"></i>确认录入';
}
} catch (e) {
layer.msg('录入失败:' + e.message, {icon: 2});
submitDrawPeriodBtn.disabled = false;
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-check mr-2"></i>确认录入';
}
}
// 封盘
async function lockPeriod(id) {
layer.confirm('确定要封盘吗?封盘后将无法继续投注。', {icon: 3, title: '确认封盘'}, async function(index) {
layer.close(index);
try {
const response = await fetch('/admin/dice-periods/lock', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({id: id})
});
const result = await response.json();
if (result.success) {
layer.msg(result.message || '封盘成功', {icon: 1}, function() {
location.reload();
});
} else {
layer.msg(result.message || '封盘失败', {icon: 2});
}
} catch (e) {
layer.msg('封盘失败:' + e.message, {icon: 2});
}
});
}
// 确认结算
async function settlePeriod(id) {
layer.confirm('确定要确认结算吗?此操作不可撤销。', {icon: 3, title: '确认结算'}, async function(index) {
layer.close(index);
try {
const response = await fetch('/admin/dice-periods/settle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({id: id})
});
const result = await response.json();
if (result.success) {
layer.msg(result.message || '结算成功', {icon: 1}, function() {
location.reload();
});
} else {
layer.msg(result.message || '结算失败', {icon: 2});
}
} catch (e) {
layer.msg('结算失败:' + e.message, {icon: 2});
}
});
}
// 开始下注
async function startPeriod(event) {
const gameId = event.currentTarget.getAttribute('data-game-id');
if (!gameId) {
layer.msg('游戏ID缺失', {icon: 2});
return;
}
layer.confirm('确定要开始新一期下注吗?', {icon: 3, title: '开始下注'}, async function(index) {
layer.close(index);
try {
const response = await fetch('/admin/dice-periods/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({game_id: parseInt(gameId)})
});
const result = await response.json();
if (result.success) {
layer.msg(result.message || '启动成功', {icon: 1}, function() {
location.reload();
});
} else {
layer.msg(result.message || '启动失败', {icon: 2});
}
} catch (e) {
layer.msg('启动失败:' + e.message, {icon: 2});
}
});
}
// 绑定事件
if (closeDrawPeriodBtn) {
closeDrawPeriodBtn.addEventListener('click', closeDrawPeriodModal);
}
if (drawPeriodBackdrop) {
drawPeriodBackdrop.addEventListener('click', closeDrawPeriodModal);
}
if (submitDrawPeriodBtn) {
submitDrawPeriodBtn.addEventListener('click', submitDrawPeriod);
}
// 骰子输入监听
if (drawDice1 && drawDice2 && drawDice3) {
[drawDice1, drawDice2, drawDice3].forEach(input => {
input.addEventListener('input', updateDrawPreview);
});
}
// 事件委托:列表操作按钮
const periodList = document.getElementById('periodList');
if (periodList) {
periodList.addEventListener('click', function(e) {
const lockBtn = e.target.closest('.period-lock-btn');
const drawBtn = e.target.closest('.period-draw-btn');
const settleBtn = e.target.closest('.period-settle-btn');
if (lockBtn) {
const id = lockBtn.getAttribute('data-id');
if (id) lockPeriod(id);
}
if (drawBtn) {
const id = drawBtn.getAttribute('data-id');
if (id) openDrawPeriodModal(id);
}
if (settleBtn) {
const id = settleBtn.getAttribute('data-id');
if (id) settlePeriod(id);
}
});
}
// 当前期号操作按钮
document.querySelectorAll('.period-lock-btn').forEach(btn => {
if (!btn.closest('#periodList')) {
btn.addEventListener('click', function() {
const id = this.getAttribute('data-id');
if (id) lockPeriod(id);
});
}
});
document.querySelectorAll('.period-draw-btn').forEach(btn => {
if (!btn.closest('#periodList')) {
btn.addEventListener('click', function() {
const id = this.getAttribute('data-id');
if (id) openDrawPeriodModal(id);
});
}
});
document.querySelectorAll('.period-settle-btn').forEach(btn => {
if (!btn.closest('#periodList')) {
btn.addEventListener('click', function() {
const id = this.getAttribute('data-id');
if (id) settlePeriod(id);
});
}
});
// Start Button
document.querySelectorAll('.period-start-btn').forEach(btn => {
btn.addEventListener('click', startPeriod);
});
});
</script>
+69
View File
@@ -0,0 +1,69 @@
<?php $empCode=$_SESSION['emp_code']??''; $perms=$_SESSION['emp_permissions']??[]; ?>
<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>员工面板 - <?=$empCode?></title><script src="https://cdn.tailwindcss.com"></script>
<style>body{background:#111827;color:#fff;font-family:system-ui}</style>
</head>
<body class="min-h-screen">
<header class="bg-gray-900 border-b border-gray-700 px-4 py-3 flex justify-between items-center">
<span class="font-bold">👷 <?=$empCode?></span>
<span class="text-xs text-gray-400">权限:<?php $pm=['deposit'=>'充值','withdraw'=>'提现']; echo implode(', ', array_map(function($p) use($pm){return $pm[$p]??$p;}, $perms)); ?></span>
<a href="/logout" class="text-red-400 text-sm">退出</a>
</header>
<div class="max-w-4xl mx-auto p-4 space-y-4">
<!-- 搜索用户 -->
<div class="bg-gray-800 rounded-xl p-4">
<input type="text" id="searchUser" placeholder="搜索用户名..." oninput="filterUsers()" class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded text-white text-sm focus:border-blue-400 focus:outline-none">
</div>
<!-- 用户列表 -->
<div class="bg-gray-800 rounded-xl p-4">
<h3 class="text-sm font-bold mb-3">用户列表</h3>
<div class="space-y-2 max-h-96 overflow-y-auto" id="userList">
<?php foreach($users??[] as $u): ?>
<div class="user-row flex items-center justify-between py-2 border-b border-gray-700 text-sm" data-name="<?=strtolower($u['username'])?>">
<div>
<span class="text-white"><?=htmlspecialchars($u['username'])?></span>
<span class="text-gray-400 ml-2">余额: <span class="text-yellow-400" id="bal_<?=$u['id']?>"><?=number_format($u['balance'],2)?></span></span>
</div>
<div class="flex gap-2">
<?php if(in_array('deposit',$perms)): ?>
<button onclick="doAdjust(<?=$u['id']?>,'deposit','<?=htmlspecialchars($u['username'])?>')" class="px-3 py-1 bg-green-600 rounded text-xs hover:bg-green-700">+ 充值</button>
<?php endif; ?>
<?php if(in_array('withdraw',$perms)): ?>
<button onclick="doAdjust(<?=$u['id']?>,'withdraw','<?=htmlspecialchars($u['username'])?>')" class="px-3 py-1 bg-red-600 rounded text-xs hover:bg-red-700">- 提现</button>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- 操作日志 -->
<div class="bg-gray-800 rounded-xl p-4">
<h3 class="text-sm font-bold mb-3">操作日志</h3>
<div class="space-y-1 max-h-48 overflow-y-auto text-xs">
<?php foreach($logs??[] as $log): ?>
<div class="flex justify-between py-1 border-b border-gray-700">
<span class="<?=$log['action']==='deposit'?'text-green-400':'text-red-400'?>"><?=$log['action']==='deposit'?'充值':'提现'?> → 用户#<?=$log['target_user_id']?></span>
<span class="text-white"><?=number_format($log['amount'],2)?></span>
<span class="text-gray-500"><?=$log['created_at']?></span>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<script>
function filterUsers(){const q=document.getElementById('searchUser').value.toLowerCase();document.querySelectorAll('.user-row').forEach(r=>r.style.display=r.dataset.name.includes(q)?'':'none');}
async function doAdjust(uid,action,name){
const label=action==='deposit'?'充值':'提现';
const amount=prompt(label+'金额(用户:'+name+'):');
if(!amount||isNaN(amount)||parseFloat(amount)<=0)return;
const remark=prompt('备注(可选):')||'';
const r=await fetch('/employee/adjust-balance',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({user_id:uid,action,amount:parseFloat(amount),remark})});
const d=await r.json();
if(d.success){alert('操作成功!');document.getElementById('bal_'+uid).textContent=parseFloat(d.new_balance).toFixed(2);}else alert(d.message);
}
</script>
</body></html>
+21
View File
@@ -0,0 +1,21 @@
<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>员工登录</title><script src="https://cdn.tailwindcss.com"></script>
<style>body{background:linear-gradient(135deg,#1a1a2e,#16213e);min-height:100vh;font-family:system-ui}</style>
</head>
<body class="flex items-center justify-center min-h-screen p-4">
<div class="w-full max-w-sm bg-white/10 backdrop-blur rounded-2xl p-8 border border-white/10">
<h1 class="text-center text-xl font-bold text-white mb-6">👷 员工登录</h1>
<form onsubmit="return doLogin(event)">
<div class="space-y-4">
<input type="text" id="user" placeholder="用户名" required class="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/30 focus:border-blue-400 focus:outline-none">
<input type="password" id="pass" placeholder="密码" required class="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/30 focus:border-blue-400 focus:outline-none">
<button type="submit" class="w-full py-3 bg-blue-600 text-white font-bold rounded-lg hover:bg-blue-700">登录</button>
</div>
</form>
<div id="msg" class="mt-3 text-center text-sm text-red-400 hidden"></div>
</div>
<script>
async function doLogin(e){e.preventDefault();
const r=await fetch('/employee/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:document.getElementById('user').value,password:document.getElementById('pass').value})});
const d=await r.json();if(d.success)window.location.href=d.redirect;else{const m=document.getElementById('msg');m.textContent=d.message;m.classList.remove('hidden');}return false;}
</script></body></html>
+56
View File
@@ -0,0 +1,56 @@
<div class="p-6">
<h2 class="text-2xl font-bold mb-4">👷 员工管理</h2>
<button onclick="showAdd()" class="mb-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm">+ 添加员工</button>
<div class="bg-white rounded-xl shadow overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50"><tr><th class="px-4 py-3 text-left">编号</th><th class="px-4 py-3">用户名</th><th class="px-4 py-3">姓名</th><th class="px-4 py-3">班次</th><th class="px-4 py-3">权限</th><th class="px-4 py-3">状态</th><th class="px-4 py-3">最后登录</th><th class="px-4 py-3">操作</th></tr></thead>
<tbody>
<?php foreach($employees??[] as $e): $perms=json_decode($e['permissions']??'[]',true); ?>
<tr class="border-t hover:bg-gray-50">
<td class="px-4 py-2 font-mono font-bold"><?=$e['emp_code']?></td>
<td class="px-4 py-2"><?=htmlspecialchars($e['username'])?></td>
<td class="px-4 py-2"><?=htmlspecialchars($e['real_name']??'')?></td>
<td class="px-4 py-2 text-center"><span class="px-2 py-1 rounded text-xs <?=$e['shift']==='day'?'bg-yellow-100 text-yellow-700':($e['shift']==='night'?'bg-indigo-100 text-indigo-700':'bg-gray-100')?>"><?=$e['shift']==='day'?'白班':($e['shift']==='night'?'夜班':'全天')?></span></td>
<td class="px-4 py-2 text-xs"><?php $permMap=['deposit'=>'充值','withdraw'=>'提现']; echo implode(', ', array_map(function($p) use($permMap){return $permMap[$p]??$p;}, $perms)); ?></td>
<td class="px-4 py-2 text-center"><?=$e['status']?'<span class="text-green-500">✓ 启用</span>':'<span class="text-red-500">✗ 禁用</span>'?></td>
<td class="px-4 py-2 text-xs text-gray-400"><?=$e['last_login']??'-'?></td>
<td class="px-4 py-2 text-center">
<button onclick='editEmp(<?=json_encode($e)?>)' class="text-blue-500 text-xs">编辑</button>
<button onclick="delEmp(<?=$e['id']?>)" class="text-red-500 text-xs ml-1">删除</button>
</td>
</tr>
<?php endforeach; ?>
</tbody></table></div>
<div id="empModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-xl p-6 w-full max-w-md">
<h3 class="font-bold mb-4" id="empTitle">添加员工</h3>
<input type="hidden" id="empId" value="0">
<div class="space-y-3">
<div class="grid grid-cols-2 gap-3">
<div><label class="text-xs text-gray-400">编号 (如 001)</label><input id="empCode" class="w-full border rounded px-3 py-2"></div>
<div><label class="text-xs text-gray-400">用户名</label><input id="empUser" class="w-full border rounded px-3 py-2"></div>
</div>
<div><label class="text-xs text-gray-400">密码 (留空则不修改)</label><input type="password" id="empPass" class="w-full border rounded px-3 py-2"></div>
<div><label class="text-xs text-gray-400">真实姓名</label><input id="empName" class="w-full border rounded px-3 py-2"></div>
<div class="grid grid-cols-2 gap-3">
<div><label class="text-xs text-gray-400">班次</label><select id="empShift" class="w-full border rounded px-3 py-2"><option value="all">全天</option><option value="day">白班 (8-20)</option><option value="night">夜班 (20-8)</option></select></div>
<div><label class="text-xs text-gray-400">状态</label><select id="empStatus" class="w-full border rounded px-3 py-2"><option value="1">启用</option><option value="0">禁用</option></select></div>
</div>
<div><label class="text-xs text-gray-400">权限</label>
<label class="flex items-center gap-2 mt-1"><input type="checkbox" id="permDeposit" checked> 充值</label>
<label class="flex items-center gap-2"><input type="checkbox" id="permWithdraw" checked> 提现</label>
</div>
</div>
<div class="flex gap-2 mt-4">
<button onclick="saveEmp()" class="flex-1 py-2 bg-blue-500 text-white rounded">保存</button>
<button onclick="document.getElementById('empModal').classList.add('hidden')" class="flex-1 py-2 bg-gray-200 rounded">取消</button>
</div>
</div></div>
</div>
<script>
function showAdd(){document.getElementById('empId').value=0;document.getElementById('empTitle').textContent='添加员工';['empCode','empUser','empPass','empName'].forEach(i=>document.getElementById(i).value='');document.getElementById('empModal').classList.remove('hidden');}
function editEmp(e){document.getElementById('empId').value=e.id;document.getElementById('empCode').value=e.emp_code;document.getElementById('empUser').value=e.username;document.getElementById('empName').value=e.real_name||'';document.getElementById('empShift').value=e.shift;document.getElementById('empStatus').value=e.status;const p=JSON.parse(e.permissions||'[]');document.getElementById('permDeposit').checked=p.includes('deposit');document.getElementById('permWithdraw').checked=p.includes('withdraw');document.getElementById('empTitle').textContent='编辑员工';document.getElementById('empModal').classList.remove('hidden');}
async function saveEmp(){const perms=[];if(document.getElementById('permDeposit').checked)perms.push('deposit');if(document.getElementById('permWithdraw').checked)perms.push('withdraw');const b={id:parseInt(document.getElementById('empId').value),emp_code:document.getElementById('empCode').value,username:document.getElementById('empUser').value,password:document.getElementById('empPass').value,real_name:document.getElementById('empName').value,shift:document.getElementById('empShift').value,status:parseInt(document.getElementById('empStatus').value),permissions:perms};const r=await fetch('/admin/employees/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});const d=await r.json();if(d.status==='success')location.reload();else alert(d.message);}
async function delEmp(id){if(!confirm('确定删除该员工?'))return;await fetch('/admin/employees/delete/'+id,{method:'POST'});location.reload();}
</script>
+200
View File
@@ -0,0 +1,200 @@
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
<i class="fas fa-yen-sign text-primary mr-3"></i>
财务管理
</h1>
<!-- 统计卡片 -->
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">总充值</p>
<h3 class="text-xl font-bold mt-1 text-success">
<?= number_format((float)($stats['total_deposit'] ?? 0), 2) ?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
<i class="fas fa-arrow-down text-success"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">总提现</p>
<h3 class="text-xl font-bold mt-1 text-danger">
<?= number_format((float)($stats['total_withdraw'] ?? 0), 2) ?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-danger/10 flex items-center justify-center">
<i class="fas fa-arrow-up text-danger"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">总投注</p>
<h3 class="text-xl font-bold mt-1 text-warning">
<?= number_format((float)($stats['total_bet'] ?? 0), 2) ?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-warning/10 flex items-center justify-center">
<i class="fas fa-coins text-warning"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">总中奖</p>
<h3 class="text-xl font-bold mt-1 text-primary">
<?= number_format((float)($stats['total_win'] ?? 0), 2) ?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<i class="fas fa-trophy text-primary"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">今日充值</p>
<h3 class="text-xl font-bold mt-1 text-success">
<?= number_format((float)($stats['today_deposit'] ?? 0), 2) ?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
<i class="fas fa-calendar-day text-success"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">今日提现</p>
<h3 class="text-xl font-bold mt-1 text-danger">
<?= number_format((float)($stats['today_withdraw'] ?? 0), 2) ?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-danger/10 flex items-center justify-center">
<i class="fas fa-calendar-day text-danger"></i>
</div>
</div>
</div>
</div>
<!-- 资金流水列表 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
<div>
<h2 class="text-xl font-semibold text-gray-800">资金流水</h2>
<p class="text-sm text-gray-500 mt-1">查看所有资金变动记录</p>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full bg-white rounded-xl overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">流水ID</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用户</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">金额</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">余额变动</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">描述</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden lg:table-cell">时间</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200" id="transactionList">
<?php if (!empty($transactions) && is_array($transactions)): ?>
<?php foreach ($transactions as $tx): ?>
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-4 py-4">
<span class="text-sm font-medium text-gray-900">#<?= htmlspecialchars((string)($tx['id'] ?? '')) ?></span>
</td>
<td class="px-4 py-4">
<div class="text-sm text-gray-900">
<?= htmlspecialchars((string)($tx['username'] ?? '未知')) ?>
</div>
<div class="text-xs text-gray-500">
ID: <?= htmlspecialchars((string)($tx['user_id'] ?? '')) ?>
</div>
</td>
<td class="px-4 py-4">
<?php
$type = $tx['type'] ?? '';
$typeMap = [
'deposit' => ['text' => '充值', 'class' => 'bg-success/10 text-success'],
'withdraw' => ['text' => '提现', 'class' => 'bg-danger/10 text-danger'],
'bet' => ['text' => '投注', 'class' => 'bg-warning/10 text-warning'],
'win' => ['text' => '中奖', 'class' => 'bg-primary/10 text-primary'],
'refund' => ['text' => '退款', 'class' => 'bg-gray-100 text-gray-700'],
'manual_deposit' => ['text' => '人工加款', 'class' => 'bg-purple-100 text-purple-700'],
'manual_withdraw' => ['text' => '人工扣款', 'class' => 'bg-orange-100 text-orange-700']
];
$typeInfo = $typeMap[$type] ?? ['text' => $type, 'class' => 'bg-gray-100 text-gray-700'];
?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $typeInfo['class'] ?>">
<?= $typeInfo['text'] ?>
</span>
</td>
<td class="px-4 py-4">
<?php
$amount = floatval($tx['amount'] ?? 0);
$amountClass = $amount >= 0 ? 'text-success' : 'text-danger';
?>
<span class="text-sm font-semibold <?= $amountClass ?>">
<?= $amount >= 0 ? '+' : '' ?><?= number_format($amount, 2) ?>
</span>
</td>
<td class="px-4 py-4">
<div class="text-sm text-gray-900">
<span class="text-gray-500"><?= number_format(floatval($tx['balance_before'] ?? 0), 2) ?></span>
<i class="fas fa-arrow-right mx-1 text-gray-400"></i>
<span class="font-semibold"><?= number_format(floatval($tx['balance_after'] ?? 0), 2) ?></span>
</div>
</td>
<td class="px-4 py-4 hidden md:table-cell">
<span class="text-sm text-gray-600">
<?= htmlspecialchars((string)($tx['description'] ?? '-')) ?>
</span>
</td>
<td class="px-4 py-4 hidden lg:table-cell">
<?php if (!empty($tx['created_at'])): ?>
<div class="text-xs text-gray-500">
<?= date('Y-m-d H:i:s', strtotime((string)$tx['created_at'])) ?>
</div>
<?php else: ?>
<span class="text-xs text-gray-400">-</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="7" class="px-6 py-12 text-center">
<div class="flex flex-col items-center">
<i class="fas fa-money-bill-wave text-gray-300 text-5xl mb-4"></i>
<h3 class="text-lg font-medium text-gray-900">暂无流水记录</h3>
<p class="mt-1 text-gray-500 text-sm">
还没有任何资金流水记录。
</p>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<script src="/Static/js/admin.js"></script>
<script>
layui.use(['layer'], function() {
var layer = layui.layer;
// 此处可以添加其他财务相关的JS逻辑
});
</script>
+1120
View File
File diff suppressed because it is too large Load Diff
+690
View File
@@ -0,0 +1,690 @@
<!-- 页面标题 -->
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
<i class="fas fa-image text-primary mr-3"></i>图片图库管理
</h1>
<!-- 主内容区 -->
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<!-- 标签页导航 -->
<div class="border-b border-gray-200">
<div class="flex">
<button id="gallery-tab-gallery" class="px-6 py-4 font-medium text-primary border-b-2 border-primary" data-tab="gallery"> <i class="fa fa-th-large mr-2"></i>图库 </button>
<button id="gallery-upload-modal-trigger" class="px-6 py-4 font-medium text-gray-500 hover:text-gray-700"> <i class="fa fa-upload mr-2"></i>上传 </button>
</div>
</div>
<!-- 图库内容 -->
<div id="gallery-content-gallery" class="p-6" data-tab="gallery">
<!-- 批量删除按钮 -->
<button id="gallery-batch-delete" class="mb-6 px-4 py-2 bg-red-500 text-white rounded-md flex items-center opacity-50 cursor-not-allowed" disabled> <i class="fa fa-trash mr-2"></i> 删除选中的图片 </button>
<!-- 图片网格 -->
<div id="gallery-media-grid" class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 mb-6"></div>
<!-- 加载更多按钮 -->
<button id="gallery-load-more" class="w-full py-2 px-4 bg-gray-100 text-gray-800 rounded-md flex items-center justify-center hover:bg-gray-200 transition-colors"> <i class="fa fa-refresh mr-2"></i> 加载更多 </button>
</div>
</div>
<!-- 上传弹出窗口 -->
<div id="gallery-upload-modal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[90vh] overflow-y-auto">
<div class="p-6 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-xl font-bold text-gray-800">上传图片</h2>
<button id="gallery-close-upload-modal" class="text-gray-500 hover:text-gray-700"> <i class="fa fa-times text-xl"></i> </button>
</div>
<div class="p-6">
<div class="space-y-6">
<!-- 上传参数设置 -->
<div class="space-y-4">
<!-- 质量设置 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">质量设置 (1-100)</label>
<div class="flex items-center space-x-4">
<input type="range" id="gallery-quality" min="35" max="100" value="60" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-500">
<span id="gallery-quality-value" class="text-sm font-medium min-w-[3rem] text-center">60</span>
</div>
</div>
<!-- 宽度设置 -->
<div>
<label for="gallery-width" class="block text-sm font-medium text-gray-700 mb-2">转换宽度</label>
<input type="number" id="gallery-width" placeholder="留空为自动" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- 高度设置 -->
<div>
<label for="gallery-height" class="block text-sm font-medium text-gray-700 mb-2">转换高度</label>
<input type="number" id="gallery-height" placeholder="留空为自动" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
</div>
</div>
<!-- 上传按钮 -->
<button id="gallery-upload-btn" class="w-full py-3 bg-blue-500 text-white rounded-md flex items-center justify-center hover:bg-blue-600 transition-colors">
<i class="fa fa-cloud-upload mr-2"></i>
<span id="gallery-upload-status">开始上传</span>
</button>
<input type="file" id="gallery-upload-input" multiple accept="image/*" class="hidden">
<!-- 上传结果 -->
<div id="gallery-upload-results" class="mt-6 space-y-4"></div>
</div>
</div>
</div>
</div>
<!-- 图片预览模态框 -->
<div id="gallery-preview-modal" class="fixed inset-0 bg-black/90 z-50 hidden items-center justify-center p-4">
<div class="relative max-w-5xl ">
<!-- 图片容器 - 用于定位关闭按钮 -->
<div class="relative inline-block">
<img src="" alt="预览图片" class="max-w-full max-h-[80vh] bg-white mx-auto object-contain">
<!-- 关闭按钮 - 绝对定位在图片右上角 -->
<button id="gallery-preview-close" class="absolute -top-8 bg-white w-8 h-8 p-0 rounded-full -right-8 text-red text-2xl hover:text-gray-300 transition-colors">
<i class="fa fa-times"></i>
</button>
</div>
</div>
</div>
<script>
// 组件状态
const galleryState = {
targetInputId: null,
currentPage: 1,
totalItems: 0,
totalPages: 0,
mediaItems: [],
isLoading: false,
activeTab: 'gallery',
selectedImageIds: []
};
// DOM元素缓存 - 使用唯一ID避免冲突
const galleryElements = {
mediaGrid: document.getElementById('gallery-media-grid'),
loadMoreBtn: document.getElementById('gallery-load-more'),
batchDeleteBtn: document.getElementById('gallery-batch-delete'),
previewModal: document.getElementById('gallery-preview-modal'),
previewImage: document.querySelector('#gallery-preview-modal img'),
previewCloseBtn: document.getElementById('gallery-preview-close'),
uploadModal: document.getElementById('gallery-upload-modal'),
uploadModalTrigger: document.getElementById('gallery-upload-modal-trigger'),
closeUploadModal: document.getElementById('gallery-close-upload-modal'),
uploadResults: document.getElementById('gallery-upload-results')
};
// 初始化函数
function initGallery() {
setupGalleryEventListeners();
fetchGalleryMediaList();
}
// 格式化文件大小
function galleryFormatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(0)) + ' ' + sizes[i];
}
// 验证图片文件
function galleryIsImageFile(file) {
const extension = file.name.split('.').pop().toLowerCase();
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif', 'avif'];
return imageExtensions.includes(extension) || /image\/.*/.test(file.type);
}
// 本地存储表单状态 - 使用唯一键名
function gallerySaveFormState(width, height) {
localStorage.setItem('galleryFormState', JSON.stringify({ width, height }));
}
function galleryLoadFormState() {
const state = localStorage.getItem('galleryFormState');
return state ? JSON.parse(state) : { width: '', height: '' };
}
// 设置事件监听 - 全部使用唯一ID
function setupGalleryEventListeners() {
// 质量滑块事件
const qualityInput = document.getElementById('gallery-quality');
const qualityValue = document.getElementById('gallery-quality-value');
if (qualityInput && qualityValue) {
qualityInput.addEventListener('input', function() {
qualityValue.textContent = this.value;
});
}
// 上传模态框控制
galleryElements.uploadModalTrigger.addEventListener('click', () => {
galleryElements.uploadModal.classList.remove('hidden');
galleryElements.uploadModal.classList.add('flex');
// 加载保存的表单状态
const { width, height } = galleryLoadFormState();
document.getElementById('gallery-width').value = width;
document.getElementById('gallery-height').value = height;
});
galleryElements.closeUploadModal.addEventListener('click', () => {
galleryElements.uploadModal.classList.add('hidden');
galleryElements.uploadModal.classList.remove('flex');
});
// 点击模态框背景关闭
galleryElements.uploadModal.addEventListener('click', (e) => {
if (e.target === galleryElements.uploadModal) {
galleryElements.uploadModal.classList.add('hidden');
galleryElements.uploadModal.classList.remove('flex');
}
});
// 加载更多按钮
galleryElements.loadMoreBtn.addEventListener('click', galleryLoadMoreImages);
// 批量删除按钮
galleryElements.batchDeleteBtn.addEventListener('click', () => {
if (galleryState.selectedImageIds.length > 0) {
galleryConfirmDeleteImage(galleryState.selectedImageIds);
}
});
// 图片网格事件委托 - 使用数据属性识别元素类型
galleryElements.mediaGrid.addEventListener('click', (e) => {
// 预览图片 - 使用数据属性选择
const previewBtn = e.target.closest('[data-action="preview"]');
if (previewBtn) {
const url = previewBtn.dataset.url;
galleryPreviewFile(url);
return;
}
// 复制链接 - 使用数据属性选择
const copyBtn = e.target.closest('[data-action="copy"]');
if (copyBtn) {
const url = copyBtn.dataset.url;
galleryCopyToClipboard(url);
return;
}
// 图片复选框 - 使用数据属性选择
const checkbox = e.target.closest('[data-type="image-checkbox"]');
if (checkbox) {
const imageId = checkbox.dataset.id;
galleryToggleImageSelection(imageId, checkbox);
return;
}
});
// 上传结果区域事件委托
galleryElements.uploadResults.addEventListener('click', (e) => {
const retryBtn = e.target.closest('[data-action="retry-upload"]');
if (retryBtn) {
document.getElementById('gallery-upload-input').click();
return;
}
});
// 上传按钮
document.getElementById('gallery-upload-btn').addEventListener('click', () => {
document.getElementById('gallery-upload-input').click();
});
// 文件选择事件
document.getElementById('gallery-upload-input').addEventListener('change', galleryHandleFileSelect);
// 预览模态框关闭
galleryElements.previewCloseBtn.addEventListener('click', () => {
galleryElements.previewModal.classList.remove('flex');
galleryElements.previewModal.classList.add('hidden');
});
galleryElements.previewModal.addEventListener('click', (e) => {
if (e.target === galleryElements.previewModal) {
galleryElements.previewModal.classList.remove('flex');
galleryElements.previewModal.classList.add('hidden');
}
});
}
// 切换图片选择状态
function galleryToggleImageSelection(imageId, checkbox) {
const index = galleryState.selectedImageIds.indexOf(imageId);
if (index === -1) {
// 选中
galleryState.selectedImageIds.push(imageId);
checkbox.checked = true;
// 使用DOM导航找到父容器并应用样式
checkbox.closest('[data-type="image-item"]').classList.add('ring-2', 'ring-blue-500', 'ring-offset-2');
} else {
// 取消选中
galleryState.selectedImageIds.splice(index, 1);
checkbox.checked = false;
checkbox.closest('[data-type="image-item"]').classList.remove('ring-2', 'ring-blue-500', 'ring-offset-2');
}
// 更新批量删除按钮状态
if (galleryState.selectedImageIds.length > 0) {
galleryElements.batchDeleteBtn.disabled = false;
galleryElements.batchDeleteBtn.classList.remove('opacity-50', 'cursor-not-allowed');
galleryElements.batchDeleteBtn.classList.add('hover:bg-red-600');
} else {
galleryElements.batchDeleteBtn.disabled = true;
galleryElements.batchDeleteBtn.classList.add('opacity-50', 'cursor-not-allowed');
galleryElements.batchDeleteBtn.classList.remove('hover:bg-red-600');
}
}
// 确认删除
function galleryConfirmDeleteImage(ids) {
const isBatch = ids.length > 1;
if (confirm(`确定要${isBatch ? '批量删除选中的' : '删除这张'}图片吗?此操作不可撤销。`)) {
galleryDeleteImages(ids);
}
}
// 删除图片函数
function galleryDeleteImages(ids) {
galleryState.isLoading = true;
galleryShowLoading();
fetch('/admin/images/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids })
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.status === 'success') {
showMessage(`成功删除${ids.length}张图片`, 'success');
galleryState.currentPage = 1;
galleryState.selectedImageIds = [];
galleryState.isLoading = false;
fetchGalleryMediaList();
} else {
showMessage(`删除失败:${data.message}`, 'error');
galleryState.isLoading = false;
galleryRenderMediaGrid();
}
})
.catch(error => {
console.error('删除失败:', error);
showMessage('网络错误,删除失败', 'error');
galleryState.isLoading = false;
galleryRenderMediaGrid();
});
}
// 获取图片列表
function fetchGalleryMediaList() {
if (galleryState.isLoading) return;
galleryState.isLoading = true;
const isInitialLoad = galleryState.currentPage === 1;
if (isInitialLoad) {
galleryShowLoading();
} else {
const loadingIndicator = galleryCreateLoadingIndicator();
galleryElements.mediaGrid.appendChild(loadingIndicator);
}
const itemsPerPage = 20;
const params = new URLSearchParams({
page: galleryState.currentPage,
limit: itemsPerPage
});
fetch(`/admin/images/list?${params}`)
.then(response => {
if (!response.ok) {
throw new Error(`列表请求失败: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.status === 'success') {
galleryState.currentPage = data.page;
galleryState.totalItems = data.total;
galleryState.totalPages = Math.ceil(data.total / itemsPerPage);
galleryState.mediaItems = isInitialLoad ? data.data : [...galleryState.mediaItems, ...data.data];
galleryRenderMediaGrid();
if (galleryState.currentPage >= galleryState.totalPages) {
galleryElements.loadMoreBtn.innerHTML = `<i class="fa fa-check mr-2"></i> 没有更多图片了`;
galleryElements.loadMoreBtn.disabled = true;
galleryElements.loadMoreBtn.classList.add('opacity-50', 'cursor-not-allowed');
} else {
galleryElements.loadMoreBtn.innerHTML = `<i class="fa fa-refresh mr-2"></i> 加载更多`;
galleryElements.loadMoreBtn.disabled = false;
galleryElements.loadMoreBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
} else {
showMessage(data.message, 'error');
galleryRenderMediaGrid();
}
})
.catch(error => {
console.error('获取图片列表失败:', error);
showMessage('网络错误,请重试', 'error');
galleryRenderMediaGrid();
})
.finally(() => {
galleryState.isLoading = false;
});
}
// 显示加载状态
function galleryShowLoading() {
galleryElements.mediaGrid.innerHTML = '';
const loadingIndicator = galleryCreateLoadingIndicator();
galleryElements.mediaGrid.appendChild(loadingIndicator);
}
// 加载更多图片
function galleryLoadMoreImages() {
if (galleryState.isLoading || galleryState.currentPage >= galleryState.totalPages) return;
galleryState.currentPage++;
fetchGalleryMediaList();
}
// 创建加载指示器
function galleryCreateLoadingIndicator() {
const indicator = document.createElement('div');
indicator.className = 'col-span-full flex flex-col items-center justify-center py-12';
indicator.innerHTML = `
<div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-blue-500 mb-4"></div>
<p class="text-gray-500">加载中...</p>
`;
return indicator;
}
// 渲染图片网格
function galleryRenderMediaGrid() {
galleryElements.mediaGrid.innerHTML = '';
const itemsToRender = galleryState.mediaItems;
if (itemsToRender.length === 0) {
galleryElements.mediaGrid.innerHTML = `
<div class="col-span-full flex flex-col items-center justify-center py-12 text-center px-4">
<i class="fa fa-picture-o text-4xl text-gray-300 mb-4"></i>
<p class="text-gray-500">没有找到图片</p>
<button onclick="document.getElementById('gallery-upload-modal-trigger').click()" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors">
<i class="fa fa-upload mr-2"></i>上传图片
</button>
</div>
`;
return;
}
// 渲染图片项 - 使用数据属性代替自定义类名
itemsToRender.forEach(item => {
const isSelected = galleryState.selectedImageIds.includes(item.id.toString());
const itemElement = document.createElement('div');
// 使用数据属性标识元素类型,而非自定义类名
itemElement.dataset.type = "image-item";
itemElement.className = `bg-white rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow ${isSelected ? 'ring-2 ring-blue-500 ring-offset-2' : ''}`;
itemElement.innerHTML = `
<div class="relative aspect-[4/3] bg-gray-100 overflow-hidden">
<!-- 复选框 - 使用数据属性标识 -->
<input type="checkbox" data-type="image-checkbox" data-id="${item.id}"
class="absolute top-2 left-2 z-10 w-4 h-4 rounded border-gray-300 text-blue-500 focus:ring-blue-500"
${isSelected ? 'checked' : ''}>
<!-- 缩略图 -->
<img src="${item.url}" alt="${item.name}" class="w-full h-full object-cover" loading="lazy">
<!-- 操作按钮 - 使用数据属性标识操作类型 -->
<div class="absolute inset-0 bg-black/50 opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center gap-2 p-2">
<button data-action="copy" data-url="${item.url}"
class="bg-white w-8 h-8 p-0 rounded-full flex items-center justify-center hover:bg-gray-100 transition-colors"
title="复制链接">
<i class="fa fa-copy text-gray-800"></i>
</button>
<button data-action="preview" data-url="${item.url}"
class="bg-white w-8 h-8 p-0 rounded-full flex items-center justify-center hover:bg-gray-100 transition-colors"
title="预览图片">
<i class="fa fa-eye text-gray-800"></i>
</button>
</div>
</div>
<!-- 图片信息 -->
<div class="p-2">
<div class="text-xs font-medium text-gray-800 truncate mb-1" title="${item.name}">${item.name}</div>
<div class="flex justify-between items-center text-xs text-gray-500">
<span>${galleryFormatFileSize(item.size)}</span>
<span>${item.width}*${item.height}</span>
</div>
</div>
`;
galleryElements.mediaGrid.appendChild(itemElement);
});
// 添加统计信息
const statsElement = document.createElement('div');
statsElement.className = 'col-span-full mt-6 pt-4 border-t border-gray-100 text-sm text-gray-500 flex justify-between items-center';
statsElement.innerHTML = `
<div>
共计 <span class="font-semibold text-gray-800">${galleryState.totalItems}</span> 张图片
</div>
<div>
已显示 <span class="font-semibold text-gray-800">${galleryState.mediaItems.length}</span> 张图片
</div>
`;
galleryElements.mediaGrid.appendChild(statsElement);
// 更新批量删除按钮状态
if (galleryState.selectedImageIds.length > 0) {
galleryElements.batchDeleteBtn.disabled = false;
galleryElements.batchDeleteBtn.classList.remove('opacity-50', 'cursor-not-allowed');
galleryElements.batchDeleteBtn.classList.add('hover:bg-red-600');
} else {
galleryElements.batchDeleteBtn.disabled = true;
galleryElements.batchDeleteBtn.classList.add('opacity-50', 'cursor-not-allowed');
galleryElements.batchDeleteBtn.classList.remove('hover:bg-red-600');
}
}
// 处理文件选择
function galleryHandleFileSelect(e) {
const files = e.target.files;
if (!files.length) return;
const validFiles = [];
const invalidFiles = [];
Array.from(files).forEach(file => {
if (!galleryIsImageFile(file)) {
invalidFiles.push({ file, reason: '不支持的文件类型,仅支持图片' });
return;
}
if (file.size > 10 * 1024 * 1024) {
invalidFiles.push({ file, reason: '文件过大,最大支持10MB' });
return;
}
validFiles.push(file);
});
galleryElements.uploadResults.innerHTML = '';
if (invalidFiles.length > 0) {
invalidFiles.forEach(({ file, reason }) => {
const errorItem = document.createElement('div');
errorItem.className = 'p-4 border border-red-200 bg-red-50 rounded-md';
errorItem.innerHTML = `
<div class="flex justify-between items-start mb-1">
<span class="font-medium text-red-800 text-sm">${file.name}</span>
<span class="text-red-600 text-xs">错误</span>
</div>
<p class="text-red-700 text-xs">${reason}</p>
`;
galleryElements.uploadResults.appendChild(errorItem);
});
}
if (validFiles.length > 0) {
galleryUploadFiles(validFiles);
}
}
// 上传文件
function galleryUploadFiles(files) {
const quality = document.getElementById('gallery-quality').value;
const width = document.getElementById('gallery-width').value;
const height = document.getElementById('gallery-height').value;
const uploadStatus = document.getElementById('gallery-upload-status');
gallerySaveFormState(width, height);
// 添加上传中的指示器
files.forEach(file => {
const progressItem = document.createElement('div');
progressItem.className = 'p-4 border border-gray-200 rounded-md overflow-hidden';
progressItem.innerHTML = `
<div class="flex justify-between items-start mb-2">
<span class="font-medium text-gray-800 text-sm">${file.name}</span>
<span class="text-blue-500 text-xs">上传中</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-1.5">
<div data-type="upload-progress" class="bg-blue-500 h-1.5 rounded-full w-0 transition-all duration-300"></div>
</div>
`;
galleryElements.uploadResults.appendChild(progressItem);
});
files.forEach((file, index) => {
const formData = new FormData();
formData.append('image', file);
formData.append('quality', quality);
if (width) formData.append('width', width);
if (height) formData.append('height', height);
const xhr = new XMLHttpRequest();
xhr.open('POST', '/admin/images/upload', true);
xhr.timeout = 120000;
xhr.ontimeout = function() {
const progressItems = galleryElements.uploadResults.querySelectorAll('.p-4');
const progressItem = progressItems[index];
galleryHandleUploadError(file, '等待返回超时,已在后台处理,稍后到图片列表中查看', progressItem);
};
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
uploadStatus.textContent = `上传中 ${Math.round(percentComplete)}%`;
const progressItems = galleryElements.uploadResults.querySelectorAll('.p-4');
const progressItem = progressItems[index];
const progressBar = progressItem.querySelector('[data-type="upload-progress"]');
progressBar.style.width = `${percentComplete}%`;
}
});
xhr.onload = function() {
uploadStatus.textContent = `开始上传`;
const progressItems = galleryElements.uploadResults.querySelectorAll('.p-4');
const progressItem = progressItems[index];
if (xhr.status >= 200 && xhr.status < 300) {
try {
const response = JSON.parse(xhr.responseText);
if (response.status === "error") {
galleryHandleUploadError(file, response.message, progressItem);
} else {
galleryHandleUploadSuccess(response, progressItem);
}
} catch (error) {
console.error('解析响应失败:', error);
galleryHandleUploadError(file, '解析服务器响应失败', progressItem);
}
} else {
try {
const responseData = JSON.parse(xhr.responseText);
galleryHandleUploadError(file, responseData.message || '服务器错误', progressItem);
} catch (parseError) {
galleryHandleUploadError(file, `服务器错误 (${xhr.status})`, progressItem);
}
}
};
xhr.onerror = function() {
uploadStatus.textContent = `开始上传`;
const progressItems = galleryElements.uploadResults.querySelectorAll('.p-4');
const progressItem = progressItems[index];
galleryHandleUploadError(file, '网络错误,请重试', progressItem);
};
xhr.send(formData);
});
}
// 处理上传成功
function galleryHandleUploadSuccess(response, progressItem) {
if (!response.data || !response.data.url) {
console.error('上传成功但缺少URL:', response);
return;
}
const newItem = {
id: response.data.id || Date.now(),
name: response.data.name,
url: response.data.url,
size: response.data.size || 0,
width: response.data.width || 0,
height: response.data.height || 0
};
// 添加到图库列表
galleryState.mediaItems.unshift(newItem);
galleryState.totalItems = galleryState.mediaItems.length;
fetchGalleryMediaList();
showMessage('图片上传成功', 'success');
setTimeout(() => {
galleryElements.uploadModal.classList.add('hidden');
galleryElements.uploadModal.classList.remove('flex');
galleryElements.uploadResults.innerHTML = '';
const fileInput = document.getElementById('gallery-upload-input');
if (fileInput) {
fileInput.value = '';
}
}, 2000);
}
// 处理上传失败
function galleryHandleUploadError(file, message, progressItem) {
progressItem.innerHTML = `
<div class="flex justify-between items-start mb-1">
<span class="font-medium text-red-800 text-sm">${file.name}</span>
<span class="text-red-600 text-xs">失败</span>
</div>
<p class="text-red-700 text-sm">${message}</p>
<button data-action="retry-upload"
class="mt-2 text-xs px-3 py-1 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 transition-colors">
重试
</button>
`;
}
// 预览图片
function galleryPreviewFile(url) {
galleryElements.previewImage.src = url;
galleryElements.previewImage.alt = '图片预览';
galleryElements.previewModal.classList.remove('hidden');
galleryElements.previewModal.classList.add('flex');
}
// 复制到剪贴板
function galleryCopyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
showMessage('链接已复制', 'success');
}).catch(err => {
console.error('无法复制文本: ', err);
showMessage('复制失败,请手动复制', 'error');
});
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', initGallery);
// 暴露全局函数(如果需要)
window.fetchGalleryMediaList = fetchGalleryMediaList;
</script>
+279
View File
@@ -0,0 +1,279 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> <?php echo htmlspecialchars($title ?? '后台管理'); ?> </title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.1/build/qrcode.min.js"></script>
<!-- Layui -->
<link rel="stylesheet" href="/Static/layui/css/layui.css">
<script src="/Static/layui/layui.js"></script>
<!-- 配置Tailwind自定义颜色 -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {primary: '#3b82f6', secondary: '#36CFC9', success: '#52C41A', warning: '#FAAD14', danger: '#FF4D4F', dark: '#1D2129', 'gray-light': '#F2F3F5', 'gray-medium': '#C9CDD4' },
fontFamily: {
inter: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
</head>
<body id="app" class="font-inter bg-gray-50 text-dark min-h-screen flex flex-col">
<!-- 顶部导航栏 -->
<header class="bg-white border-b border-gray-200 sticky top-0 z-40">
<div class="mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<a href="/admin/dashboard" class="flex items-center text-primary font-bold text-xl">
<?php
$adminSettings = \App\Core\SettingsHelper::getAll();
?>
<img src="<?= htmlspecialchars($adminSettings['site_logo'] ?? '/Static/tm68/logo_tm68.png.png') ?>" class="w-10 mr-2">
<span>管理后台</span>
</a>
</div>
<!-- 左侧区域:移动端菜单按钮 + 用户区域 -->
<div class="flex items-center space-x-4">
<!-- 用户菜单 -->
<div class="relative order-2 group">
<!-- 触发按钮 -->
<button class="flex items-center space-x-2 focus:outline-none user-menu-button">
<img class="h-8 w-8 rounded-full object-cover" src="https://picsum.photos/200/200?random=1" alt="<?=htmlspecialchars($_SESSION['username'] ?? '用户')?>的头像">
<span class="hidden md:inline-block text-sm font-medium"> <?=htmlspecialchars($_SESSION['username'] ?? '用户')?> </span>
<i class="fas fa-chevron-down text-xs text-gray-500 transition-transform duration-200 group-hover:rotate-180"></i>
</button>
<!-- 下拉菜单 (默认隐藏) -->
<div class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg py-1 z-50 transform opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 origin-top-right scale-95 group-hover:scale-100">
<!-- 个人资料 -->
<div id="profile" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors">
<i class="fas fa-user mr-2 text-gray-500"></i>个人资料
</div>
<!-- 分割线 -->
<div class="border-t border-gray-200 my-1"></div>
<!-- 退出登录 -->
<a href="/admin/logout" class="block px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors">
<i class="fas fa-sign-out-alt mr-2"></i>退出登录
</a>
</div>
</div>
<button id="mobile-menu-button" class="md:hidden p-2 rounded-md hover:bg-gray-light order-3">
<i class="fas fa-bars text-gray-600"></i>
</button>
</div>
</div>
</div>
</header>
<!-- 主要内容区 -->
<div class="flex flex-1 overflow-hidden">
<!-- 侧边栏导航 - 固定不动 -->
<aside id="sidebar" class="w-64 bg-white border-r border-gray-200 fixed left-0 top-16 h-[calc(100vh-4rem)] z-30 transform -translate-x-full md:translate-x-0 transition-transform duration-300 ease-in-out overflow-y-auto ">
<div class="p-4 h-full flex flex-col">
<nav class="space-y-1 flex-1">
<a href="/admin/dashboard" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-home w-5 text-center"></i>
<span>控制台首页</span>
</a>
<a href="/admin/users" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-users w-5 text-center"></i>
<span>用户管理</span>
</a>
<?php
if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'):
?>
<a href="/admin/admins" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-user-shield w-5 text-center"></i>
<span>管理员管理</span>
</a>
<?php
endif;
?>
<a href="/admin/pk10-periods" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-flag-checkered w-5 text-center"></i>
<span>PK10 期号管理</span>
</a>
<a href="/admin/auto-period" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-clock w-5 text-center"></i>
<span>自动开期设置</span>
</a>
<a href="/admin/games" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-gamepad w-5 text-center"></i>
<span>游戏与赔率</span>
</a>
<a href="/admin/water" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-sliders-h w-5 text-center"></i>
<span>放水控制</span>
</a>
<a href="/admin/bets" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-list-alt w-5 text-center"></i>
<span>投注记录</span>
</a>
<a href="/admin/finance" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-dollar-sign w-5 text-center"></i>
<span>财务管理</span>
</a>
<a href="/admin/agents" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-handshake w-5 text-center"></i>
<span>代理管理</span>
</a>
<a href="/admin/employees" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-id-badge w-5 text-center"></i>
<span>员工管理</span>
</a>
<a href="/admin/virtual" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-ghost w-5 text-center"></i>
<span>虚拟账户</span>
</a>
<a href="/admin/reports" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-chart-bar w-5 text-center"></i>
<span>数据报表</span>
</a>
<a href="/admin/settings" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-cog w-5 text-center"></i>
<span>系统设置</span>
</a>
<?php
if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'):
?>
<a href="/admin/plugins" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-puzzle-piece w-5 text-center"></i>
<span>插件管理</span>
</a>
<?php
endif;
global $pluginManager;
$menus = $pluginManager->getEnabledPluginMenus();
if (is_array($menus) && !empty($menus)) {
echo "<div class='mt-4 px-4 text-xs text-gray-400 tracking-wide'>插件扩展</div>";
foreach ($menus as $menu) {
$title = htmlspecialchars($menu['title'] ?? '');
$icon = htmlspecialchars($menu['icon'] ?? 'fa fa-plug');
$path = htmlspecialchars($menu['path'] ?? '#');
$hasChildren = !empty($menu['children']) && is_array($menu['children']);
echo "<div class='menu-group'>";
echo "<a href='{$path}' class='flex items-center justify-between gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light plugin-menu-link main-menu-item " . ($hasChildren ? 'menu-parent' : '') . "'>";
echo " <div class='flex items-center gap-3'> <i class='{$icon} w-5 text-center'></i> <span>{$title}</span> </div>";
if ($hasChildren) {
echo "<i class='fas fa-chevron-down text-xs text-gray-500 transition-transform duration-200'></i>";
}
echo "</a>";
if ($hasChildren) {
echo "<div class='submenu hidden pl-8'>";
foreach ($menu['children'] as $child) {
$childTitle = htmlspecialchars($child['title'] ?? '');
$childPath = htmlspecialchars($child['path'] ?? '#');
$childIcon = htmlspecialchars($child['icon'] ?? 'fa fa-circle');
echo "<a href='{$childPath}' class='flex items-center gap-3 px-6 py-2 rounded-lg text-sm transition-all mt-1 duration-200 hover:bg-gray-light plugin-submenu-link submenu-item'>";
echo " <i class='{$childIcon} w-4 text-center'></i> <span>{$childTitle}</span>";
echo "</a>";
}
echo "</div>";
}
echo "</div>";
}
}
?>
</nav>
<div class="mt-6 pt-6 border-t border-gray-200">
<div class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 text-center hover:bg-danger/10">
<span>当前版本:V 1.0.0</span>
</div>
</div>
</div>
</aside>
<!-- 主内容 - 可滚动 -->
<main id="main-content" class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8 bg-gray-50 md:ml-64 transition-all duration-300">
<?php
// 插件主体内容
if (isset($Content)) {
echo $Content;
} else {
echo "<div class='bg-white rounded-lg p-6 shadow-[0_4px_20px_rgba(0,0,0,0.08)] mb-6'>
<h1 class='text-2xl font-semibold mb-4'>控制台首页</h1>
<p class='text-gray-600 mb-6'>欢迎使用系统控制台,请从左侧菜单选择需要操作的功能。</p>
<div class='space-y-6'>";
}
?>
</main>
</div>
<!-- 个人资料弹窗 (默认隐藏) -->
<div id="profileModal" class="fixed inset-0 z-50 flex items-center justify-center invisible opacity-0 transition-all duration-300">
<!-- 背景遮罩 -->
<div class="absolute inset-0 bg-black bg-opacity-50" id="profileModalBackdrop"></div>
<!-- 弹窗内容 -->
<div class="relative bg-white rounded-lg shadow-xl w-full max-w-md mx-4 transform scale-95 transition-transform duration-300">
<!-- 弹窗头部 -->
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex justify-between items-center">
<h3 class="text-lg font-semibold">编辑个人资料</h3>
<button id="closeProfileModal" class="text-gray-500 hover:text-gray-700"> <i class="fas fa-times"></i> </button>
</div>
</div>
<!-- 表单内容 -->
<form id="profileForm" class="p-6">
<!-- 隐藏的用户ID -->
<input type="hidden" id="profileUserId" name="id" value="<?=htmlspecialchars($_SESSION['user_id'] ?? '')?>">
<!-- 用户名 -->
<div class="mb-4">
<label for="profileUsername" class="block text-sm font-medium text-gray-700 mb-1"> 用户名 </label>
<input type="text" id="profileUsername" name="username" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" value="<?=htmlspecialchars($_SESSION['username'] ?? '')?>">
</div>
<!-- 邮箱 -->
<div class="mb-4">
<label for="profileEmail" class="block text-sm font-medium text-gray-700 mb-1"> 邮箱地址 </label>
<input type="email" id="profileEmail" name="email" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors">
</div>
<!-- 密码 (可选修改) -->
<div class="mb-6">
<label for="profilePassword" class="block text-sm font-medium text-gray-700 mb-1"> 密码(不填则不修改) </label>
<input type="password" id="profilePassword" name="password" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="至少8位,包含字母和数字">
<p class="mt-1 text-xs text-gray-500">不修改密码请留空</p>
</div>
<!-- 角色(空容器,等待JS填充) -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1"> 用户角色 </label>
<div id="userRoleDisplay" class="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-lg text-gray-700"></div>
<input type="hidden" name="role" id="userRoleInput">
</div>
<!-- 状态(空容器,等待JS填充) -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-1"> 账号状态 </label>
<div id="userStatusDisplay" class="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-lg"></div>
<input type="hidden" name="status" id="userStatusInput">
</div>
<!-- 提交按钮 -->
<div class="flex justify-end space-x-3">
<button type="button" id="cancelProfileBtn" class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors"> 取消 </button>
<button type="submit" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors"> 保存修改 </button>
</div>
</form>
</div>
</div>
<!-- 页脚 -->
<footer class="bg-white border-t border-gray-200 py-4 md:ml-64 transition-all duration-300">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col md:flex-row justify-between items-center">
<p class="text-sm text-gray-500"> &copy; <?=date('Y')?> PK10 极速赛车后台管理系统 </p>
</div>
</div>
</footer>
<!-- 移动端菜单遮罩层 -->
<div id="sidebar-overlay" class="fixed inset-0 bg-black bg-opacity-50 z-20 hidden md:hidden"></div>
<script> const targetUsername = '<?= htmlspecialchars($_SESSION[' username '] ?? ' ') ?>'; </script>
<script src="/Static/js/controller.js"> </script>
<script src="/Static/js/admin.js"> </script>
</body>
</html>
+175
View File
@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>后台管理登录</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 配置Tailwind自定义颜色 -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3b82f6',
'primary-light': '#93c5fd',
'primary-dark': '#2563eb',
},
}
}
}
</script>
</head>
<body class="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-slate-950 to-slate-900 p-4">
<!-- 成功消息容器 -->
<div id="successMessage" class="fixed top-4 right-4 px-6 py-3 rounded-lg shadow shadow-lg flex items-center z-50 transition-all duration-300 transform translate-x-full bg-primary-light border border-primary/20 text-primary-dark">
<i class="fas fa-check-circle mr-2"></i>
<span id="successText"></span>
<button onclick="hideMessages()" class="ml-4 text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<!-- 错误消息容器 -->
<div id="errorMessage" class="fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg flex items-center z-50 transition-all duration-300 transform translate-x-full bg-red-50 border border-red-200 text-red-700">
<i class="fas fa-exclamation-circle mr-2"></i>
<span id="errorText"></span>
<button onclick="hideMessages()" class="ml-4 text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="w-full max-w-md">
<div class="auth-card bg-white/95 backdrop-blur rounded-2xl overflow-hidden w-full border border-slate-200 shadow-xl shadow-slate-900/20">
<div class="px-8 pt-8 pb-4">
<div class="flex flex-col items-center space-y-4">
<a href="#" class="flex items-center text-primary font-bold text-2xl">
<img src="/Static/img/logo.png" class="w-11 h-11 mr-3 rounded-xl shadow-sm" alt="Admin Logo">
<span class="text-slate-800">管理后台</span>
</a>
</div>
</div>
<div class="px-8 pb-8">
<?php if (!empty($error)): ?>
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-6 relative" role="alert">
<i class="fas fa-exclamation-circle mr-2"></i>
<span class="block sm:inline"><?=htmlspecialchars($error)?></span>
</div>
<?php endif; ?>
<form id="loginForm" method="post" action="?s=login" class="space-y-6">
<div class="space-y-1">
<label for="username_or_email" class="block text-sm font-medium text-gray-700 mb-1">用户名或邮箱</label>
<input type="text" id="username_or_email" name="username_or_email" required
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary transition-colors duration-300"
placeholder="请输入用户名或邮箱">
</div>
<div class="space-y-1">
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">密码</label>
<div class="relative">
<input type="password" id="password" name="password" required
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary transition-colors duration-300"
placeholder="请输入您的密码">
<button type="button" id="togglePassword"
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-primary transition-colors duration-300">
<i class="far fa-eye"></i>
</button>
</div>
</div>
<div class="flex items-center justify-between text-xs text-slate-400">
<span>为保护数据安全,请勿在公共设备上勾选浏览器保存密码</span>
</div>
<button type="submit"
class="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition-all duration-300">
登录
</button>
</form>
</div>
</div>
<p class="mt-6 text-center text-[11px] text-slate-400">
© <?= date('Y') ?> 管理系统后台 · Internal Use Only
</p>
</div>
<script>
// 显示消息提示
function showMessage(text, type = 'success') {
// 隐藏所有消息
hideMessages();
// 显示对应类型的消息
const messageElement = document.getElementById(type + 'Message');
const textElement = document.getElementById(type + 'Text');
textElement.textContent = text;
messageElement.classList.remove('translate-x-full');
// 3秒后自动隐藏
setTimeout(hideMessages, 3000);
}
// 隐藏所有消息
function hideMessages() {
document.getElementById('successMessage').classList.add('translate-x-full');
document.getElementById('errorMessage').classList.add('translate-x-full');
}
// 密码可见性切换
document.getElementById('togglePassword').addEventListener('click', function() {
const passwordInput = document.getElementById('password');
const icon = this.querySelector('i');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
icon.classList.remove('far', 'fa-eye');
icon.classList.add('far', 'fa-eye-slash');
} else {
passwordInput.type = 'password';
icon.classList.remove('far', 'fa-eye-slash');
icon.classList.add('far', 'fa-eye');
}
});
// 登录表单处理
document.getElementById('loginForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = {
username_or_email: document.getElementById('username_or_email').value,
password: document.getElementById('password').value,
};
try {
const response = await fetch('/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
credentials: 'include'
});
// 直接解析JSON响应(与后端JSON格式匹配)
const result = await response.json();
if (result.success) {
showMessage(result.message);
// 使用后端返回的跳转地址
if (result.redirect) {
setTimeout(() => {
window.location.href = result.redirect;
}, 1500);
}
} else {
showMessage(result.message || '登录失败,请检查账号密码', 'error');
}
} catch (error) {
console.error('登录请求失败:', error);
showMessage('网络错误,登录失败', 'error');
}
});
</script>
</body>
</html>
+941
View File
@@ -0,0 +1,941 @@
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
<i class="fas fa-calendar-alt text-primary mr-3"></i>
期号管理
</h1>
<!-- 游戏期号管理区域 -->
<?php if (!empty($gamesList) && is_array($gamesList)): ?>
<?php foreach ($gamesList as $game): ?>
<?php
$gameId = $game['id'];
$gameName = $game['name'];
$currentPeriod = isset($currentPeriods[$gameId]) ? $currentPeriods[$gameId] : null;
?>
<!-- 单个游戏的期号卡片 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-6 border-l-4 border-primary">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-800 flex items-center">
<i class="fas fa-gamepad text-primary mr-2"></i>
<?= htmlspecialchars($gameName) ?>
</h2>
<?php if ($currentPeriod): ?>
<!-- 有当前期号 -->
<div class="flex gap-2">
<?php if ($currentPeriod['status'] === 'pending'): ?>
<button
type="button"
class="period-lock-btn inline-block bg-warning hover:bg-warning/90 text-white px-4 py-2 rounded-lg text-sm"
data-id="<?= $currentPeriod['id'] ?>"
>
<i class="fas fa-lock mr-2"></i>封盘
</button>
<?php endif; ?>
<?php if ($currentPeriod['status'] === 'locked'): ?>
<button
type="button"
class="period-draw-btn inline-block bg-success hover:bg-success/90 text-white px-4 py-2 rounded-lg text-sm"
data-id="<?= $currentPeriod['id'] ?>"
>
<i class="fas fa-dice mr-2"></i>开奖
</button>
<?php endif; ?>
<?php if ($currentPeriod['status'] === 'drawn'): ?>
<button
type="button"
class="period-settle-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
data-id="<?= $currentPeriod['id'] ?>"
>
<i class="fas fa-coins mr-2"></i>结算
</button>
<?php endif; ?>
<?php if ($currentPeriod['status'] === 'settled'): ?>
<!-- 已结算,显示开始新一期按钮 -->
<button
type="button"
class="period-start-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
data-game-id="<?= $gameId ?>"
>
<i class="fas fa-play mr-2"></i>开始新一期
</button>
<?php endif; ?>
</div>
<?php else: ?>
<!-- 无当前期号,显示开始按钮 -->
<button
type="button"
class="period-start-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
data-game-id="<?= $gameId ?>"
>
<i class="fas fa-play mr-2"></i>开始新一期
</button>
<?php endif; ?>
</div>
<?php if ($currentPeriod): ?>
<!-- 当前期号信息 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<p class="text-sm text-gray-500 mb-1">期号</p>
<p class="text-lg font-bold text-gray-900"><?= htmlspecialchars($currentPeriod['period_number']) ?></p>
</div>
<div>
<p class="text-sm text-gray-500 mb-1">状态</p>
<?php
$status = $currentPeriod['status'];
$statusMap = [
'pending' => ['text' => '待开奖', 'class' => 'bg-warning/10 text-warning'],
'locked' => ['text' => '已封盘', 'class' => 'bg-danger/10 text-danger'],
'drawn' => ['text' => '已开奖', 'class' => 'bg-primary/10 text-primary'],
'settled' => ['text' => '已结算', 'class' => 'bg-success/10 text-success']
];
$statusInfo = $statusMap[$status] ?? $statusMap['pending'];
?>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm <?= $statusInfo['class'] ?>">
<?= $statusInfo['text'] ?>
</span>
</div>
<div>
<p class="text-sm text-gray-500 mb-1">开始时间</p>
<p class="text-sm text-gray-900"><?= htmlspecialchars($currentPeriod['start_time'] ?? '-') ?></p>
</div>
<div>
<p class="text-sm text-gray-500 mb-1">开奖结果</p>
<?php if (!empty($currentPeriod['dice1'])): ?>
<p class="text-lg font-bold text-gray-900">
<?= $currentPeriod['dice1'] ?>.<?= $currentPeriod['dice2'] ?>.<?= $currentPeriod['dice3'] ?>
<span class="text-sm text-gray-500">(<?= $currentPeriod['total'] ?>)</span>
</p>
<?php else: ?>
<p class="text-sm text-gray-400">未开奖</p>
<?php endif; ?>
</div>
</div>
<?php else: ?>
<!-- 无当前期号提示 -->
<div class="text-center py-4">
<p class="text-sm text-gray-500">暂无进行中的期号,点击"开始新一期"按钮启动</p>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
<!-- 统计卡片 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">期号总数</p>
<h3 class="text-2xl font-bold mt-1">
<?= isset($periods) && is_array($periods) ? count($periods) : 0 ?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<i class="fas fa-list text-primary"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">待开奖</p>
<h3 class="text-2xl font-bold mt-1 text-warning">
<?php
$pendingCount = 0;
if (isset($periods) && is_array($periods)) {
foreach ($periods as $p) {
if (isset($p['status']) && $p['status'] === 'pending') {
$pendingCount++;
}
}
}
echo $pendingCount;
?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-warning/10 flex items-center justify-center">
<i class="fas fa-clock text-warning"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">已开奖</p>
<h3 class="text-2xl font-bold mt-1 text-success">
<?php
$drawnCount = 0;
if (isset($periods) && is_array($periods)) {
foreach ($periods as $p) {
if (isset($p['status']) && ($p['status'] === 'drawn' || $p['status'] === 'settled')) {
$drawnCount++;
}
}
}
echo $drawnCount;
?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
<i class="fas fa-check-circle text-success"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">已结算</p>
<h3 class="text-2xl font-bold mt-1 text-primary">
<?php
$settledCount = 0;
if (isset($periods) && is_array($periods)) {
foreach ($periods as $p) {
if (isset($p['status']) && $p['status'] === 'settled') {
$settledCount++;
}
}
}
echo $settledCount;
?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<i class="fas fa-coins text-primary"></i>
</div>
</div>
</div>
</div>
<!-- 期号列表 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
<div>
<h2 class="text-xl font-semibold text-gray-800">期号列表</h2>
<p class="text-sm text-gray-500 mt-1">管理游戏期号和开奖结果</p>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full bg-white rounded-xl overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">期号</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">关联游戏</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">开奖结果</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">创建时间</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200" id="periodList">
<?php if (!empty($periods) && is_array($periods)): ?>
<?php foreach ($periods as $period): ?>
<tr class="hover:bg-gray-50 transition-colors" data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>">
<td class="px-4 py-4">
<div class="text-sm font-semibold text-gray-900">
<?= htmlspecialchars((string)($period['period_number'] ?? '')) ?>
</div>
<?php if (!empty($period['auto_generated'])): ?>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] bg-gray-100 text-gray-600 mt-1">
自动生成
</span>
<?php endif; ?>
</td>
<td class="px-4 py-4">
<?php
$gameId = $period['game_id'] ?? null;
$gameName = $gameId && isset($games[$gameId]) ? $games[$gameId] : '未关联';
?>
<span class="text-sm text-gray-600"><?= htmlspecialchars($gameName) ?></span>
</td>
<td class="px-4 py-4">
<?php
$status = $period['status'] ?? 'pending';
$statusMap = [
'pending' => ['text' => '待开奖', 'class' => 'bg-warning/10 text-warning'],
'locked' => ['text' => '已封盘', 'class' => 'bg-danger/10 text-danger'],
'drawn' => ['text' => '已开奖', 'class' => 'bg-primary/10 text-primary'],
'settled' => ['text' => '已结算', 'class' => 'bg-success/10 text-success']
];
$statusInfo = $statusMap[$status] ?? $statusMap['pending'];
?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $statusInfo['class'] ?>">
<span class="w-2 h-2 rounded-full mr-1 <?= str_replace('/10', '', $statusInfo['class']) ?>"></span>
<?= $statusInfo['text'] ?>
</span>
</td>
<td class="px-4 py-4">
<?php
// 获取游戏类型
$gameId = $period['game_id'] ?? 0;
$gameType = 'dice';
if (isset($games[$gameId])) {
// 从游戏列表获取类型(需要查询数据库)
// 简化处理:通过 dice3 是否为 null 判断
if ($period['dice3'] === null && !empty($period['result'])) {
$gameType = 'xocdia';
}
}
// 根据游戏类型显示开奖结果
if ($gameType === 'xocdia'):
// Xóc Đĩa: 显示硬币颜色
if (!empty($period['result'])):
$coins = json_decode($period['result'], true);
if (is_array($coins) && count($coins) === 4):
$redCount = $period['dice1'] ?? 0;
$whiteCount = $period['dice2'] ?? 0;
?>
<div class="text-sm text-gray-900">
<span class="font-semibold">
<?php foreach ($coins as $coin): ?>
<span class="inline-block w-5 h-5 rounded-full <?= $coin === 'red' ? 'bg-red-500' : 'bg-gray-200' ?> border border-gray-300 mr-1"></span>
<?php endforeach; ?>
</span>
<span class="text-gray-600 ml-2">
(<?= $redCount ?>Đ <?= $whiteCount ?>T)
</span>
<span class="ml-1 px-2 py-0.5 rounded text-[11px] <?= ($redCount == 0 || $redCount == 2 || $redCount == 4) ? 'bg-blue-100 text-blue-700' : 'bg-red-100 text-red-700' ?>">
<?= ($redCount == 0 || $redCount == 2 || $redCount == 4) ? 'Chẵn' : 'Lẻ' ?>
</span>
</div>
<?php
else:
?>
<span class="text-sm text-gray-400">数据格式错误</span>
<?php
endif;
else:
?>
<span class="text-sm text-gray-400">未开奖</span>
<?php
endif;
else:
// 骰子游戏: 显示骰子点数
if (!empty($period['dice1']) && !empty($period['dice2']) && !empty($period['dice3'])):
?>
<div class="text-sm text-gray-900">
<span class="font-semibold">
<?= htmlspecialchars((string)$period['dice1']) ?>.
<?= htmlspecialchars((string)$period['dice2']) ?>.
<?= htmlspecialchars((string)$period['dice3']) ?>
</span>
<span class="text-gray-500 ml-1">
(<?= htmlspecialchars((string)($period['total'] ?? '')) ?>)
</span>
<?php if (!empty($period['result'])): ?>
<span class="ml-1 px-2 py-0.5 rounded text-[11px] <?= $period['result'] === 'Tài' ? 'bg-red-100 text-red-700' : 'bg-blue-100 text-blue-700' ?>">
<?= htmlspecialchars((string)$period['result']) ?>
</span>
<?php endif; ?>
</div>
<?php
else:
?>
<span class="text-sm text-gray-400">未开奖</span>
<?php
endif;
endif;
?>
</td>
<td class="px-4 py-4 hidden md:table-cell">
<?php if (!empty($period['created_at'])): ?>
<div class="text-xs text-gray-500">
<?= date('Y-m-d H:i', strtotime((string)$period['created_at'])) ?>
</div>
<?php else: ?>
<span class="text-xs text-gray-400">时间未知</span>
<?php endif; ?>
</td>
<td class="px-4 py-4 text-right text-sm font-medium">
<div class="flex items-center justify-end gap-2">
<?php if (($period['status'] ?? '') === 'pending'): ?>
<button
type="button"
class="period-lock-btn text-gray-500 hover:text-warning"
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
title="封盘"
>
<i class="fas fa-lock"></i>
</button>
<?php endif; ?>
<?php if (($period['status'] ?? '') === 'locked' || ($period['status'] ?? '') === 'pending'): ?>
<button
type="button"
class="period-draw-btn text-gray-500 hover:text-primary"
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
title="录入开奖"
>
<i class="fas fa-dice"></i>
</button>
<?php endif; ?>
<?php if (($period['status'] ?? '') === 'drawn'): ?>
<button
type="button"
class="period-draw-btn text-gray-500 hover:text-warning"
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
title="修改结果"
>
<i class="fas fa-edit"></i>
</button>
<button
type="button"
class="period-settle-btn text-gray-500 hover:text-success"
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
title="确认结算"
>
<i class="fas fa-check-circle"></i>
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="6" class="px-6 py-12 text-center">
<div class="flex flex-col items-center">
<i class="fas fa-calendar-alt text-gray-300 text-5xl mb-4"></i>
<h3 class="text-lg font-medium text-gray-900">暂无期号</h3>
<p class="mt-1 text-gray-500 text-sm">
当前还没有任何期号。
</p>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- 录入开奖结果模态框 -->
<div
id="drawPeriodBackdrop"
class="fixed inset-0 bg-black/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 z-40"
></div>
<div
id="drawPeriodModal"
class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-none transition-all duration-300 scale-95"
>
<div class="bg-white rounded-xl shadow-xl w-full max-w-md max-h-[90vh] overflow-hidden">
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
<h3 class="text-xl font-bold text-gray-800 flex items-center">
<i class="fas fa-dice text-primary mr-2"></i>
录入开奖结果
</h3>
<button id="closeDrawPeriodBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
<i class="fas fa-times"></i>
</button>
</div>
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-130px)]">
<form id="drawPeriodForm" class="space-y-4">
<input type="hidden" id="drawPeriodId" name="id">
<input type="hidden" id="drawGameType" name="game_type">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
期号
</label>
<p id="drawPeriodNumber" class="text-lg font-bold text-gray-900"></p>
</div>
<!-- 骰子游戏输入 -->
<div id="diceInputSection" class="grid grid-cols-3 gap-4">
<div>
<label for="drawDice1" class="block text-sm font-medium text-gray-700 mb-1">
骰子1 <span class="text-red-500">*</span>
</label>
<input
type="number"
id="drawDice1"
name="dice1"
min="1"
max="6"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm text-center text-lg font-bold"
placeholder="1-6"
>
</div>
<div>
<label for="drawDice2" class="block text-sm font-medium text-gray-700 mb-1">
骰子2 <span class="text-red-500">*</span>
</label>
<input
type="number"
id="drawDice2"
name="dice2"
min="1"
max="6"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm text-center text-lg font-bold"
placeholder="1-6"
>
</div>
<div>
<label for="drawDice3" class="block text-sm font-medium text-gray-700 mb-1">
骰子3 <span class="text-red-500">*</span>
</label>
<input
type="number"
id="drawDice3"
name="dice3"
min="1"
max="6"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm text-center text-lg font-bold"
placeholder="1-6"
>
</div>
</div>
<!-- Xóc Đĩa 硬币输入 -->
<div id="xocdiaInputSection" class="hidden space-y-3">
<p class="text-sm text-gray-600">选择4个硬币的颜色:</p>
<div class="grid grid-cols-4 gap-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">硬币1</label>
<select id="coin1" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white">
<option value="red">红色</option>
<option value="white">白色</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">硬币2</label>
<select id="coin2" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white">
<option value="red">红色</option>
<option value="white">白色</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">硬币3</label>
<select id="coin3" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white">
<option value="red">红色</option>
<option value="white">白色</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">硬币4</label>
<select id="coin4" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white">
<option value="red">红色</option>
<option value="white">白色</option>
</select>
</div>
</div>
</div>
<div id="drawResultPreview" class="hidden p-4 bg-gray-50 rounded-lg">
<p class="text-sm text-gray-600 mb-1">开奖结果预览:</p>
<p class="text-lg font-bold">
<span id="drawResultText"></span>
<span id="drawResultTotal" class="ml-2 text-gray-500"></span>
<span id="drawResultType" class="ml-2"></span>
</p>
</div>
<div class="pt-4 border-t border-gray-100">
<button
type="button"
id="submitDrawPeriodBtn"
class="w-full bg-primary hover:bg-primary/90 text-white px-4 py-2.5 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center justify-center"
>
<i class="fas fa-check mr-2"></i>
确认录入
</button>
</div>
</form>
</div>
</div>
</div>
<script src="/Static/js/admin.js"></script>
<script>
layui.use(['layer'], function() {
var layer = layui.layer;
// 获取元素
const closeDrawPeriodBtn = document.getElementById('closeDrawPeriodBtn');
const drawPeriodBackdrop = document.getElementById('drawPeriodBackdrop');
const drawPeriodModal = document.getElementById('drawPeriodModal');
const drawPeriodForm = document.getElementById('drawPeriodForm');
const submitDrawPeriodBtn = document.getElementById('submitDrawPeriodBtn');
const drawDice1 = document.getElementById('drawDice1');
const drawDice2 = document.getElementById('drawDice2');
const drawDice3 = document.getElementById('drawDice3');
const drawResultPreview = document.getElementById('drawResultPreview');
const drawResultText = document.getElementById('drawResultText');
const drawResultTotal = document.getElementById('drawResultTotal');
const drawResultType = document.getElementById('drawResultType');
// 打开录入开奖模态框
function openDrawPeriodModal(periodId) {
fetch(`/admin/periods/${periodId}`)
.then(res => res.json())
.then(data => {
if (data.success) {
const period = data.data;
document.getElementById('drawPeriodId').value = period.id;
document.getElementById('drawPeriodNumber').textContent = period.period_number;
// 获取游戏类型
const gameId = period.game_id;
// 获取游戏信息(包括类型)
fetch(`/admin/games/${gameId}`)
.then(res => res.json())
.then(gameData => {
if (gameData.success && gameData.data) {
const gameType = gameData.data.type || 'dice';
document.getElementById('drawGameType').value = gameType;
// 根据游戏类型显示不同的输入界面
const diceSection = document.getElementById('diceInputSection');
const xocdiaSection = document.getElementById('xocdiaInputSection');
if (gameType === 'xocdia') {
diceSection.classList.add('hidden');
xocdiaSection.classList.remove('hidden');
// 清空骰子输入
drawDice1.value = '';
drawDice2.value = '';
drawDice3.value = '';
// 如果已有开奖结果,填充硬币颜色
if (period.result) {
try {
const coins = JSON.parse(period.result);
if (Array.isArray(coins) && coins.length === 4) {
document.getElementById('coin1').value = coins[0];
document.getElementById('coin2').value = coins[1];
document.getElementById('coin3').value = coins[2];
document.getElementById('coin4').value = coins[3];
}
} catch (e) {
// 忽略解析错误
}
}
} else {
diceSection.classList.remove('hidden');
xocdiaSection.classList.add('hidden');
// 填充已有的骰子数据
drawDice1.value = period.dice1 || '';
drawDice2.value = period.dice2 || '';
drawDice3.value = period.dice3 || '';
}
updateDrawPreview();
// 游戏类型设置完成后再打开模态框
drawPeriodBackdrop.classList.remove('opacity-0', 'pointer-events-none');
drawPeriodModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
drawPeriodModal.classList.add('scale-100');
} else {
layer.msg('获取游戏信息失败', {icon: 2});
}
})
.catch(e => {
layer.msg('获取游戏信息失败:' + e.message, {icon: 2});
});
} else {
layer.msg(data.message || '获取期号信息失败', {icon: 2});
}
})
.catch(e => {
layer.msg('获取期号信息失败:' + e.message, {icon: 2});
});
}
// 关闭录入开奖模态框
function closeDrawPeriodModal() {
drawPeriodBackdrop.classList.add('opacity-0', 'pointer-events-none');
drawPeriodModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
drawPeriodModal.classList.remove('scale-100');
drawPeriodForm.reset();
drawResultPreview.classList.add('hidden');
}
// 更新开奖结果预览
function updateDrawPreview() {
const d1 = parseInt(drawDice1.value) || 0;
const d2 = parseInt(drawDice2.value) || 0;
const d3 = parseInt(drawDice3.value) || 0;
if (d1 >= 1 && d1 <= 6 && d2 >= 1 && d2 <= 6 && d3 >= 1 && d3 <= 6) {
const total = d1 + d2 + d3;
let result = '';
let resultClass = '';
// 检查是否为爆子
if (d1 === d2 && d2 === d3) {
result = 'Bão';
resultClass = 'bg-green-100 text-green-700 px-2 py-1 rounded text-sm';
} else if (total >= 4 && total <= 10) {
result = 'Xỉu';
resultClass = 'bg-blue-100 text-blue-700 px-2 py-1 rounded text-sm';
} else {
result = 'Tài';
resultClass = 'bg-red-100 text-red-700 px-2 py-1 rounded text-sm';
}
drawResultText.textContent = `${d1}.${d2}.${d3}`;
drawResultTotal.textContent = `(总和: ${total})`;
drawResultType.innerHTML = `<span class="${resultClass}">${result}</span>`;
drawResultPreview.classList.remove('hidden');
} else {
drawResultPreview.classList.add('hidden');
}
}
// 提交录入开奖
async function submitDrawPeriod() {
const gameType = document.getElementById('drawGameType').value;
const data = {
id: document.getElementById('drawPeriodId').value,
auto: false
};
console.log('=== 开奖提交调试信息 ===');
console.log('游戏类型:', gameType);
console.log('期号ID:', data.id);
// 根据游戏类型收集数据
if (gameType === 'xocdia') {
// Xóc Đĩa: 收集硬币颜色
const coins = [
document.getElementById('coin1').value,
document.getElementById('coin2').value,
document.getElementById('coin3').value,
document.getElementById('coin4').value
];
data.coins = coins;
console.log('硬币颜色:', coins);
} else {
// 骰子游戏: 收集骰子点数
const dice1 = parseInt(drawDice1.value);
const dice2 = parseInt(drawDice2.value);
const dice3 = parseInt(drawDice3.value);
if (!dice1 || !dice2 || !dice3 || dice1 < 1 || dice1 > 6 || dice2 < 1 || dice2 > 6 || dice3 < 1 || dice3 > 6) {
layer.msg('请输入有效的骰子点数(1-6', {icon: 2});
return;
}
data.dice1 = dice1;
data.dice2 = dice2;
data.dice3 = dice3;
console.log('骰子点数:', dice1, dice2, dice3);
}
console.log('发送的数据:', JSON.stringify(data));
submitDrawPeriodBtn.disabled = true;
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>录入中...';
try {
const response = await fetch('/admin/periods/draw', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(data)
});
const result = await response.json();
console.log('服务器响应:', result);
if (result.success) {
layer.msg(result.message || '开奖结果录入成功', {icon: 1}, function() {
location.reload();
});
} else {
layer.msg(result.message || '录入失败', {icon: 2});
submitDrawPeriodBtn.disabled = false;
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-check mr-2"></i>确认录入';
}
} catch (e) {
console.error('提交错误:', e);
layer.msg('录入失败:' + e.message, {icon: 2});
submitDrawPeriodBtn.disabled = false;
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-check mr-2"></i>确认录入';
}
}
// 封盘
async function lockPeriod(id) {
layer.confirm('确定要封盘吗?封盘后将无法继续投注。', {icon: 3, title: '确认封盘'}, async function(index) {
layer.close(index);
try {
const response = await fetch('/admin/periods/lock', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({id: id})
});
const result = await response.json();
if (result.success) {
layer.msg(result.message || '封盘成功', {icon: 1}, function() {
location.reload();
});
} else {
layer.msg(result.message || '封盘失败', {icon: 2});
}
} catch (e) {
layer.msg('封盘失败:' + e.message, {icon: 2});
}
});
}
// 确认结算
async function settlePeriod(id) {
layer.confirm('确定要确认结算吗?结算后将自动创建下一期,此操作不可撤销。', {icon: 3, title: '确认结算'}, async function(index) {
layer.close(index);
try {
const response = await fetch('/admin/periods/settle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({id: id})
});
const result = await response.json();
if (result.success) {
layer.msg(result.message || '结算成功', {icon: 1}, function() {
location.reload();
});
} else {
layer.msg(result.message || '结算失败', {icon: 2});
}
} catch (e) {
layer.msg('结算失败:' + e.message, {icon: 2});
}
});
}
// 开始下注
async function startPeriod(event) {
// 从按钮的 data-game-id 属性获取游戏ID
const gameId = event.currentTarget.getAttribute('data-game-id');
if (!gameId) {
layer.msg('游戏ID缺失', {icon: 2});
return;
}
layer.confirm('确定要开始新一期下注吗?', {icon: 3, title: '开始下注'}, async function(index) {
layer.close(index);
try {
const response = await fetch('/admin/periods/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({game_id: parseInt(gameId)})
});
const result = await response.json();
if (result.success) {
layer.msg(result.message || '启动成功', {icon: 1}, function() {
location.reload();
});
} else {
layer.msg(result.message || '启动失败', {icon: 2});
}
} catch (e) {
layer.msg('启动失败:' + e.message, {icon: 2});
}
});
}
// 绑定事件
if (closeDrawPeriodBtn) {
closeDrawPeriodBtn.addEventListener('click', closeDrawPeriodModal);
}
if (drawPeriodBackdrop) {
drawPeriodBackdrop.addEventListener('click', closeDrawPeriodModal);
}
if (submitDrawPeriodBtn) {
submitDrawPeriodBtn.addEventListener('click', submitDrawPeriod);
}
// 骰子输入监听
if (drawDice1 && drawDice2 && drawDice3) {
[drawDice1, drawDice2, drawDice3].forEach(input => {
input.addEventListener('input', updateDrawPreview);
});
}
// 事件委托:列表操作按钮
const periodList = document.getElementById('periodList');
if (periodList) {
periodList.addEventListener('click', function(e) {
const lockBtn = e.target.closest('.period-lock-btn');
const drawBtn = e.target.closest('.period-draw-btn');
const settleBtn = e.target.closest('.period-settle-btn');
if (lockBtn) {
const id = lockBtn.getAttribute('data-id');
if (id) lockPeriod(id);
}
if (drawBtn) {
const id = drawBtn.getAttribute('data-id');
if (id) openDrawPeriodModal(id);
}
if (settleBtn) {
const id = settleBtn.getAttribute('data-id');
if (id) settlePeriod(id);
}
});
}
// 当前期号操作按钮(如果在页面中存在)
document.querySelectorAll('.period-lock-btn').forEach(btn => {
if (!btn.closest('#periodList')) {
btn.addEventListener('click', function() {
const id = this.getAttribute('data-id');
if (id) lockPeriod(id);
});
}
});
document.querySelectorAll('.period-draw-btn').forEach(btn => {
if (!btn.closest('#periodList')) {
btn.addEventListener('click', function() {
const id = this.getAttribute('data-id');
if (id) openDrawPeriodModal(id);
});
}
});
document.querySelectorAll('.period-settle-btn').forEach(btn => {
if (!btn.closest('#periodList')) {
btn.addEventListener('click', function() {
const id = this.getAttribute('data-id');
if (id) settlePeriod(id);
});
}
});
// Start Button
document.querySelectorAll('.period-start-btn').forEach(btn => {
btn.addEventListener('click', startPeriod);
});
});
</script>
+107
View File
@@ -0,0 +1,107 @@
<div class="p-6">
<h2 class="text-2xl font-bold mb-4">🏎️ PK10 期号管理</h2>
<!-- 统计 -->
<div class="grid grid-cols-4 gap-4 mb-6">
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-sm">总计</div><div class="text-2xl font-bold"><?=$stats['total']??0?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-sm">待开奖</div><div class="text-2xl font-bold text-yellow-500"><?=$stats['pending']??0?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-sm">已开奖</div><div class="text-2xl font-bold text-blue-500"><?=$stats['drawn']??0?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-sm">已结算</div><div class="text-2xl font-bold text-green-500"><?=$stats['settled']??0?></div></div>
</div>
<!-- 当前期号 -->
<div class="bg-white rounded-xl p-6 shadow mb-6">
<h3 class="font-bold mb-3">当前期号</h3>
<?php if($current): ?>
<div class="flex items-center justify-between flex-wrap gap-3">
<div>
<span class="text-gray-400">期号:</span> <span class="font-mono font-bold"><?=$current['period_number']?></span>
<span class="ml-4 px-2 py-1 rounded text-xs <?=$current['status']==='pending'?'bg-yellow-100 text-yellow-700':($current['status']==='drawn'?'bg-blue-100 text-blue-700':'bg-green-100 text-green-700')?>"><?=$current['status']==='pending'?'待开奖':($current['status']==='locked'?'已封盘':($current['status']==='drawn'?'已开奖':'已结算'))?></span>
</div>
<div class="flex gap-2">
<?php if($current['status']==='pending'): ?>
<button onclick="doAction('lock',<?=$current['id']?>)" class="px-4 py-2 bg-orange-500 text-white rounded hover:bg-orange-600 text-sm">🔒 封盘</button>
<button onclick="doAction('draw',<?=$current['id']?>)" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm">🎲 自动开奖</button>
<button onclick="showManualDraw(<?=$current['id']?>)" class="px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600 text-sm">✏️ 手动开奖</button>
<?php elseif($current['status']==='locked'): ?>
<button onclick="doAction('draw',<?=$current['id']?>)" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm">🎲 自动开奖</button>
<button onclick="showManualDraw(<?=$current['id']?>)" class="px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600 text-sm">✏️ 手动开奖</button>
<?php elseif($current['status']==='drawn'): ?>
<div class="flex gap-1 items-center mr-4">
<?php if($current['pk10']): for($i=1;$i<=10;$i++): $v=$current['pk10']['rank_'.$i]??0; ?>
<span class="w-7 h-7 rounded-full bg-gray-200 flex items-center justify-center text-xs font-bold"><?=$v?></span>
<?php endfor; endif; ?>
</div>
<button onclick="doAction('settle',<?=$current['id']?>)" class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 text-sm">💰 结算</button>
<?php endif; ?>
</div>
</div>
<?php else: ?>
<div class="flex items-center justify-between">
<span class="text-gray-400">暂无进行中的期号</span>
<button onclick="doAction('start',0)" class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 text-sm">▶ 开始新期</button>
</div>
<?php endif; ?>
</div>
<!-- 手动开奖弹窗 -->
<div id="manualModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-xl p-6 w-full max-w-md">
<h3 class="font-bold mb-4">手动开奖 - 输入结果 (1-10)</h3>
<div class="grid grid-cols-5 gap-2 mb-4">
<?php for($i=1;$i<=10;$i++): ?>
<div>
<label class="text-xs text-gray-400">第<?=$i?>名</label>
<input type="number" min="1" max="10" id="mr<?=$i?>" class="w-full border rounded px-2 py-1 text-center" placeholder="<?=$i?>">
</div>
<?php endfor; ?>
</div>
<div class="flex gap-2">
<button onclick="submitManualDraw()" class="flex-1 py-2 bg-purple-500 text-white rounded hover:bg-purple-600">确认开奖</button>
<button onclick="document.getElementById('manualModal').classList.add('hidden')" class="flex-1 py-2 bg-gray-200 rounded hover:bg-gray-300">取消</button>
</div>
</div>
</div>
<!-- 历史列表 -->
<div class="bg-white rounded-xl shadow overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50"><tr>
<th class="px-4 py-3 text-left">期号</th><th class="px-4 py-3 text-left">状态</th><th class="px-4 py-3 text-left">结果</th><th class="px-4 py-3 text-left">冠亚和</th><th class="px-4 py-3 text-left">时间</th>
</tr></thead>
<tbody>
<?php foreach($periods??[] as $p): ?>
<tr class="border-t hover:bg-gray-50">
<td class="px-4 py-2 font-mono text-xs"><?=$p['period_number']?></td>
<td class="px-4 py-2"><span class="px-2 py-1 rounded text-xs <?=$p['status']==='settled'?'bg-green-100 text-green-700':($p['status']==='drawn'?'bg-blue-100 text-blue-700':'bg-yellow-100 text-yellow-700')?>"><?=$p['status']==='settled'?'已结算':($p['status']==='drawn'?'已开奖':'待开奖')?></span></td>
<td class="px-4 py-2">
<?php if($p['pk10']): ?>
<div class="flex gap-1"><?php for($i=1;$i<=10;$i++): $v=$p['pk10']['rank_'.$i]??0; ?><span class="w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs"><?=$v?></span><?php endfor; ?></div>
<?php else: ?>---<?php endif; ?>
</td>
<td class="px-4 py-2"><?=$p['pk10']['champion_sum']??'-'?></td>
<td class="px-4 py-2 text-gray-400 text-xs"><?=$p['created_at']??''?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<script>
let manualPeriodId=0;
function showManualDraw(id){manualPeriodId=id;document.getElementById('manualModal').classList.remove('hidden');}
async function submitManualDraw(){
const result=[];for(let i=1;i<=10;i++){const v=parseInt(document.getElementById('mr'+i).value);if(!v||v<1||v>10){alert('每个名次请输入1-10的数字');return;}result.push(v);}
if(new Set(result).size!==10){alert('10个名次的车号不能重复');return;}
await doAction('draw',manualPeriodId,{manual:true,result});
document.getElementById('manualModal').classList.add('hidden');
}
async function doAction(action,periodId,extra={}){
const body={period_id:periodId,...extra};
const url='/admin/pk10-periods/'+action;
const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const d=await r.json();
if(d.status==='success'){location.reload();}else{alert(d.message||'操作失败');}
}
</script>
+291
View File
@@ -0,0 +1,291 @@
<div class="mb-6 flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
<div>
<h1 class="text-2xl font-bold text-dark">插件管理</h1>
<p class="text-gray-500 mt-1">管理、安装和卸载系统插件</p>
</div>
<div class="relative">
<button class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg flex items-center gap-2 btn-effect">
<a target="_blank" href="https://plugins.juhe.me" >
<i class="fas fa-th-large"></i>
<span>插件库</span>
</a>
</button>
<div id="uploadOverlay" class="hidden fixed inset-0 bg-black/50 z-40 transition-opacity duration-300"></div>
<div id="uploadForm" class="hidden fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-80 bg-white rounded-lg shadow-lg p-4 z-50 border border-gray-200 transition-all duration-300 scale-95 opacity-0">
<h3 class="font-medium mb-3">上传插件</h3>
<div class="mb-3">
<label class="block text-sm text-gray-600 mb-1">选择插件包 (.zip)</label>
<input type="file" id="pluginZip" accept=".zip" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<p class="text-xs text-gray-500 mt-1">支持的格式: .zip</p>
</div>
<div class="flex gap-2">
<button type="button" id="submitUpload" class="flex-1 bg-primary text-white px-3 py-2 rounded-md text-sm btn-effect">
确认上传
</button>
<button type="button" id="cancelUpload" class="px-3 py-2 border border-gray-300 rounded-md text-sm btn-effect">
取消
</button>
</div>
</div>
</div>
</div>
<!-- 插件统计卡片 -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">总插件数</p>
<h3 class="text-2xl font-bold mt-1">
<?=count($plugins) ?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<i class="fas fa-puzzle-piece text-primary"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">已启用</p>
<h3 class="text-2xl font-bold mt-1">
<?=count(array_filter($plugins, function($p) { return $p['status']; })) ?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
<i class="fas fa-check text-success"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">未启用</p>
<h3 class="text-2xl font-bold mt-1">
<?=count(array_filter($plugins, function($p) { return !$p['status']; })) ?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-gray-medium/30 flex items-center justify-center">
<i class="fas fa-times text-gray-medium"></i>
</div>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 flex flex-wrap items-center justify-between gap-4">
<h2 class="text-lg font-semibold">插件列表</h2>
<button id="uploadBtn" class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg
flex items-center gap-2 btn-effect text-sm">
<i class="fas fa-upload"></i>
<span>上传插件</span>
</button>
</div>
<?php if (empty($plugins)): ?>
<div class="p-10 text-center border-b border-gray-200">
<i class="fas fa-puzzle-piece text-5xl text-gray-300 mb-4"></i>
<h3 class="text-lg font-medium mb-2">暂无插件</h3>
<p class="text-gray-500 mb-6">请上传并安装插件来扩展系统功能</p>
<button id="emptyStateUploadBtn" class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg flex items-center gap-2 btn-effect mx-auto">
<i class="fas fa-upload"></i>
<span>上传插件</span>
</button>
</div>
<?php else: ?>
<div class="divide-y divide-gray-200">
<?php foreach ($plugins as $plugin): ?>
<div class="p-4 hover:bg-gray-50 transition-colors">
<div class="flex flex-wrap md:flex-nowrap justify-between items-start gap-4">
<!-- 插件信息 -->
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 mb-2">
<i class="<?= htmlspecialchars($plugin['icon']) ?> text-primary text-xl"></i>
<h3 class="font-semibold text-gray-900 truncate">
<?= htmlspecialchars($plugin['title']) ?>
<span class="text-sm font-normal text-gray-500 ml-2">v<?= htmlspecialchars($plugin['version']) ?></span>
</h3>
<?php if ($plugin['status']): ?>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
已启用
</span>
<?php else: ?>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
<?= $plugin['installed'] ? '已禁用' : '未安装' ?>
</span>
<?php endif; ?>
</div>
<p class="text-gray-600 text-sm mb-2">
<span class="font-medium">简介:</span>
<?= htmlspecialchars($plugin['description']) ?>
</p>
<p class="text-gray-600 text-sm">
<span class="font-medium">URL:</span>
<a href="<?= htmlspecialchars($plugin['url']) ?>" class="text-primary hover:underline">
<?= htmlspecialchars($plugin['url']) ?>
</a>
</p>
</div>
<!-- 操作按钮 -->
<div class="flex gap-2 shrink-0">
<?php if ($plugin['installed']): ?>
<button class="px-3 py-1.5 rounded border text-sm transition-colors btn-effect
<?= $plugin['status'] ? 'border-red-200 bg-red-50 text-red-700 hover:bg-red-100' : 'border-green-200 bg-green-50 text-green-700 hover:bg-green-100' ?>"
data-action="toggle"
data-url="/admin/plugins/toggle/<?= $plugin['name'] ?>">
<?= $plugin['status'] ? '禁用' : '启用' ?>
</button>
<button class="px-3 py-1.5 rounded border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 text-sm transition-colors btn-effect"
data-action="uninstall"
data-url="/admin/plugins/uninstall/<?= $plugin['name'] ?>"
data-confirm="确定要卸载此插件吗?卸载此插件将会删除所有和此插件有关的数据。此操作不可恢复!">
<i class="fas fa-trash-alt"></i>
</button>
<?php else: ?>
<button class="px-3 py-1.5 rounded border border-blue-200 bg-blue-50 text-blue-700 hover:bg-blue-100 text-sm transition-colors btn-effect"
data-action="install"
data-url="/admin/plugins/install/<?= $plugin['name'] ?>">
安装
</button>
<button class="px-3 py-1.5 rounded border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 text-sm transition-colors btn-effect"
data-action="delete"
data-url="/admin/plugins/delete/<?= $plugin['name'] ?>"
data-confirm="确定要删除此插件安装包吗?">
<i class="fas fa-trash-alt"></i>
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<div class="mt-8 bg-blue-50 border border-blue-100 rounded-xl p-5">
<div class="flex">
<i class="fas fa-info-circle text-primary mt-0.5 mr-3"></i>
<div>
<h3 class="font-medium text-primary mb-2">插件管理说明</h3>
<ul class="text-sm text-gray-700 space-y-1">
<li><i class="fas fa-angle-right mr-1 text-primary/70"></i> 插件以ZIP格式上传,系统会自动解压并安装</li>
<li><i class="fas fa-angle-right mr-1 text-primary/70"></i> 禁用插件不会删除数据,卸载插件将清除所有相关数据</li>
<li><i class="fas fa-angle-right mr-1 text-primary/70"></i> 未安装的插件可以直接删除安装包,不会影响系统</li>
<li><i class="fas fa-angle-right mr-1 text-primary/70"></i> 请只安装来自可信来源的插件,以确保系统安全</li>
</ul>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-action]').forEach(btn => {
btn.addEventListener('click', async () => {
const url = btn.dataset.url;
const confirmMsg = btn.dataset.confirm;
if (confirmMsg && !confirm(confirmMsg)) return;
try {
const res = await fetch(url, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
});
if (!res.ok) throw new Error('请求失败');
const json = await res.json();
if (json.success) {
showMessage(json.message, 'success');
setTimeout(() => {
location.reload();
}, 2000);
} else {
showMessage(json.message, 'error');
}
} catch (e) {
showMessage(e.message, 'warning');
}
});
});
// ========= 插件上传弹窗功能 =========
const uploadBtn = document.getElementById('uploadBtn');
const emptyStateUploadBtn = document.getElementById('emptyStateUploadBtn');
const uploadForm = document.getElementById('uploadForm');
const uploadOverlay = document.getElementById('uploadOverlay');
const cancelUpload = document.getElementById('cancelUpload');
const submitUpload = document.getElementById('submitUpload');
const pluginZip = document.getElementById('pluginZip');
const showUploadForm = () => {
uploadOverlay.classList.remove('hidden');
uploadForm.classList.remove('hidden');
setTimeout(() => {
uploadOverlay.classList.add('opacity-100');
uploadForm.classList.remove('scale-95', 'opacity-0');
uploadForm.classList.add('scale-100', 'opacity-100');
}, 10);
document.body.style.overflow = 'hidden';
};
const hideUploadForm = () => {
uploadOverlay.classList.remove('opacity-100');
uploadForm.classList.remove('scale-100', 'opacity-100');
uploadForm.classList.add('scale-95', 'opacity-0');
setTimeout(() => {
uploadOverlay.classList.add('hidden');
uploadForm.classList.add('hidden');
document.body.style.overflow = '';
pluginZip.value = '';
}, 300);
};
const handleUpload = () => {
if (!pluginZip.files.length) return;
const file = pluginZip.files[0];
if (!file.name.endsWith('.zip')) return;
submitUpload.disabled = true;
submitUpload.textContent = '上传中...';
const formData = new FormData();
formData.append('plugin_zip', file);
fetch('/admin/plugins/upload', {
method: 'POST',
body: formData
}).then(response => response.json())
.then(data => {
if (data.success) {
showMessage(data.message, 'success');
setTimeout(() => {
location.reload();
}, 2000);
} else {
alert(data.message || '安装失败');
submitUpload.disabled = false;
submitUpload.textContent = '安装插件';
}
})
.catch(() => {
alert('网络错误');
submitUpload.disabled = false;
submitUpload.textContent = '安装插件';
});
};
if (uploadBtn) uploadBtn.addEventListener('click', showUploadForm);
if (emptyStateUploadBtn) emptyStateUploadBtn.addEventListener('click', showUploadForm);
if (cancelUpload) cancelUpload.addEventListener('click', hideUploadForm);
if (submitUpload) submitUpload.addEventListener('click', handleUpload);
if (uploadOverlay) uploadOverlay.addEventListener('click', hideUploadForm);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !uploadForm.classList.contains('hidden')) {
hideUploadForm();
}
});
});
</script>
+60
View File
@@ -0,0 +1,60 @@
<div class="p-6">
<h2 class="text-2xl font-bold mb-4">📊 数据报表</h2>
<!-- 日期筛选 -->
<div class="bg-white rounded-xl p-4 shadow mb-6 flex items-center gap-4 flex-wrap">
<label class="text-sm text-gray-400">起始:</label><input type="date" value="<?=$dateFrom?>" id="dateFrom" class="border rounded px-3 py-2 text-sm">
<label class="text-sm text-gray-400">截止:</label><input type="date" value="<?=$dateTo?>" id="dateTo" class="border rounded px-3 py-2 text-sm">
<button onclick="location.href='/admin/reports?from='+document.getElementById('dateFrom').value+'&to='+document.getElementById('dateTo').value" class="px-4 py-2 bg-blue-500 text-white rounded text-sm">筛选</button>
</div>
<!-- 总览卡片 -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">总投注</div><div class="text-xl font-bold text-blue-500"><?=number_format($totalBet,2)?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">总中奖</div><div class="text-xl font-bold text-red-500"><?=number_format($totalWin,2)?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">平台盈利</div><div class="text-xl font-bold <?=$profit>=0?'text-green-500':'text-red-500'?>"><?=number_format($profit,2)?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">佣金</div><div class="text-xl font-bold text-orange-500"><?=number_format($totalCommission,2)?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">总充值</div><div class="text-xl font-bold text-green-500"><?=number_format($totalDeposit,2)?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">总提现</div><div class="text-xl font-bold text-red-500"><?=number_format($totalWithdraw,2)?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">总用户</div><div class="text-xl font-bold"><?=$totalUsers?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">新增用户</div><div class="text-xl font-bold text-blue-500"><?=$newUsers?></div></div>
</div>
<!-- 每日明细 -->
<div class="bg-white rounded-xl shadow overflow-hidden mb-6">
<h3 class="font-bold p-4 border-b">每日明细</h3>
<table class="w-full text-sm">
<thead class="bg-gray-50"><tr><th class="px-4 py-2 text-left">日期</th><th class="px-4 py-2">投注</th><th class="px-4 py-2">中奖</th><th class="px-4 py-2">盈利</th><th class="px-4 py-2">充值</th><th class="px-4 py-2">提现</th></tr></thead>
<tbody>
<?php foreach($dailyStats??[] as $d): $dp=$d['bets']-$d['wins']; ?>
<tr class="border-t hover:bg-gray-50">
<td class="px-4 py-2"><?=$d['date']?></td>
<td class="px-4 py-2 text-center"><?=number_format($d['bets'],2)?></td>
<td class="px-4 py-2 text-center"><?=number_format($d['wins'],2)?></td>
<td class="px-4 py-2 text-center <?=$dp>=0?'text-green-500':'text-red-500'?>"><?=number_format($dp,2)?></td>
<td class="px-4 py-2 text-center"><?=number_format($d['deposits'],2)?></td>
<td class="px-4 py-2 text-center"><?=number_format($d['withdraws'],2)?></td>
</tr>
<?php endforeach; ?>
</tbody></table></div>
<!-- 代理报表 -->
<?php if(!empty($agentStats)): ?>
<div class="bg-white rounded-xl shadow overflow-hidden">
<h3 class="font-bold p-4 border-b">代理报表</h3>
<table class="w-full text-sm">
<thead class="bg-gray-50"><tr><th class="px-4 py-2 text-left">代理</th><th class="px-4 py-2">玩家数</th><th class="px-4 py-2">投注</th><th class="px-4 py-2">中奖</th><th class="px-4 py-2">佣金</th><th class="px-4 py-2">盈利</th></tr></thead>
<tbody>
<?php foreach($agentStats as $as): ?>
<tr class="border-t hover:bg-gray-50">
<td class="px-4 py-2"><?=htmlspecialchars($as['user']['username']??'')?></td>
<td class="px-4 py-2 text-center"><?=$as['players']?></td>
<td class="px-4 py-2 text-center"><?=number_format($as['bets'],2)?></td>
<td class="px-4 py-2 text-center"><?=number_format($as['wins'],2)?></td>
<td class="px-4 py-2 text-center text-orange-500"><?=number_format($as['commission'],2)?></td>
<td class="px-4 py-2 text-center <?=$as['profit']>=0?'text-green-500':'text-red-500'?>"><?=number_format($as['profit'],2)?></td>
</tr>
<?php endforeach; ?>
</tbody></table></div>
<?php endif; ?>
</div>
+388
View File
@@ -0,0 +1,388 @@
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
<i class="fas fa-cog text-primary mr-3"></i>
系统设置
</h1>
<!-- 设置表单 -->
<form id="settingsForm" class="space-y-6">
<!-- Logo设置 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-image text-primary mr-2"></i>
Logo设置
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Logo -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">网站Logo</label>
<div class="flex items-center gap-4">
<div class="flex-shrink-0">
<img id="site_logo_preview" src="<?= htmlspecialchars($settings['site_logo'] ?? '') ?>"
alt="Logo预览"
class="w-32 h-32 object-contain border border-gray-200 rounded-lg bg-gray-50 p-2"
style="<?= empty($settings['site_logo'] ?? '') ? 'display:none;' : '' ?>">
</div>
<div class="flex-1">
<input type="file" id="site_logo_file" name="site_logo_file" accept="image/jpeg,image/png,image/gif,image/webp"
class="hidden" onchange="handleLogoUpload(this, 'site_logo')">
<button type="button" onclick="document.getElementById('site_logo_file').click()"
class="w-full px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors">
<i class="fas fa-upload mr-2"></i>选择图片
</button>
<input type="hidden" id="site_logo" name="site_logo" value="<?= htmlspecialchars($settings['site_logo'] ?? '') ?>">
<p class="text-xs text-gray-500 mt-2">建议尺寸:200x60px,支持JPG/PNG/GIF/WEBP,前端统一使用此Logo</p>
</div>
</div>
</div>
<!-- Favicon -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">网站图标 (Favicon)</label>
<div class="flex items-center gap-4">
<div class="flex-shrink-0">
<img id="site_favicon_preview" src="<?= htmlspecialchars($settings['site_favicon'] ?? '') ?>"
alt="Favicon预览"
class="w-16 h-16 object-contain border border-gray-200 rounded-lg bg-gray-50 p-2"
style="<?= empty($settings['site_favicon'] ?? '') ? 'display:none;' : '' ?>">
</div>
<div class="flex-1">
<input type="file" id="site_favicon_file" name="site_favicon_file" accept="image/x-icon,image/vnd.microsoft.icon,image/png"
class="hidden" onchange="handleLogoUpload(this, 'site_favicon')">
<button type="button" onclick="document.getElementById('site_favicon_file').click()"
class="w-full px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors">
<i class="fas fa-upload mr-2"></i>选择图标
</button>
<input type="hidden" id="site_favicon" name="site_favicon" value="<?= htmlspecialchars($settings['site_favicon'] ?? '') ?>">
<p class="text-xs text-gray-500 mt-2">建议尺寸:32x32px,支持ICO/PNG格式</p>
</div>
</div>
</div>
</div>
</div>
<!-- 网站基本信息 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-info-circle text-primary mr-2"></i>
网站基本信息
</h3>
<div class="space-y-4">
<div>
<label for="site_title" class="block text-sm font-medium text-gray-700 mb-1">网站标题</label>
<input type="text" id="site_title" name="site_title"
value="<?= htmlspecialchars($settings['site_title'] ?? '') ?>"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="请输入网站标题">
</div>
<div>
<label for="site_description" class="block text-sm font-medium text-gray-700 mb-1">网站描述</label>
<textarea id="site_description" name="site_description" rows="3"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors resize-none"
placeholder="请输入网站描述"><?= htmlspecialchars($settings['site_description'] ?? '') ?></textarea>
<p class="text-xs text-gray-500 mt-1">用于SEO优化,建议控制在150字以内</p>
</div>
<div>
<label for="site_keywords" class="block text-sm font-medium text-gray-700 mb-1">网站关键词</label>
<input type="text" id="site_keywords" name="site_keywords"
value="<?= htmlspecialchars($settings['site_keywords'] ?? '') ?>"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="请输入关键词,用逗号分隔">
<p class="text-xs text-gray-500 mt-1">多个关键词用逗号分隔</p>
</div>
<div>
<label for="site_copyright" class="block text-sm font-medium text-gray-700 mb-1">版权信息</label>
<input type="text" id="site_copyright" name="site_copyright"
value="<?= htmlspecialchars($settings['site_copyright'] ?? '') ?>"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="请输入版权信息">
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex justify-end gap-3 pt-4">
<button type="button" onclick="resetSettings()"
class="px-6 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors">
<i class="fas fa-undo mr-2"></i>重置
</button>
<button type="button" id="saveSettingsBtn"
class="px-6 py-2 bg-primary hover:bg-primary/90 text-white rounded-lg shadow hover:shadow-md transition-all duration-200">
<i class="fas fa-save mr-2"></i>保存设置
</button>
</div>
<!-- SMTP 邮件配置 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-envelope text-primary mr-2"></i>
SMTP 邮件配置
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">SMTP 服务器</label>
<input type="text" id="smtp_host" name="smtp_host" value="<?= htmlspecialchars($settings['smtp_host'] ?? '') ?>"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="如 smtp.gmail.com">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">端口</label>
<input type="number" id="smtp_port" name="smtp_port" value="<?= htmlspecialchars($settings['smtp_port'] ?? '465') ?>"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="465">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">加密方式</label>
<select id="smtp_encryption" name="smtp_encryption"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors">
<option value="ssl" <?= ($settings['smtp_encryption'] ?? 'ssl') === 'ssl' ? 'selected' : '' ?>>SSL (端口465)</option>
<option value="tls" <?= ($settings['smtp_encryption'] ?? '') === 'tls' ? 'selected' : '' ?>>TLS (端口587)</option>
<option value="none" <?= ($settings['smtp_encryption'] ?? '') === 'none' ? 'selected' : '' ?>>无加密 (端口25)</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">SMTP 用户名</label>
<input type="text" id="smtp_user" name="smtp_user" value="<?= htmlspecialchars($settings['smtp_user'] ?? '') ?>"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="your@email.com">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">SMTP 密码</label>
<input type="password" id="smtp_pass" name="smtp_pass" value="<?= htmlspecialchars($settings['smtp_pass'] ?? '') ?>"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="密码或应用专用密码">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">发件人地址</label>
<input type="text" id="smtp_from" name="smtp_from" value="<?= htmlspecialchars($settings['smtp_from'] ?? '') ?>"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="noreply@yourdomain.com(留空则用SMTP用户名)">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">发件人名称</label>
<input type="text" id="smtp_from_name" name="smtp_from_name" value="<?= htmlspecialchars($settings['smtp_from_name'] ?? 'PK10') ?>"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="PK10">
</div>
<div class="flex items-end">
<button type="button" onclick="testSmtp()"
class="w-full px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors">
<i class="fas fa-paper-plane mr-2"></i>发送测试邮件
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-3">
<i class="fas fa-info-circle mr-1"></i>
常用配置:Gmail(smtp.gmail.com:465/SSL)、Outlook(smtp-mail.outlook.com:587/TLS)、QQ邮箱(smtp.qq.com:465/SSL)、163邮箱(smtp.163.com:465/SSL)
</p>
</div>
</form>
<script>
function testSmtp(){
const email=prompt('输入接收测试邮件的邮箱地址:');
if(!email)return;
// 先保存当前配置再测试
const data={};
document.querySelectorAll('#settingsForm [name^="smtp_"]').forEach(el=>{data[el.name]=el.value;});
fetch('/admin/settings/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)})
.then(()=>fetch('/admin/settings/smtp-test',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email})}))
.then(r=>r.json())
.then(d=>{showMessage(d.message,d.success?'success':'error');})
.catch(()=>{showMessage('测试失败','error');});
}
</script>
<script>
// Logo上传处理
function handleLogoUpload(input, logoType) {
const file = input.files[0];
if (!file) return;
// 验证文件大小(5MB
if (file.size > 5 * 1024 * 1024) {
alert('文件大小不能超过5MB');
input.value = '';
return;
}
// 验证文件类型
const allowedTypes = logoType === 'site_favicon'
? ['image/x-icon', 'image/vnd.microsoft.icon', 'image/png']
: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
alert('不支持的文件类型');
input.value = '';
return;
}
// 预览图片
const reader = new FileReader();
reader.onload = function(e) {
const previewId = logoType + '_preview';
const previewImg = document.getElementById(previewId);
if (previewImg) {
previewImg.src = e.target.result;
previewImg.style.display = '';
}
};
reader.readAsDataURL(file);
// 立即上传
uploadLogo(file, logoType);
}
// 上传Logo到服务器
function uploadLogo(file, logoType) {
const formData = new FormData();
formData.append(logoType + '_file', file);
formData.append('logo_type', logoType);
// 显示上传中状态
const saveBtn = document.getElementById('saveSettingsBtn');
const originalText = saveBtn.innerHTML;
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>上传中...';
fetch('/admin/settings/save', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// 更新隐藏字段的值
const hiddenInput = document.getElementById(logoType);
const previewImg = document.getElementById(logoType + '_preview');
if (data.data && data.data[logoType]) {
if (hiddenInput) hiddenInput.value = data.data[logoType];
if (previewImg) {
previewImg.src = data.data[logoType];
previewImg.style.display = '';
}
}
// 显示成功提示
showMessage('Logo上传成功', 'success');
} else {
alert('上传失败: ' + (data.message || '未知错误'));
}
})
.catch(error => {
console.error('Upload error:', error);
alert('上传失败,请稍后重试');
})
.finally(() => {
saveBtn.disabled = false;
saveBtn.innerHTML = originalText;
});
}
// 保存设置
document.getElementById('saveSettingsBtn').addEventListener('click', function() {
const form = document.getElementById('settingsForm');
const formData = new FormData(form);
// 将FormData转换为JSON对象
const data = {};
for (let [key, value] of formData.entries()) {
// 跳过文件输入
if (key.endsWith('_file')) continue;
data[key] = value;
}
// 发送保存请求
const saveBtn = this;
const originalText = saveBtn.innerHTML;
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>保存中...';
fetch('/admin/settings/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
showMessage('设置保存成功', 'success');
// 如果有返回的数据,更新页面
if (data.data) {
updateSettingsPreview(data.data);
}
} else {
showMessage('保存失败: ' + (data.message || '未知错误'), 'error');
}
})
.catch(error => {
console.error('Save error:', error);
showMessage('保存失败,请稍后重试', 'error');
})
.finally(() => {
saveBtn.disabled = false;
saveBtn.innerHTML = originalText;
});
});
// 重置设置
function resetSettings() {
if (confirm('确定要重置所有设置吗?')) {
location.reload();
}
}
// 更新预览
function updateSettingsPreview(settings) {
const logoPreview = document.getElementById('site_logo_preview');
if (logoPreview) {
if (settings.site_logo) {
logoPreview.src = settings.site_logo;
logoPreview.style.display = '';
} else {
logoPreview.src = '';
logoPreview.style.display = 'none';
}
}
const faviconPreview = document.getElementById('site_favicon_preview');
if (faviconPreview) {
if (settings.site_favicon) {
faviconPreview.src = settings.site_favicon;
faviconPreview.style.display = '';
} else {
faviconPreview.src = '';
faviconPreview.style.display = 'none';
}
}
}
// 显示消息提示
function showMessage(message, type = 'success') {
// 创建提示元素
const messageEl = document.createElement('div');
messageEl.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 ${
type === 'success' ? 'bg-green-500 text-white' : 'bg-red-500 text-white'
}`;
messageEl.innerHTML = `
<div class="flex items-center">
<i class="fas ${type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'} mr-2"></i>
<span>${message}</span>
</div>
`;
document.body.appendChild(messageEl);
// 3秒后自动移除
setTimeout(() => {
messageEl.remove();
}, 3000);
}
// 页面加载时获取设置
document.addEventListener('DOMContentLoaded', function() {
// 设置已经在页面加载时通过PHP变量传递,无需额外请求
});
</script>
+767
View File
@@ -0,0 +1,767 @@
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
<i class="fa fa-users text-primary mr-3"></i>
用户管理中心
</h1>
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
<!-- 搜索和操作区 -->
<div class="flex justify-between items-center mb-6 gap-4">
<h2 class="text-xl font-semibold text-gray-700 whitespace-nowrap">用户列表</h2>
<div class="flex gap-3">
<!-- 新增用户按钮 -->
<button id="openFormBtn" class="bg-primary hover:bg-primary/90 text-white px-5 py-2.5 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center whitespace-nowrap">
<i class="fa fa-plus mr-2"></i>
<span>新增用户</span>
</button>
</div>
</div>
<!-- 用户列表表格 -->
<div class="overflow-x-auto">
<table class="w-full bg-white rounded-xl shadow-md overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用户信息</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider ">角色</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">余额</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">注册时间</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200" id="userList">
<?php if (!empty($users) && is_array($users)): ?>
<?php foreach ($users as $user): ?>
<tr class="hover:bg-gray-50 transition-colors" data-id="<?php echo $user['id']; ?>">
<!-- 用户名和头像单元格 - 自适应宽度 -->
<td class="px-4 py-4 whitespace-nowrap">
<div class="flex items-center gap-3">
<img src="<?php
if (!empty($user['avatar'])) {
echo htmlspecialchars($user['avatar']);
} else {
// 管理员与普通用户使用不同CDN头像
echo $user['role'] === 'admin'
? "https://robohash.org/admin" . $user['id'] . "?size=40x40"
: "https://robohash.org/user" . $user['id'] . "?size=40x40";
}
?>"
alt="用户头像" class="w-10 h-10 rounded-full object-cover border border-gray-200 flex-shrink-0">
<div class="min-w-0 flex-1">
<div class="text-sm font-medium text-gray-900 truncate"><?php echo htmlspecialchars($user['username']); ?></div>
<div class="text-xs text-gray-500 truncate"><?php echo htmlspecialchars($user['email']); ?></div>
</div>
</div>
</td>
<!-- 角色单元格 - 自适应宽度 -->
<td class="px-4 py-4 whitespace-nowrap">
<?php
if ($user['role'] === 'admin') {
$roleClass = 'bg-red-100 text-red-800';
$roleText = '管理员';
} else {
$roleClass = 'bg-blue-100 text-blue-800';
$roleText = '平台用户';
}
?>
<span class="inline-block px-2 py-1 text-xs rounded-full <?php echo $roleClass; ?>">
<?php echo $roleText; ?>
</span>
</td>
<!-- 余额单元格 - 自适应宽度 -->
<td class="px-4 py-4 whitespace-nowrap">
<div class="flex items-center gap-2">
<span class="text-sm font-semibold text-green-600">
<?php echo number_format($user['balance'] ?? 0, 2, ',', '.'); ?>
</span>
<div class="flex gap-1">
<button class="balance-increase-btn text-green-500 hover:text-green-700 text-xs"
data-id="<?php echo $user['id']; ?>"
data-username="<?php echo htmlspecialchars($user['username']); ?>"
title="增加余额">
<i class="fa fa-plus-circle"></i>
</button>
<button class="balance-decrease-btn text-red-500 hover:text-red-700 text-xs"
data-id="<?php echo $user['id']; ?>"
data-username="<?php echo htmlspecialchars($user['username']); ?>"
title="减少余额">
<i class="fa fa-minus-circle"></i>
</button>
</div>
</div>
</td>
<!-- 创建时间 - 自适应宽度 -->
<td class="px-4 py-4 whitespace-nowrap hidden sm:table-cell">
<div class="text-sm text-gray-500"><?php echo date('Y-m-d H:i', strtotime($user['created_at'])); ?></div>
</td>
<!-- 状态 - 自适应宽度 -->
<td class="px-4 py-4 whitespace-nowrap">
<?php
$statusClass = $user['status'] ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
$statusText = $user['status'] ? '启用' : '停用';
?>
<span class="inline-block px-2 py-1 text-xs rounded-full <?php echo $statusClass; ?>">
<?php echo $statusText; ?>
</span>
</td>
<!-- 操作按钮 - 自适应宽度 -->
<td class="px-4 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex items-center justify-end gap-2">
<button class="view-btn text-gray-500 hover:text-purple-500"
data-id="<?php echo $user['id']; ?>" title="查看详情">
<i class="fa fa-eye"></i>
</button>
<button class="edit-btn text-gray-500 hover:text-blue-500"
data-id="<?php echo $user['id']; ?>" title="编辑">
<i class="fa fa-pencil"></i>
</button>
<button class="delete-btn text-gray-500 hover:text-red-500"
data-id="<?php echo $user['id']; ?>" title="删除">
<i class="fa fa-trash"></i>
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="8" class="px-6 py-12 text-center">
<div class="flex flex-col items-center">
<i class="fa fa-users text-gray-300 text-5xl mb-4"></i>
<h3 class="text-lg font-medium text-gray-900">没有找到用户</h3>
<p class="mt-1 text-gray-500">尝试调整筛选条件或添加新用户</p>
<button class="mt-4 bg-primary hover:bg-primary/90 text-white px-5 py-2 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center"
onclick="openFormModal()">
<i class="fa fa-plus mr-2"></i>
<span>添加新用户</span>
</button>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- 分页控件 -->
<div class="flex justify-between items-center mt-6">
<p class="text-sm text-gray-500">显示 1 至 <?php echo min(10, count($users ?? [])); ?> 条,共 <?php echo count($users ?? []); ?> 条</p>
</div>
</div>
<!-- 用户表单弹窗背景 -->
<div id="formBackdrop" class="fixed inset-0 bg-black/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 z-40"></div>
<!-- 用户表单弹窗 -->
<div id="formModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-events-none pointer-none transition transition-all duration-300 scale-95">
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-hidden">
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
<h3 id="formTitle" class="text-xl font-bold text-gray-800 flex items-center">
<i class="fa fa-plus-circle text-primary mr-2"></i>
创建新用户
</h3>
<button id="closeFormBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
<i class="fa fa-times"></i>
</button>
</div>
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-130px)]">
<form id="userForm" class="space-y-5">
<input type="hidden" id="userId" name="id">
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">用户名 <span class="text-red-500">*</span></label>
<input type="text" id="username" name="username" required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="请输入用户名">
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">邮箱 <span class="text-red-500">*</span></label>
<input type="email" id="email" name="email" required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="请输入邮箱地址">
</div>
<!-- 用户表单表单弹窗中密码字段部分修改 -->
<div id="passwordField">
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">
密码 <span class="text-red-500">*</span>
</label>
<input type="password" id="password" name="password" required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="请输入密码">
<p class="mt-1 text-xs text-gray-500">密码长度至少8位,包含字母和数字</p>
</div>
<!-- 用户角色固定为普通用户 -->
<input type="hidden" id="role" name="role" value="user">
<div>
<label class="flex items-center">
<input type="checkbox" id="status" name="status" value="1" checked
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary">
<span class="ml-2 text-sm text-gray-700">启用用户</span>
</label>
</div>
</form>
</div>
<div class="border-t border-gray-100 px-6 py-4 flex justify-end gap-3">
<button id="cancelBtn" class="px-5 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors">
取消
</button>
<button id="submitBtn" type="button"
class="bg-primary hover:bg-primary/90 text-white px-5 py-2 rounded-lg shadow hover:shadow-md transition-all duration-200">
保存用户
</button>
</div>
</div>
</div>
<!-- 用户详情弹窗 -->
<div id="detailModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-none transition-all duration-300 scale-95">
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-hidden">
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
<h3 class="text-xl font-bold text-gray-800 flex items-center">
<i class="fa fa-user-circle text-primary mr-2"></i>
用户详情
</h3>
<button id="closeDetailBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
<i class="fa fa-times"></i>
</button>
</div>
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-100px)]">
<div class="flex flex-col items-center mb-6">
<img id="detailAvatar" src="https://picsum.photos/seed/user/100/100" alt="用户头像" class="w-24 h-24 rounded-full mb-4">
<h4 id="detailUsername" class="text-xl font-bold text-gray-800">用户名</h4>
<p id="detailRole" class="mt-1 px-3 py-1 text-sm rounded-full bg-green-100 text-green-800">普通用户</p>
</div>
<div class="space-y-4">
<div class="grid grid-cols-3 gap-4 items-center">
<span class="text-sm text-gray-500">ID</span>
<span id="detailId" class="col-span-2 text-gray-800">--</span>
</div>
<div class="w-full h-px bg-gray-100"></div>
<div class="grid grid-cols-3 gap-4 items-center">
<span class="text-sm text-gray-500">余额</span>
<span id="detailBalance" class="col-span-2 text-green-600 font-semibold">--</span>
</div>
<div class="w-full h-px bg-gray-100"></div>
<div class="grid grid-cols-3 gap-4 items-center">
<span class="text-sm text-gray-500">邮箱</span>
<span id="detailEmail" class="col-span-2 text-gray-800">--</span>
</div>
<div class="w-full h-px bg-gray-100"></div>
<div class="grid grid-cols-3 gap-4 items-center">
<span class="text-sm text-gray-500">状态</span>
<span id="detailStatus" class="col-span-2">
<span class="inline-block px-2 py-1 text-xs rounded-full bg-green-100 text-green-800">启用</span>
</span>
</div>
<div class="w-full h-px bg-gray-100"></div>
<div class="grid grid-cols-3 gap-4 items-center">
<span class="text-sm text-gray-500">创建时间</span>
<span id="detailCreatedAt" class="col-span-2 text-gray-800">--</span>
</div>
<div class="w-full h-px bg-gray-100"></div>
<div class="grid grid-cols-3 gap-4 items-center">
<span class="text-sm text-gray-500">最后登录</span>
<span id="detailLastLogin" class="col-span-2 text-gray-800">--</span>
</div>
</div>
</div>
<div class="border-t border-gray-100 px-6 py-4 flex justify-end">
<button id="closeDetailBtn2" class="px-5 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors">
关闭
</button>
</div>
</div>
</div>
<script>
console.log('用户管理JS加载完成');
document.addEventListener('DOMContentLoaded', function() {
// 缓存DOM元素
const formModal = document.getElementById('formModal');
const formBackdrop = document.getElementById('formBackdrop');
const detailModal = document.getElementById('detailModal');
const openFormBtn = document.getElementById('openFormBtn');
const closeFormBtn = document.getElementById('closeFormBtn');
const cancelBtn = document.getElementById('cancelBtn');
const submitBtn = document.getElementById('submitBtn');
const formTitle = document.getElementById('formTitle');
const userForm = document.getElementById('userForm');
const userList = document.getElementById('userList');
const passwordField = document.getElementById('passwordField');
const closeDetailBtn = document.getElementById('closeDetailBtn');
const closeDetailBtn2 = document.getElementById('closeDetailBtn2');
const searchInput = document.getElementById('searchInput');
// 检查元素是否存在
function checkElements() {
const elements = [
formModal, formBackdrop, openFormBtn,
closeFormBtn, cancelBtn, submitBtn
];
const missing = elements.filter(el => !el);
if (missing.length > 0) {
console.error('缺少必要的DOM元素,功能无法正常工作');
return false;
}
return true;
}
// 显示表单弹窗
function openFormModal() {
if (!checkElements()) return;
resetForm();
formModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
formModal.classList.add('scale-100');
formBackdrop.classList.remove('opacity-0', 'pointer-events-none');
document.body.style.overflow = 'hidden';
void formModal.offsetWidth; // 强制重绘
}
// 隐藏表单弹窗
function closeFormModal() {
if (!checkElements()) return;
formModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
formModal.classList.remove('scale-100');
formBackdrop.classList.add('opacity-0', 'pointer-events-none');
document.body.style.overflow = '';
}
// 显示详情弹窗
function openDetailModal() {
detailModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
detailModal.classList.add('scale-100');
formBackdrop.classList.remove('opacity-0', 'pointer-events-none');
document.body.style.overflow = 'hidden';
void detailModal.offsetWidth;
}
// 隐藏详情弹窗
function closeDetailModal() {
detailModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
detailModal.classList.remove('scale-100');
formBackdrop.classList.add('opacity-0', 'pointer-events-none');
document.body.style.overflow = '';
}
// 重置表单(新增模式)
function resetForm() {
userForm.reset();
document.getElementById('userId').value = '';
formTitle.innerHTML = '<i class="fa fa-plus-circle text-primary mr-2"></i> 创建新用户';
// 新增模式:密码必填设置
const passwordLabel = document.querySelector('#passwordField label');
const passwordInput = document.getElementById('password');
passwordLabel.innerHTML = '密码 <span class="text-red-500">*</span>';
passwordInput.required = true;
passwordInput.placeholder = '请输入密码';
passwordField.style.display = 'block';
submitBtn.innerHTML = '保存用户';
submitBtn.disabled = false;
}
// 加载用户数据(编辑模式)
async function loadUserData(id) {
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 加载中...';
try {
const response = await fetch(`/admin/users/${id}`);
if (!response.ok) throw new Error('获取数据失败');
const data = await response.json();
if (data.success && data.data) {
const { id, username, email, status } = data.data;
document.getElementById('userId').value = id;
document.getElementById('username').value = username || '';
document.getElementById('email').value = email || '';
// 角色固定为普通用户,无需设置
document.getElementById('status').checked = status == 1;
formTitle.innerHTML = '<i class="fa fa-pencil text-primary mr-2"></i> 编辑用户';
// 编辑模式:密码可选设置
const passwordLabel = document.querySelector('#passwordField label');
const passwordInput = document.getElementById('password');
passwordLabel.innerHTML = '密码(不填则不修改)';
passwordInput.required = false;
passwordInput.placeholder = '不修改密码请留空';
passwordField.style.display = 'block';
} else {
throw new Error(data.message || '获取数据失败');
}
} catch (e) {
showMessage(e.message, 'error');
closeFormModal();
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = '保存用户';
}
}
// 加载用户详情
async function loadUserDetail(id) {
try {
const response = await fetch(`/admin/users/${id}`);
if (!response.ok) throw new Error('获取详情失败');
const data = await response.json();
if (data.success && data.data) {
const { id, username, email, role, status, created_at, last_login, avatar, balance } = data.data;
// 填充详情数据
document.getElementById('detailId').textContent = id;
document.getElementById('detailUsername').textContent = username || '未知用户';
document.getElementById('detailEmail').textContent = email || '未设置';
// 格式化余额为越南盾格式(点号分隔千位,逗号分隔小数)
var balanceValue = balance !== undefined ? parseFloat(balance) : 0;
var parts = balanceValue.toFixed(2).split('.');
var integerPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, '.');
var formattedBalance = integerPart + ',' + parts[1];
document.getElementById('detailBalance').textContent = formattedBalance;
document.getElementById('detailCreatedAt').textContent = created_at ? new Date(created_at).toLocaleString() : '未知';
document.getElementById('detailLastLogin').textContent = last_login ? new Date(last_login).toLocaleString() : '从未登录';
document.getElementById('detailAvatar').src = avatar || `https://picsum.photos/seed/user${id}/100/100`;
// 设置角色标签样式
let roleClass = 'bg-green-100 text-green-800';
let roleText = '平台用户';
if (role === 'admin') {
roleClass = 'bg-red-100 text-red-800';
roleText = '管理员';
}
document.getElementById('detailRole').className = `mt-1 px-3 py-1 text-sm rounded-full ${roleClass}`;
document.getElementById('detailRole').textContent = roleText;
// 设置状态标签样式
const statusClass = status ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
const statusText = status ? '启用' : '禁用';
document.getElementById('detailStatus').innerHTML =
`<span class="inline-block px-2 py-1 text-xs rounded-full ${statusClass}">${statusText}</span>`;
openDetailModal();
} else {
throw new Error(data.message || '获取详情失败');
}
} catch (e) {
showMessage(e.message, 'error');
}
}
// 表单验证
function validateForm() {
const username = document.getElementById('username').value.trim();
const email = document.getElementById('email').value.trim();
const password = document.getElementById('password').value.trim();
const isEditMode = !!document.getElementById('userId').value;
if (!username) {
showMessage('请输入用户名', 'error');
return false;
}
if (!email) {
showMessage('请输入邮箱地址', 'error');
return false;
}
// 验证邮箱格式
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
showMessage('请输入有效的邮箱地址', 'error');
return false;
}
// 仅在新增或编辑时填写了密码的情况下验证长度
if ((!isEditMode || password) && password.length < 8) {
showMessage('密码长度至少8位', 'error');
return false;
}
return true;
}
// 提交表单(创建/更新)
async function submitFormData() {
if (!validateForm()) return;
const formData = new FormData(userForm);
const isEditMode = !!document.getElementById('userId').value;
const statusCheckbox = document.getElementById('status');
formData.delete('status'); // 先除可能存在的旧值
formData.append('status', statusCheckbox.checked ? '1' : '0');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 保存中...';
try {
const response = await fetch('/admin/users/update', {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
if (data.success) {
showMessage(isEditMode ? '用户更新成功' : '用户创建成功');
closeFormModal();
setTimeout(() => location.reload(), 1000);
} else {
throw new Error(data.message || (isEditMode ? '更新失败' : '创建失败'));
}
} catch (e) {
showMessage(e.message, 'error');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = '保存用户';
}
}
// 删除用户
async function deleteUser(id) {
if (!confirm('确定要删除该用户吗?此操作不可恢复!')) return;
try {
const response = await fetch(`/admin/users/delete/${id}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
showMessage('用户已删除');
// 移除DOM元素
const row = document.querySelector(`tr[data-id="${id}"]`);
if (row) {
row.remove();
// 检查是否还有数据行
const rows = userList.querySelectorAll('tr:not(:last-child)');
if (rows.length === 0) {
userList.innerHTML = `
<tr>
<td colspan="8" class="px-6 py-10 text-center text-gray-500 border border-dashed border-gray-200">
<div>
<i class="fa fa-info-circle text-2xl mb-2 text-gray-300"></i>
<p>暂无用户数据</p>
</div>
</td>
</tr>`;
}
}
} else {
throw new Error(data.message || '删除失败');
}
} catch (e) {
showMessage(e.message, 'error');
}
}
// 绑定事件
if (checkElements()) {
// 打开表单
openFormBtn.addEventListener('click', openFormModal);
// 关闭表单
closeFormBtn.addEventListener('click', closeFormModal);
cancelBtn.addEventListener('click', closeFormModal);
formBackdrop.addEventListener('click', () => {
if (!formModal.classList.contains('invisible')) closeFormModal();
if (!detailModal.classList.contains('invisible')) closeDetailModal();
});
// 关闭详情
closeDetailBtn.addEventListener('click', closeDetailModal);
closeDetailBtn2.addEventListener('click', closeDetailModal);
// 提交表单
submitBtn.addEventListener('click', submitFormData);
// 编辑、删除、查看、余额操作事件委托
userList.addEventListener('click', function(e) {
const editBtn = e.target.closest('.edit-btn');
const deleteBtn = e.target.closest('.delete-btn');
const viewBtn = e.target.closest('.view-btn');
const increaseBtn = e.target.closest('.balance-increase-btn');
const decreaseBtn = e.target.closest('.balance-decrease-btn');
if (editBtn) {
const id = editBtn.getAttribute('data-id');
if (id) {
openFormModal();
setTimeout(() => loadUserData(id), 300);
}
} else if (deleteBtn) {
const id = deleteBtn.getAttribute('data-id');
if (id) deleteUser(id);
} else if (viewBtn) {
const id = viewBtn.getAttribute('data-id');
if (id) loadUserDetail(id);
} else if (increaseBtn) {
const id = increaseBtn.getAttribute('data-id');
const username = increaseBtn.getAttribute('data-username');
if (id) adjustBalance(id, username, 'increase');
} else if (decreaseBtn) {
const id = decreaseBtn.getAttribute('data-id');
const username = decreaseBtn.getAttribute('data-username');
if (id) adjustBalance(id, username, 'decrease');
}
});
// 余额调整功能
function adjustBalance(userId, username, action) {
const actionText = action === 'increase' ? '增加' : '减少';
const actionColor = action === 'increase' ? '#5FB878' : '#FF5722';
// 使用 layui 的 prompt 弹窗
layui.use('layer', function(){
var layer = layui.layer;
layer.prompt({
formType: 0, // 0=文本输入框
value: '',
title: actionText + '余额',
area: ['400px', 'auto'],
btn: ['确定', '取消'],
btnAlign: 'c',
yes: function(index, layero){
var amountInput = layero.find('input');
var amount = amountInput.val();
if (!amount || amount.trim() === '' || isNaN(amount) || parseFloat(amount) <= 0) {
layer.msg('请输入有效的金额', {icon: 2, time: 2000});
return;
}
var amountValue = parseFloat(amount);
// 格式化金额为越南盾格式(点号分隔千位,逗号分隔小数)
var parts = amountValue.toFixed(2).split('.');
var integerPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, '.');
var formattedAmount = integerPart + ',' + parts[1];
// 关闭输入弹窗
layer.close(index);
// 使用 layui 的 confirm 确认弹窗
layer.confirm(
'确定要' + actionText + '用户 <span style="color: #1890ff; font-weight: bold;">"' + username + '"</span> 的余额 <span style="color: ' + actionColor + '; font-weight: bold;">' + formattedAmount + '</span> 吗?',
{
icon: 3,
title: '确认操作',
btn: ['确定', '取消'],
btnAlign: 'c',
area: ['450px', 'auto']
},
function(confirmIndex){
// 执行余额调整
executeBalanceAdjust(userId, username, action, amountValue, actionText, layer, confirmIndex);
}
);
}
});
});
}
// 执行余额调整请求
async function executeBalanceAdjust(userId, username, action, amount, actionText, layer, confirmIndex) {
// 关闭确认弹窗
layer.close(confirmIndex);
// 显示加载提示
var loadIndex = layer.load(1, {
content: '正在处理...',
shade: [0.3, '#000']
});
try {
const response = await fetch(`/admin/users/balance/${userId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
amount: amount,
action: action
})
});
const data = await response.json();
// 关闭加载提示
layer.close(loadIndex);
if (data.success) {
layer.msg('余额' + actionText + '成功!', {
icon: 1,
time: 2000,
shade: 0.3
}, function(){
// 刷新页面以更新余额显示
location.reload();
});
} else {
layer.msg(data.message || actionText + '失败', {
icon: 2,
time: 3000
});
}
} catch (e) {
layer.close(loadIndex);
layer.msg('操作失败:' + e.message, {
icon: 2,
time: 3000
});
}
}
// ESC键关闭弹窗
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
if (!formModal.classList.contains('invisible')) closeFormModal();
if (!detailModal.classList.contains('invisible')) closeDetailModal();
}
});
// 阻止表单默认提交
userForm.addEventListener('submit', e => {
e.preventDefault();
submitFormData();
});
}
});
</script>
+39
View File
@@ -0,0 +1,39 @@
<div class="p-6">
<h2 class="text-2xl font-bold mb-4">👻 虚拟账户</h2>
<button onclick="document.getElementById('addModal').classList.remove('hidden')" class="mb-4 px-4 py-2 bg-blue-500 text-white rounded text-sm">+ 创建虚拟账户</button>
<div class="bg-white rounded-xl shadow overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50"><tr><th class="px-4 py-3 text-left">用户名</th><th class="px-4 py-3">余额</th><th class="px-4 py-3">创建时间</th><th class="px-4 py-3">操作</th></tr></thead>
<tbody>
<?php foreach($virtuals??[] as $v): ?>
<tr class="border-t hover:bg-gray-50">
<td class="px-4 py-2"><?=htmlspecialchars($v['username'])?> <span class="text-xs text-purple-500">虚拟</span></td>
<td class="px-4 py-2 text-center font-mono"><?=number_format($v['balance'],2)?></td>
<td class="px-4 py-2 text-xs text-gray-400"><?=$v['created_at']??''?></td>
<td class="px-4 py-2 text-center">
<button onclick="adjustBal(<?=$v['id']?>,'<?=htmlspecialchars($v['username'])?>')" class="text-blue-500 text-xs">±余额</button>
<button onclick="delVirt(<?=$v['id']?>)" class="text-red-500 text-xs ml-1">删除</button>
</td>
</tr>
<?php endforeach; ?>
</tbody></table></div>
<div id="addModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-xl p-6 w-full max-w-sm">
<h3 class="font-bold mb-4">创建虚拟账户</h3>
<div class="space-y-3">
<input id="vUser" placeholder="用户名" class="w-full border rounded px-3 py-2">
<input id="vPass" placeholder="密码 (默认: 123456)" class="w-full border rounded px-3 py-2">
<input id="vBal" type="number" placeholder="初始余额" value="100000" class="w-full border rounded px-3 py-2">
</div>
<div class="flex gap-2 mt-4">
<button onclick="createVirt()" class="flex-1 py-2 bg-blue-500 text-white rounded">创建</button>
<button onclick="document.getElementById('addModal').classList.add('hidden')" class="flex-1 py-2 bg-gray-200 rounded">取消</button>
</div>
</div></div>
</div>
<script>
async function createVirt(){const r=await fetch('/admin/virtual/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:document.getElementById('vUser').value,password:document.getElementById('vPass').value||'123456',balance:parseFloat(document.getElementById('vBal').value)})});const d=await r.json();if(d.status==='success')location.reload();else alert(d.message);}
async function adjustBal(id,name){const a=prompt('调整 '+name+' 的余额(正数=增加,负数=减少):');if(!a)return;const r=await fetch('/admin/virtual/balance',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,amount:parseFloat(a)})});const d=await r.json();if(d.status==='success')location.reload();else alert(d.message);}
async function delVirt(id){if(!confirm('确定删除?'))return;await fetch('/admin/virtual/delete/'+id,{method:'POST'});location.reload();}
</script>
+110
View File
@@ -0,0 +1,110 @@
<div class="p-6">
<h2 class="text-2xl font-bold mb-2">🌊 放水控制 & 投注限额</h2>
<p class="text-gray-500 text-sm mb-6">放水 = 控制玩家胜率。百分比越低,平台赢得越多。设为50%表示公平对赌。</p>
<?php
// 投注类型中文映射
$betTypeLabels = [
// PK10
'rank' => '🏎️ 名次(猜第N名是几号车)',
'bs' => '🔢 大小(名次车号 ≥6大 ≤5小)',
'oe' => '🎯 单双(名次车号的奇偶)',
'dt' => '🐉 龙虎(前名次 vs 后名次比大小)',
'sum' => '➕ 冠亚和值(冠军+亚军车号之和)',
'sum_bs' => '📊 冠亚和大小(和值≥12大 ≤11小)',
// 骰子
'tai' => '🎲 大(总点数 ≥11',
'xiu' => '🎲 小(总点数 ≤10',
'chan' => '🎲 双(总点数为偶数)',
'le' => '🎲 单(总点数为奇数)',
'number' => '🔢 押点数(猜总和具体数字)',
'dice' => '🎯 单骰(猜某颗骰子的点数)',
'combo' => '💎 豹子(三颗相同)',
// Xóc Đĩa
'even' => '⚪ 偶数红',
'odd' => '🔴 奇数红',
'4red' => '🔴🔴🔴🔴 四红',
'4white' => '⚪⚪⚪⚪ 四白',
'big_small' => '🎲 大小',
'odd_even' => '🎲 单双',
];
function getBetLabel($type, $labels) {
return $labels[$type] ?? $type;
}
?>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 放水配置 -->
<div class="bg-white rounded-xl p-6 shadow">
<h3 class="font-bold mb-2">胜率控制</h3>
<p class="text-gray-400 text-xs mb-4">数值含义:玩家赢的概率百分比。例如 40% = 玩家有40%概率赢,平台抽成约60%。</p>
<div class="space-y-3" id="waterList">
<?php foreach($configs??[] as $c): ?>
<div class="flex items-center gap-3 py-2 border-b">
<span class="flex-1 text-sm" title="<?=htmlspecialchars($c['bet_type'])?>"><?=getBetLabel($c['bet_type'], $betTypeLabels)?></span>
<input type="number" step="0.1" min="0" max="100" value="<?=$c['win_rate_pct']?>" class="w-20 border rounded px-2 py-1 text-sm text-center water-pct" data-type="<?=$c['bet_type']?>" data-game="<?=$c['game_id']?>">
<span class="text-xs text-gray-400">%</span>
<label class="flex items-center gap-1"><input type="checkbox" class="water-enabled" data-type="<?=$c['bet_type']?>" <?=$c['enabled']?'checked':''?>><span class="text-xs">启用</span></label>
</div>
<?php endforeach; ?>
<?php if(empty($configs)): ?>
<div class="text-gray-400 text-sm text-center py-4">暂无放水配置,请先在「游戏与赔率」中配置游戏赔率</div>
<?php endif; ?>
</div>
<button onclick="saveWater()" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm">💾 保存放水配置</button>
</div>
<!-- 限额配置 -->
<div class="bg-white rounded-xl p-6 shadow">
<h3 class="font-bold mb-2">投注限额</h3>
<p class="text-gray-400 text-xs mb-4">控制每种玩法的单笔最小/最大金额,以及每期总投注上限。</p>
<div class="space-y-3" id="limitList">
<?php foreach($limits??[] as $l): ?>
<div class="flex items-center gap-2 py-2 border-b flex-wrap">
<span class="w-full text-sm mb-1 font-medium" title="<?=htmlspecialchars($l['bet_type'])?>"><?=getBetLabel($l['bet_type'], $betTypeLabels)?></span>
<div class="flex items-center gap-1">
<span class="text-xs text-gray-400">单笔最小:</span>
<input type="number" value="<?=$l['min_amount']?>" class="w-24 border rounded px-2 py-1 text-sm text-center limit-min" data-type="<?=$l['bet_type']?>" data-game="<?=$l['game_id']?>">
</div>
<div class="flex items-center gap-1">
<span class="text-xs text-gray-400">单笔最大:</span>
<input type="number" value="<?=$l['max_amount']?>" class="w-24 border rounded px-2 py-1 text-sm text-center limit-max" data-type="<?=$l['bet_type']?>">
</div>
<div class="flex items-center gap-1">
<span class="text-xs text-gray-400">每期上限:</span>
<input type="number" value="<?=$l['max_per_period']?>" class="w-24 border rounded px-2 py-1 text-sm text-center limit-period" data-type="<?=$l['bet_type']?>">
</div>
</div>
<?php endforeach; ?>
<?php if(empty($limits)): ?>
<div class="text-gray-400 text-sm text-center py-4">暂无限额配置</div>
<?php endif; ?>
</div>
<button onclick="saveLimits()" class="mt-4 px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 text-sm">💾 保存限额</button>
</div>
</div>
</div>
<script>
async function saveWater(){
const items=[];
document.querySelectorAll('.water-pct').forEach(el=>{
items.push({game_id:el.dataset.game,bet_type:el.dataset.type,win_rate_pct:parseFloat(el.value),
enabled:document.querySelector('.water-enabled[data-type="'+el.dataset.type+'"]').checked?1:0});
});
const r=await fetch('/admin/water/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({items})});
const d=await r.json();alert(d.status==='success'?'保存成功!':d.message);
}
async function saveLimits(){
const items=[];
document.querySelectorAll('.limit-min').forEach(el=>{
const t=el.dataset.type;
items.push({game_id:el.dataset.game,bet_type:t,min_amount:parseFloat(el.value),
max_amount:parseFloat(document.querySelector('.limit-max[data-type="'+t+'"]').value),
max_per_period:parseFloat(document.querySelector('.limit-period[data-type="'+t+'"]').value)});
});
const r=await fetch('/admin/water/limits',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({items})});
const d=await r.json();alert(d.status==='success'?'保存成功!':d.message);
}
</script>
+783
View File
@@ -0,0 +1,783 @@
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
<i class="fas fa-coins text-primary mr-3"></i>
Xóc Đĩa 游戏期号管理
</h1>
<!-- 游戏期号管理区域 -->
<?php if (!empty($gamesList) && is_array($gamesList)): ?>
<?php foreach ($gamesList as $game): ?>
<?php
$gameId = $game['id'];
$gameName = $game['name'];
$currentPeriod = isset($currentPeriods[$gameId]) ? $currentPeriods[$gameId] : null;
?>
<!-- 单个游戏的期号卡片 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-6 border-l-4 border-primary">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-800 flex items-center">
<i class="fas fa-gamepad text-primary mr-2"></i>
<?= htmlspecialchars($gameName) ?>
</h2>
<?php if ($currentPeriod): ?>
<!-- 有当前期号 -->
<div class="flex gap-2">
<?php if ($currentPeriod['status'] === 'pending'): ?>
<button
type="button"
class="period-lock-btn inline-block bg-warning hover:bg-warning/90 text-white px-4 py-2 rounded-lg text-sm"
data-id="<?= $currentPeriod['id'] ?>"
>
<i class="fas fa-lock mr-2"></i>封盘
</button>
<?php endif; ?>
<?php if ($currentPeriod['status'] === 'locked'): ?>
<button
type="button"
class="period-draw-btn inline-block bg-success hover:bg-success/90 text-white px-4 py-2 rounded-lg text-sm"
data-id="<?= $currentPeriod['id'] ?>"
>
<i class="fas fa-coins mr-2"></i>开奖
</button>
<?php endif; ?>
<?php if ($currentPeriod['status'] === 'drawn'): ?>
<button
type="button"
class="period-settle-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
data-id="<?= $currentPeriod['id'] ?>"
>
<i class="fas fa-coins mr-2"></i>结算
</button>
<?php endif; ?>
<?php if ($currentPeriod['status'] === 'settled'): ?>
<button
type="button"
class="period-start-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
data-game-id="<?= $gameId ?>"
>
<i class="fas fa-play mr-2"></i>开始新一期
</button>
<?php endif; ?>
</div>
<?php else: ?>
<button
type="button"
class="period-start-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
data-game-id="<?= $gameId ?>"
>
<i class="fas fa-play mr-2"></i>开始新一期
</button>
<?php endif; ?>
</div>
<?php if ($currentPeriod): ?>
<!-- 当前期号信息 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<p class="text-sm text-gray-500 mb-1">期号</p>
<p class="text-lg font-bold text-gray-900"><?= htmlspecialchars($currentPeriod['period_number']) ?></p>
</div>
<div>
<p class="text-sm text-gray-500 mb-1">状态</p>
<?php
$status = $currentPeriod['status'];
$statusMap = [
'pending' => ['text' => '待开奖', 'class' => 'bg-warning/10 text-warning'],
'locked' => ['text' => '已封盘', 'class' => 'bg-danger/10 text-danger'],
'drawn' => ['text' => '已开奖', 'class' => 'bg-primary/10 text-primary'],
'settled' => ['text' => '已结算', 'class' => 'bg-success/10 text-success']
];
$statusInfo = $statusMap[$status] ?? $statusMap['pending'];
?>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm <?= $statusInfo['class'] ?>">
<?= $statusInfo['text'] ?>
</span>
</div>
<div>
<p class="text-sm text-gray-500 mb-1">开始时间</p>
<p class="text-sm text-gray-900"><?= htmlspecialchars($currentPeriod['start_time'] ?? '-') ?></p>
</div>
<div>
<p class="text-sm text-gray-500 mb-1">开奖结果</p>
<?php if (!empty($currentPeriod['result'])): ?>
<?php
$coins = json_decode($currentPeriod['result'], true);
if (is_array($coins) && count($coins) === 4):
$redCount = $currentPeriod['dice1'] ?? 0;
$whiteCount = $currentPeriod['dice2'] ?? 0;
?>
<div class="text-sm text-gray-900">
<span class="font-semibold">
<?php foreach ($coins as $coin): ?>
<span class="inline-block w-5 h-5 rounded-full <?= $coin === 'red' ? 'bg-red-500' : 'bg-gray-200' ?> border border-gray-300 mr-1"></span>
<?php endforeach; ?>
</span>
<span class="text-gray-600 ml-2">
(<?= $redCount ?>Đ <?= $whiteCount ?>T)
</span>
</div>
<?php else: ?>
<p class="text-sm text-gray-400">数据格式错误</p>
<?php endif; ?>
<?php else: ?>
<p class="text-sm text-gray-400">未开奖</p>
<?php endif; ?>
</div>
</div>
<?php else: ?>
<div class="text-center py-4">
<p class="text-sm text-gray-500">暂无进行中的期号,点击"开始新一期"按钮启动</p>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
<!-- 统计卡片 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">期号总数</p>
<h3 class="text-2xl font-bold mt-1">
<?= isset($periods) && is_array($periods) ? count($periods) : 0 ?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<i class="fas fa-list text-primary"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">待开奖</p>
<h3 class="text-2xl font-bold mt-1 text-warning">
<?php
$pendingCount = 0;
if (isset($periods) && is_array($periods)) {
foreach ($periods as $p) {
if (isset($p['status']) && $p['status'] === 'pending') {
$pendingCount++;
}
}
}
echo $pendingCount;
?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-warning/10 flex items-center justify-center">
<i class="fas fa-clock text-warning"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">已开奖</p>
<h3 class="text-2xl font-bold mt-1 text-success">
<?php
$drawnCount = 0;
if (isset($periods) && is_array($periods)) {
foreach ($periods as $p) {
if (isset($p['status']) && ($p['status'] === 'drawn' || $p['status'] === 'settled')) {
$drawnCount++;
}
}
}
echo $drawnCount;
?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
<i class="fas fa-check-circle text-success"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">已结算</p>
<h3 class="text-2xl font-bold mt-1 text-primary">
<?php
$settledCount = 0;
if (isset($periods) && is_array($periods)) {
foreach ($periods as $p) {
if (isset($p['status']) && $p['status'] === 'settled') {
$settledCount++;
}
}
}
echo $settledCount;
?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<i class="fas fa-coins text-primary"></i>
</div>
</div>
</div>
</div>
<!-- 期号列表 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
<div>
<h2 class="text-xl font-semibold text-gray-800">期号列表</h2>
<p class="text-sm text-gray-500 mt-1">管理Xóc Đĩa 游戏期号和开奖结果</p>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full bg-white rounded-xl overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">期号</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">关联游戏</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">开奖结果</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">创建时间</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200" id="periodList">
<?php if (!empty($periods) && is_array($periods)): ?>
<?php foreach ($periods as $period): ?>
<tr class="hover:bg-gray-50 transition-colors" data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>">
<td class="px-4 py-4">
<div class="text-sm font-semibold text-gray-900">
<?= htmlspecialchars((string)($period['period_number'] ?? '')) ?>
</div>
<?php if (!empty($period['auto_generated'])): ?>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] bg-gray-100 text-gray-600 mt-1">
自动生成
</span>
<?php endif; ?>
</td>
<td class="px-4 py-4">
<?php
$gameId = $period['game_id'] ?? null;
$gameName = $gameId && isset($games[$gameId]) ? $games[$gameId] : '未关联';
?>
<span class="text-sm text-gray-600"><?= htmlspecialchars($gameName) ?></span>
</td>
<td class="px-4 py-4">
<?php
$status = $period['status'] ?? 'pending';
$statusMap = [
'pending' => ['text' => '待开奖', 'class' => 'bg-warning/10 text-warning'],
'locked' => ['text' => '已封盘', 'class' => 'bg-danger/10 text-danger'],
'drawn' => ['text' => '已开奖', 'class' => 'bg-primary/10 text-primary'],
'settled' => ['text' => '已结算', 'class' => 'bg-success/10 text-success']
];
$statusInfo = $statusMap[$status] ?? $statusMap['pending'];
?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $statusInfo['class'] ?>">
<span class="w-2 h-2 rounded-full mr-1 <?= str_replace('/10', '', $statusInfo['class']) ?>"></span>
<?= $statusInfo['text'] ?>
</span>
</td>
<td class="px-4 py-4">
<?php if (!empty($period['result'])): ?>
<?php
$coins = json_decode($period['result'], true);
if (is_array($coins) && count($coins) === 4):
$redCount = $period['dice1'] ?? 0;
$whiteCount = $period['dice2'] ?? 0;
?>
<div class="text-sm text-gray-900">
<span class="font-semibold">
<?php foreach ($coins as $coin): ?>
<span class="inline-block w-5 h-5 rounded-full <?= $coin === 'red' ? 'bg-red-500' : 'bg-gray-200' ?> border border-gray-300 mr-1"></span>
<?php endforeach; ?>
</span>
<span class="text-gray-600 ml-2">
(<?= $redCount ?>Đ <?= $whiteCount ?>T)
</span>
<span class="ml-1 px-2 py-0.5 rounded text-[11px] <?= ($redCount == 0 || $redCount == 2 || $redCount == 4) ? 'bg-blue-100 text-blue-700' : 'bg-red-100 text-red-700' ?>">
<?= ($redCount == 0 || $redCount == 2 || $redCount == 4) ? 'Chẵn' : 'Lẻ' ?>
</span>
</div>
<?php else: ?>
<span class="text-sm text-gray-400">数据格式错误</span>
<?php endif; ?>
<?php else: ?>
<span class="text-sm text-gray-400">未开奖</span>
<?php endif; ?>
</td>
<td class="px-4 py-4 hidden md:table-cell">
<?php if (!empty($period['created_at'])): ?>
<div class="text-xs text-gray-500">
<?= date('Y-m-d H:i', strtotime((string)$period['created_at'])) ?>
</div>
<?php else: ?>
<span class="text-xs text-gray-400">时间未知</span>
<?php endif; ?>
</td>
<td class="px-4 py-4 text-right text-sm font-medium">
<div class="flex items-center justify-end gap-2">
<?php if (($period['status'] ?? '') === 'pending'): ?>
<button
type="button"
class="period-lock-btn text-gray-500 hover:text-warning"
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
title="封盘"
>
<i class="fas fa-lock"></i>
</button>
<?php endif; ?>
<?php if (($period['status'] ?? '') === 'locked' || ($period['status'] ?? '') === 'pending'): ?>
<button
type="button"
class="period-draw-btn text-gray-500 hover:text-primary"
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
title="录入开奖"
>
<i class="fas fa-coins"></i>
</button>
<?php endif; ?>
<?php if (($period['status'] ?? '') === 'drawn'): ?>
<button
type="button"
class="period-draw-btn text-gray-500 hover:text-warning"
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
title="修改结果"
>
<i class="fas fa-edit"></i>
</button>
<button
type="button"
class="period-settle-btn text-gray-500 hover:text-success"
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
title="确认结算"
>
<i class="fas fa-check-circle"></i>
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="6" class="px-6 py-12 text-center">
<div class="flex flex-col items-center">
<i class="fas fa-coins text-gray-300 text-5xl mb-4"></i>
<h3 class="text-lg font-medium text-gray-900">暂无期号</h3>
<p class="mt-1 text-gray-500 text-sm">
当前还没有任何Xóc Đĩa 游戏期号。
</p>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- 录入开奖结果模态框 -->
<div
id="drawPeriodBackdrop"
class="fixed inset-0 bg-black/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 z-40"
></div>
<div
id="drawPeriodModal"
class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-none transition-all duration-300 scale-95"
>
<div class="bg-white rounded-xl shadow-xl w-full max-w-md max-h-[90vh] overflow-hidden">
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
<h3 class="text-xl font-bold text-gray-800 flex items-center">
<i class="fas fa-coins text-primary mr-2"></i>
录入开奖结果
</h3>
<button id="closeDrawPeriodBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
<i class="fas fa-times"></i>
</button>
</div>
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-130px)]">
<form id="drawPeriodForm" class="space-y-4">
<input type="hidden" id="drawPeriodId" name="id">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
期号
</label>
<p id="drawPeriodNumber" class="text-lg font-bold text-gray-900"></p>
</div>
<!-- Xóc Đĩa 硬币输入 -->
<div id="coinInputSection" class="space-y-3">
<p class="text-sm text-gray-600">选择4个硬币的颜色:</p>
<div class="grid grid-cols-4 gap-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">硬币1</label>
<select id="coin1" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white">
<option value="red">红色</option>
<option value="white">白色</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">硬币2</label>
<select id="coin2" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white">
<option value="red">红色</option>
<option value="white">白色</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">硬币3</label>
<select id="coin3" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white">
<option value="red">红色</option>
<option value="white">白色</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">硬币4</label>
<select id="coin4" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white">
<option value="red">红色</option>
<option value="white">白色</option>
</select>
</div>
</div>
</div>
<div id="drawResultPreview" class="hidden p-4 bg-gray-50 rounded-lg">
<p class="text-sm text-gray-600 mb-1">开奖结果预览:</p>
<p class="text-lg font-bold">
<span id="drawResultText"></span>
<span id="drawResultTotal" class="ml-2 text-gray-500"></span>
<span id="drawResultType" class="ml-2"></span>
</p>
</div>
<div class="pt-4 border-t border-gray-100">
<button
type="button"
id="submitDrawPeriodBtn"
class="w-full bg-primary hover:bg-primary/90 text-white px-4 py-2.5 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center justify-center"
>
<i class="fas fa-check mr-2"></i>
确认录入
</button>
</div>
</form>
</div>
</div>
</div>
<script src="/Static/js/admin.js"></script>
<script>
layui.use(['layer'], function() {
var layer = layui.layer;
// 获取元素
const closeDrawPeriodBtn = document.getElementById('closeDrawPeriodBtn');
const drawPeriodBackdrop = document.getElementById('drawPeriodBackdrop');
const drawPeriodModal = document.getElementById('drawPeriodModal');
const drawPeriodForm = document.getElementById('drawPeriodForm');
const submitDrawPeriodBtn = document.getElementById('submitDrawPeriodBtn');
const coin1 = document.getElementById('coin1');
const coin2 = document.getElementById('coin2');
const coin3 = document.getElementById('coin3');
const coin4 = document.getElementById('coin4');
const drawResultPreview = document.getElementById('drawResultPreview');
const drawResultText = document.getElementById('drawResultText');
const drawResultTotal = document.getElementById('drawResultTotal');
const drawResultType = document.getElementById('drawResultType');
// 打开录入开奖模态框
function openDrawPeriodModal(periodId) {
fetch(`/admin/xocdia-periods/${periodId}`)
.then(res => res.json())
.then(data => {
if (data.success) {
const period = data.data;
document.getElementById('drawPeriodId').value = period.id;
document.getElementById('drawPeriodNumber').textContent = period.period_number;
// 填充已有的硬币数据
if (period.result) {
try {
const coins = JSON.parse(period.result);
if (Array.isArray(coins) && coins.length === 4) {
coin1.value = coins[0];
coin2.value = coins[1];
coin3.value = coins[2];
coin4.value = coins[3];
}
} catch (e) {
// 忽略解析错误
}
}
updateDrawPreview();
drawPeriodBackdrop.classList.remove('opacity-0', 'pointer-events-none');
drawPeriodModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
drawPeriodModal.classList.add('scale-100');
} else {
layer.msg(data.message || '获取期号信息失败', {icon: 2});
}
})
.catch(e => {
layer.msg('获取期号信息失败:' + e.message, {icon: 2});
});
}
// 关闭录入开奖模态框
function closeDrawPeriodModal() {
drawPeriodBackdrop.classList.add('opacity-0', 'pointer-events-none');
drawPeriodModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
drawPeriodModal.classList.remove('scale-100');
drawPeriodForm.reset();
drawResultPreview.classList.add('hidden');
}
// 更新开奖结果预览
function updateDrawPreview() {
const coins = [coin1.value, coin2.value, coin3.value, coin4.value];
const redCount = coins.filter(c => c === 'red').length;
const whiteCount = 4 - redCount;
let result = '';
let resultClass = '';
// 判断单双
if (redCount === 0 || redCount === 2 || redCount === 4) {
result = 'Chẵn';
resultClass = 'bg-blue-100 text-blue-700 px-2 py-1 rounded text-sm';
} else {
result = 'Lẻ';
resultClass = 'bg-red-100 text-red-700 px-2 py-1 rounded text-sm';
}
// 显示预览
let coinDisplay = coins.map(c =>
`<span class="inline-block w-5 h-5 rounded-full ${c === 'red' ? 'bg-red-500' : 'bg-gray-200'} border border-gray-300 mr-1"></span>`
).join('');
drawResultText.innerHTML = coinDisplay;
drawResultTotal.textContent = `(${redCount}Đ ${whiteCount}T)`;
drawResultType.innerHTML = `<span class="${resultClass}">${result}</span>`;
drawResultPreview.classList.remove('hidden');
}
// 提交录入开奖
async function submitDrawPeriod() {
const coins = [coin1.value, coin2.value, coin3.value, coin4.value];
const data = {
id: document.getElementById('drawPeriodId').value,
auto: false,
coins: coins
};
submitDrawPeriodBtn.disabled = true;
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>录入中...';
try {
const response = await fetch('/admin/xocdia-periods/draw', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
layer.msg(result.message || '开奖结果录入成功', {icon: 1}, function() {
location.reload();
});
} else {
layer.msg(result.message || '录入失败', {icon: 2});
submitDrawPeriodBtn.disabled = false;
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-check mr-2"></i>确认录入';
}
} catch (e) {
layer.msg('录入失败:' + e.message, {icon: 2});
submitDrawPeriodBtn.disabled = false;
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-check mr-2"></i>确认录入';
}
}
// 封盘
async function lockPeriod(id) {
layer.confirm('确定要封盘吗?封盘后将无法继续投注。', {icon: 3, title: '确认封盘'}, async function(index) {
layer.close(index);
try {
const response = await fetch('/admin/xocdia-periods/lock', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({id: id})
});
const result = await response.json();
if (result.success) {
layer.msg(result.message || '封盘成功', {icon: 1}, function() {
location.reload();
});
} else {
layer.msg(result.message || '封盘失败', {icon: 2});
}
} catch (e) {
layer.msg('封盘失败:' + e.message, {icon: 2});
}
});
}
// 确认结算
async function settlePeriod(id) {
layer.confirm('确定要确认结算吗?此操作不可撤销。', {icon: 3, title: '确认结算'}, async function(index) {
layer.close(index);
try {
const response = await fetch('/admin/xocdia-periods/settle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({id: id})
});
const result = await response.json();
if (result.success) {
layer.msg(result.message || '结算成功', {icon: 1}, function() {
location.reload();
});
} else {
layer.msg(result.message || '结算失败', {icon: 2});
}
} catch (e) {
layer.msg('结算失败:' + e.message, {icon: 2});
}
});
}
// 开始下注
async function startPeriod(event) {
const gameId = event.currentTarget.getAttribute('data-game-id');
if (!gameId) {
layer.msg('游戏ID缺失', {icon: 2});
return;
}
layer.confirm('确定要开始新一期下注吗?', {icon: 3, title: '开始下注'}, async function(index) {
layer.close(index);
try {
const response = await fetch('/admin/xocdia-periods/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({game_id: parseInt(gameId)})
});
const result = await response.json();
if (result.success) {
layer.msg(result.message || '启动成功', {icon: 1}, function() {
location.reload();
});
} else {
layer.msg(result.message || '启动失败', {icon: 2});
}
} catch (e) {
layer.msg('启动失败:' + e.message, {icon: 2});
}
});
}
// 绑定事件
if (closeDrawPeriodBtn) {
closeDrawPeriodBtn.addEventListener('click', closeDrawPeriodModal);
}
if (drawPeriodBackdrop) {
drawPeriodBackdrop.addEventListener('click', closeDrawPeriodModal);
}
if (submitDrawPeriodBtn) {
submitDrawPeriodBtn.addEventListener('click', submitDrawPeriod);
}
// 硬币选择监听
if (coin1 && coin2 && coin3 && coin4) {
[coin1, coin2, coin3, coin4].forEach(select => {
select.addEventListener('change', updateDrawPreview);
});
}
// 事件委托:列表操作按钮
const periodList = document.getElementById('periodList');
if (periodList) {
periodList.addEventListener('click', function(e) {
const lockBtn = e.target.closest('.period-lock-btn');
const drawBtn = e.target.closest('.period-draw-btn');
const settleBtn = e.target.closest('.period-settle-btn');
if (lockBtn) {
const id = lockBtn.getAttribute('data-id');
if (id) lockPeriod(id);
}
if (drawBtn) {
const id = drawBtn.getAttribute('data-id');
if (id) openDrawPeriodModal(id);
}
if (settleBtn) {
const id = settleBtn.getAttribute('data-id');
if (id) settlePeriod(id);
}
});
}
// 当前期号操作按钮
document.querySelectorAll('.period-lock-btn').forEach(btn => {
if (!btn.closest('#periodList')) {
btn.addEventListener('click', function() {
const id = this.getAttribute('data-id');
if (id) lockPeriod(id);
});
}
});
document.querySelectorAll('.period-draw-btn').forEach(btn => {
if (!btn.closest('#periodList')) {
btn.addEventListener('click', function() {
const id = this.getAttribute('data-id');
if (id) openDrawPeriodModal(id);
});
}
});
document.querySelectorAll('.period-settle-btn').forEach(btn => {
if (!btn.closest('#periodList')) {
btn.addEventListener('click', function() {
const id = this.getAttribute('data-id');
if (id) settlePeriod(id);
});
}
});
// Start Button
document.querySelectorAll('.period-start-btn').forEach(btn => {
btn.addEventListener('click', startPeriod);
});
});
</script>
BIN
View File
Binary file not shown.
+23
View File
@@ -0,0 +1,23 @@
<?php /** 底部5栏导航 */ $navActive = $navActive ?? ''; $t = $t ?? function($k){return $k;}; ?>
<nav class="nav-bottom">
<a href="/" class="nav-item <?=$navActive==='game'?'active':''?>">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="15" rx="2" ry="2"/><polyline points="17 2 12 7 7 2"/></svg>
<span><?=$t('nav_game')?></span>
</a>
<a href="/lottery" class="nav-item <?=$navActive==='lottery'?'active':''?>">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="20" x2="12" y2="10"/><line x1="18" y1="20" x2="18" y2="4"/><line x1="6" y1="20" x2="6" y2="16"/></svg>
<span><?=$t('nav_lottery')?></span>
</a>
<a href="/details" class="nav-item <?=$navActive==='details'?'active':''?>">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
<span><?=$t('nav_details')?></span>
</a>
<a href="#" class="nav-item <?=$navActive==='chat'?'active':''?>">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
<span><?=$t('nav_chat')?></span>
</a>
<a href="/profile" class="nav-item <?=$navActive==='profile'?'active':''?>">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<span><?=$t('nav_profile')?></span>
</a>
</nav>
+263
View File
@@ -0,0 +1,263 @@
<?php use App\Core\I18n; I18n::init(); $t = function($k,$p=[]){return I18n::t($k,$p);}; $page=$page??'home'; ?>
<!DOCTYPE html><html lang="<?=I18n::getLang()?>">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>代理后台</title><script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>body{background:#0f172a;color:#fff;font-family:system-ui}.sidebar a.active{background:rgba(234,179,8,.15);color:#eab308;border-right:3px solid #eab308}</style>
</head>
<body class="min-h-screen flex">
<!-- 侧边栏 -->
<aside class="sidebar w-56 bg-slate-900 border-r border-white/5 min-h-screen hidden md:block shrink-0">
<div class="p-4 border-b border-white/5">
<div class="text-yellow-400 font-bold text-lg">🏎️ 代理后台</div>
<div class="text-white/40 text-xs mt-1"><?=htmlspecialchars($user['username']??'')?></div>
<div class="text-xs mt-1"><span class="text-white/30">邀请码:</span> <span class="text-yellow-400 font-mono"><?=htmlspecialchars($agent['agent_code']??'')?></span></div>
</div>
<nav class="p-2 space-y-1">
<a href="/agent" class="flex items-center gap-3 px-3 py-2 rounded text-sm hover:bg-white/5 <?=$page==='home'?'active':''?>"><i class="fas fa-home w-5 text-center"></i>数据总览</a>
<a href="/agent/odds" class="flex items-center gap-3 px-3 py-2 rounded text-sm hover:bg-white/5 <?=$page==='odds'?'active':''?>"><i class="fas fa-sliders-h w-5 text-center"></i>赔率设置</a>
<a href="/agent/bets" class="flex items-center gap-3 px-3 py-2 rounded text-sm hover:bg-white/5 <?=$page==='bets'?'active':''?>"><i class="fas fa-list-alt w-5 text-center"></i>投注记录</a>
<a href="/agent/commissions" class="flex items-center gap-3 px-3 py-2 rounded text-sm hover:bg-white/5 <?=$page==='commissions'?'active':''?>"><i class="fas fa-coins w-5 text-center"></i>佣金明细</a>
<div class="border-t border-white/5 my-2"></div>
<a href="/" class="flex items-center gap-3 px-3 py-2 rounded text-sm text-white/40 hover:bg-white/5"><i class="fas fa-gamepad w-5 text-center"></i>返回投注</a>
<a href="/logout" class="flex items-center gap-3 px-3 py-2 rounded text-sm text-red-400/60 hover:bg-white/5"><i class="fas fa-sign-out-alt w-5 text-center"></i>退出</a>
</nav>
</aside>
<!-- 手机顶部导航 -->
<div class="md:hidden fixed top-0 left-0 right-0 z-50 bg-slate-900 border-b border-white/5 px-4 py-2 flex items-center justify-between">
<span class="text-yellow-400 font-bold">🏎️ 代理后台</span>
<button onclick="document.getElementById('mobileMenu').classList.toggle('hidden')" class="text-white/60"><i class="fas fa-bars"></i></button>
</div>
<div id="mobileMenu" class="md:hidden fixed top-10 left-0 right-0 z-40 bg-slate-900 border-b border-white/5 hidden">
<div class="flex flex-wrap gap-1 p-2">
<a href="/agent" class="px-3 py-1 rounded text-xs <?=$page==='home'?'bg-yellow-500/20 text-yellow-400':'text-white/60'?>">总览</a>
<a href="/agent/odds" class="px-3 py-1 rounded text-xs <?=$page==='odds'?'bg-yellow-500/20 text-yellow-400':'text-white/60'?>">赔率</a>
<a href="/agent/bets" class="px-3 py-1 rounded text-xs <?=$page==='bets'?'bg-yellow-500/20 text-yellow-400':'text-white/60'?>">投注</a>
<a href="/agent/commissions" class="px-3 py-1 rounded text-xs <?=$page==='commissions'?'bg-yellow-500/20 text-yellow-400':'text-white/60'?>">佣金</a>
<a href="/" class="px-3 py-1 rounded text-xs text-white/40">返回</a>
</div>
</div>
<!-- 主内容 -->
<main class="flex-1 p-4 md:p-6 mt-12 md:mt-0 overflow-auto">
<?php if($page==='home'): ?>
<!-- ========== 数据总览 ========== -->
<h2 class="text-xl font-bold mb-4">📊 数据总览</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
<div class="bg-white/5 rounded-xl p-4 text-center">
<div class="text-white/40 text-xs">玩家数</div>
<div class="text-2xl font-bold mt-1"><?=count($players??[])?></div>
</div>
<div class="bg-white/5 rounded-xl p-4 text-center">
<div class="text-white/40 text-xs">总投注额</div>
<div class="text-yellow-400 font-bold text-lg mt-1"><?=number_format($totalBets??0,2)?></div>
</div>
<div class="bg-white/5 rounded-xl p-4 text-center">
<div class="text-white/40 text-xs">今日佣金</div>
<div class="text-green-400 font-bold text-lg mt-1"><?=number_format($todayComm,2)?></div>
</div>
<div class="bg-white/5 rounded-xl p-4 text-center">
<div class="text-white/40 text-xs">累计佣金</div>
<div class="text-green-400 font-bold text-lg mt-1"><?=number_format($totalComm,2)?></div>
</div>
</div>
<!-- 分享链接 -->
<div class="bg-white/5 rounded-xl p-4 mb-4">
<h3 class="text-sm font-bold mb-2">📎 邀请链接</h3>
<div class="flex gap-2">
<input type="text" readonly value="<?=($_SERVER['REQUEST_SCHEME']??'http').'://'.$_SERVER['HTTP_HOST']?>/login?invite=<?=htmlspecialchars($agent['agent_code']??'')?>" class="flex-1 px-3 py-2 bg-white/5 border border-white/10 rounded text-xs text-white font-mono" id="shareLink">
<button onclick="navigator.clipboard.writeText(document.getElementById('shareLink').value);this.textContent='已复制!';setTimeout(()=>this.textContent='复制',1500)" class="px-4 py-2 bg-yellow-500 text-black rounded text-xs font-bold hover:bg-yellow-400">复制</button>
</div>
</div>
<!-- 下级代理 -->
<?php if(!empty($subAgents)): ?>
<div class="bg-white/5 rounded-xl p-4 mb-4">
<h3 class="text-sm font-bold mb-3">👥 下级代理</h3>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead><tr class="text-white/40 text-xs border-b border-white/5"><th class="text-left py-2 px-2">用户名</th><th class="px-2">邀请码</th><th class="px-2">佣金%</th><th class="px-2">玩家数</th><th class="px-2">状态</th></tr></thead>
<tbody>
<?php foreach($subAgents as $sa): ?>
<tr class="border-b border-white/5 text-xs">
<td class="py-2 px-2"><?=htmlspecialchars($sa['user']['username']??'')?></td>
<td class="px-2 text-center font-mono text-yellow-400"><?=$sa['agent_code']?></td>
<td class="px-2 text-center"><?=$sa['commission_rate']?>%</td>
<td class="px-2 text-center"><?=$sa['player_count']?></td>
<td class="px-2 text-center"><?=$sa['status']?'<span class="text-green-400">启用</span>':'<span class="text-red-400">禁用</span>'?></td>
</tr>
<?php endforeach; ?>
</tbody></table></div>
</div>
<?php endif; ?>
<!-- 玩家列表 -->
<div class="bg-white/5 rounded-xl p-4">
<h3 class="text-sm font-bold mb-3">🎮 我的玩家</h3>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead><tr class="text-white/40 text-xs border-b border-white/5"><th class="text-left py-2 px-2">ID</th><th class="text-left px-2">用户名</th><th class="px-2">余额</th><th class="px-2">注册时间</th><th class="px-2">状态</th></tr></thead>
<tbody>
<?php foreach($players??[] as $p): ?>
<tr class="border-b border-white/5 text-xs">
<td class="py-2 px-2 text-white/40"><?=$p['id']?></td>
<td class="px-2"><?=htmlspecialchars($p['username'])?></td>
<td class="px-2 text-center text-yellow-400"><?=number_format($p['balance'],2)?></td>
<td class="px-2 text-center text-white/40"><?=date('m-d H:i',strtotime($p['created_at']))?></td>
<td class="px-2 text-center"><?=$p['status']?'<span class="text-green-400">✓</span>':'<span class="text-red-400">✗</span>'?></td>
</tr>
<?php endforeach; ?>
<?php if(empty($players)): ?><tr><td colspan="5" class="text-center py-6 text-white/20">暂无玩家</td></tr><?php endif; ?>
</tbody></table></div>
</div>
<?php elseif($page==='odds'): ?>
<!-- ========== 赔率设置 ========== -->
<h2 class="text-xl font-bold mb-4">⚙️ 赔率设置</h2>
<p class="text-white/40 text-xs mb-4">设置你名下玩家的赔率。赔率不能超过上级设定的上限。</p>
<div class="space-y-4">
<?php
$oddsGroups = [
'大小 (Big/Small)' => ['type'=>'bs','targets'=>[]],
'单双 (Odd/Even)' => ['type'=>'oe','targets'=>[]],
'龙虎 (Dragon/Tiger)' => ['type'=>'dt','targets'=>[]],
'冠亚和大小 (Sum BS)' => ['type'=>'sum_bs','targets'=>[]],
];
// 大小
for($r=1;$r<=10;$r++){$oddsGroups['大小 (Big/Small)']['targets'][]=['target'=>"rank{$r}_big",'label'=>"第{$r}名 大"];$oddsGroups['大小 (Big/Small)']['targets'][]=['target'=>"rank{$r}_small",'label'=>"第{$r}名 小"];}
// 单双
for($r=1;$r<=10;$r++){$oddsGroups['单双 (Odd/Even)']['targets'][]=['target'=>"rank{$r}_odd",'label'=>"第{$r}名 单"];$oddsGroups['单双 (Odd/Even)']['targets'][]=['target'=>"rank{$r}_even",'label'=>"第{$r}名 双"];}
// 龙虎
$dtPairs=[[1,10],[2,9],[3,8],[4,7],[5,6]];
foreach($dtPairs as $i=>$p){$n=$i+1;$oddsGroups['龙虎 (Dragon/Tiger)']['targets'][]=['target'=>"dt{$n}_dragon",'label'=>"第{$p[0]}vs{$p[1]} 龙"];$oddsGroups['龙虎 (Dragon/Tiger)']['targets'][]=['target'=>"dt{$n}_tiger",'label'=>"第{$p[0]}vs{$p[1]} 虎"];}
// 冠亚和大小
$oddsGroups['冠亚和大小 (Sum BS)']['targets']=[['target'=>'sum_big','label'=>'和大'],['target'=>'sum_small','label'=>'和小'],['target'=>'sum_odd','label'=>'和单'],['target'=>'sum_even','label'=>'和双']];
?>
<?php foreach($oddsGroups as $groupName=>$group): ?>
<div class="bg-white/5 rounded-xl p-4">
<h3 class="text-sm font-bold mb-3"><?=$groupName?></h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-2">
<?php
// 同类型只显示一个代表(大小/单双每个名次赔率相同)
$type=$group['type'];
$firstTarget=$group['targets'][0]['target']??'';
$parentKey=$type.'_'.$firstTarget;
$parentVal=$parentMap[$parentKey]??1.95;
$myVal=$myMap[$parentKey]??$parentVal;
?>
<div class="col-span-full flex items-center gap-3 bg-white/5 rounded p-3">
<span class="text-white/40 text-xs w-24">统一赔率</span>
<span class="text-white/30 text-xs">上限: <?=$parentVal?></span>
<input type="number" step="0.01" min="1" max="<?=$parentVal?>" value="<?=$myVal?>" class="w-24 px-2 py-1 bg-white/10 border border-white/10 rounded text-sm text-white" data-type="<?=$type?>" data-targets='<?=json_encode(array_column($group['targets'],'target'))?>'>
</div>
</div>
</div>
<?php endforeach; ?>
<!-- 名次赔率 -->
<div class="bg-white/5 rounded-xl p-4">
<h3 class="text-sm font-bold mb-3">名次投注 (Rank) — 猜车号</h3>
<?php $parentRank=$parentMap['rank_rank1_1']??9.80; $myRank=$myMap['rank_rank1_1']??$parentRank; ?>
<div class="flex items-center gap-3 bg-white/5 rounded p-3">
<span class="text-white/40 text-xs w-24">统一赔率</span>
<span class="text-white/30 text-xs">上限: <?=$parentRank?></span>
<input type="number" step="0.01" min="1" max="<?=$parentRank?>" value="<?=$myRank?>" class="w-24 px-2 py-1 bg-white/10 border border-white/10 rounded text-sm text-white" id="rankOddsInput" data-type="rank">
</div>
</div>
<button onclick="saveAllOdds()" class="w-full py-3 bg-yellow-500 text-black font-bold rounded-xl hover:bg-yellow-400 text-sm">保存所有赔率</button>
</div>
<script>
async function saveAllOdds(){
const odds=[];
const gameId=<?=$game['id']??0?>;
// 收集各类型
document.querySelectorAll('input[data-type]').forEach(inp=>{
const type=inp.dataset.type;
const val=parseFloat(inp.value);
if(type==='rank'){
// 名次:100个target
for(let r=1;r<=10;r++)for(let c=1;c<=10;c++) odds.push({bet_type:'rank',bet_target:'rank'+r+'_'+c,odds:val});
} else if(inp.dataset.targets){
JSON.parse(inp.dataset.targets).forEach(t=>odds.push({bet_type:type,bet_target:t,odds:val}));
}
});
const r=await fetch('/api/agent/set-odds',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({game_id:gameId,odds:odds})});
const d=await r.json();
alert(d.message||'完成');if(d.success)location.reload();
}
</script>
<?php elseif($page==='bets'): ?>
<!-- ========== 投注记录 ========== -->
<h2 class="text-xl font-bold mb-4">📋 玩家投注记录</h2>
<div class="bg-white/5 rounded-xl overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead><tr class="text-white/40 text-xs border-b border-white/5 bg-white/5">
<th class="text-left py-3 px-3">玩家</th><th class="px-3">期号</th><th class="px-3">下注内容</th><th class="px-3">金额</th><th class="px-3">赔率</th><th class="px-3">状态</th><th class="px-3">赢额</th><th class="px-3">时间</th>
</tr></thead>
<tbody>
<?php foreach($betRecords??[] as $b):
// 投注类型中文转换
$bt=$b['bet_type']; $bv=$b['bet_value']; $label='';
if($bt==='rank'&&preg_match('/^rank(\d+)_(\d+)$/',$bv,$m)){$rn=(int)$m[1];$label=($rn===1?'冠军':($rn===2?'亚军':'第'.$rn.'名')).' '.$m[2].'号车';}
elseif($bt==='bs'&&preg_match('/^rank(\d+)_(big|small)$/',$bv,$m)){$rn=(int)$m[1];$label=($rn===1?'冠军':($rn===2?'亚军':'第'.$rn.'名')).' '.($m[2]==='big'?'大':'小');}
elseif($bt==='oe'&&preg_match('/^rank(\d+)_(odd|even)$/',$bv,$m)){$rn=(int)$m[1];$label=($rn===1?'冠军':($rn===2?'亚军':'第'.$rn.'名')).' '.($m[2]==='odd'?'单':'双');}
elseif($bt==='dt'&&preg_match('/^dt(\d+)_(dragon|tiger)$/',$bv,$m)){$ps=[1=>[1,10],2=>[2,9],3=>[3,8],4=>[4,7],5=>[5,6]];$p=$ps[(int)$m[1]]??[0,0];$label='龙虎 '.$p[0].'vs'.$p[1].' '.($m[2]==='dragon'?'龙':'虎');}
elseif($bt==='sum'&&preg_match('/^sum_(\d+)$/',$bv,$m)){$label='冠亚和 '.$m[1];}
elseif($bt==='sum_bs'){$sm=['sum_big'=>'和大','sum_small'=>'和小','sum_odd'=>'和单','sum_even'=>'和双'];$label=$sm[$bv]??$bv;}
else{$tm=['xiu'=>'小','tai'=>'大','chan'=>'双','le'=>'单','number'=>'点数','dice'=>'单骰','combo'=>'豹子','big_small'=>'大小','odd_even'=>'单双'];$vm=['big'=>'大','small'=>'小','odd'=>'单','even'=>'双','4red'=>'4红','4white'=>'4白','3red1white'=>'3红1白','1red3white'=>'1红3白'];$label=($tm[$bt]??$bt).' '.($vm[$bv]??$bv);}
?>
<tr class="border-b border-white/5 text-xs hover:bg-white/5">
<td class="py-2 px-3"><?=htmlspecialchars($b['username'])?></td>
<td class="px-3 font-mono text-white/40"><?=$b['period_number']?></td>
<td class="px-3"><?=htmlspecialchars($label)?></td>
<td class="px-3 text-yellow-400"><?=number_format($b['amount'],2)?></td>
<td class="px-3"><?=$b['odds']?></td>
<td class="px-3"><?php
$sc=['pending'=>'text-white/40','win'=>'text-green-400','lose'=>'text-red-400','settled'=>'text-blue-400'];
$sl=['pending'=>'待开','win'=>'赢','lose'=>'输','settled'=>'已结'];
echo '<span class="'.($sc[$b['status']]??'').'">'.($sl[$b['status']]??$b['status']).'</span>';
?></td>
<td class="px-3 <?=($b['win_amount']??0)>0?'text-green-400':'text-white/30'?>"><?=number_format($b['win_amount']??0,2)?></td>
<td class="px-3 text-white/30"><?=date('m-d H:i',strtotime($b['created_at']))?></td>
</tr>
<?php endforeach; ?>
<?php if(empty($betRecords)): ?><tr><td colspan="8" class="text-center py-8 text-white/20">暂无投注记录</td></tr><?php endif; ?>
</tbody></table></div>
</div>
<?php elseif($page==='commissions'): ?>
<!-- ========== 佣金明细 ========== -->
<h2 class="text-xl font-bold mb-4">💰 佣金明细</h2>
<div class="bg-white/5 rounded-xl overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead><tr class="text-white/40 text-xs border-b border-white/5 bg-white/5">
<th class="text-left py-3 px-3">来源玩家</th><th class="px-3">投注额</th><th class="px-3">佣金</th><th class="px-3">类型</th><th class="px-3">时间</th>
</tr></thead>
<tbody>
<?php foreach($records??[] as $r): ?>
<tr class="border-b border-white/5 text-xs hover:bg-white/5">
<td class="py-2 px-3"><?=htmlspecialchars($r['username'])?></td>
<td class="px-3 text-yellow-400"><?=number_format($r['bet_amount'],2)?></td>
<td class="px-3 text-green-400 font-bold"><?=number_format($r['commission'],2)?></td>
<td class="px-3"><?=$r['type']==='bet'?'投注佣金':'反水'?></td>
<td class="px-3 text-white/30"><?=date('m-d H:i',strtotime($r['created_at']))?></td>
</tr>
<?php endforeach; ?>
<?php if(empty($records)): ?><tr><td colspan="5" class="text-center py-8 text-white/20">暂无佣金记录</td></tr><?php endif; ?>
</tbody></table></div>
</div>
<?php endif; ?>
</main>
</body></html>
+356
View File
@@ -0,0 +1,356 @@
<?php
/**
* Xóc Đĩa 投注面板组件
* 从 xocdia.php 提取,可在 live.php 中复用
*
* 依赖变量:
* - $oddsList (array): 赔率列表
* - $game (array): 游戏信息
* - $currentPeriod (array): 当前期号
*/
// 赔率查找辅助函数
$getOdds = function($type, $target = 'all') use ($oddsList) {
if (!empty($oddsList)) {
foreach ($oddsList as $item) {
if ($item['type'] == $type && $item['target'] == $target) {
return (float)$item['odds'] == (int)$item['odds'] ? (int)$item['odds'] : $item['odds'];
}
}
}
// 默认值(防止赔率未配置)
if ($type == 'chan' || $type == 'le') return 0.96;
if ($type == 'exact') {
if (in_array($target, ['4red', '4white'])) return 10.0;
if (in_array($target, ['3red1white', '3white1red'])) return 3.5;
}
return 1;
};
?>
<style>
/* Xoc Dia Specific Styles */
.xocdia-top-row {
display: flex;
gap: 12px;
margin-bottom: 12px;
min-height: 100px;
}
.xocdia-big-btn {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 12px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
border: 2px solid rgba(255,255,255,0.1);
}
.xocdia-big-btn:hover {
transform: translateY(-2px);
filter: brightness(1.1);
}
.xocdia-big-btn.bet-chan {
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%);
box-shadow: 0 4px 15px rgba(30, 58, 138, 0.4);
}
.xocdia-big-btn.bet-le {
background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%);
box-shadow: 0 4px 15px rgba(185, 28, 28, 0.4);
}
.xocdia-title {
font-size: 28px;
font-weight: 900;
color: #fff;
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
margin-bottom: 4px;
letter-spacing: 1px;
}
.xocdia-subtitle {
font-size: 11px;
color: rgba(255,255,255,0.8);
margin-bottom: 4px;
}
.xocdia-odds {
background: rgba(0,0,0,0.3);
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 700;
color: #ffd700;
border: 1px solid rgba(255,215,0,0.3);
}
.xocdia-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-bottom: 15px;
}
.xocdia-cell {
background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%);
border: 1px solid #cbd5e1;
border-radius: 12px;
padding: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
cursor: pointer;
transition: all 0.2s;
min-height: 80px;
position: relative;
}
.xocdia-cell:hover {
transform: translateY(-2px);
border-color: #3b82f6;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
.xocdia-dots {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
width: 28px;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
display: block;
box-shadow: inset 0 -1px 2px rgba(0,0,0,0.2);
}
.dot.red {
background-color: #ef4444;
border: 1px solid #dc2626;
}
.dot.white {
background-color: #fff;
border: 1px solid #cbd5e1;
}
.xocdia-label {
font-size: 11px;
font-weight: 800;
color: #334155;
text-align: center;
line-height: 1.2;
}
.xocdia-cell .bet-odds-mini {
margin-top: 0;
background: rgba(0,0,0,0.05);
color: #64748b;
}
/* Mobile Responsive Adjustments */
@media (max-width: 768px) {
.xocdia-grid {
grid-template-columns: repeat(2, 1fr);
}
.xocdia-top-row {
min-height: 80px;
}
}
/* PC Adjustments */
@media (min-width: 769px) {
.xocdia-top-row {
min-height: 120px;
}
.xocdia-grid {
grid-template-columns: repeat(4, 1fr);
}
}
</style>
<!-- Xóc Đĩa 投注面板 -->
<div class="live-panel bet-panel bet-panel-v2" style="position: relative;">
<!-- 状态遮罩层 -->
<div id="betOverlay" class="bet-overlay" style="position: absolute; inset: 0; background: rgba(0,0,0,0.6); z-index: 50; display: flex; align-items: center; justify-content: center; flex-direction: column; border-radius: 16px;">
<div id="countdownDisplay" class="countdown-number" style="display: none; font-size: 64px; color: #ffeb3b; font-weight: bold; text-shadow: 0 0 20px rgba(255,235,59,0.8);"></div>
<div id="statusDisplay" class="status-text" style="color: #fff; font-size: 24px; font-weight: bold; text-transform: uppercase; letter-spacing: 2px;">⏱️ ĐANG TẢI...</div>
</div>
<!-- 倒计时 -->
<div id="activeCountdown" style="text-align: right; margin-bottom: 10px; font-weight: bold; color: #333;">
<i class="fa-regular fa-clock"></i> <span id="activeTimer" style="color: #d93025; font-size: 18px;">60</span>s
</div>
<!-- 主要玩法:双/单 -->
<div class="xocdia-top-row">
<div class="xocdia-big-btn bet-chan" data-bet-type="chan" data-bet-value="all" data-odds="<?= $getOdds('chan', 'all') ?>">
<div class="xocdia-title">CHẴN</div>
<div class="xocdia-subtitle">4 Đỏ, 4 Trắng, 2 Đỏ 2 Trắng</div>
<div class="xocdia-odds">1:<?= $getOdds('chan', 'all') ?></div>
<div class="bet-amount-display" style="display:none"></div>
</div>
<div class="xocdia-big-btn bet-le" data-bet-type="le" data-bet-value="all" data-odds="<?= $getOdds('le', 'all') ?>">
<div class="xocdia-title">LẺ</div>
<div class="xocdia-subtitle">3 Đỏ 1 Trắng, 3 Trắng 1 Đỏ</div>
<div class="xocdia-odds">1:<?= $getOdds('le', 'all') ?></div>
<div class="bet-amount-display" style="display:none"></div>
</div>
</div>
<!-- 精确颜色组合 -->
<div class="xocdia-grid">
<div class="xocdia-cell" data-bet-type="exact" data-bet-value="4red" data-odds="<?= $getOdds('exact', '4red') ?>">
<div class="xocdia-dots">
<span class="dot red"></span><span class="dot red"></span>
<span class="dot red"></span><span class="dot red"></span>
</div>
<div class="xocdia-label">4 ĐỎ</div>
<div class="bet-odds-mini">1:<?= $getOdds('exact', '4red') ?></div>
<div class="bet-amount-display" style="display:none"></div>
</div>
<div class="xocdia-cell" data-bet-type="exact" data-bet-value="3red1white" data-odds="<?= $getOdds('exact', '3red1white') ?>">
<div class="xocdia-dots">
<span class="dot red"></span><span class="dot red"></span>
<span class="dot red"></span><span class="dot white"></span>
</div>
<div class="xocdia-label">3 ĐỎ 1 TRẮNG</div>
<div class="bet-odds-mini">1:<?= $getOdds('exact', '3red1white') ?></div>
<div class="bet-amount-display" style="display:none"></div>
</div>
<div class="xocdia-cell" data-bet-type="exact" data-bet-value="3white1red" data-odds="<?= $getOdds('exact', '3white1red') ?>">
<div class="xocdia-dots">
<span class="dot white"></span><span class="dot white"></span>
<span class="dot white"></span><span class="dot red"></span>
</div>
<div class="xocdia-label">3 TRẮNG 1 ĐỎ</div>
<div class="bet-odds-mini">1:<?= $getOdds('exact', '3white1red') ?></div>
<div class="bet-amount-display" style="display:none"></div>
</div>
<div class="xocdia-cell" data-bet-type="exact" data-bet-value="4white" data-odds="<?= $getOdds('exact', '4white') ?>">
<div class="xocdia-dots">
<span class="dot white"></span><span class="dot white"></span>
<span class="dot white"></span><span class="dot white"></span>
</div>
<div class="xocdia-label">4 TRẮNG</div>
<div class="bet-odds-mini">1:<?= $getOdds('exact', '4white') ?></div>
<div class="bet-amount-display" style="display:none"></div>
</div>
</div>
<!-- 操作按钮 -->
<div class="bet-actions bet-actions-v2">
<button class="bet-action bet-action-green" type="button" id="confirmBetBtn">
<i class="fa-solid fa-check"></i> <span>ĐẶT CƯỢC</span>
</button>
<button class="bet-action bet-action-red" type="button" id="cancelBetBtn">
<i class="fa-solid fa-xmark"></i> <span>HỦY</span>
</button>
</div>
<!-- 筹码选择 -->
<div class="bet-chips bet-chips-v2">
<button class="bet-chip-btn" data-amount="100000" data-label="100K">
<div class="bet-chip-display chip-red">100K</div>
</button>
<button class="bet-chip-btn" data-amount="500000" data-label="500K">
<div class="bet-chip-display chip-blue">500K</div>
</button>
<button class="bet-chip-btn" data-amount="1000000" data-label="1M">
<div class="bet-chip-display chip-gold">1M</div>
</button>
<button class="bet-chip-btn" data-amount="5000000" data-label="5M">
<div class="bet-chip-display chip-green">5M</div>
</button>
</div>
</div>
<script>
// Xóc Đĩa 投注逻辑(使用全局 betState)
// 确保这段代码只在 Xóc Đĩa 游戏时执行
if (typeof window.xocdiaInitialized === 'undefined') {
window.xocdiaInitialized = true;
// 等待 live.php 的主脚本加载完成
document.addEventListener('DOMContentLoaded', function() {
layui.use(['layer'], function(){
var layer = layui.layer;
// 使用全局 betState(由 live.php 创建)
// 不要创建新的 betState,避免冲突
// 筹码选择已经由 live.php 处理,不需要重复绑定
// --- Bet Placement (Xóc Đĩa specific) ---
document.querySelectorAll('.xocdia-big-btn, .xocdia-cell').forEach(el => {
el.addEventListener('click', function() {
// 使用全局 betState
if(!window.betState || !window.betState.selectedChip) {
layer.msg('Vui lòng chọn chip (số tiền) trước!');
return;
}
var type = this.getAttribute('data-bet-type');
var value = this.getAttribute('data-bet-value');
var odds = parseFloat(this.getAttribute('data-odds'));
var key = type + '_' + value;
// Add to global bet state (使用与 live.php 一致的数据结构)
if(!window.betState.bets[key]) {
window.betState.bets[key] = {
amount: 0,
chipInfo: window.betState.selectedChip,
betInfo: {
type: type,
value: value,
odds: odds
}
};
}
window.betState.bets[key].amount += window.betState.selectedChip.amount;
// Visual Update
updateBetVisual(this, window.betState.bets[key].amount, window.betState.selectedChip.color);
this.classList.add('bet-selected');
});
});
function updateBetVisual(el, totalAmount, color) {
var display = el.querySelector('.bet-amount-display');
display.style.display = 'flex';
display.innerHTML = `
<div class="bet-chip-wrapper">
<div class="bet-chip-icon chip-${color}"></div>
</div>
<div class="bet-amount-text">${formatAmount(totalAmount)}</div>
`;
}
function formatAmount(n) {
if(n >= 1000000) return (n/1000000) + 'M';
if(n >= 1000) return (n/1000) + 'K';
return n;
}
// --- Actions ---
// 取消按钮已经由 live.php 处理
// 确认按钮已经由 live.php 处理
// 不需要重复绑定确认和取消按钮
// live.php 的主脚本会处理这些按钮
});
});
}
</script>
+82
View File
@@ -0,0 +1,82 @@
<?php use App\Core\I18n; I18n::init(); $t = function($k,$p=[]){return I18n::t($k,$p);};
// 投注翻译函数
function fmtBet($type,$val,$t){
if($type==='rank'&&preg_match('/^rank(\d+)_(\d+)$/',$val,$m)){$r=(int)$m[1];$rl=$r===1?$t('champion'):($r===2?$t('runner_up'):$t('rank_n',['n'=>$r]));return $rl.' #'.$m[2];}
if($type==='bs'&&preg_match('/^rank(\d+)_(big|small)$/',$val,$m)){$r=(int)$m[1];$rl=$r===1?$t('champion'):($r===2?$t('runner_up'):$t('rank_n',['n'=>$r]));return $rl.' '.($m[2]==='big'?$t('big'):$t('small'));}
if($type==='oe'&&preg_match('/^rank(\d+)_(odd|even)$/',$val,$m)){$r=(int)$m[1];$rl=$r===1?$t('champion'):($r===2?$t('runner_up'):$t('rank_n',['n'=>$r]));return $rl.' '.($m[2]==='odd'?$t('odd'):$t('even'));}
if($type==='dt'&&preg_match('/^dt(\d+)_(dragon|tiger)$/',$val,$m)){$ps=[1=>[1,10],2=>[2,9],3=>[3,8],4=>[4,7],5=>[5,6]];$p=$ps[(int)$m[1]]??[0,0];return $t('rank_n',['n'=>$p[0]]).'vs'.$t('rank_n',['n'=>$p[1]]).' '.($m[2]==='dragon'?$t('dragon'):$t('tiger'));}
if($type==='sum'&&preg_match('/^sum_(\d+)$/',$val,$m))return $t('sum').' '.$m[1];
if($type==='sum_bs'){$map=['sum_big'=>$t('sum_big'),'sum_small'=>$t('sum_small'),'sum_odd'=>$t('sum_odd'),'sum_even'=>$t('sum_even')];return $map[$val]??$val;}
return $type.':'.$val;
}
?>
<!DOCTYPE html><html lang="<?=I18n::getLang()?>">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title><?=$t('bet_details')?></title>
<link rel="stylesheet" href="/Static/css/app.css">
<style>
.det-header{background:#fff;padding:16px 15px;box-shadow:0 2px 8px rgba(0,0,0,.04);position:sticky;top:0;z-index:100}
.det-tabs{display:flex;background:#fff;border-bottom:1px solid #F0F0F0;margin-top:0}
.det-tab{flex:1;text-align:center;padding:15px 0;font-size:15px;color:#666;cursor:pointer;position:relative;transition:all .3s;background:none;border:none}
.det-tab.active{color:#333;font-weight:600}
.det-tab.active::after{content:'';position:absolute;bottom:0;left:50%;transform:translateX(-50%);width:60px;height:3px;background:linear-gradient(90deg,#1E90FF,#4A9FFF);border-radius:2px}
.table-header{background:#fff;display:flex;padding:12px 15px;border-bottom:1px solid #F0F0F0;font-size:13px;color:#999}
.bet-row{background:#fff;display:flex;align-items:center;padding:14px 15px;border-bottom:1px solid #F5F5F5;transition:background .2s}
.bet-row:active{background:#FAFAFA}
.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:80px 20px;background:#fff;min-height:50vh}
.empty-icon{width:80px;height:80px;border-radius:12px;background:#E8E8E8;display:flex;align-items:center;justify-content:center;margin-bottom:20px}
.empty-text{font-size:15px;color:#999}
.col-info{flex:1;min-width:0}.col-amount{flex:0 0 80px;text-align:right}.col-result{flex:0 0 70px;text-align:right}
.status-win{color:var(--success);font-weight:600}.status-lose{color:var(--danger);font-weight:600}.status-pending{color:var(--warn);font-weight:600}
</style>
</head>
<body style="background:#F5F5F5;padding-bottom:60px">
<div class="det-header">
<div style="font-weight:700;font-size:18px;color:#333"><?=$t('bet_details')?></div>
</div>
<div class="det-tabs">
<button class="det-tab active" onclick="showPanel('settled',this)"><?=$t('settled_details')?></button>
<button class="det-tab" onclick="showPanel('pending',this)"><?=$t('unsettled_details')?></button>
</div>
<div id="panelSettled">
<?php if(empty($settled)): ?>
<div class="empty-state"><div class="empty-icon"><svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg></div><div class="empty-text"><?=$t('no_data')?></div></div>
<?php else: ?>
<div class="table-header"><span class="col-info"><?=$t('play_type')?></span><span class="col-amount"><?=$t('amount')?></span><span class="col-result"><?=$t('result')?></span></div>
<?php foreach($settled as $b): ?>
<div class="bet-row">
<div class="col-info">
<div style="font-size:14px;color:#333;font-weight:500"><?=htmlspecialchars(fmtBet($b['bet_type'],$b['bet_value'],$t))?></div>
<div style="font-size:11px;color:#999;margin-top:2px"><?=$b['period_number']??''?> · <?=$b['created_at']??''?></div>
</div>
<div class="col-amount" style="font-size:14px;color:#333"><?=number_format((float)$b['amount'],2)?><br><span style="font-size:10px;color:#999">x<?=$b['odds']??''?></span></div>
<div class="col-result"><span class="status-<?=$b['status']?>"><?=$b['status']==='win'?'+'.number_format((float)$b['win_amount'],2):($b['status']==='lose'?'-'.number_format((float)$b['amount'],2):$b['status'])?></span></div>
</div>
<?php endforeach; endif; ?>
</div>
<div id="panelPending" class="hidden">
<?php if(empty($pending)): ?>
<div class="empty-state"><div class="empty-icon"><svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div><div class="empty-text"><?=$t('no_data')?></div></div>
<?php else: ?>
<div class="table-header"><span class="col-info"><?=$t('play_type')?></span><span class="col-amount"><?=$t('amount')?></span><span class="col-result"><?=$t('status')?></span></div>
<?php foreach($pending as $b): ?>
<div class="bet-row">
<div class="col-info">
<div style="font-size:14px;color:#333;font-weight:500"><?=htmlspecialchars(fmtBet($b['bet_type'],$b['bet_value'],$t))?></div>
<div style="font-size:11px;color:#999;margin-top:2px"><?=$b['period_number']??''?> · <?=$b['created_at']??''?></div>
</div>
<div class="col-amount" style="font-size:14px;color:#333"><?=number_format((float)$b['amount'],2)?><br><span style="font-size:10px;color:#999">x<?=$b['odds']??''?></span></div>
<div class="col-result"><span class="status-pending"><?=$t('awaiting_draw')?></span></div>
</div>
<?php endforeach; endif; ?>
</div>
<?php $navActive='details'; include __DIR__.'/_nav.php'; ?>
<script>
function showPanel(id,btn){document.getElementById('panelSettled').classList.toggle('hidden',id!=='settled');document.getElementById('panelPending').classList.toggle('hidden',id!=='pending');document.querySelectorAll('.det-tab').forEach(t=>t.classList.remove('active'));btn.classList.add('active');}
</script>
</body></html>
+225
View File
@@ -0,0 +1,225 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>操作提示</title>
<!-- 引入Font Awesome图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.error-container {
background-color: #ffffff;
width: 100%;
max-width: 600px;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: transform 0.3s ease;
}
.error-container:hover {
transform: translateY(-5px);
}
.error-header {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5253 100%);
color: white;
padding: 30px 20px;
text-align: center;
position: relative;
overflow: hidden;
}
.error-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
opacity: 0.4;
}
.error-icon {
font-size: 50px;
margin-bottom: 15px;
position: relative;
z-index: 1;
animation: pulse 2s infinite;
}
.error-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
position: relative;
z-index: 1;
}
.error-subtitle {
font-size: 16px;
opacity: 0.9;
position: relative;
z-index: 1;
}
.error-body {
padding: 30px 20px;
text-align: center;
}
.error-message {
font-size: 18px;
color: #4a4a4a;
line-height: 1.6;
margin-bottom: 30px;
padding: 0 20px;
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.error-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px;
}
.btn {
padding: 12px 24px;
border-radius: 30px;
border: none;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-refresh {
background-color: #4285f4;
color: white;
}
.btn-refresh:hover {
background-color: #3367d6;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(66, 133, 244, 0.3);
}
.btn-back {
background-color: #f1f3f4;
color: #202124;
}
.btn-back:hover {
background-color: #e8eaed;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.btn-home {
background-color: #34a853;
color: white;
}
.btn-home:hover {
background-color: #2d8643;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(52, 168, 83, 0.3);
}
.footer-note {
margin-top: 30px;
color: #868686;
font-size: 14px;
opacity: 0.8;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
@media (max-width: 480px) {
.error-header {
padding: 20px 15px;
}
.error-icon {
font-size: 40px;
}
.error-title {
font-size: 20px;
}
.error-body {
padding: 20px 15px;
}
.error-message {
font-size: 16px;
margin-bottom: 20px;
}
.btn {
width: 100%;
justify-content: center;
padding: 10px 20px;
font-size: 15px;
}
}
</style>
</head>
<body>
<div class="error-container">
<div class="error-header">
<div class="error-icon">
<i class="fas fa-exclamation-circle"></i>
</div>
<h2 class="error-title">操作提示</h2>
<p class="error-subtitle">很抱歉,出现了一些问题</p>
</div>
<div class="error-body">
<div class="error-message">
<?php echo htmlspecialchars($error); // 显示错误信息 ?>
</div>
<div class="error-actions">
<button class="btn btn-refresh" onclick="window.location.reload()">
<i class="fas fa-sync-alt"></i> 刷新页面
</button>
<button class="btn btn-back" onclick="window.history.back()">
<i class="fas fa-arrow-left"></i> 返回上一页
</button>
<button class="btn btn-home" onclick="window.location.href='/'">
<i class="fas fa-home"></i> 首页
</button>
</div>
<p class="footer-note">如有疑问,请联系系统管理员</p>
</div>
</div>
</body>
</html>
+97
View File
@@ -0,0 +1,97 @@
<?php use App\Core\I18n; I18n::init(); $t = function($k,$p=[]){return I18n::t($k,$p);}; ?>
<!DOCTYPE html><html lang="<?=I18n::getLang()?>">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>PK10 - <?=$t('home')?></title>
<link rel="stylesheet" href="/Static/css/app.css">
<style>
.user-card{margin:15px;padding:16px;background:#fff;border-radius:16px;display:flex;align-items:center;gap:12px;box-shadow:0 2px 8px rgba(0,0,0,.08)}
.user-avatar{width:44px;height:44px;border-radius:50%;background:#E8F4FF;display:flex;align-items:center;justify-content:center}
.user-avatar svg{width:24px;height:24px;color:#1E90FF}
.user-info{flex:1}
.user-name{font-size:16px;font-weight:600;color:#333;margin-bottom:4px}
.user-balance{font-size:20px;font-weight:700;color:#333}
.action-buttons{display:flex;gap:8px}
.act-btn{display:flex;flex-direction:column;align-items:center;gap:4px;cursor:pointer;background:none;border:none;padding:0}
.act-btn svg{width:24px;height:24px}
.act-btn span{font-size:12px;color:#666}
.nav-tabs{display:flex;justify-content:space-around;align-items:center;height:50px;background:#fff;border-bottom:1px solid #F0F0F0}
.nav-tab{font-size:15px;color:#666;cursor:pointer;position:relative;padding:12px 0;background:none;border:none}
.nav-tab.active{color:#1E90FF;font-weight:600}
.nav-tab.active::after{content:'';position:absolute;bottom:0;left:0;right:0;height:2px;background:#1E90FF}
.content-area{display:flex;min-height:calc(100vh - 200px)}
.left-menu{width:70px;background:#fff;padding:8px 0;border-right:1px solid #F0F0F0}
.left-menu .mi{height:50px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;font-size:12px;color:#666;cursor:pointer}
.left-menu .mi.active{color:#1E90FF;background:#F0F8FF}
.left-menu .mi svg{width:20px;height:20px}
.game-area{flex:1;padding:12px;background:#F5F5F5}
.game-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.game-card{height:100px;border-radius:12px;position:relative;display:block;cursor:pointer;overflow:hidden;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,.06);text-decoration:none}
.game-card:active{transform:scale(.98)}
.game-card-inner{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;background:linear-gradient(135deg,#F0F8FF,#E0EFFF);border-radius:12px}
.game-card-inner svg{width:36px;height:36px;color:#1E90FF}
.game-card-inner .gname{font-size:13px;font-weight:600;color:#333}
.game-card.disabled{opacity:.5;pointer-events:none}
.section-title{font-size:16px;font-weight:600;color:#333;margin-bottom:12px;padding:0 0 0 4px}
</style>
</head>
<body style="background:#F5F5F5;padding-bottom:60px">
<!-- 用户卡片 -->
<div class="user-card">
<div class="user-avatar"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></div>
<div class="user-info">
<div class="user-name"><?=htmlspecialchars($user['username']??'')?></div>
<div class="user-balance"><?=number_format($user['balance']??0,2)?></div>
</div>
<div class="action-buttons">
<button class="act-btn" onclick="alert('<?=$t('deposit')?>')">
<svg viewBox="0 0 24 24" fill="none" stroke="#1E90FF" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>
<span><?=$t('deposit')?></span>
</button>
<button class="act-btn" onclick="alert('<?=$t('withdraw')?>')">
<svg viewBox="0 0 24 24" fill="none" stroke="#1E90FF" stroke-width="2"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
<span><?=$t('withdraw')?></span>
</button>
<button class="act-btn" onclick="alert('<?=$t('transfer')?>')">
<svg viewBox="0 0 24 24" fill="none" stroke="#1E90FF" stroke-width="2"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
<span><?=$t('transfer')?></span>
</button>
</div>
</div>
<!-- 导航标签 -->
<div class="nav-tabs">
<button class="nav-tab active"><?=$t('lottery_tab')?></button>
</div>
<!-- 内容区 -->
<div class="content-area">
<div class="left-menu">
<div class="mi active">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/></svg>
<span><?=$t('racing')?></span>
</div>
</div>
<div class="game-area">
<div class="section-title"><?=$t('racing')?></div>
<div class="game-grid">
<?php if($game): ?>
<a href="/game/pk10" class="game-card">
<div class="game-card-inner">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="7" cy="17" r="2"/><circle cx="17" cy="17" r="2"/><path d="M5 17H3v-4l2-5h9l4 5h3v4h-2"/><path d="M5 12h14"/></svg>
<div class="gname"><?=htmlspecialchars($game['name']??$t('pk10_title'))?></div>
</div>
</a>
<?php endif; ?>
<div class="game-card disabled">
<div class="game-card-inner" style="background:linear-gradient(135deg,#F5F5F5,#EBEBEB)">
<svg viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2"><rect x="2" y="7" width="20" height="15" rx="2" ry="2"/><polyline points="17 2 12 7 7 2"/></svg>
<div class="gname" style="color:#999"><?=$t('more_games')?></div>
</div>
</div>
</div>
</div>
</div>
<?php $navActive='game'; include __DIR__.'/_nav.php'; ?>
</body></html>
+15
View File
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($settings['site_title'] ?? 'PK10 Speed Racing') ?></title>
<meta name="description" content="<?= htmlspecialchars($settings['site_description'] ?? '') ?>">
<link rel="shortcut icon" href="<?= htmlspecialchars($settings['site_favicon'] ?? '/Static/css/favicon.ico') ?>" type="image/x-icon">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-900 text-white min-h-screen">
<?= $Content ?? '' ?>
</body>
</html>
+1826
View File
File diff suppressed because it is too large Load Diff
+80
View File
@@ -0,0 +1,80 @@
<?php use App\Core\I18n; I18n::init(); $t = function($k,$p=[]){return I18n::t($k,$p);}; ?>
<!DOCTYPE html><html lang="<?=I18n::getLang()?>">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>PK10 - <?=$t('login_title')?></title>
<link rel="stylesheet" href="/Static/css/app.css">
<style>
body{background:linear-gradient(135deg,#E8F4F8,#F0F0F0);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:16px}
.auth-card{background:#fff;border-radius:24px;padding:32px 24px;box-shadow:0 8px 32px rgba(0,0,0,.1);width:100%;max-width:400px}
.auth-tabs{display:flex;background:#F5F5F5;border-radius:12px;padding:4px;margin-bottom:24px}
.auth-tab{flex:1;padding:10px;text-align:center;border-radius:10px;font-size:14px;font-weight:600;color:var(--text3);cursor:pointer;transition:all .2s;border:none;background:none}
.auth-tab.active{background:var(--primary);color:#fff;box-shadow:0 2px 8px rgba(30,144,255,.3)}
.auth-input{width:100%;padding:14px 16px;background:#F8F9FA;border:1px solid var(--border);border-radius:12px;font-size:14px;color:var(--text);outline:none;transition:border .2s}
.auth-input:focus{border-color:var(--primary);background:#fff;box-shadow:0 0 0 3px rgba(30,144,255,.1)}
.auth-input::placeholder{color:#bbb}
.auth-btn{width:100%;padding:14px;border:none;border-radius:12px;font-size:15px;font-weight:700;cursor:pointer;transition:all .2s}
.auth-btn-login{background:linear-gradient(135deg,var(--primary),var(--primary-dark));color:#fff}
.auth-btn-login:hover{transform:translateY(-1px);box-shadow:0 4px 16px rgba(30,144,255,.35)}
.auth-btn-reg{background:linear-gradient(135deg,var(--success),#388E3C);color:#fff}
.auth-btn-reg:hover{transform:translateY(-1px);box-shadow:0 4px 16px rgba(76,175,80,.35)}
.lang-bar{display:flex;justify-content:center;gap:6px;margin-bottom:16px}
.lang-btn{padding:4px 10px;border-radius:16px;font-size:11px;text-decoration:none;transition:all .2s}
.lang-btn.active{background:var(--primary);color:#fff}
.lang-btn:not(.active){background:#fff;color:var(--text3);border:1px solid var(--border)}
.code-row{display:flex;gap:8px}
.code-btn{padding:14px 16px;background:rgba(30,144,255,.1);color:var(--primary);border:none;border-radius:12px;font-size:13px;white-space:nowrap;cursor:pointer;font-weight:600}
.remember-row{display:flex;align-items:center;gap:6px;font-size:13px;color:var(--text2)}
#msg{margin-top:12px;text-align:center;font-size:13px}
.msg-ok{color:var(--success)}.msg-err{color:var(--danger)}
</style>
</head>
<body>
<div style="width:100%;max-width:400px">
<div class="lang-bar">
<?php foreach(I18n::LANGUAGES as $code=>$name): ?>
<a href="?lang=<?=$code?>" class="lang-btn <?=I18n::getLang()===$code?'active':''?>"><?=$name?></a>
<?php endforeach; ?>
</div>
<div class="auth-card">
<div style="text-align:center;margin-bottom:20px">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#1E90FF" stroke-width="2"><circle cx="7" cy="17" r="2"/><circle cx="17" cy="17" r="2"/><path d="M5 17H3v-4l2-5h9l4 5h3v4h-2"/><path d="M5 12h14"/></svg>
<h1 style="font-size:22px;font-weight:800;color:#1E90FF;margin:4px 0">PK10</h1>
<p style="font-size:12px;color:var(--text3)"><?=$t('app_name')?></p>
</div>
<div class="auth-tabs">
<button onclick="showTab('login')" id="tabLogin" class="auth-tab active"><?=$t('login')?></button>
<button onclick="showTab('register')" id="tabRegister" class="auth-tab"><?=$t('register')?></button>
</div>
<form id="loginForm" onsubmit="return doLogin(event)">
<div class="space-y-4">
<input type="text" id="loginUser" placeholder="<?=$t('username')?>" required class="auth-input">
<input type="password" id="loginPass" placeholder="<?=$t('password')?>" required class="auth-input">
<label class="remember-row"><input type="checkbox" id="loginRemember"> <?=$t('remember_me')?></label>
<button type="submit" class="auth-btn auth-btn-login"><?=$t('login')?></button>
</div>
</form>
<form id="registerForm" class="hidden" onsubmit="return doRegister(event)">
<div class="space-y-4">
<input type="text" id="regUser" placeholder="<?=$t('username')?>" required class="auth-input">
<input type="email" id="regEmail" placeholder="<?=$t('email')?>" required class="auth-input">
<div class="code-row">
<input type="text" id="regCode" placeholder="<?=$t('verify_code')?>" required class="auth-input" style="flex:1">
<button type="button" onclick="sendCode()" id="sendCodeBtn" class="code-btn"><?=$t('send_code')?></button>
</div>
<input type="password" id="regPass" placeholder="<?=$t('password')?>" required class="auth-input">
<input type="password" id="regPass2" placeholder="<?=$t('confirm_password')?>" required class="auth-input">
<input type="text" id="regInvite" placeholder="<?=$t('invite_code_optional')?>" class="auth-input">
<button type="submit" class="auth-btn auth-btn-reg"><?=$t('register')?></button>
</div>
</form>
</div>
<div id="msg" class="hidden"></div>
</div>
<script>
function showTab(t){document.getElementById('loginForm').classList.toggle('hidden',t!=='login');document.getElementById('registerForm').classList.toggle('hidden',t!=='register');document.getElementById('tabLogin').className='auth-tab '+(t==='login'?'active':'');document.getElementById('tabRegister').className='auth-tab '+(t==='register'?'active':'');}
function showMsg(m,ok){const e=document.getElementById('msg');e.textContent=m;e.className='mt-2 text-center text-sm '+(ok?'msg-ok':'msg-err');}
async function doLogin(e){e.preventDefault();const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:document.getElementById('loginUser').value,password:document.getElementById('loginPass').value,remember:document.getElementById('loginRemember').checked})});const d=await r.json();if(d.success)window.location.href=d.redirect||'/';else showMsg(d.message,false);return false;}
async function doRegister(e){e.preventDefault();const r=await fetch('/api/auth/register',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:document.getElementById('regUser').value,email:document.getElementById('regEmail').value,verify_code:document.getElementById('regCode').value,password:document.getElementById('regPass').value,confirm_password:document.getElementById('regPass2').value,invite_code:document.getElementById('regInvite').value})});const d=await r.json();showMsg(d.message,d.success);if(d.success)setTimeout(()=>showTab('login'),1500);return false;}
let cdTimer=0;async function sendCode(){if(cdTimer>0)return;const email=document.getElementById('regEmail').value;if(!email){showMsg('Enter email',false);return;}const r=await fetch('/api/auth/send-code',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email})});const d=await r.json();showMsg(d.message,d.success);if(d.success){cdTimer=60;const btn=document.getElementById('sendCodeBtn');const tick=setInterval(()=>{cdTimer--;btn.textContent=cdTimer+'s';if(cdTimer<=0){clearInterval(tick);btn.textContent='<?=$t('send_code')?>';cdTimer=0;}},1000);}}
</script>
</body></html>
+43
View File
@@ -0,0 +1,43 @@
<?php use App\Core\I18n; I18n::init(); $t = function($k,$p=[]){return I18n::t($k,$p);}; ?>
<!DOCTYPE html><html lang="<?=I18n::getLang()?>">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title><?=$t('lottery_history')?></title>
<link rel="stylesheet" href="/Static/css/app.css">
<style>
.lotto-header{background:#fff;padding:12px 16px;border-bottom:1px solid var(--border);position:sticky;top:0;z-index:10;display:flex;align-items:center;justify-content:space-between}
.period-row{background:#fff;border-radius:12px;padding:12px;margin-bottom:8px;box-shadow:0 1px 4px rgba(0,0,0,.04)}
.balls{display:flex;gap:3px;flex-wrap:wrap;margin-top:6px}
.ball{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#fff}
</style>
</head>
<body class="pb-nav" style="background:var(--bg)">
<div class="lotto-header">
<span style="font-weight:700;font-size:16px;display:flex;align-items:center;gap:6px"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#1E90FF" stroke-width="2"><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/></svg><?=$t('lottery_history')?></span>
<span style="font-size:12px;color:var(--text3)"><?=htmlspecialchars($game['name']??'PK10')?></span>
</div>
<div style="padding:12px;max-width:600px;margin:0 auto">
<?php if(empty($periods)): ?>
<div style="text-align:center;color:var(--text3);padding:40px;font-size:13px"><?=$t('no_data')?></div>
<?php else: foreach($periods as $p): ?>
<div class="period-row">
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="font-size:13px;font-weight:600;color:var(--text)"><?=$p['period_number']?></span>
<span style="font-size:10px;color:var(--text3)"><?=$p['drawn_at']??$p['updated_at']??''?></span>
</div>
<div class="balls">
<?php for($i=1;$i<=10;$i++): $v=$p['rank_'.$i]??''; if(!$v) continue;
$colors=['','#e74c3c','#3498db','#2ecc71','#f39c12','#9b59b6','#1abc9c','#e67e22','#e91e63','#00bcd4','#8bc34a']; ?>
<div class="ball" style="background:<?=$colors[(int)$v]??'#999'?>"><?=$v?></div>
<?php endfor; ?>
</div>
<?php $sum=($p['rank_1']??0)+($p['rank_2']??0); if($sum): ?>
<div style="margin-top:4px;font-size:11px;color:var(--text3)"><?=$t('sum_label')?>: <span style="color:var(--primary);font-weight:600"><?=$sum?></span></div>
<?php endif; ?>
</div>
<?php endforeach; endif; ?>
</div>
<?php $navActive='lottery'; include __DIR__.'/_nav.php'; ?>
</body></html>
+350
View File
@@ -0,0 +1,350 @@
<?php use App\Core\I18n; I18n::init(); $t = function($k,$p=[]){return I18n::t($k,$p);}; $gameId = $game['id'] ?? 0; ?>
<!DOCTYPE html><html lang="<?=I18n::getLang()?>">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<title><?=$t('pk10_title')?></title>
<link rel="stylesheet" href="/Static/css/app.css">
<style>
/* 赛车赛道 — 保留3D透视 */
.race-wrapper{perspective:800px;perspective-origin:50% 100%}
.race-track{background:linear-gradient(180deg,#2d5a2d 0%,#1e4a1e 50%,#163a16 100%);border-radius:12px;overflow:hidden;position:relative;transform:rotateX(25deg);transform-origin:50% 100%;box-shadow:0 -20px 60px rgba(0,128,0,.08),inset 0 0 80px rgba(0,0,0,.3)}
.race-track::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:repeating-linear-gradient(90deg,transparent,transparent calc(10% - 1px),rgba(255,255,255,.05) calc(10% - 1px),rgba(255,255,255,.05) 10%);pointer-events:none;z-index:1}
.finish-line{position:absolute;right:5%;top:0;bottom:0;width:4px;background:repeating-linear-gradient(180deg,#fff 0,#fff 4px,#000 4px,#000 8px);opacity:.4;z-index:2}
.start-line{position:absolute;left:5%;top:0;bottom:0;width:2px;background:rgba(255,255,255,.2);z-index:2}
.track-lane{height:36px;display:flex;align-items:center;border-bottom:1px solid rgba(255,255,255,.06);padding:0 8px;position:relative;overflow:hidden}
.track-lane:nth-child(odd){background:rgba(255,255,255,.02)}
.track-lane .progress-bar{position:absolute;left:0;top:0;height:100%;opacity:.15;width:5%;border-radius:0 4px 4px 0;transition:width .3s ease}
.car-container{position:absolute;left:3%;top:50%;transform:translateY(-50%);z-index:10;display:flex;align-items:center;gap:4px;transition:left 0.3s ease}
.race-car{width:32px;height:24px;border-radius:4px 12px 12px 4px;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:11px;color:#fff;flex-shrink:0;position:relative;box-shadow:2px 2px 6px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,255,255,.3);text-shadow:0 1px 2px rgba(0,0,0,.5)}
.race-car::after{content:'';position:absolute;right:-6px;top:50%;transform:translateY(-50%);width:0;height:0;border-left:6px solid currentColor;border-top:4px solid transparent;border-bottom:4px solid transparent;opacity:.4}
.rc1{background:linear-gradient(180deg,#ff6b6b,#c0392b);color:#c0392b}.rc2{background:linear-gradient(180deg,#74b9ff,#2980b9);color:#2980b9}
.rc3{background:linear-gradient(180deg,#55efc4,#27ae60);color:#27ae60}.rc4{background:linear-gradient(180deg,#ffeaa7,#f39c12);color:#f39c12}
.rc5{background:linear-gradient(180deg,#dda0dd,#8e44ad);color:#8e44ad}.rc6{background:linear-gradient(180deg,#81ecec,#16a085);color:#16a085}
.rc7{background:linear-gradient(180deg,#fab1a0,#e67e22);color:#e67e22}.rc8{background:linear-gradient(180deg,#fd79a8,#e91e63);color:#e91e63}
.rc9{background:linear-gradient(180deg,#a0d2db,#00bcd4);color:#00bcd4}.rc10{background:linear-gradient(180deg,#c8e6c9,#689f38);color:#689f38}
.exhaust{position:absolute;left:-8px;top:50%;transform:translateY(-50%);font-size:8px;opacity:0;pointer-events:none}
.car-container.moving .exhaust{animation:exhaustSmoke .8s infinite}
@keyframes exhaustSmoke{0%{opacity:.6;transform:translateY(-50%) translateX(0) scale(1)}100%{opacity:0;transform:translateY(-50%) translateX(-15px) scale(1.5)}}
.car-container.sprinting .race-car{box-shadow:2px 2px 6px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,255,255,.3),-8px 0 20px rgba(255,255,255,.1)}
.car-container.sprinting .exhaust{animation:exhaustSmoke .4s infinite}
.lane-label{position:absolute;left:4px;top:50%;transform:translateY(-50%);color:rgba(255,255,255,.25);font-size:10px;z-index:0}
.rank-badge{position:absolute;right:-24px;top:50%;transform:translateY(-50%);font-size:9px;font-weight:700;color:#ffd700;opacity:0;transition:opacity .5s;text-shadow:0 0 4px rgba(255,215,0,.6)}
.car-container.show-rank .rank-badge{opacity:1}
@keyframes readyShake{0%,100%{transform:translateY(-50%) translateX(0)}25%{transform:translateY(-50%) translateX(2px)}75%{transform:translateY(-50%) translateX(-2px)}}
.car-container.ready{animation:readyShake .3s infinite}
/* 顶部栏 */
.pk-header{position:sticky;top:0;z-index:40;background:#fff;border-bottom:1px solid #F0F0F0;padding:10px 15px;display:flex;align-items:center;justify-content:space-between}
.pk-header .title{font-weight:700;font-size:16px;color:#333;display:flex;align-items:center;gap:6px}
.pk-header .stats-btn{color:#333;font-size:14px;display:flex;align-items:center;gap:4px;text-decoration:none;cursor:pointer;background:none;border:none}
.pk-header .bal{color:var(--primary);font-weight:700;font-family:monospace;font-size:15px;display:flex;align-items:center;gap:4px}
/* 上期结果区 */
.result-bar{background:#fff;padding:10px 15px;border-bottom:1px solid #F0F0F0}
.result-balls{display:flex;gap:3px;flex-wrap:wrap;margin-top:4px}
.result-tags{display:flex;gap:3px;flex-wrap:wrap;margin-top:6px}
.result-tags span{display:inline-block;padding:2px 6px;border:1px solid #ddd;border-radius:4px;font-size:11px;color:#666}
/* 期号状态栏 */
.period-bar{background:#fff;padding:10px 15px;border-bottom:1px solid #F0F0F0;display:flex;align-items:center;justify-content:space-between}
.period-badge{font-size:13px;font-weight:600}
.period-badge.open{color:var(--success)}.period-badge.closed{color:var(--danger)}.period-badge.drawing{color:var(--warn)}
.seal-label{font-size:13px;font-weight:700;color:#333}
.seal-status{color:var(--danger);font-weight:600;font-size:13px}
.draw-label{font-size:13px;font-weight:700;color:#333}
.draw-countdown{color:var(--primary);font-weight:700;font-size:15px;font-family:monospace}
/* 左右布局 */
.bet-layout{display:flex;min-height:50vh}
.side-menu{width:72px;background:#fff;border-right:1px solid #F0F0F0;flex-shrink:0}
.side-menu .mi{display:flex;align-items:center;justify-content:center;height:48px;font-size:13px;color:#666;cursor:pointer;border-left:3px solid transparent;transition:all .2s}
.side-menu .mi.active{color:var(--primary);font-weight:600;border-left-color:var(--primary);background:#F0F8FF}
.bet-content{flex:1;overflow-y:auto;max-height:60vh;background:#fff}
/* 投注行 */
.bet-section-title{text-align:center;padding:10px 0 6px;font-size:14px;font-weight:600;color:#333;border-bottom:1px solid #F0F0F0;position:relative}
.bet-section-title::after{content:'▲';font-size:8px;color:#999;margin-left:4px}
.bet-row{display:flex;border-bottom:1px solid #F5F5F5}
.bet-cell{flex:1;display:flex;align-items:center;justify-content:center;gap:8px;padding:14px 10px;cursor:pointer;font-size:14px;color:#333;transition:background .15s}
.bet-cell:first-child{border-right:1px solid #F5F5F5}
.bet-cell:active,.bet-cell.selected{background:#E8F4FF}
.bet-cell .odds{color:var(--primary);font-weight:600;font-size:14px}
/* 筹码 */
.chip-bar{background:#fff;padding:8px 15px;border-top:1px solid #F0F0F0;display:flex;align-items:center;gap:8px}
.chip{border-radius:50%;width:40px;height:40px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:#fff;cursor:pointer;transition:transform .15s}
.chip.active{transform:scale(1.15);box-shadow:0 0 0 2px var(--primary)}
/* 提交栏 */
.submit-bar{background:#fff;padding:10px 15px;border-top:1px solid #F0F0F0;display:flex;align-items:center;gap:10px}
.submit-bar .info{flex:1;font-size:12px;color:#999}
.submit-bar .info b{color:var(--primary)}
.bet-amount-badge{position:absolute;top:-4px;right:-4px;background:var(--danger);color:#fff;font-size:9px;font-weight:700;min-width:18px;height:18px;border-radius:9px;display:flex;align-items:center;justify-content:center;padding:0 4px;z-index:2}
.bet-cell,.bet-btn{position:relative}
</style>
</head>
<body class="pb-nav" style="background:#F5F5F5">
<!-- 顶部栏 -->
<header class="pk-header">
<div class="title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#1E90FF" stroke-width="2"><circle cx="7" cy="17" r="2"/><circle cx="17" cy="17" r="2"/><path d="M5 17H3v-4l2-5h9l4 5h3v4h-2"/><path d="M5 12h14"/></svg>
<?=$t('pk10_title')?>
</div>
<button class="stats-btn" onclick="location.href='/game/pk10/stats'"><?=$t('statistics')?><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg></button>
<div class="bal">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
<span id="balanceDisplay"><?=number_format($user['balance']??0,2)?></span>
<select onchange="setLang(this.value)" style="padding:2px 4px;border:1px solid #ddd;border-radius:4px;font-size:10px;background:#fff;color:#666">
<?php foreach(I18n::LANGUAGES as $c=>$n): ?><option value="<?=$c?>" <?=I18n::getLang()===$c?'selected':''?>><?=$n?></option><?php endforeach; ?>
</select>
<?php if(!empty($isAgent)): ?><a href="/agent" style="color:#1E90FF;font-size:12px;text-decoration:none"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/></svg></a><?php endif; ?>
</div>
</header>
<!-- 上期结果区 -->
<div class="result-bar">
<div style="display:flex;align-items:center;gap:8px">
<span style="font-size:13px;font-weight:600;color:#333" id="lastPeriodNum">---</span>
<span style="font-size:11px;color:#999"><?=$t('last_result')?></span>
</div>
<div class="result-balls" id="lastResult"><span style="color:#999">---</span></div>
<div class="result-tags" id="lastResultTags"></div>
</div>
<!-- 当前期状态栏 -->
<div class="period-bar">
<div style="display:flex;align-items:center;gap:6px">
<span style="font-size:13px;font-weight:600;color:#333" id="periodNum">---</span>
<span class="seal-label"><?=$t('seal_plate')?></span>
<span id="statusBadge" class="seal-status"><?=$t('betting')?></span>
</div>
<div style="display:flex;align-items:center;gap:6px">
<span class="draw-label"><?=$t('draw_time')?></span>
<span class="draw-countdown" id="countdown">--</span>
</div>
</div>
<!-- 3D赛车动画区 — 完整保留 -->
<div class="race-wrapper">
<div class="race-track p-2" id="raceArea">
<div class="start-line"></div>
<div class="finish-line"></div>
<?php for($i=1;$i<=10;$i++): ?>
<div class="track-lane" id="trackLane<?=$i?>">
<div class="progress-bar c<?=$i?>" id="bar<?=$i?>"></div>
<span class="lane-label">#<?=$i?></span>
<div class="car-container" id="carContainer<?=$i?>">
<div class="race-car rc<?=$i?>"><?=$i?></div>
<span class="exhaust">•</span>
<span class="rank-badge" id="rankBadge<?=$i?>"></span>
</div>
</div>
<?php endfor; ?>
</div>
</div>
<!-- 左右布局投注面板 -->
<div id="betPanel">
<div class="bet-layout">
<!-- 左侧菜单 -->
<div class="side-menu">
<div class="mi active" onclick="showBetTab('bs',this)" id="tabBs"><?=$t('two_side')?></div>
<div class="mi" onclick="showBetTab('rank',this)" id="tabRank"><?=$t('rank_1_10')?></div>
<div class="mi" onclick="showBetTab('sum',this)" id="tabSum"><?=$t('sum_bs_tab')?></div>
</div>
<!-- 右侧投注内容 -->
<div class="bet-content">
<!-- 两面(大小+单双+龙虎)-->
<div id="panelBs">
<!-- 冠亚和 大小单双 -->
<div class="bet-section-title"><?=$t('sum')?></div>
<div class="bet-row">
<div class="bet-cell" onclick="addBet('sum_bs','sum_big',this)"><?=$t('sum_bs_big')?> <span class="odds">2.2</span></div>
<div class="bet-cell" onclick="addBet('sum_bs','sum_small',this)"><?=$t('sum_bs_small')?> <span class="odds">1.79</span></div>
</div>
<div class="bet-row">
<div class="bet-cell" onclick="addBet('sum_bs','sum_odd',this)"><?=$t('sum_bs_odd')?> <span class="odds">1.79</span></div>
<div class="bet-cell" onclick="addBet('sum_bs','sum_even',this)"><?=$t('sum_bs_even')?> <span class="odds">2.2</span></div>
</div>
<?php for($r=1;$r<=10;$r++): $label=$r<=2?($r==1?$t('champion'):$t('runner_up')):$t('rank_n',['n'=>$r]); ?>
<!-- 名次标题 -->
<div class="bet-section-title"><?=$label?></div>
<div class="bet-row">
<div class="bet-cell" onclick="addBet('bs','rank<?=$r?>_big',this)"><?=$t('big')?> <span class="odds">1.995</span></div>
<div class="bet-cell" onclick="addBet('bs','rank<?=$r?>_small',this)"><?=$t('small')?> <span class="odds">1.995</span></div>
</div>
<div class="bet-row">
<div class="bet-cell" onclick="addBet('oe','rank<?=$r?>_odd',this)"><?=$t('odd')?> <span class="odds">1.995</span></div>
<div class="bet-cell" onclick="addBet('oe','rank<?=$r?>_even',this)"><?=$t('even')?> <span class="odds">1.995</span></div>
</div>
<?php if($r<=5): $pairs=[[1,10],[2,9],[3,8],[4,7],[5,6]]; $p=$pairs[$r-1]; ?>
<div class="bet-row">
<div class="bet-cell" onclick="addBet('dt','dt<?=$r?>_dragon',this)"><?=$t('dragon')?> <span class="odds">1.995</span></div>
<div class="bet-cell" onclick="addBet('dt','dt<?=$r?>_tiger',this)"><?=$t('tiger')?> <span class="odds">1.995</span></div>
</div>
<?php endif; endfor; ?>
</div>
<!-- 1~10名 号码投注 -->
<div id="panelRank" class="hidden" style="padding:10px">
<?php for($r=1;$r<=10;$r++): $label=$r<=2?($r==1?$t('champion'):$t('runner_up')):$t('rank_n',['n'=>$r]); ?>
<div style="margin-bottom:10px">
<div style="font-size:13px;font-weight:600;color:#333;margin-bottom:6px"><?=$label?></div>
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:4px">
<?php for($c=1;$c<=10;$c++): ?>
<div onclick="addBet('rank','rank<?=$r?>_<?=$c?>',this)" class="bet-btn" data-odds="9.80">
<div class="car c<?=$c?>" style="width:24px;height:24px;font-size:11px;margin:0 auto 2px"><?=$c?></div>
<div style="color:#999;font-size:10px">9.80</div>
</div>
<?php endfor; ?>
</div>
</div>
<?php endfor; ?>
</div>
<!-- 冠亚和值 -->
<div id="panelSum" class="hidden" style="padding:10px">
<div style="font-size:13px;font-weight:600;color:#333;margin-bottom:8px"><?=$t('sum_value')?> (3-19)</div>
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:4px">
<?php $sumOdds=[3=>140,4=>70,5=>35,6=>23,7=>17.5,8=>14,9=>11.6,10=>10,11=>8.8,12=>8.8,13=>10,14=>11.6,15=>14,16=>17.5,17=>23,18=>35,19=>70];
foreach($sumOdds as $v=>$o): ?>
<div onclick="addBet('sum','sum_<?=$v?>',this)" class="bet-btn">
<div style="font-weight:700"><?=$v?></div><div style="color:#999;font-size:10px"><?=$o?></div>
</div>
<?php endforeach; ?>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:10px">
<div onclick="addBet('sum_bs','sum_big',this)" class="bet-cell" style="border:1px solid #F0F0F0;border-radius:8px"><?=$t('sum_big')?> <span class="odds">1.95</span></div>
<div onclick="addBet('sum_bs','sum_small',this)" class="bet-cell" style="border:1px solid #F0F0F0;border-radius:8px"><?=$t('sum_small')?> <span class="odds">1.95</span></div>
<div onclick="addBet('sum_bs','sum_odd',this)" class="bet-cell" style="border:1px solid #F0F0F0;border-radius:8px"><?=$t('sum_odd')?> <span class="odds">1.95</span></div>
<div onclick="addBet('sum_bs','sum_even',this)" class="bet-cell" style="border:1px solid #F0F0F0;border-radius:8px"><?=$t('sum_even')?> <span class="odds">1.95</span></div>
</div>
</div>
</div><!-- /bet-content -->
</div><!-- /bet-layout -->
<!-- 筹码+提交栏 -->
<div class="chip-bar">
<div onclick="selectChip(10)" class="chip active" style="background:#e74c3c" data-amount="10">10</div>
<div onclick="selectChip(50)" class="chip" style="background:#3498db" data-amount="50">50</div>
<div onclick="selectChip(100)" class="chip" style="background:#2ecc71" data-amount="100">100</div>
<div onclick="selectChip(500)" class="chip" style="background:#9b59b6" data-amount="500">500</div>
<div onclick="showCustomChip()" class="chip" style="background:#f39c12" data-amount="custom" id="customChipBtn"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></div>
</div>
<div id="customChipInput" class="hidden" style="padding:8px 15px;background:#fff;display:flex;gap:8px">
<input type="number" id="customAmount" min="1" step="1" placeholder="<?=$t('amount')?>" class="input" style="flex:1;text-align:center">
<button onclick="applyCustomChip()" class="btn btn-primary btn-sm"><?=$t('confirm')?></button>
</div>
<div class="submit-bar">
<div class="info"><?=$t('total')?>: <b id="betTotal">0</b> x<span id="betCount">0</span></div>
<button onclick="clearBets()" class="btn btn-outline btn-sm"><?=$t('cancel')?></button>
<button onclick="submitBets()" id="submitBtn" class="btn btn-primary btn-sm"><?=$t('place_bet')?></button>
</div>
<!-- 本期已下注 -->
<div style="background:#fff;padding:10px 15px;border-top:1px solid #F0F0F0" id="myBetsPanel">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
<span style="color:#999;font-size:12px"><?=$t('current_bets')?></span>
<span style="color:#999;font-size:11px" id="myBetsCount">0</span>
</div>
<div id="myBetsList" style="max-height:140px;overflow-y:auto">
<div style="color:#999;font-size:12px;text-align:center;padding:8px"><?=$t('no_data')?></div>
</div>
</div>
<!-- 上期结果面板 -->
<div class="hidden" style="background:#fff;padding:10px 15px;border-top:1px solid #F0F0F0" id="lastResultPanel">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
<span style="color:#999;font-size:12px"><?=$t('last_result_label')?></span>
<span style="font-size:12px;font-weight:700" id="lastProfitDisplay"></span>
</div>
<div id="lastMyBetsList" style="max-height:128px;overflow-y:auto"></div>
</div>
</div><!-- /betPanel -->
<!-- 历史记录 -->
<div style="background:#fff;margin-top:8px;padding:10px 15px;border-top:1px solid #F0F0F0">
<div style="color:#999;font-size:12px;margin-bottom:6px"><?=$t('history')?></div>
<div id="historyList" style="font-size:12px;max-height:200px;overflow-y:auto"></div>
</div>
<!-- 底部导航 -->
<?php $navActive='game'; include __DIR__.'/_nav.php'; ?>
<script>
const GAME_ID=<?=$gameId?>;
let selectedChip=10,bets={},periodData=null,pollTimer=null;
const rankNames={1:'<?=$t('champion')?>',2:'<?=$t('runner_up')?>',3:'<?=$t('rank_n',['n'=>3])?>',4:'<?=$t('rank_n',['n'=>4])?>',5:'<?=$t('rank_n',['n'=>5])?>',6:'<?=$t('rank_n',['n'=>6])?>',7:'<?=$t('rank_n',['n'=>7])?>',8:'<?=$t('rank_n',['n'=>8])?>',9:'<?=$t('rank_n',['n'=>9])?>',10:'<?=$t('rank_n',['n'=>10])?>'};
const dtPairs={1:[1,10],2:[2,9],3:[3,8],4:[4,7],5:[5,6]};
const I18N={big:'<?=$t('big')?>',small:'<?=$t('small')?>',odd:'<?=$t('odd')?>',even:'<?=$t('even')?>',dragon:'<?=$t('dragon')?>',tiger:'<?=$t('tiger')?>',sum:'<?=$t('sum')?>',sum_big:'<?=$t('sum_big')?>',sum_small:'<?=$t('sum_small')?>',sum_odd:'<?=$t('sum_odd')?>',sum_even:'<?=$t('sum_even')?>',car:'<?=$t('car_no')?>',no_bets:'<?=$t('no_data')?>',total:'<?=$t('total')?>',items:'',profit:'<?=$t('win')?>',loss:'<?=$t('lose')?>',draw:'<?=$t('pending')?>',net_err:'<?=$t('error')?>',rank_prefix:'<?=$t('rank_n',['n'=>''])?>',drawing:'<?=$t('drawing')?>',settled_label:'<?=$t('settled')?>'};
function formatBetLabel(type,value){
let 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)$/))){const 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'){const 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){selectedChip=a;document.querySelectorAll('.chip').forEach(c=>{const amt=c.dataset.amount;c.classList.toggle('active',amt!=='custom'&&parseInt(amt)===a);});document.getElementById('customChipInput').classList.add('hidden');}
function showCustomChip(){const b=document.getElementById('customChipInput');b.classList.toggle('hidden');if(!b.classList.contains('hidden'))document.getElementById('customAmount').focus();}
function applyCustomChip(){const v=parseInt(document.getElementById('customAmount').value);if(!v||v<=0){showToast('<?=$t('error')?>: > 0','error');return;}selectedChip=v;document.querySelectorAll('.chip').forEach(c=>c.classList.remove('active'));const cb=document.getElementById('customChipBtn');cb.classList.add('active');cb.textContent=v>=1000?(v/1000)+'k':v;document.getElementById('customChipInput').classList.add('hidden');}
function addBet(type,target,el){if(periodData&&periodData.status!=='pending'){showToast('<?=$t('period_closed')?>','error');return;}const key=type+'_'+target;if(!bets[key])bets[key]={type,value:target,amount:0,el:el};bets[key].amount+=selectedChip;el.classList.add('selected');let 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(e=>{e.classList.remove('selected');const b=e.querySelector('.bet-amount-badge');if(b)b.remove();});updateBetSummary();}
function updateBetSummary(){let total=0,count=0;Object.values(bets).forEach(b=>{total+=b.amount;count++;});document.getElementById('betTotal').textContent=total.toFixed(0);document.getElementById('betCount').textContent=count;}
async function submitBets(){const arr=Object.values(bets);if(!arr.length){showToast('<?=$t('place_bet')?>','error');return;}if(!periodData||!periodData.period_number){showToast('<?=$t('waiting')?>','error');return;}try{const r=await fetch('/api/bet',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({game_id:GAME_ID,period_number:periodData.period_number,bets:arr})});const d=await r.json();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(e){showToast(I18N.net_err,'error');}}
function showBetTab(tab,btn){['Rank','Bs','Sum'].forEach(t=>{const p=document.getElementById('panel'+t);if(p)p.classList.toggle('hidden',t.toLowerCase()!==tab);});document.querySelectorAll('.side-menu .mi').forEach(m=>m.classList.remove('active'));if(btn)btn.classList.add('active');}
// ===== 赛车动画系统(完整保留) =====
let raceAnimState='idle',lastAnimatedResult=null,idleAnimTimer=null,carPositions={},raceFrameTimer=null;
const FINISH_LINE=88,START_LINE=3;
for(let i=1;i<=10;i++)carPositions[i]=START_LINE;
function setCarPosition(n,pct,dur){const c=document.getElementById('carContainer'+n);const b=document.getElementById('bar'+n);if(!c)return;c.style.transition='left '+dur+'s ease';c.style.left=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){const c=document.getElementById('carContainer'+n);if(!c)return;c.classList.toggle('moving',moving);c.classList.toggle('sprinting',!!sprinting);c.classList.remove('ready');}
function setRankBadge(n,rank){const badge=document.getElementById('rankBadge'+n);if(!badge)return;const c=document.getElementById('carContainer'+n);if(rank>0){const 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';for(let i=1;i<=10;i++){setCarPosition(i,START_LINE+Math.random()*5,0.5);setCarMoving(i,true,false);setRankBadge(i,0);}idleAnimTimer=setInterval(function(){if(raceAnimState!=='waiting'){stopIdleAnimation();return;}for(let i=1;i<=10;i++){if(Math.random()<0.5){const cur=carPositions[i]||START_LINE;const target=Math.max(START_LINE,Math.min(15,cur+(Math.random()-0.5)*6));setCarPosition(i,target,0.5+Math.random()*0.3);}}},800);}
function stopIdleAnimation(){if(idleAnimTimer){clearInterval(idleAnimTimer);idleAnimTimer=null;}if(raceFrameTimer){clearInterval(raceFrameTimer);raceFrameTimer=null;}for(let i=1;i<=10;i++){setCarMoving(i,false,false);const c=document.getElementById('carContainer'+i);if(c)c.classList.remove('ready');}}
function startReadyAnimation(){stopIdleAnimation();raceAnimState='ready';for(let i=1;i<=10;i++){setCarPosition(i,START_LINE,0.8);setCarMoving(i,true,false);setRankBadge(i,0);const c=document.getElementById('carContainer'+i);if(c)c.classList.add('ready');}}
// ANIM: race
function animateRace(result){if(!result||result.length<10)return;const rk=result.join(',');if(lastAnimatedResult===rk&&raceAnimState==='finished')return;lastAnimatedResult=rk;stopIdleAnimation();raceAnimState='racing';const rankMap={};for(let i=0;i<10;i++)rankMap[result[i]]=i;const carSpeeds={},carTargets={};for(let i=1;i<=10;i++){const rank=rankMap[i]!==undefined?rankMap[i]:9;carSpeeds[i]=1.0-rank*0.06;carTargets[i]=FINISH_LINE-rank*7.5;setCarMoving(i,true,true);setRankBadge(i,0);}
for(let i=1;i<=10;i++){const s=carSpeeds[i];setCarPosition(i,15+s*20+Math.random()*5,1.2+Math.random()*0.3);}
setTimeout(function(){if(raceAnimState!=='racing')return;for(let i=1;i<=10;i++){const s=carSpeeds[i];setCarPosition(i,35+s*30+Math.random()*5,1.5+Math.random()*0.5);}},1500);
setTimeout(function(){if(raceAnimState!=='racing')return;for(let i=1;i<=10;i++){const dur=1.5+rankMap[i]*0.05+Math.random()*0.2;setCarPosition(i,carTargets[i],dur);}},3500);
setTimeout(function(){if(raceAnimState!=='racing')return;raceAnimState='finished';for(let i=1;i<=10;i++){setCarMoving(i,false,false);const rank=rankMap[i]!==undefined?rankMap[i]+1:0;setRankBadge(i,rank);}},5500);}
function resetRace(){stopIdleAnimation();raceAnimState='idle';lastAnimatedResult=null;for(let i=1;i<=10;i++){const c=document.getElementById('carContainer'+i);const b=document.getElementById('bar'+i);if(c){c.style.transition='left 0.5s ease';c.style.left=START_LINE+'%';c.classList.remove('moving','sprinting','ready','show-rank');}if(b){b.style.transition='width 0.5s ease';b.style.width='5%';}carPositions[i]=START_LINE;setRankBadge(i,0);}}
function renderResult(result,container){if(!result)return;const 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(k=>{const v=result[k];if(!v)return;const 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);});
const tags=document.getElementById('lastResultTags');if(!tags)return;tags.innerHTML='';const r1=+result.rank_1,r2=+result.rank_2;if(r1&&r2){const sum=r1+r2;const items=[sum,(sum>=12?I18N.big:I18N.small),(sum%2?I18N.odd:I18N.even)];for(let i=1;i<=5;i++){const a=+result['rank_'+i],b=+result['rank_'+(11-i)];if(a&&b)items.push(a>b?I18N.dragon:I18N.tiger);}items.forEach(t=>{const s=document.createElement('span');s.textContent=t;tags.appendChild(s);});}}
// 投注记录渲染
function renderMyBets(myBets){const list=document.getElementById('myBetsList');const 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;let total=0,html='';myBets.forEach(b=>{const label=formatBetLabel(b.bet_type,b.bet_value);const amt=parseFloat(b.amount);total+=amt;const 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){const panel=document.getElementById('lastResultPanel');const list=document.getElementById('lastMyBetsList');const profitEl=document.getElementById('lastProfitDisplay');if(!lastMyBets||!lastMyBets.length){panel.classList.add('hidden');return;}panel.classList.remove('hidden');let totalWin=0,totalBet=0,html='';lastMyBets.forEach(b=>{const label=formatBetLabel(b.bet_type,b.bet_value);const amt=parseFloat(b.amount);totalBet+=amt;if(b.status==='win'){const 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;const 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';}}
// 倒计时
let localCountdown=0,lastPollStatus='',lastPeriodId=0,drawnResultShown=false;
setInterval(function(){if(localCountdown>0)localCountdown--;const m=Math.floor(localCountdown/60),s=localCountdown%60;document.getElementById('countdown').textContent=localCountdown>0?m+':'+(s<10?'0':'')+s:'--';},1000);
// 轮询期号
async function pollPeriod(){try{const r=await fetch('/api/period/current?game_id='+GAME_ID);const d=await r.json();if(!d.success)return;periodData=d.data;
const serverRemaining=periodData.remaining_seconds||0;const periodChanged=(periodData.id&&periodData.id!==lastPeriodId);const statusChanged=(periodData.status!==lastPollStatus);const driftTooMuch=Math.abs(localCountdown-serverRemaining)>3;
if(periodChanged||statusChanged||driftTooMuch||localCountdown<=0)localCountdown=serverRemaining;
if(d.balance!==null&&d.balance!==undefined)document.getElementById('balanceDisplay').textContent=parseFloat(d.balance).toFixed(2);
// 状态徽章
const badge=document.getElementById('statusBadge');const statusMap={pending:['<?=$t('betting')?>','seal-status','color:var(--success)'],locked:['<?=$t('sealed')?>','seal-status','color:var(--danger)'],drawn:[I18N.drawing,'seal-status','color:var(--warn)'],settled:[I18N.settled_label,'seal-status','color:var(--warn)']};const s=statusMap[periodData.status]||statusMap.pending;badge.textContent=s[0];badge.className=s[1];badge.style.cssText=s[2];
// 期号
const fullPn=periodData.period_number||'---';const shortPn=fullPn.length>8?fullPn.slice(0,-4)+'-'+fullPn.slice(-4):fullPn;const pnEl=document.getElementById('periodNum');pnEl.textContent=shortPn;pnEl.title=fullPn;
// 提取结果
function extractResultArr(res){if(!res)return null;const 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(v=>v)?arr:null;}
// 投注中→待机动画
if(periodData.status==='pending'){if(statusChanged||periodChanged){if(d.last_result&&periodChanged&&raceAnimState!=='finished'){const ra=extractResultArr(d.last_result);if(ra){animateRace(ra);renderResult(d.last_result,'lastResult');const lp=d.last_result.period_number||'';document.getElementById('lastPeriodNum').textContent=lp.length>4?'#'+lp.slice(-4):lp;setTimeout(function(){resetRace();setTimeout(startIdleAnimation,500);},6500);}else{resetRace();setTimeout(startIdleAnimation,600);}}else{resetRace();setTimeout(startIdleAnimation,600);}}else if(raceAnimState==='idle'){startIdleAnimation();}}
// 封盘→比赛或预备
if(periodData.status==='locked'){const raceRes=extractResultArr(d.race_result);if(raceRes&&raceAnimState!=='racing'&&raceAnimState!=='finished'){animateRace(raceRes);}else if(!raceRes&&(statusChanged||(raceAnimState!=='ready'&&raceAnimState!=='racing'&&raceAnimState!=='finished'))){startReadyAnimation();}}
// 开奖/结算
if(d.last_result&&(periodData.status==='drawn'||periodData.status==='settled')){if(statusChanged||periodChanged){const lp=d.last_result.period_number||'';document.getElementById('lastPeriodNum').textContent=lp.length>4?'#'+lp.slice(-4):lp;const ra=extractResultArr(d.last_result);if(ra&&raceAnimState!=='finished')animateRace(ra);setTimeout(()=>{renderResult(d.last_result,'lastResult');},5500);}}
// 首次加载
if(d.last_result&&!lastAnimatedResult&&raceAnimState==='idle'){const ra=extractResultArr(d.last_result);if(ra){const lp=d.last_result.period_number||'';document.getElementById('lastPeriodNum').textContent=lp.length>4?'#'+lp.slice(-4):lp;animateRace(ra);setTimeout(()=>{renderResult(d.last_result,'lastResult');if(periodData.status==='pending')setTimeout(function(){resetRace();setTimeout(startIdleAnimation,500);},1500);},5500);}}
renderMyBets(d.my_bets||[]);renderLastMyBets(d.last_my_bets||[]);
// 历史
if(d.history&&periodChanged){const hl=document.getElementById('historyList');hl.innerHTML='';d.history.forEach(h=>{const row=document.createElement('div');row.className='card';row.style.cssText='padding:6px 8px;margin-bottom:4px';const pn=h.period_number||'';const sp=pn.length>4?'#'+pn.slice(-4):pn;let balls='';['rank_1','rank_2','rank_3','rank_4','rank_5','rank_6','rank_7','rank_8','rank_9','rank_10'].forEach(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);});}
lastPollStatus=periodData.status;lastPeriodId=periodData.id||0;
}catch(e){console.error('pollPeriod error:',e);}}
function showToast(msg,type){const 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(()=>t.remove(),2500);}
async function setLang(l){await fetch('/api/set-lang',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({lang:l})});location.reload();}
pollPeriod();
(function dynamicPoll(){const delay=(lastPollStatus==='locked'||lastPollStatus==='drawn'||lastPollStatus==='settled')?1500:3000;setTimeout(function(){pollPeriod().then(dynamicPoll).catch(dynamicPoll);},delay);})();
</script>
</body></html>
+165
View File
@@ -0,0 +1,165 @@
<?php use App\Core\I18n; I18n::init(); $t = function($k,$p=[]){return I18n::t($k,$p);}; ?>
<!DOCTYPE html><html lang="<?=I18n::getLang()?>">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title><?=$t('statistics')?></title>
<link rel="stylesheet" href="/Static/css/app.css">
<style>
.stats-header{background:#fff;padding:12px 15px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #F0F0F0;position:sticky;top:0;z-index:10}
.stats-header .back{background:none;border:none;font-size:20px;color:#333;cursor:pointer;padding:0 8px 0 0}
.stats-tabs{display:flex;background:#fff;border-bottom:1px solid #F0F0F0}
.stats-tab{flex:1;text-align:center;padding:14px 0;font-size:15px;color:#666;cursor:pointer;position:relative;background:none;border:none}
.stats-tab.active{color:#333;font-weight:600}
.stats-tab.active::after{content:'';position:absolute;bottom:0;left:30%;right:30%;height:2px;background:var(--primary)}
.filter-card{background:#fff;margin:10px;border-radius:12px;padding:15px;box-shadow:0 1px 6px rgba(0,0,0,.06)}
.filter-title{text-align:center;color:var(--primary);font-size:14px;font-weight:600;margin-bottom:10px}
.tag-row{display:flex;flex-wrap:wrap;gap:8px;justify-content:center}
.tag{padding:6px 14px;border:1px solid #ddd;border-radius:20px;font-size:13px;color:#666;cursor:pointer;background:#fff;transition:all .2s}
.tag.active{border-color:var(--primary);color:var(--primary);background:#F0F8FF}
.ball-grid{display:flex;justify-content:center;gap:6px;flex-wrap:wrap;margin:15px 0}
.ball-col{display:flex;flex-direction:column;align-items:center;gap:4px}
.ball-col .count{font-size:11px;color:#999}
.bead-grid{display:grid;grid-template-columns:repeat(10,1fr);gap:2px;margin:10px;font-size:12px;text-align:center}
.bead-grid .cell{padding:6px 2px;color:#666;border-bottom:1px dotted #eee}
/* PLACEHOLDER_STREAK */
.streak-table{width:100%;border-collapse:collapse;margin:10px}
.streak-table th{background:var(--primary);color:#fff;padding:10px;font-size:14px}
.streak-table td{padding:10px 12px;border-bottom:1px solid #F0F0F0;font-size:14px;text-align:center}
.streak-table tr:nth-child(even){background:#FAFAFA}
.streak-badge{display:inline-block;padding:3px 10px;border-radius:4px;color:#fff;font-size:12px;font-weight:600}
</style>
</head>
<body class="pb-nav" style="background:#F5F5F5">
<div class="stats-header">
<button class="back" onclick="history.back()"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg></button>
<span style="font-weight:700;font-size:16px"><?=$t('statistics')?><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:middle;margin-left:4px"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg></span>
<span style="width:28px"></span>
</div>
<div class="stats-tabs">
<button class="stats-tab active" onclick="showStatsTab('bead',this)"><?=$t('road_bead')?></button>
<button class="stats-tab" onclick="showStatsTab('streak',this)"><?=$t('two_side_streak')?></button>
</div>
<!-- 路珠 Tab -->
<div id="tabBead">
<?php
$rankLabels=[];
for($i=1;$i<=10;$i++) $rankLabels[$i]=$i<=2?($i==1?$t('champion'):$t('runner_up')):$t('rank_n',['n'=>$i]);
?>
<div class="filter-card">
<div class="filter-title"><?=$t('filter_rank')?></div>
<div class="tag-row" id="rankTags">
<?php for($i=1;$i<=10;$i++): ?>
<span class="tag <?=$i==1?'active':''?>" onclick="filterRank(<?=$i?>,this)"><?=$rankLabels[$i]?></span>
<?php endfor; ?>
</div>
</div>
<div class="filter-card">
<div class="ball-grid" id="ballStats"></div>
</div>
<div class="filter-card">
<div class="filter-title"><?=$t('filter_bead')?></div>
<div class="tag-row" id="beadTags">
<span class="tag active" onclick="filterBead('rank',this)"><?=$t('champion')?></span>
<span class="tag" onclick="filterBead('bs',this)"><?=$t('big')?>/<?=$t('small')?></span>
<span class="tag" onclick="filterBead('oe',this)"><?=$t('odd')?>/<?=$t('even')?></span>
<span class="tag" onclick="filterBead('sum',this)"><?=$t('sum')?></span>
<span class="tag" onclick="filterBead('sum_bs',this)"><?=$t('sum_bs_big')?>/<?=$t('sum_bs_small')?></span>
<span class="tag" onclick="filterBead('sum_oe',this)"><?=$t('sum_bs_odd')?>/<?=$t('sum_bs_even')?></span>
</div>
<div class="bead-grid" id="beadGrid"></div>
</div>
</div>
<!-- 两面长龙 Tab -->
<div id="tabStreak" class="hidden">
<div style="margin:10px;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 1px 6px rgba(0,0,0,.06)">
<table class="streak-table">
<thead><tr><th><?=$t('rank_number')?></th><th><?=$t('periods_count')?></th></tr></thead>
<tbody id="streakBody"></tbody>
</table>
</div>
</div>
<?php $navActive='game'; include __DIR__.'/_nav.php'; ?>
<script>
const DATA=<?=json_encode(array_values($results??[]))?>;
const COLORS=['','#e74c3c','#3498db','#2ecc71','#f39c12','#9b59b6','#1abc9c','#e67e22','#e91e63','#00bcd4','#8bc34a'];
const rankLabels=<?=json_encode(array_values($rankLabels))?>;
const I18N={big:'<?=$t('big')?>',small:'<?=$t('small')?>',odd:'<?=$t('odd')?>',even:'<?=$t('even')?>',dragon:'<?=$t('dragon')?>',tiger:'<?=$t('tiger')?>',sum_big:'<?=$t('sum_bs_big')?>',sum_small:'<?=$t('sum_bs_small')?>',sum_odd:'<?=$t('sum_bs_odd')?>',sum_even:'<?=$t('sum_bs_even')?>'};
let curRank=1,curBead='rank';
function showStatsTab(id,btn){
document.getElementById('tabBead').classList.toggle('hidden',id!=='bead');
document.getElementById('tabStreak').classList.toggle('hidden',id!=='streak');
document.querySelectorAll('.stats-tab').forEach(t=>t.classList.remove('active'));
btn.classList.add('active');
if(id==='streak')renderStreak();
}
function filterRank(r,el){curRank=r;document.querySelectorAll('#rankTags .tag').forEach(t=>t.classList.remove('active'));el.classList.add('active');renderBalls();renderBead();}
function filterBead(type,el){curBead=type;document.querySelectorAll('#beadTags .tag').forEach(t=>t.classList.remove('active'));el.classList.add('active');renderBead();}
function renderBalls(){
const counts={};for(let i=1;i<=10;i++)counts[i]=0;
DATA.forEach(r=>{const v=+r['rank_'+curRank];if(v)counts[v]++;});
const el=document.getElementById('ballStats');
let h='';for(let i=1;i<=10;i++){h+='<div class="ball-col"><div class="car c'+i+'" style="width:32px;height:32px;font-size:13px">'+i+'</div><div class="count">'+counts[i]+'</div></div>';}
el.innerHTML=h;
}
function renderBead(){
const grid=document.getElementById('beadGrid');
const rows=DATA.slice(0,20);// last 20 periods, 10 cols each
let h='';
rows.forEach(r=>{
for(let c=1;c<=10;c++){
let val='';
if(curBead==='rank'){val=r['rank_'+c]||'';h+='<div class="cell" style="color:'+COLORS[+val||0]+'">'+val+'</div>';}
else if(curBead==='bs'){const v=+r['rank_'+c];val=v>5?I18N.big:I18N.small;h+='<div class="cell">'+val+'</div>';}
else if(curBead==='oe'){const v=+r['rank_'+c];val=v%2?I18N.odd:I18N.even;h+='<div class="cell">'+val+'</div>';}
else if(curBead==='sum'){const s=(+r.rank_1)+(+r.rank_2);h+='<div class="cell" style="grid-column:span 10;font-weight:600">'+s+'</div>';break;}
else if(curBead==='sum_bs'){const s=(+r.rank_1)+(+r.rank_2);val=s>=12?I18N.sum_big:I18N.sum_small;h+='<div class="cell" style="grid-column:span 10">'+val+'</div>';break;}
else if(curBead==='sum_oe'){const s=(+r.rank_1)+(+r.rank_2);val=s%2?I18N.sum_odd:I18N.sum_even;h+='<div class="cell" style="grid-column:span 10">'+val+'</div>';break;}
}
});
grid.innerHTML=h;
}
function renderStreak(){
// 计算两面长龙:每个名次的大小/单双/龙虎连续出现次数
const streaks=[];
for(let r=1;r<=10;r++){
if(DATA.length<2)continue;
const v0=+DATA[0]['rank_'+r];
// 大小
const bs0=v0>5?'big':'small';let bsCnt=1;
for(let i=1;i<DATA.length;i++){const v=+DATA[i]['rank_'+r];if((v>5?'big':'small')===bs0)bsCnt++;else break;}
if(bsCnt>=2)streaks.push({rank:rankLabels[r-1],type:bs0==='big'?I18N.big:I18N.small,color:bs0==='big'?'#e74c3c':'#3498db',count:bsCnt});
// 单双
const oe0=v0%2?'odd':'even';let oeCnt=1;
for(let i=1;i<DATA.length;i++){const v=+DATA[i]['rank_'+r];if((v%2?'odd':'even')===oe0)oeCnt++;else break;}
if(oeCnt>=2)streaks.push({rank:rankLabels[r-1],type:oe0==='odd'?I18N.odd:I18N.even,color:oe0==='odd'?'#f39c12':'#9b59b6',count:oeCnt});
// 龙虎 (rank 1-5)
if(r<=5){const a=+DATA[0]['rank_'+r],b=+DATA[0]['rank_'+(11-r)];const dt0=a>b?'dragon':'tiger';let dtCnt=1;
for(let i=1;i<DATA.length;i++){const a2=+DATA[i]['rank_'+r],b2=+DATA[i]['rank_'+(11-r)];if((a2>b2?'dragon':'tiger')===dt0)dtCnt++;else break;}
if(dtCnt>=2)streaks.push({rank:rankLabels[r-1],type:dt0==='dragon'?I18N.dragon:I18N.tiger,color:dt0==='dragon'?'#e74c3c':'#3498db',count:dtCnt});}
}
// 冠亚和大小单双
if(DATA.length>=2){
const s0=(+DATA[0].rank_1)+(+DATA[0].rank_2);
const sbs0=s0>=12?'big':'small';let sbsCnt=1;
for(let i=1;i<DATA.length;i++){const s=(+DATA[i].rank_1)+(+DATA[i].rank_2);if((s>=12?'big':'small')===sbs0)sbsCnt++;else break;}
if(sbsCnt>=2)streaks.push({rank:I18N.sum_big.replace(I18N.big,'').trim()||'<?=$t('sum')?>',type:sbs0==='big'?I18N.sum_big:I18N.sum_small,color:sbs0==='big'?'#e74c3c':'#3498db',count:sbsCnt});
const soe0=s0%2?'odd':'even';let soeCnt=1;
for(let i=1;i<DATA.length;i++){const s=(+DATA[i].rank_1)+(+DATA[i].rank_2);if((s%2?'odd':'even')===soe0)soeCnt++;else break;}
if(soeCnt>=2)streaks.push({rank:'<?=$t('sum_bs_tab')?>',type:soe0==='odd'?I18N.sum_odd:I18N.sum_even,color:soe0==='odd'?'#f39c12':'#9b59b6',count:soeCnt});
}
streaks.sort((a,b)=>b.count-a.count);
const body=document.getElementById('streakBody');
body.innerHTML=streaks.map(s=>'<tr><td style="text-align:left;color:var(--primary)">'+s.rank+' <span class="streak-badge" style="background:'+s.color+'">'+s.type+'</span></td><td style="font-weight:600">'+s.count+'</td></tr>').join('');
}
renderBalls();renderBead();
</script>
</body></html>
+147
View File
@@ -0,0 +1,147 @@
<?php use App\Core\I18n; I18n::init(); $t = function($k,$p=[]){return I18n::t($k,$p);}; ?>
<!DOCTYPE html><html lang="<?=I18n::getLang()?>">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title><?=$t('my_profile')?></title>
<link rel="stylesheet" href="/Static/css/app.css">
<style>
.header-bg{background:linear-gradient(135deg,#E8F4FF,#D0E8FF);padding:15px 15px 80px}
.top-toolbar{display:flex;justify-content:flex-end;gap:20px;margin-bottom:30px}
.top-toolbar .icon{cursor:pointer;opacity:.9}
.top-toolbar .icon:hover{opacity:1}
.user-info{display:flex;align-items:center;gap:15px}
.avatar-circle{width:60px;height:60px;border-radius:50%;border:3px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.1);background:#c8dff5;display:flex;align-items:center;justify-content:center}
.avatar-circle svg{width:32px;height:32px;color:#7ab0e0}
.username{flex:1;font-size:18px;font-weight:600;color:#333;display:flex;align-items:center;gap:5px}
.wallet-card{background:#fff;margin:-60px 15px 15px;padding:20px;border-radius:16px;box-shadow:0 2px 12px rgba(0,0,0,.08);position:relative;z-index:2}
.wallet-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px}
.wallet-title{font-size:16px;font-weight:600;color:#333}
.wallet-actions{display:flex;gap:20px}
.action-btn{display:flex;flex-direction:column;align-items:center;gap:4px;cursor:pointer;background:none;border:none;padding:0}
.action-btn span{font-size:12px;color:#1E90FF}
.balance-num{font-size:32px;font-weight:700;color:#333;margin:10px 0}
.section{background:#fff;margin:0 15px 15px;border-radius:12px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.06)}
.section-title{font-size:16px;font-weight:600;color:#333;padding:15px 20px;border-bottom:1px solid #F5F5F5}
.menu-item{display:flex;align-items:center;padding:16px 20px;border-bottom:1px solid #F5F5F5;cursor:pointer;transition:background .2s}
.menu-item:last-child{border-bottom:none}
.menu-item:active{background:#F8F8F8}
.menu-icon{margin-right:15px;display:flex}
.menu-text{flex:1;font-size:15px;color:#333}
.menu-arrow{font-size:20px;color:#CCC}
</style>
</head>
<body style="background:#F5F5F5;padding-bottom:60px">
<!-- 顶部背景 -->
<div class="header-bg">
<div class="top-toolbar">
<a href="/logout" class="icon" title="<?=$t('logout')?>">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#666" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
</a>
</div>
<div class="user-info">
<div class="avatar-circle"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></div>
<div class="username"><?=htmlspecialchars($user['username']??'')?><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="3"><polyline points="9 18 15 12 9 6"/></svg></div>
</div>
</div>
<!-- 钱包卡片 -->
<div class="wallet-card">
<div class="wallet-header">
<div class="wallet-title"><?=$t('balance')?></div>
<div class="wallet-actions">
<button class="action-btn" onclick="alert('<?=$t('deposit')?>')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#1E90FF" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>
<span><?=$t('deposit')?></span>
</button>
<button class="action-btn" onclick="alert('<?=$t('withdraw')?>')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#1E90FF" stroke-width="2"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
<span><?=$t('withdraw')?></span>
</button>
<button class="action-btn" onclick="alert('<?=$t('transfer')?>')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#1E90FF" stroke-width="2"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
<span><?=$t('transfer')?></span>
</button>
</div>
</div>
<div class="balance-num"><?=number_format($user['balance']??0,2)?></div>
</div>
<!-- USDT绑定 -->
<div class="section">
<div class="section-title"><?=$t('usdt_address')?></div>
<div style="padding:16px 20px">
<?php if(!empty($user['usdt_address'])): ?>
<div style="background:#F0FFF0;border-radius:8px;padding:10px;font-size:13px;font-family:monospace;color:#4CAF50;word-break:break-all"><?=htmlspecialchars($user['usdt_address'])?> (<?=$user['usdt_chain']?>)</div>
<?php else: ?>
<div style="display:flex;gap:8px">
<input type="text" id="usdtAddr" placeholder="T..." class="input" style="flex:1;font-size:13px">
<button onclick="bindUsdt()" class="btn btn-primary btn-sm"><?=$t('bind_address')?></button>
</div>
<?php endif; ?>
</div>
</div>
<!-- 投注记录 -->
<div class="section">
<div class="section-title"><?=$t('bet_history')?></div>
<?php
function formatBetDisplay($betType, $betValue, $t) {
if ($betType==='rank'&&preg_match('/^rank(\d+)_(\d+)$/',$betValue,$m)){$r=(int)$m[1];$rl=$r===1?$t('champion'):($r===2?$t('runner_up'):$t('rank_n',['n'=>$r]));return $rl.' #'.$m[2];}
if ($betType==='bs'&&preg_match('/^rank(\d+)_(big|small)$/',$betValue,$m)){$r=(int)$m[1];$rl=$r===1?$t('champion'):($r===2?$t('runner_up'):$t('rank_n',['n'=>$r]));return $rl.' '.($m[2]==='big'?$t('big'):$t('small'));}
if ($betType==='oe'&&preg_match('/^rank(\d+)_(odd|even)$/',$betValue,$m)){$r=(int)$m[1];$rl=$r===1?$t('champion'):($r===2?$t('runner_up'):$t('rank_n',['n'=>$r]));return $rl.' '.($m[2]==='odd'?$t('odd'):$t('even'));}
if ($betType==='dt'&&preg_match('/^dt(\d+)_(dragon|tiger)$/',$betValue,$m)){$ps=[1=>[1,10],2=>[2,9],3=>[3,8],4=>[4,7],5=>[5,6]];$p=$ps[(int)$m[1]]??[0,0];return $t('rank_n',['n'=>$p[0]]).'vs'.$t('rank_n',['n'=>$p[1]]).' '.($m[2]==='dragon'?$t('dragon'):$t('tiger'));}
if ($betType==='sum'&&preg_match('/^sum_(\d+)$/',$betValue,$m))return $t('sum').' '.$m[1];
if ($betType==='sum_bs'){$map=['sum_big'=>$t('sum_big'),'sum_small'=>$t('sum_small'),'sum_odd'=>$t('sum_odd'),'sum_even'=>$t('sum_even')];return $map[$betValue]??$betValue;}
return $betType.':'.$betValue;
}
?>
<?php if(empty($bets)): ?>
<div style="text-align:center;color:#999;padding:40px 20px;font-size:14px"><?=$t('no_data')?></div>
<?php else: foreach($bets as $b): ?>
<div class="menu-item" style="cursor:default">
<div style="flex:1;min-width:0">
<div style="font-size:14px;color:#333"><?=htmlspecialchars(formatBetDisplay($b['bet_type'],$b['bet_value'],$t))?></div>
<div style="font-size:11px;color:#999;margin-top:2px"><?=$b['period_number']?> · <?=$b['created_at']?></div>
</div>
<div style="text-align:right">
<div style="font-size:14px;color:#333"><?=number_format($b['amount'],2)?> x<?=$b['odds']?></div>
<span style="font-size:12px;color:<?=$b['status']==='win'?'#4CAF50':($b['status']==='lose'?'#FF4444':'#FF9800')?>;font-weight:600"><?=$t($b['status'])?></span>
</div>
</div>
<?php endforeach; endif; ?>
</div>
<!-- 交易记录 -->
<div class="section">
<div class="section-title"><?=$t('transaction_history')?></div>
<?php if(empty($transactions)): ?>
<div style="text-align:center;color:#999;padding:40px 20px;font-size:14px"><?=$t('no_data')?></div>
<?php else: foreach($transactions as $tx): $isPos=(float)$tx['amount']>0; ?>
<div class="menu-item" style="cursor:default">
<div style="flex:1"><div style="font-size:14px;color:#333"><?=$tx['type']?></div><div style="font-size:11px;color:#999;margin-top:2px"><?=$tx['created_at']?></div></div>
<span style="font-size:15px;font-weight:600;color:<?=$isPos?'#4CAF50':'#FF4444'?>"><?=$isPos?'+':''?><?=number_format($tx['amount'],2)?></span>
</div>
<?php endforeach; endif; ?>
</div>
<!-- 其他 -->
<div class="section">
<div class="section-title"><?=$t('other')?></div>
<a href="/details" class="menu-item" style="text-decoration:none">
<div class="menu-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#666" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg></div>
<div class="menu-text"><?=$t('bet_details')?></div>
<div class="menu-arrow"></div>
</a>
<a href="/lottery" class="menu-item" style="text-decoration:none">
<div class="menu-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#666" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
<div class="menu-text"><?=$t('lottery_history')?></div>
<div class="menu-arrow"></div>
</a>
</div>
<?php $navActive='profile'; include __DIR__.'/_nav.php'; ?>
<script>
async function bindUsdt(){const addr=document.getElementById('usdtAddr').value.trim();const r=await fetch('/api/bind-usdt',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({usdt_address:addr})});const d=await r.json();alert(d.message);if(d.success)location.reload();}
</script>
</body></html>
+424
View File
@@ -0,0 +1,424 @@
<html lang="vi">
<?php
// Mock Odds for Xoc Dia (Static Preview)
$odds = [
'chan' => 0.96,
'le' => 0.96,
'4red' => 12,
'4white' => 12,
'3red1white' => 2.6,
'3white1red' => 2.6
];
?>
<head>
<title>TM68 - Xóc Đĩa Live</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<!-- Reuse existing styles -->
<link rel="stylesheet" href="/Static/css/style.css">
<link rel="stylesheet" href="/Static/ckplayer/css/ckplayer.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/Static/layui/css/layui.css">
<style>
/* 隐藏ckplayer控制条 */
.ckplayer-ckplayer .ck-bar, .ckplayer-ckplayer .ck-center-play, .ckplayer-ckplayer .ck-controls {
display: none !important;
}
/* Xoc Dia Specific Styles */
.xocdia-top-row {
display: flex;
gap: 12px;
margin-bottom: 12px;
min-height: 100px;
}
.xocdia-big-btn {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 12px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
border: 2px solid rgba(255,255,255,0.1);
}
.xocdia-big-btn:hover {
transform: translateY(-2px);
filter: brightness(1.1);
}
.xocdia-big-btn.bet-chan {
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%);
box-shadow: 0 4px 15px rgba(30, 58, 138, 0.4);
}
.xocdia-big-btn.bet-le {
background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%);
box-shadow: 0 4px 15px rgba(185, 28, 28, 0.4);
}
.xocdia-title {
font-size: 28px;
font-weight: 900;
color: #fff;
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
margin-bottom: 4px;
letter-spacing: 1px;
}
.xocdia-subtitle {
font-size: 11px;
color: rgba(255,255,255,0.8);
margin-bottom: 4px;
}
.xocdia-odds {
background: rgba(0,0,0,0.3);
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 700;
color: #ffd700;
border: 1px solid rgba(255,215,0,0.3);
}
.xocdia-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-bottom: 15px;
}
.xocdia-cell {
background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%);
border: 1px solid #cbd5e1;
border-radius: 12px;
padding: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
cursor: pointer;
transition: all 0.2s;
min-height: 80px;
position: relative;
}
.xocdia-cell:hover {
transform: translateY(-2px);
border-color: #3b82f6;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
.xocdia-dots {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
width: 28px;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
display: block;
box-shadow: inset 0 -1px 2px rgba(0,0,0,0.2);
}
.dot.red {
background-color: #ef4444;
border: 1px solid #dc2626;
}
.dot.white {
background-color: #fff;
border: 1px solid #cbd5e1;
}
.xocdia-label {
font-size: 11px;
font-weight: 800;
color: #334155;
text-align: center;
line-height: 1.2;
}
.xocdia-cell .bet-odds-mini {
margin-top: 0;
background: rgba(0,0,0,0.05);
color: #64748b;
}
/* Mobile Responsive Adjustments */
@media (max-width: 768px) {
.xocdia-grid {
grid-template-columns: repeat(2, 1fr);
}
.xocdia-top-row {
min-height: 80px;
}
}
/* PC Specific Adjustments matching live.php style */
@media (min-width: 769px) {
html, body {
background: #000;
overflow: hidden;
height: 100%;
}
.live-wrapper {
height: 100vh;
max-width: 1600px;
margin: 0 auto;
padding: 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.live-layout {
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 20px;
flex: 1;
min-height: 0;
}
.bet-panel-v2 {
height: 100%;
overflow-y: auto;
background: rgba(255, 255, 255, 0.98);
border-radius: 16px;
padding: 20px;
}
.xocdia-top-row {
min-height: 120px;
}
.xocdia-grid {
grid-template-columns: repeat(4, 1fr);
}
}
</style>
</head>
<body class="live-body" style="background-color: black;">
<img id="bg-img" src="/Static/tm68/background.png" alt="Background" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; z-index: -1;">
<div class="live-wrapper">
<!-- Top Bar (Reused) -->
<div class="live-topbar">
<div class="live-topbar-left">
<button class="live-circle-btn" type="button" onclick="window.location.href='/'"><i class="fa-solid fa-arrow-left"></i></button>
<div class="live-balance">
<div class="live-balance-coin"><i class="fa-solid fa-dollar-sign"></i></div>
<div class="live-balance-text">
<div class="live-balance-id">ID: <?= htmlspecialchars($user['username'] ?? 'User') ?></div>
<div class="live-balance-amount"><?= number_format(($user['balance'] ?? 0) / 1000, 0, ',', '.') ?>k</div>
</div>
<button class="live-circle-btn live-refresh"><i class="fa-solid fa-rotate-right"></i></button>
</div>
</div>
<div class="live-topbar-right">
<button class="lobby-icon-btn"><i class="fa-solid fa-user"></i></button>
<button class="lobby-icon-btn"><i class="fa-solid fa-bars"></i></button>
</div>
</div>
<div class="live-layout live-layout-v2">
<!-- Left: Video Stream -->
<div class="live-left">
<div class="live-panel live-stream live-stream-v2" style="background: #000; position: relative;">
<div class="live-stream-main">
<!-- Placeholder for CKPlayer -->
<div id="tm68LivePlayer" class="live-player" data-stream="<?= htmlspecialchars($streamUrl ?? '') ?>"></div>
<div class="live-player-info" style="position: absolute; bottom: 10px; right: 10px; color: #fff; text-align: right; z-index: 10;">
<div class="live-player-period">G1202512200001</div>
<div class="live-player-datetime">Loading...</div>
</div>
</div>
</div>
<!-- Roadmap (Optional for Xoc Dia, simplified) -->
<div class="live-panel live-history" style="margin-top: 15px; background: rgba(255,255,255,0.9); padding: 10px; border-radius: 12px; height: 150px;">
<div style="font-size: 12px; font-weight: bold; color: #333; margin-bottom: 5px;">Lịch sử cầu</div>
<div style="display: flex; gap: 5px; flex-wrap: wrap;">
<!-- Mock History Dots -->
<span class="dot red"></span><span class="dot white"></span><span class="dot red"></span><span class="dot red"></span>
<span class="dot white"></span><span class="dot white"></span><span class="dot red"></span><span class="dot white"></span>
</div>
</div>
</div>
<!-- Right: Xoc Dia Betting Panel -->
<div class="live-panel bet-panel bet-panel-v2">
<!-- Overlay for Countdown/Lock -->
<div id="betOverlay" class="bet-overlay hidden" style="position: absolute; inset: 0; background: rgba(0,0,0,0.6); z-index: 50; display: none; align-items: center; justify-content: center; flex-direction: column; border-radius: 16px;">
<div id="statusDisplay" class="status-text" style="color: #fff; font-size: 24px; font-weight: bold;">ĐANG TẢI...</div>
</div>
<!-- Timer -->
<div id="activeCountdown" style="text-align: right; margin-bottom: 10px; font-weight: bold; color: #333;">
<i class="fa-regular fa-clock"></i> <span id="activeTimer" style="color: #d93025; font-size: 18px;">60</span>s
</div>
<!-- Main Bets: Chan / Le -->
<div class="xocdia-top-row">
<div class="xocdia-big-btn bet-chan" data-bet-type="chan" data-odds="1:<?= $odds['chan'] ?>">
<div class="xocdia-title">CHẴN</div>
<div class="xocdia-subtitle">4 Đỏ, 4 Trắng, 2 Đỏ 2 Trắng</div>
<div class="xocdia-odds">1:<?= $odds['chan'] ?></div>
<div class="bet-amount-display" style="display:none"></div>
</div>
<div class="xocdia-big-btn bet-le" data-bet-type="le" data-odds="1:<?= $odds['le'] ?>">
<div class="xocdia-title">LẺ</div>
<div class="xocdia-subtitle">3 Đỏ 1 Trắng, 3 Trắng 1 Đỏ</div>
<div class="xocdia-odds">1:<?= $odds['le'] ?></div>
<div class="bet-amount-display" style="display:none"></div>
</div>
</div>
<!-- Grid Bets: Specific Colors -->
<div class="xocdia-grid">
<div class="xocdia-cell" data-bet-type="4red" data-odds="1:<?= $odds['4red'] ?>">
<div class="xocdia-dots"><span class="dot red"></span><span class="dot red"></span><span class="dot red"></span><span class="dot red"></span></div>
<div class="xocdia-label">4 ĐỎ</div>
<div class="bet-odds-mini">1:<?= $odds['4red'] ?></div>
<div class="bet-amount-display" style="display:none"></div>
</div>
<div class="xocdia-cell" data-bet-type="3red1white" data-odds="1:<?= $odds['3red1white'] ?>">
<div class="xocdia-dots"><span class="dot red"></span><span class="dot red"></span><span class="dot red"></span><span class="dot white"></span></div>
<div class="xocdia-label">3 ĐỎ 1 TRẮNG</div>
<div class="bet-odds-mini">1:<?= $odds['3red1white'] ?></div>
<div class="bet-amount-display" style="display:none"></div>
</div>
<div class="xocdia-cell" data-bet-type="3white1red" data-odds="1:<?= $odds['3white1red'] ?>">
<div class="xocdia-dots"><span class="dot white"></span><span class="dot white"></span><span class="dot white"></span><span class="dot red"></span></div>
<div class="xocdia-label">3 TRẮNG 1 ĐỎ</div>
<div class="bet-odds-mini">1:<?= $odds['3white1red'] ?></div>
<div class="bet-amount-display" style="display:none"></div>
</div>
<div class="xocdia-cell" data-bet-type="4white" data-odds="1:<?= $odds['4white'] ?>">
<div class="xocdia-dots"><span class="dot white"></span><span class="dot white"></span><span class="dot white"></span><span class="dot white"></span></div>
<div class="xocdia-label">4 TRẮNG</div>
<div class="bet-odds-mini">1:<?= $odds['4white'] ?></div>
<div class="bet-amount-display" style="display:none"></div>
</div>
</div>
<!-- Actions -->
<div class="bet-actions bet-actions-v2">
<button class="bet-action bet-action-green" type="button" id="confirmBetBtn">
<i class="fa-solid fa-check"></i> <span>ĐẶT CƯỢC</span>
</button>
<button class="bet-action bet-action-red" type="button" id="cancelBetBtn">
<i class="fa-solid fa-xmark"></i> <span>HỦY</span>
</button>
</div>
<!-- Chips -->
<div class="bet-chips bet-chips-v2">
<button class="bet-chip-btn" data-amount="100000" data-label="100K"><div class="bet-chip-display chip-red">100K</div></button>
<button class="bet-chip-btn" data-amount="500000" data-label="500K"><div class="bet-chip-display chip-blue">500K</div></button>
<button class="bet-chip-btn" data-amount="1000000" data-label="1M"><div class="bet-chip-display chip-gold">1M</div></button>
<button class="bet-chip-btn" data-amount="5000000" data-label="5M"><div class="bet-chip-display chip-green">5M</div></button>
</div>
</div>
</div>
</div>
<script src="/Static/layui/layui.js"></script>
<script src="/Static/ckplayer/js/ckplayer.js"></script>
<script>
layui.use(['layer'], function(){
var layer = layui.layer;
var betState = { selectedChip: null, bets: {} };
// --- Chip Selection ---
document.querySelectorAll('.bet-chip-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.bet-chip-btn').forEach(b => b.classList.remove('chip-selected'));
this.classList.add('chip-selected');
var amount = this.getAttribute('data-amount');
var color = this.querySelector('.bet-chip-display').className.match(/chip-(\w+)/)[1];
betState.selectedChip = { amount: parseInt(amount), color: color };
});
});
// --- Bet Placement ---
document.querySelectorAll('.xocdia-big-btn, .xocdia-cell').forEach(el => {
el.addEventListener('click', function() {
if(!betState.selectedChip) {
layer.msg('Vui lòng chọn chip (số tiền) trước!');
return;
}
var type = this.getAttribute('data-bet-type');
var odds = this.getAttribute('data-odds');
// Add to bet state
if(!betState.bets[type]) {
betState.bets[type] = { amount: 0, odds: odds };
}
betState.bets[type].amount += betState.selectedChip.amount;
// Visual Update
updateBetVisual(this, betState.bets[type].amount, betState.selectedChip.color);
this.classList.add('bet-selected');
});
});
function updateBetVisual(el, totalAmount, color) {
var display = el.querySelector('.bet-amount-display');
display.style.display = 'flex';
display.innerHTML = `
<div class="bet-chip-wrapper">
<div class="bet-chip-icon chip-${color}"></div>
</div>
<div class="bet-amount-text">${formatAmount(totalAmount)}</div>
`;
}
function formatAmount(n) {
if(n >= 1000000) return (n/1000000) + 'M';
if(n >= 1000) return (n/1000) + 'K';
return n;
}
// --- Actions ---
document.getElementById('cancelBetBtn').addEventListener('click', function() {
betState.bets = {};
document.querySelectorAll('.bet-amount-display').forEach(d => {
d.innerHTML = '';
d.style.display = 'none';
});
document.querySelectorAll('.bet-selected').forEach(e => e.classList.remove('bet-selected'));
});
document.getElementById('confirmBetBtn').addEventListener('click', function() {
var keys = Object.keys(betState.bets);
if(keys.length === 0) return;
layer.msg('Đang gửi cược...', {icon: 16, shade: 0.01});
// Simulation of API Call
setTimeout(() => {
layer.msg('Đặt cược thành công!', {icon: 1});
// Reset visual but maybe keep state until confirmed by server in real app
document.getElementById('cancelBetBtn').click();
}, 1000);
});
// --- Clock Simulation ---
setInterval(() => {
var d = new Date();
document.querySelector('.live-player-datetime').innerText = d.toLocaleTimeString();
}, 1000);
});
</script>
</body>
</html>