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
+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>