Files
chatroom/resources/views/admin/game-configs/index.blade.php
lkddi b62a9f6240 功能:后台游戏历史记录查询中心 + 游戏管理页实时统计
- 新增 GameHistoryController,提供各游戏历史记录查询接口
  - 百家乐:局次列表 + 单局下注明细(含结果分布统计)
  - 老虎机:转动记录含图案分布,支持结果类型/玩家名筛选
  - 赛马:场次列表 + 单场下注明细(含马匹信息展示)
  - 神秘箱子:投放/领取历史,支持箱子类型/领取状态筛选
  - 神秘占卜:签文等级分布统计 + 历史记录,支持等级/玩家名筛选
- 新增 /admin/game-history/ 路由组(stats + 各游戏历史 + 单局详情共9条路由)
- 游戏管理页(/admin/game-configs)优化:
  - 每个游戏卡片新增「📋 历史记录」直达按钮
  - 新增「📊 加载实时统计」按钮,AJAX 异步拉取并展示各游戏汇总卡片
- 更新 GAMES_TODO.md,标记通用待办已完成
2026-03-03 23:40:31 +08:00

508 lines
28 KiB
PHP
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.
@extends('admin.layouts.app')
@section('title', '游戏管理')
@section('content')
<div class="space-y-6">
{{-- 页头 --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex justify-between items-center">
<div>
<h2 class="text-lg font-bold text-gray-800">🎮 游戏管理</h2>
<p class="text-xs text-gray-500 mt-1">统一管理聊天室所有娱乐游戏的开关状态与核心参数,所有游戏默认关闭。</p>
</div>
<button onclick="loadGameStats()"
class="px-4 py-2 bg-indigo-50 text-indigo-700 rounded-lg text-sm font-bold hover:bg-indigo-100 transition">
📊 加载实时统计
</button>
</div>
{{-- 实时统计摘要区AJAX 异步加载,默认隐藏) --}}
<div id="game-stats-panel" class="hidden">
<div class="grid grid-cols-2 md:grid-cols-5 gap-4" id="game-stats-grid">
{{-- JS 动态填充 --}}
</div>
</div>
@if (session('success'))
<div class="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-lg text-sm">
{{ session('success') }}
</div>
@endif
{{-- 游戏卡片列表 --}}
<div class="grid grid-cols-1 gap-6">
@foreach ($games as $game)
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"
id="game-card-{{ $game->game_key }}">
{{-- 卡片头部:游戏名 + 开关 --}}
<div
class="flex items-center justify-between p-5 border-b border-gray-100
{{ $game->enabled ? 'bg-emerald-50' : 'bg-gray-50' }}">
<div class="flex items-center gap-3">
<span class="text-3xl">{{ $game->icon }}</span>
<div>
<div class="font-bold text-gray-800 flex items-center gap-2">
{{ $game->name }}
<span id="badge-{{ $game->game_key }}"
class="text-xs px-2 py-0.5 rounded-full font-bold
{{ $game->enabled ? 'bg-emerald-100 text-emerald-700' : 'bg-gray-200 text-gray-500' }}">
{{ $game->enabled ? '运行中' : '已关闭' }}
</span>
</div>
<div class="text-xs text-gray-500 mt-0.5">{{ $game->description }}</div>
</div>
</div>
{{-- 操作按钮组 --}}
<div class="flex items-center gap-2">
{{-- 历史记录链接 --}}
@php
$historyRoute = match ($game->game_key) {
'baccarat' => 'admin.game-history.baccarat',
'slot_machine' => 'admin.game-history.slot',
'mystery_box' => 'admin.game-history.mystery-box',
'horse_racing' => 'admin.game-history.horse',
'fortune_telling' => 'admin.game-history.fortune',
default => null,
};
@endphp
@if ($historyRoute)
<a href="{{ route($historyRoute) }}"
class="px-4 py-2 bg-indigo-50 text-indigo-700 rounded-lg font-bold text-sm hover:bg-indigo-100 transition shadow-sm">
📋 历史记录
</a>
@endif
{{-- 大开关按钮 --}}
<button onclick="toggleGame('{{ $game->game_key }}', {{ $game->id }})"
id="toggle-btn-{{ $game->game_key }}"
class="px-5 py-2 rounded-lg font-bold text-sm transition shadow-sm
{{ $game->enabled ? 'bg-red-500 hover:bg-red-600 text-white' : 'bg-emerald-500 hover:bg-emerald-600 text-white' }}">
{{ $game->enabled ? '⏸ 关闭游戏' : '▶ 开启游戏' }}
</button>
</div>
</div>
{{-- 参数配置区域 --}}
<div class="p-5">
<form action="{{ route('admin.game-configs.params', $game) }}" method="POST">
@csrf
@php
$params = $game->params ?? [];
$labels = gameParamLabels($game->game_key);
@endphp
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
@foreach ($params as $paramKey => $paramValue)
@php $meta = $labels[$paramKey] ?? ['label' => $paramKey, 'type' => 'number', 'unit' => ''] @endphp
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">
{{ $meta['label'] }}
@if ($meta['unit'])
<span class="font-normal text-gray-400">{{ $meta['unit'] }}</span>
@endif
</label>
@if ($meta['type'] === 'boolean')
<select name="params[{{ $paramKey }}]"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
<option value="1" {{ $paramValue ? 'selected' : '' }}></option>
<option value="0" {{ !$paramValue ? 'selected' : '' }}></option>
</select>
@elseif ($meta['type'] === 'array')
<input type="text" name="params[{{ $paramKey }}]"
value="{{ implode(',', (array) $paramValue) }}"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400"
placeholder="多个值用逗号分隔">
@else
<input type="{{ $meta['type'] }}" name="params[{{ $paramKey }}]"
value="{{ $paramValue }}" step="{{ $meta['step'] ?? 1 }}"
min="{{ $meta['min'] ?? 0 }}"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
@endif
</div>
@endforeach
</div>
<div class="mt-4 flex items-center gap-3">
<button type="submit"
class="px-6 py-2 bg-indigo-600 text-white rounded-lg font-bold hover:bg-indigo-700 transition text-sm shadow-sm">
💾 保存参数
</button>
<span class="text-xs text-gray-400">修改后立即生效缓存60秒刷新</span>
</div>
</form>
{{-- 神秘箱子:手动投放区域 --}}
@if ($game->game_key === 'mystery_box')
<div class="mt-4 pt-4 border-t border-gray-100">
<div class="text-xs font-bold text-gray-600 mb-2">🎯 手动投放箱子</div>
<div class="flex items-center gap-3 flex-wrap">
<button onclick="dropBox('normal', {{ $game->id }})"
style="padding:8px 16px; background:linear-gradient(135deg,#059669,#10b981); color:#fff; border:none; border-radius:8px; font-size:13px; font-weight:700; cursor:pointer; transition:opacity .15s;"
onmouseover="this.style.opacity='.85'" onmouseout="this.style.opacity='1'">
📦 投放普通箱
</button>
<button onclick="dropBox('rare', {{ $game->id }})"
style="padding:8px 16px; background:linear-gradient(135deg,#7c3aed,#a78bfa); color:#fff; border:none; border-radius:8px; font-size:13px; font-weight:700; cursor:pointer; transition:opacity .15s;"
onmouseover="this.style.opacity='.85'" onmouseout="this.style.opacity='1'">
💎 投放稀有箱
</button>
<button onclick="dropBox('trap', {{ $game->id }})"
style="padding:8px 16px; background:linear-gradient(135deg,#7f1d1d,#ef4444); color:#fff; border:none; border-radius:8px; font-size:13px; font-weight:700; cursor:pointer; transition:opacity .15s;"
onmouseover="this.style.opacity='.85'" onmouseout="this.style.opacity='1'">
☠️ 投放黑化箱
</button>
<span class="text-xs text-gray-400">直接向 #1 房间投放,立即广播暗号</span>
</div>
</div>
@endif
</div>
</div>
@endforeach
@if ($games->isEmpty())
<div class="bg-white rounded-xl border border-gray-100 p-12 text-center text-gray-400">
暂无游戏配置,请先运行 <code class="bg-gray-100 px-2 py-1 rounded">php artisan db:seed
--class=GameConfigSeeder</code>
</div>
@endif
</div>
</div>
<script>
/**
* 切换游戏开启/关闭状态
*/
function toggleGame(gameKey, gameId) {
fetch(`/admin/game-configs/${gameId}/toggle`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
})
.then(r => r.json())
.then(data => {
if (!data.ok) return;
const enabled = data.enabled;
const card = document.getElementById(`game-card-${gameKey}`);
const badge = document.getElementById(`badge-${gameKey}`);
const btn = document.getElementById(`toggle-btn-${gameKey}`);
const header = card?.querySelector('.flex.items-center.justify-between');
// 更新徽章
badge.textContent = enabled ? '运行中' : '已关闭';
badge.className = `text-xs px-2 py-0.5 rounded-full font-bold ${enabled
? 'bg-emerald-100 text-emerald-700'
: 'bg-gray-200 text-gray-500'}`;
// 更新按钮
btn.textContent = enabled ? '⏸ 关闭游戏' : '▶ 开启游戏';
btn.className = `px-5 py-2 rounded-lg font-bold text-sm transition shadow-sm ${enabled
? 'bg-red-500 hover:bg-red-600 text-white'
: 'bg-emerald-500 hover:bg-emerald-600 text-white'}`;
// 更新头部背景
if (header) {
header.classList.toggle('bg-emerald-50', enabled);
header.classList.toggle('bg-gray-50', !enabled);
}
// 全局弹窗提示
window.adminDialog.alert(data.message, enabled ? '游戏已开启' : '游戏已关闭', enabled ? '✅' : '⏸');
});
}
/**
* 管理员手动投放神秘箱子
*
* @param {string} boxType 箱子类型normal | rare | trap
*/
function dropBox(boxType) {
const typeNames = {
normal: '普通箱',
rare: '稀有箱',
trap: '黑化箱'
};
const typeIcons = {
normal: '📦',
rare: '💎',
trap: '☠️'
};
const name = typeNames[boxType] || boxType;
const icon = typeIcons[boxType] || '📦';
window.adminDialog.confirm(
`确定要向 <b>#1 房间</b> 投放一个「${name}」吗?<br><span style="color:#64748b; font-size:12px;">箱子投放后将立即在公屏广播暗号,用户限时领取。</span>`,
`投放${name}`,
() => {
fetch('/admin/mystery-box/drop', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
box_type: boxType
}),
})
.then(r => r.json())
.then(data => {
window.adminDialog.alert(
data.message || (data.ok ? '投放成功!' : '投放失败'),
data.ok ? '投放成功' : '投放失败',
data.ok ? icon : '❌'
);
})
.catch(() => window.adminDialog.alert('网络错误,请重试', '网络错误', '🌐'));
},
icon
);
}
</script>
<script>
/**
* 加载各游戏实时统计摘要并渲染到顶部面板
*/
function loadGameStats() {
const panel = document.getElementById('game-stats-panel');
const grid = document.getElementById('game-stats-grid');
grid.innerHTML = '<div class="col-span-5 text-center text-gray-400 text-sm py-4">⏳ 加载中...</div>';
panel.classList.remove('hidden');
fetch('{{ route('admin.game-history.stats') }}', {
headers: {
'Accept': 'application/json'
}
})
.then(r => r.json())
.then(data => {
const cards = [{
icon: '🎲',
title: '百家乐',
items: [{
label: '总局数',
value: data.baccarat.total_rounds.toLocaleString()
},
{
label: '总下注',
value: data.baccarat.total_bets.toLocaleString() + ' 笔'
},
{
label: '今日局数',
value: data.baccarat.today_rounds.toLocaleString()
},
],
color: 'border-red-200 bg-red-50',
},
{
icon: '🎰',
title: '老虎机',
items: [{
label: '总转动',
value: data.slot.total_spins.toLocaleString() + ' 次'
},
{
label: '三7大奖',
value: data.slot.jackpot_count.toLocaleString() + ' 次'
},
{
label: '今日转动',
value: data.slot.today_spins.toLocaleString()
},
],
color: 'border-amber-200 bg-amber-50',
},
{
icon: '🐎',
title: '赛马竞猜',
items: [{
label: '总场次',
value: data.horse.total_races.toLocaleString()
},
{
label: '总注池',
value: data.horse.total_pool.toLocaleString() + ' 金'
},
{
label: '今日场次',
value: data.horse.today_races.toLocaleString()
},
],
color: 'border-emerald-200 bg-emerald-50',
},
{
icon: '📦',
title: '神秘箱子',
items: [{
label: '总投放',
value: data.mystery_box.total_dropped.toLocaleString()
},
{
label: '已领取',
value: data.mystery_box.total_claimed.toLocaleString()
},
{
label: '今日投放',
value: data.mystery_box.today_dropped.toLocaleString()
},
],
color: 'border-purple-200 bg-purple-50',
},
{
icon: '🔮',
title: '神秘占卜',
items: [{
label: '总占卜',
value: data.fortune.total_times.toLocaleString() + ' 次'
},
{
label: '上上签',
value: data.fortune.jackpot_count.toLocaleString()
},
{
label: '今日次数',
value: data.fortune.today_times.toLocaleString()
},
],
color: 'border-indigo-200 bg-indigo-50',
},
];
grid.innerHTML = cards.map(card => `
<div class="bg-white rounded-xl shadow-sm border ${card.color} p-4">
<div class="flex items-center gap-2 mb-3">
<span class="text-xl">${card.icon}</span>
<span class="font-bold text-gray-700 text-sm">${card.title}</span>
</div>
<div class="space-y-1.5">
${card.items.map(item => `
<div class="flex justify-between text-xs">
<span class="text-gray-500">${item.label}</span>
<span class="font-bold text-gray-700">${item.value}</span>
</div>
`).join('')}
</div>
</div>
`).join('');
})
.catch(() => {
grid.innerHTML = '<div class="col-span-5 text-center text-red-400 text-sm py-4">❌ 加载失败,请重试</div>';
});
}
</script>
@endsection
@php
/**
* 返回各游戏参数的中文标签说明。
*
* @param string $gameKey
* @return array<string, array{label: string, type: string, unit: string}>
*/
function gameParamLabels(string $gameKey): array
{
return match ($gameKey) {
'baccarat' => [
'interval_minutes' => ['label' => '开局间隔', 'type' => 'number', 'unit' => '分钟', 'min' => 1],
'bet_window_seconds' => ['label' => '押注窗口', 'type' => 'number', 'unit' => '秒', 'min' => 10],
'min_bet' => ['label' => '最低押注', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'max_bet' => ['label' => '最高押注', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'payout_big' => ['label' => '大赔率1:N', 'type' => 'number', 'unit' => '', 'min' => 1],
'payout_small' => ['label' => '小赔率1:N', 'type' => 'number', 'unit' => '', 'min' => 1],
'payout_triple' => ['label' => '豹子赔率1:N', 'type' => 'number', 'unit' => '', 'min' => 1],
'kill_points' => ['label' => '庄家收割点数', 'type' => 'array', 'unit' => '逗号分隔'],
],
'slot_machine' => [
'cost_per_spin' => ['label' => '每次旋转消耗', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'house_edge_percent' => [
'label' => '庄家边际',
'type' => 'number',
'unit' => '%',
'min' => 0,
'step' => 0.1,
],
'daily_limit' => ['label' => '每日转动上限', 'type' => 'number', 'unit' => '次0=不限)', 'min' => 0],
'jackpot_payout' => ['label' => '三个7赔率1:N', 'type' => 'number', 'unit' => '', 'min' => 1],
'triple_payout' => ['label' => '三💎赔率1:N', 'type' => 'number', 'unit' => '', 'min' => 1],
'same_payout' => ['label' => '其他三同1:N', 'type' => 'number', 'unit' => '', 'min' => 1],
'pair_payout' => ['label' => '两同赔率1:N', 'type' => 'number', 'unit' => '', 'min' => 1],
'curse_enabled' => ['label' => '开启诅咒(三💀)', 'type' => 'boolean', 'unit' => ''],
],
'mystery_box' => [
'auto_drop_enabled' => ['label' => '自动定时投放', 'type' => 'boolean', 'unit' => ''],
'auto_interval_hours' => ['label' => '自动投放间隔', 'type' => 'number', 'unit' => '小时', 'min' => 1],
'claim_window_seconds' => ['label' => '领取窗口', 'type' => 'number', 'unit' => '秒', 'min' => 10],
// 新键名
'normal_reward_min' => ['label' => '普通箱最低奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'normal_reward_max' => ['label' => '普通箱最高奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'rare_reward_min' => ['label' => '稀有箱最低奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'rare_reward_max' => ['label' => '稀有箱最高奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'trap_penalty_min' => ['label' => '黑化箱最低惩罚', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'trap_penalty_max' => ['label' => '黑化箱最高惩罚', 'type' => 'number', 'unit' => '金币', 'min' => 1],
// 旧键名兼容(数据库中已存在的旧配置)
'min_reward' => ['label' => '普通箱最低奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'max_reward' => ['label' => '普通箱最高奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'rare_min_reward' => ['label' => '稀有箱最低奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'rare_max_reward' => ['label' => '稀有箱最高奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'trap_chance_percent' => [
'label' => '黑化箱概率',
'type' => 'number',
'unit' => '%',
'min' => 0,
'max' => 100,
],
],
'horse_racing' => [
'interval_minutes' => ['label' => '比赛间隔', 'type' => 'number', 'unit' => '分钟', 'min' => 5],
'bet_window_seconds' => ['label' => '押注窗口', 'type' => 'number', 'unit' => '秒', 'min' => 10],
'race_duration' => ['label' => '跑马动画时长', 'type' => 'number', 'unit' => '秒', 'min' => 10],
'horse_count' => ['label' => '参赛马匹数', 'type' => 'number', 'unit' => '匹', 'min' => 2, 'max' => 8],
'min_bet' => ['label' => '最低押注', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'max_bet' => ['label' => '最高押注', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'house_take_percent' => [
'label' => '庄家抽水',
'type' => 'number',
'unit' => '%',
'min' => 0,
'max' => 20,
],
],
'fortune_telling' => [
'free_count_per_day' => ['label' => '每日免费次数', 'type' => 'number', 'unit' => '次', 'min' => 0],
'extra_cost' => ['label' => '额外次数消耗', 'type' => 'number', 'unit' => '金币', 'min' => 0],
'buff_duration_hours' => ['label' => '加成持续时间', 'type' => 'number', 'unit' => '小时', 'min' => 1],
'jackpot_chance' => [
'label' => '上上签概率',
'type' => 'number',
'unit' => '%',
'min' => 0,
'max' => 100,
],
'good_chance' => ['label' => '上签概率', 'type' => 'number', 'unit' => '%', 'min' => 0, 'max' => 100],
'bad_chance' => ['label' => '下签概率', 'type' => 'number', 'unit' => '%', 'min' => 0, 'max' => 100],
'curse_chance' => [
'label' => '大凶签概率',
'type' => 'number',
'unit' => '%',
'min' => 0,
'max' => 100,
],
],
'fishing' => [
'fishing_cost' => ['label' => '每次抛竿消耗', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'fishing_wait_min' => ['label' => '浮漂等待最短', 'type' => 'number', 'unit' => '秒', 'min' => 1],
'fishing_wait_max' => ['label' => '浮漂等待最长', 'type' => 'number', 'unit' => '秒', 'min' => 1],
'fishing_cooldown' => ['label' => '收竿后冷却时间', 'type' => 'number', 'unit' => '秒', 'min' => 10],
],
default => [],
};
}
@endphp