Files
pk10/App/Views/Admin/games.php
T

1121 lines
54 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>
<div class="grid grid-cols-1 md: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">
<?= isset($games) && is_array($games) ? count($games) : 0 ?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<i class="fas fa-dice 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">
<?php
$enabledCount = 0;
if (isset($games) && is_array($games)) {
foreach ($games as $g) {
if (!empty($g['status'])) {
$enabledCount++;
}
}
}
echo $enabledCount;
?>
</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">
<?php
$disabledCount = 0;
if (isset($games) && is_array($games)) {
foreach ($games as $g) {
if (empty($g['status'])) {
$disabledCount++;
}
}
}
echo $disabledCount;
?>
</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-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 class="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
<div class="flex-1 relative">
<input
type="text"
id="gameSearchInput"
class="w-full pl-9 pr-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary/50 focus:border-primary"
placeholder="按游戏名称或标识搜索"
>
<span class="absolute inset-y-0 left-3 flex items-center text-gray-400 text-sm">
<i class="fas fa-search"></i>
</span>
</div>
<select
id="gameStatusFilter"
class="sm:w-40 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary/50 focus:border-primary bg-white"
>
<option value="">全部状态</option>
<option value="1">仅显示启用</option>
<option value="0">仅显示未启用</option>
</select>
<button
id="openGameFormBtn"
type="button"
class="sm:w-auto 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 justify-center text-sm"
>
<i class="fas fa-plus mr-2"></i>
<span>新增游戏</span>
</button>
</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 hidden lg:table-cell">类型与玩法</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">状态</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="gameList">
<?php if (!empty($games) && is_array($games)): ?>
<?php foreach ($games as $game): ?>
<tr class="hover:bg-gray-50 transition-colors" data-id="<?= htmlspecialchars((string)($game['id'] ?? '')) ?>">
<td class="px-4 py-4 align-top">
<div class="flex items-start gap-3">
<div class="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
<i class="fas fa-dice text-primary"></i>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<span class="text-sm font-semibold text-gray-900 truncate">
<?= htmlspecialchars((string)($game['name'] ?? '未命名游戏')) ?>
</span>
<?php if (!empty($game['code'])): ?>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] bg-gray-100 text-gray-600">
<?= htmlspecialchars((string)$game['code']) ?>
</span>
<?php endif; ?>
</div>
<?php if (!empty($game['description'])): ?>
<p class="mt-1 text-xs text-gray-500 line-clamp-2">
<?= htmlspecialchars((string)$game['description']) ?>
</p>
<?php else: ?>
<p class="mt-1 text-xs text-gray-400">
暂无描述
</p>
<?php endif; ?>
</div>
</div>
</td>
<td class="px-4 py-4 align-top">
<?php if (!empty($game['stream_url'])): ?>
<div class="max-w-xs">
<p class="text-xs text-gray-500 break-all">
<?= htmlspecialchars((string)$game['stream_url']) ?>
</p>
<p class="mt-1 text-[11px] text-gray-400">
建议使用稳定的直播推流地址
</p>
</div>
<?php else: ?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-[11px] bg-warning/10 text-warning">
<i class="fas fa-exclamation-triangle mr-1"></i>
未配置
</span>
<?php endif; ?>
</td>
<td class="px-4 py-4 align-top hidden lg:table-cell">
<div class="space-y-1 text-xs text-gray-600">
<div>
<span class="text-gray-500">类型:</span>
<span><?= htmlspecialchars((string)($game['type'] ?? '骰子游戏')) ?></span>
</div>
<div class="flex flex-wrap gap-1 mt-1">
<?php
$tags = $game['tags'] ?? null;
if (is_string($tags)) {
$tags = explode(',', $tags);
}
if (is_array($tags) && count($tags) > 0):
foreach ($tags as $tag):
?>
<span class="px-2 py-0.5 rounded-full bg-gray-100 text-gray-600 text-[11px]">
<?= htmlspecialchars(trim((string)$tag)) ?>
</span>
<?php endforeach; ?>
<?php else: ?>
<span class="text-gray-400">未配置玩法标签</span>
<?php endif; ?>
</div>
</div>
</td>
<td class="px-4 py-4 align-top hidden md:table-cell">
<?php if (!empty($game['updated_at'])): ?>
<div class="text-xs text-gray-500">
<?= date('Y-m-d H:i', strtotime((string)$game['updated_at'])) ?>
</div>
<?php elseif (!empty($game['created_at'])): ?>
<div class="text-xs text-gray-500">
创建于 <?= date('Y-m-d H:i', strtotime((string)$game['created_at'])) ?>
</div>
<?php else: ?>
<span class="text-xs text-gray-400">时间未知</span>
<?php endif; ?>
</td>
<td class="px-4 py-4 align-top">
<?php
$status = !empty($game['status']);
$statusClass = $status ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
$statusText = $status ? '启用' : '停用';
?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $statusClass; ?>">
<span class="w-2 h-2 rounded-full mr-1 <?= $status ? 'bg-green-500' : 'bg-gray-400'; ?>"></span>
<?= $statusText; ?>
</span>
</td>
<td class="px-4 py-4 align-top text-right text-sm font-medium">
<div class="flex items-center justify-end gap-2">
<button
type="button"
class="game-edit-btn text-gray-500 hover:text-blue-500"
data-id="<?= htmlspecialchars((string)($game['id'] ?? '')) ?>"
>
<i class="fas fa-edit"></i>
</button>
<button
type="button"
class="game-odds-btn text-gray-500 hover:text-purple-500 hidden sm:inline-flex"
data-id="<?= htmlspecialchars((string)($game['id'] ?? '')) ?>"
>
<i class="fas fa-percentage"></i>
</button>
<button
type="button"
class="game-toggle-status-btn text-gray-500 hover:text-yellow-500"
data-id="<?= htmlspecialchars((string)($game['id'] ?? '')) ?>"
>
<i class="fas fa-power-off"></i>
</button>
</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>
<button
type="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 text-sm"
onclick="document.getElementById('openGameFormBtn').click();"
>
<i class="fas fa-plus mr-2"></i>
<span>新增游戏</span>
</button>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div
id="gameFormBackdrop"
class="fixed inset-0 bg-black/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 z-40"
></div>
<div
id="gameFormModal"
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-xl max-h-[90vh] overflow-hidden">
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
<h3 id="gameFormTitle" class="text-xl font-bold text-gray-800 flex items-center">
<i class="fas fa-dice text-primary mr-2"></i>
配置游戏
</h3>
<button id="closeGameFormBtn" 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="gameForm" class="space-y-5">
<input type="hidden" id="gameId" name="id">
<div>
<label for="gameName" class="block text-sm font-medium text-gray-700 mb-1">
游戏名称 <span class="text-red-500">*</span>
</label>
<input
type="text"
id="gameName"
name="name"
required
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="例如:Tài Xỉu"
>
<input type="hidden" id="gameCode" name="code">
</div>
<div>
<label for="gameStreamUrl" class="block text-sm font-medium text-gray-700 mb-1">
直播流地址
</label>
<input
type="text"
id="gameStreamUrl"
name="stream_url"
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>
<label class="block text-sm font-medium text-gray-700 mb-1">
游戏封面图片
</label>
<div class="flex items-center gap-3">
<div class="flex-1">
<input
type="text"
id="gameImage"
name="image"
readonly
class="w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-50 text-sm"
placeholder="点击右侧按钮选择图片"
>
</div>
<button
type="button"
onclick="OpenGallery('gameImage', 'gameImagePreview')"
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm whitespace-nowrap"
>
<i class="fas fa-image mr-2"></i>选择图片
</button>
</div>
<div id="gameImagePreview" class="mt-2" style="display: none;">
<img src="" alt="预览图" class="max-w-xs h-32 object-cover rounded-lg border border-gray-200">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="gameType" class="block text-sm font-medium text-gray-700 mb-1">
游戏类型
</label>
<select
id="gameType"
name="type"
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 bg-white"
>
<option value="dice">骰子游戏</option>
<option value="xocdia">Xóc Đĩa(色碟)</option>
<option value="other">其他</option>
</select>
</div>
<div>
<label for="gameSortOrder" class="block text-sm font-medium text-gray-700 mb-1">
排序权重
</label>
<input
type="number"
id="gameSortOrder"
name="sort_order"
value="0"
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"
>
<p class="mt-1 text-[11px] text-gray-400">数字越大越靠前,默认为 0。</p>
</div>
</div>
<div>
<span class="block text-sm font-medium text-gray-700 mb-2">
支持玩法标签
</span>
<div class="grid grid-cols-2 md:grid-cols-3 gap-2 text-sm">
<!-- 骰子游戏标签 -->
<label class="inline-flex items-center">
<input
type="checkbox"
name="tags[]"
value="大小"
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary"
>
<span class="ml-2 text-gray-700">大小</span>
</label>
<label class="inline-flex items-center">
<input
type="checkbox"
name="tags[]"
value="单双"
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary"
>
<span class="ml-2 text-gray-700">单双</span>
</label>
<label class="inline-flex items-center">
<input
type="checkbox"
name="tags[]"
value="总点数"
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary"
>
<span class="ml-2 text-gray-700">总点数</span>
</label>
<label class="inline-flex items-center">
<input
type="checkbox"
name="tags[]"
value="三同号"
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary"
>
<span class="ml-2 text-gray-700">三同号</span>
</label>
<label class="inline-flex items-center">
<input
type="checkbox"
name="tags[]"
value="两同号"
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary"
>
<span class="ml-2 text-gray-700">两同号</span>
</label>
<!-- Xóc Đĩa 标签 -->
<label class="inline-flex items-center">
<input
type="checkbox"
name="tags[]"
value="traditional"
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary"
>
<span class="ml-2 text-gray-700">传统玩法</span>
</label>
<label class="inline-flex items-center">
<input
type="checkbox"
name="tags[]"
value="hot"
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary"
>
<span class="ml-2 text-gray-700">热门</span>
</label>
<label class="inline-flex items-center">
<input
type="checkbox"
name="tags[]"
value="其他"
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary"
>
<span class="ml-2 text-gray-700">其他</span>
</label>
</div>
</div>
<!-- 状态和推荐选项(默认启用并显示在游戏大厅) -->
<input type="hidden" id="gameStatus" name="status" value="1">
<input type="hidden" id="gameFeatured" name="featured" value="1">
</form>
</div>
<div class="border-t border-gray-100 px-6 py-4 flex justify-end gap-3">
<button
id="cancelGameFormBtn"
type="button"
class="px-5 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 text-sm"
>
取消
</button>
<button
id="submitGameFormBtn"
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 text-sm"
>
保存游戏
</button>
</div>
</div>
</div>
<div
id="oddsFormModal"
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-5xl max-h-[90vh] overflow-hidden flex flex-col">
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center flex-shrink-0">
<h3 id="oddsFormTitle" class="text-xl font-bold text-gray-800 flex items-center">
<i class="fas fa-percentage text-purple-500 mr-2"></i>
配置赔率
</h3>
<button id="closeOddsFormBtn" 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 flex-1 bg-gray-50/50">
<form id="oddsForm" class="space-y-6">
<input type="hidden" id="oddsGameId" name="game_id">
<div class="bg-blue-50 border-l-4 border-blue-500 p-4 mb-4 rounded-r">
<div class="flex">
<div class="flex-shrink-0">
<i class="fas fa-info-circle text-blue-500"></i>
</div>
<div class="ml-3">
<p class="text-sm text-blue-700">
<b>赔率说明:</b>输入的数字 = 玩家赢了能赚多少倍。
<br>
例如:赔率 <b>0.97</b> → 玩家下注100中奖后,赚97(拿回100+97=197
<br>
例如:赔率 <b>30</b> → 玩家下注100中奖后,赚3000(拿回100+3000=3100
<br>
<span class="text-blue-500">赔率越高,玩家赢得越多,平台风险越大。</span>
</p>
</div>
</div>
</div>
<!-- 赔率配置区域 -->
<div id="oddsContainer" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<!-- 动态生成 -->
</div>
</form>
</div>
<div class="border-t border-gray-100 px-6 py-4 flex justify-end gap-3 flex-shrink-0 bg-white">
<button
id="cancelOddsFormBtn"
type="button"
class="px-5 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 text-sm"
>
取消
</button>
<button
id="submitOddsFormBtn"
type="button"
class="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2 rounded-lg shadow hover:shadow-md transition-all duration-200 text-sm font-medium"
>
<i class="fas fa-save mr-2"></i>保存赔率配置
</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 缓存DOM元素
const gameFormModal = document.getElementById('gameFormModal');
const gameFormBackdrop = document.getElementById('gameFormBackdrop');
const openGameFormBtn = document.getElementById('openGameFormBtn');
const closeGameFormBtn = document.getElementById('closeGameFormBtn');
const cancelGameFormBtn = document.getElementById('cancelGameFormBtn');
const submitGameFormBtn = document.getElementById('submitGameFormBtn');
const gameFormTitle = document.getElementById('gameFormTitle');
const gameForm = document.getElementById('gameForm');
const gameList = document.getElementById('gameList');
// 检查元素是否存在
function checkElements() {
const elements = [
gameFormModal, gameFormBackdrop, openGameFormBtn,
closeGameFormBtn, cancelGameFormBtn, submitGameFormBtn
];
const missing = elements.filter(el => !el);
if (missing.length > 0) {
console.error('缺少必要的DOM元素,功能无法正常工作');
return false;
}
return true;
}
// 显示表单弹窗
function openGameFormModal() {
if (!checkElements()) return;
resetGameForm();
gameFormModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
gameFormModal.classList.add('scale-100');
gameFormBackdrop.classList.remove('opacity-0', 'pointer-events-none');
document.body.style.overflow = 'hidden';
void gameFormModal.offsetWidth; // 强制重绘
}
// 隐藏表单弹窗
function closeGameFormModal() {
if (!checkElements()) return;
gameFormModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
gameFormModal.classList.remove('scale-100');
gameFormBackdrop.classList.add('opacity-0', 'pointer-events-none');
document.body.style.overflow = '';
}
// 重置表单(新增模式)
function resetGameForm() {
gameForm.reset();
document.getElementById('gameId').value = '';
document.getElementById('gameCode').value = '';
document.getElementById('gameStatus').value = '1';
document.getElementById('gameFeatured').value = '1';
document.getElementById('gameImagePreview').style.display = 'none';
gameFormTitle.innerHTML = '<i class="fas fa-dice text-primary mr-2"></i> 配置游戏';
submitGameFormBtn.innerHTML = '保存游戏';
submitGameFormBtn.disabled = false;
}
// 加载游戏数据(编辑模式)
async function loadGameData(id) {
submitGameFormBtn.disabled = true;
submitGameFormBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 加载中...';
try {
const response = await fetch(`/admin/games/${id}`);
if (!response.ok) throw new Error('获取数据失败');
const data = await response.json();
if (data.success && data.data) {
const game = data.data;
document.getElementById('gameId').value = game.id || '';
document.getElementById('gameName').value = game.name || '';
document.getElementById('gameCode').value = game.code || '';
document.getElementById('gameStreamUrl').value = game.stream_url || '';
document.getElementById('gameType').value = game.type || 'dice';
document.getElementById('gameSortOrder').value = game.sort_order || 0;
document.getElementById('gameStatus').value = game.status == 1 ? '1' : '0';
document.getElementById('gameFeatured').value = game.featured == 1 ? '1' : '0';
// 处理图片
if (game.image) {
document.getElementById('gameImage').value = game.image;
const previewImg = document.querySelector('#gameImagePreview img');
if (previewImg) {
previewImg.src = game.image;
document.getElementById('gameImagePreview').style.display = 'block';
}
} else {
document.getElementById('gameImage').value = '';
document.getElementById('gameImagePreview').style.display = 'none';
}
// 处理标签复选框
const tags = game.tags ? game.tags.split(',') : [];
document.querySelectorAll('input[name="tags[]"]').forEach(checkbox => {
checkbox.checked = tags.includes(checkbox.value);
});
gameFormTitle.innerHTML = '<i class="fas fa-edit text-primary mr-2"></i> 编辑游戏';
} else {
throw new Error(data.message || '获取数据失败');
}
} catch (e) {
showMessage(e.message, 'error');
closeGameFormModal();
} finally {
submitGameFormBtn.disabled = false;
submitGameFormBtn.innerHTML = '保存游戏';
}
}
// 表单验证
function validateGameForm() {
const name = document.getElementById('gameName').value.trim();
if (!name) {
showMessage('请输入游戏名称', 'error');
return false;
}
return true;
}
// 提交表单(创建/更新)
async function submitGameFormData() {
if (!validateGameForm()) return;
const formData = new FormData(gameForm);
const isEditMode = !!document.getElementById('gameId').value;
// status和featured已经是隐藏字段,保持默认值1即可
submitGameFormBtn.disabled = true;
submitGameFormBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 保存中...';
try {
const response = await fetch('/admin/games/update', {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
if (data.success) {
showMessage(isEditMode ? '游戏更新成功' : '游戏创建成功');
closeGameFormModal();
setTimeout(() => location.reload(), 1000);
} else {
throw new Error(data.message || (isEditMode ? '更新失败' : '创建失败'));
}
} catch (e) {
showMessage(e.message, 'error');
} finally {
submitGameFormBtn.disabled = false;
submitGameFormBtn.innerHTML = '保存游戏';
}
}
// 删除游戏
async function deleteGame(id) {
if (!confirm('确定要删除该游戏吗?此操作不可恢复!')) return;
try {
const response = await fetch(`/admin/games/delete/${id}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
showMessage('游戏已删除');
setTimeout(() => location.reload(), 500);
} else {
throw new Error(data.message || '删除失败');
}
} catch (e) {
showMessage(e.message, 'error');
}
}
// 切换游戏状态
async function toggleGameStatus(id) {
try {
const response = await fetch(`/admin/games/toggle/${id}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
showMessage(data.message);
setTimeout(() => location.reload(), 500);
} else {
throw new Error(data.message || '操作失败');
}
} catch (e) {
showMessage(e.message, 'error');
}
}
// 绑定事件
if (checkElements()) {
// 打开表单
openGameFormBtn.addEventListener('click', openGameFormModal);
// 关闭表单
closeGameFormBtn.addEventListener('click', closeGameFormModal);
cancelGameFormBtn.addEventListener('click', closeGameFormModal);
gameFormBackdrop.addEventListener('click', () => {
if (!gameFormModal.classList.contains('invisible')) closeGameFormModal();
});
// 提交表单
submitGameFormBtn.addEventListener('click', submitGameFormData);
// 编辑、删除、切换状态事件委托
if (gameList) {
gameList.addEventListener('click', function(e) {
const editBtn = e.target.closest('.game-edit-btn');
const deleteBtn = e.target.closest('.game-delete-btn');
const toggleBtn = e.target.closest('.game-toggle-status-btn');
const oddsBtn = e.target.closest('.game-odds-btn');
if (editBtn) {
const id = editBtn.getAttribute('data-id');
if (id) {
openGameFormModal();
setTimeout(() => loadGameData(id), 300);
}
} else if (oddsBtn) {
const id = oddsBtn.getAttribute('data-id');
if (id) {
openOddsFormModal();
loadOddsData(id);
}
} else if (deleteBtn) {
const id = deleteBtn.getAttribute('data-id');
if (id) deleteGame(id);
} else if (toggleBtn) {
const id = toggleBtn.getAttribute('data-id');
if (id) toggleGameStatus(id);
}
});
}
// ESC键关闭弹窗
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
if (!gameFormModal.classList.contains('invisible')) closeGameFormModal();
}
});
// ================= 赔率配置功能 =================
const oddsFormModal = document.getElementById('oddsFormModal');
const closeOddsFormBtn = document.getElementById('closeOddsFormBtn');
const cancelOddsFormBtn = document.getElementById('cancelOddsFormBtn');
const submitOddsFormBtn = document.getElementById('submitOddsFormBtn');
const oddsForm = document.getElementById('oddsForm');
const oddsContainer = document.getElementById('oddsContainer');
const oddsFormTitle = document.getElementById('oddsFormTitle');
// gameFormBackdrop is already defined
function openOddsFormModal() {
if (!oddsFormModal) return;
oddsFormModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
oddsFormModal.classList.add('scale-100');
gameFormBackdrop.classList.remove('opacity-0', 'pointer-events-none');
document.body.style.overflow = 'hidden';
}
function closeOddsFormModal() {
if (!oddsFormModal) return;
oddsFormModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
oddsFormModal.classList.remove('scale-100');
// Only hide backdrop if gameFormModal is also closed
if (gameFormModal && gameFormModal.classList.contains('invisible')) {
gameFormBackdrop.classList.add('opacity-0', 'pointer-events-none');
document.body.style.overflow = '';
}
}
async function loadOddsData(gameId) {
document.getElementById('oddsGameId').value = gameId;
oddsContainer.innerHTML = '<div class="col-span-full text-center py-10"><i class="fas fa-spinner fa-spin text-3xl text-gray-300"></i><p class="mt-2 text-gray-500">加载赔率配置...</p></div>';
try {
const response = await fetch(`/admin/games/odds/${gameId}`);
const json = await response.json();
if (json.success) {
renderOddsForm(json.data.odds);
const gameName = json.data.game ? json.data.game.name : '';
oddsFormTitle.innerHTML = `<i class="fas fa-percentage text-purple-500 mr-2"></i> 配置赔率 ${gameName ? '- ' + gameName : ''}`;
} else {
showMessage(json.message, 'error');
closeOddsFormModal();
}
} catch (e) {
showMessage('加载失败: ' + e.message, 'error');
closeOddsFormModal();
}
}
function renderOddsForm(oddsList) {
oddsContainer.innerHTML = '';
// ========== 分组定义 ==========
const groups = {
// PK10 分组
'pk10_rank': { title: '🏎️ PK10 名次(猜第N名是几号车)', icon: 'fa-flag-checkered', items: [], order: 1 },
'pk10_bs': { title: '🔢 PK10 大小(车号≥6大 ≤5小)', icon: 'fa-arrows-alt-v', items: [], order: 2 },
'pk10_oe': { title: '🎯 PK10 单双(车号的奇偶)', icon: 'fa-adjust', items: [], order: 3 },
'pk10_dt': { title: '🐉 PK10 龙虎(前名次vs后名次比大小)', icon: 'fa-dragon', items: [], order: 4 },
'pk10_sum': { title: ' PK10 冠亚和值(冠军+亚军车号之和)', icon: 'fa-plus-circle', items: [], order: 5 },
'pk10_sum_bs': { title: '📊 PK10 冠亚和大小单双', icon: 'fa-chart-bar', items: [], order: 6 },
// 骰子分组
'dice_basic': { title: '🎲 骰子 大小/单双', icon: 'fa-balance-scale', items: [], order: 10 },
'dice_sum': { title: '🔢 骰子 点数竞猜(4-17点)', icon: 'fa-sort-numeric-up', items: [], order: 11 },
'dice_single': { title: '🎯 骰子 单骰点数', icon: 'fa-dice-one', items: [], order: 12 },
'dice_combo': { title: '💎 骰子 豹子(三颗相同)', icon: 'fa-cubes', items: [], order: 13 },
// Xóc Đĩa 分组
'xd_basic': { title: '🪙 色碟 基础玩法', icon: 'fa-circle', items: [], order: 20 },
'xd_special': { title: '🪙 色碟 特殊组合', icon: 'fa-star', items: [], order: 21 },
// 兜底
'other': { title: '⚙️ 其他', icon: 'fa-cog', items: [], order: 99 }
};
// ========== PK10 target 中文翻译 ==========
const rankNames = {1:'冠军',2:'亚军',3:'第3名',4:'第4名',5:'第5名',6:'第6名',7:'第7名',8:'第8名',9:'第9名',10:'第10名'};
const dtPairs = {1:[1,10],2:[2,9],3:[3,8],4:[4,7],5:[5,6]};
function getPK10Label(type, target) {
let m;
if (type === 'rank') {
m = target.match(/^rank(\d+)_(\d+)$/);
if (m) return (rankNames[+m[1]]||'第'+m[1]+'名') + ' ' + m[2] + '号车';
return target;
}
if (type === 'bs') {
m = target.match(/^rank(\d+)_(big|small)$/);
if (m) return (rankNames[+m[1]]||'第'+m[1]+'名') + ' ' + (m[2]==='big'?'大':'小');
return target;
}
if (type === 'oe') {
m = target.match(/^rank(\d+)_(odd|even)$/);
if (m) return (rankNames[+m[1]]||'第'+m[1]+'名') + ' ' + (m[2]==='odd'?'单':'双');
return target;
}
if (type === 'dt') {
m = target.match(/^dt(\d+)_(dragon|tiger)$/);
if (m) {
const p = dtPairs[+m[1]] || [m[1], 11-m[1]];
return '第' + p[0] + 'vs第' + p[1] + ' ' + (m[2]==='dragon'?'龙':'虎');
}
return target;
}
if (type === 'sum') {
m = target.match(/^sum_(\d+)$/);
if (m) return '和值 ' + m[1];
return target;
}
if (type === 'sum_bs') {
const sumMap = {sum_big:'和大',sum_small:'和小',sum_odd:'和单',sum_even:'和双'};
return sumMap[target] || target;
}
return target;
}
// ========== 分类逻辑 ==========
oddsList.forEach(item => {
const t = item.type;
let groupKey = 'other';
// PK10 类型
if (t === 'rank') groupKey = 'pk10_rank';
else if (t === 'bs') groupKey = 'pk10_bs';
else if (t === 'oe') groupKey = 'pk10_oe';
else if (t === 'dt') groupKey = 'pk10_dt';
else if (t === 'sum' && item.target && item.target.startsWith('sum_')) groupKey = 'pk10_sum';
else if (t === 'sum_bs') groupKey = 'pk10_sum_bs';
// 骰子类型
else if (['tai', 'xiu', 'chan', 'le'].includes(t)) groupKey = 'dice_basic';
else if (t === 'sum' || t === 'number') groupKey = 'dice_sum';
else if (t === 'dice') groupKey = 'dice_single';
else if (t === 'combo') groupKey = 'dice_combo';
// Xóc Đĩa 类型
else if (['even', 'odd', 'big_small', 'odd_even'].includes(t)) groupKey = 'xd_basic';
else if (['4red', '4white'].includes(t)) groupKey = 'xd_special';
if (!groups[groupKey]) groups[groupKey] = { title: '其他', icon: 'fa-cog', items: [], order: 99 };
groups[groupKey].items.push(item);
});
// ========== 标签翻译 ==========
function getLabel(item) {
const t = item.type;
const tg = item.target || '';
// PK10
if (['rank','bs','oe','dt','sum_bs'].includes(t)) return getPK10Label(t, tg);
if (t === 'sum' && tg.startsWith('sum_')) return getPK10Label(t, tg);
// 骰子
if (t === 'tai') return '大(总点≥11';
if (t === 'xiu') return '小(总点≤10';
if (t === 'chan') return '双(总点偶数)';
if (t === 'le') return '单(总点奇数)';
if (t === 'sum' || t === 'number') return '押 ' + tg + ' 点';
if (t === 'dice') return '单骰 ' + tg + ' 点';
if (t === 'combo' && tg === 'any_triple') return '任意豹子';
if (t === 'combo' && tg === 'specific_triple') return '指定豹子';
if (t === 'combo') return '豹子 ' + tg;
// Xóc Đĩa
if (t === 'even') return '偶数红';
if (t === 'odd') return '奇数红';
if (t === 'big_small') return '大小';
if (t === 'odd_even') return '单双';
if (t === '4red') return '四红 🔴🔴🔴🔴';
if (t === '4white') return '四白 ⚪⚪⚪⚪';
return t.toUpperCase() + (tg ? ' ' + tg : '');
}
function getBadgeClass(item) {
const t = item.type;
if (t === 'rank') return 'bg-yellow-100 text-yellow-800';
if (t === 'bs') return 'bg-blue-100 text-blue-700';
if (t === 'oe') return 'bg-green-100 text-green-700';
if (t === 'dt') return 'bg-red-100 text-red-700';
if (t === 'sum' && (item.target||'').startsWith('sum_')) return 'bg-orange-100 text-orange-700';
if (t === 'sum_bs') return 'bg-indigo-100 text-indigo-700';
if (['tai','xiu'].includes(t)) return 'bg-blue-100 text-blue-700';
if (['chan','le'].includes(t)) return 'bg-green-100 text-green-700';
if (t === 'sum' || t === 'number') return 'bg-orange-100 text-orange-700 font-bold';
if (t === 'dice') return 'bg-purple-100 text-purple-700';
if (t === 'combo') return 'bg-red-100 text-red-700';
if (['even','odd','big_small','odd_even'].includes(t)) return 'bg-teal-100 text-teal-700';
if (['4red','4white'].includes(t)) return 'bg-pink-100 text-pink-700';
return 'bg-gray-100 text-gray-600';
}
// ========== 按 order 排序渲染 ==========
const sortedGroups = Object.entries(groups).sort((a,b) => (a[1].order||99) - (b[1].order||99));
for (const [key, group] of sortedGroups) {
if (group.items.length === 0) continue;
const groupHtml = `
<div class="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden col-span-1 md:col-span-2 lg:col-span-1 xl:col-span-1">
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200 flex items-center">
<i class="fas ${group.icon} text-gray-400 mr-2"></i>
<h4 class="font-bold text-gray-700 text-sm">${group.title}</h4>
<span class="ml-auto text-xs text-gray-400">${group.items.length}项</span>
</div>
<div class="p-4 space-y-3">
${group.items.map(item => {
const label = getLabel(item);
const badgeClass = getBadgeClass(item);
return `
<div class="flex items-center justify-between group">
<span class="px-2 py-1 rounded text-xs ${badgeClass} min-w-[80px] text-center" title="${item.type}:${item.target||''}">${label}</span>
<div class="flex items-center gap-2 relative">
<span class="text-xs text-gray-400 absolute right-full mr-2 whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity">1 : </span>
<input type="number" step="0.01"
class="w-24 px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-shadow text-right font-medium"
value="${item.odds}"
data-type="${item.type}"
data-target="${item.target}"
>
</div>
</div>
`;
}).join('')}
</div>
</div>
`;
oddsContainer.innerHTML += groupHtml;
}
}
async function submitOdds() {
const gameId = document.getElementById('oddsGameId').value;
const inputs = oddsContainer.querySelectorAll('input[type="number"]');
const oddsData = [];
inputs.forEach(input => {
oddsData.push({
type: input.dataset.type,
target: input.dataset.target,
odds: input.value
});
});
submitOddsFormBtn.disabled = true;
submitOddsFormBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> 保存中...';
try {
const response = await fetch('/admin/games/odds/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
game_id: gameId,
odds: oddsData
})
});
const json = await response.json();
if (json.success) {
showMessage('赔率配置已更新', 'success');
closeOddsFormModal();
} else {
showMessage(json.message, 'error');
}
} catch (e) {
showMessage('保存失败: ' + e.message, 'error');
} finally {
submitOddsFormBtn.disabled = false;
submitOddsFormBtn.innerHTML = '<i class="fas fa-save mr-2"></i>保存赔率配置';
}
}
if (closeOddsFormBtn) closeOddsFormBtn.addEventListener('click', closeOddsFormModal);
if (cancelOddsFormBtn) cancelOddsFormBtn.addEventListener('click', closeOddsFormModal);
if (submitOddsFormBtn) submitOddsFormBtn.addEventListener('click', submitOdds);
// 阻止表单默认提交
gameForm.addEventListener('submit', e => {
e.preventDefault();
submitGameFormData();
});
// 图片预览功能(当SetImageUrl被调用时更新预览)
const gameImageInput = document.getElementById('gameImage');
const gameImagePreview = document.querySelector('#gameImagePreview img');
if (gameImageInput && gameImagePreview) {
// 监听输入框变化
gameImageInput.addEventListener('input', function() {
if (this.value) {
gameImagePreview.src = this.value;
document.getElementById('gameImagePreview').style.display = 'block';
} else {
document.getElementById('gameImagePreview').style.display = 'none';
}
});
// 如果SetImageUrl函数存在,保存引用以便更新预览
if (typeof window.SetImageUrl === 'function') {
const originalSetImageUrl = window.SetImageUrl;
window.SetImageUrl = function(url) {
originalSetImageUrl(url);
if (gameImageInput && gameImagePreview) {
gameImageInput.value = url;
gameImagePreview.src = url;
document.getElementById('gameImagePreview').style.display = 'block';
}
};
}
}
}
});
</script>