671 lines
43 KiB
PHP
671 lines
43 KiB
PHP
@extends('admin.layouts.app')
|
||
|
||
@section('title', '游戏管理')
|
||
|
||
@section('content')
|
||
@php
|
||
$riddleTypeOptions = \App\Models\Riddle::typeOptions();
|
||
$availableRooms = \App\Models\Room::orderBy('id')->get();
|
||
@endphp
|
||
|
||
<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 type="button" data-game-stats-url="{{ route('admin.game-history.stats') }}"
|
||
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>
|
||
|
||
{{-- 游戏卡片列表 --}}
|
||
<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',
|
||
'lottery' => 'admin.game-history.lottery',
|
||
'gomoku' => 'admin.game-history.gomoku',
|
||
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 type="button" data-game-toggle-url="{{ route('admin.game-configs.toggle', $game) }}"
|
||
data-game-key="{{ $game->game_key }}"
|
||
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"
|
||
@if ($game->game_key === 'idiom') data-idiom-config-form @endif>
|
||
@csrf
|
||
|
||
@php
|
||
$params = $game->params ?? [];
|
||
$labels = gameParamLabels($game->game_key);
|
||
$hiddenLegacyKeys = $game->game_key === 'mystery_box'
|
||
? ['min_reward', 'max_reward', 'rare_min_reward', 'rare_max_reward']
|
||
: [];
|
||
$paramKeys = array_values(array_unique(array_merge(array_keys($labels), array_keys($params))));
|
||
$paramKeys = array_values(array_filter($paramKeys, fn ($key) => ! in_array($key, $hiddenLegacyKeys, true)));
|
||
@endphp
|
||
|
||
@if ($game->game_key === 'idiom')
|
||
@php
|
||
$sharedConfig = gameRiddleSharedConfig($params);
|
||
$checkedRoomIds = collect($sharedConfig['room_ids'])->map(fn ($roomId) => (int) $roomId)->all();
|
||
@endphp
|
||
<div class="space-y-4">
|
||
<div class="rounded-xl border border-indigo-100 bg-indigo-50/60 p-4 text-xs leading-6 text-indigo-700">
|
||
猜成语与脑筋急转弯共用同一套奖励、过期时间、自动出题间隔与参与房间范围配置。
|
||
手动出题时再单独选择题型即可。
|
||
</div>
|
||
|
||
<div class="rounded-2xl border border-slate-200 bg-slate-50/70 p-4" data-idiom-config-card>
|
||
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||
<div>
|
||
<div class="text-sm font-bold text-slate-800">猜谜活动公共设置</div>
|
||
<div class="text-xs text-slate-500">以下参数会同时作用于猜成语与脑筋急转弯。</div>
|
||
</div>
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
@foreach ($riddleTypeOptions as $typeLabel)
|
||
<span class="rounded-full bg-white px-3 py-1 text-xs font-semibold text-slate-500 shadow-sm">{{ $typeLabel }}</span>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||
<div>
|
||
<label class="mb-1 block text-xs font-bold text-gray-600">答对奖励金币</label>
|
||
<input type="number" name="params[reward_gold]"
|
||
value="{{ old('params.reward_gold', $sharedConfig['reward_gold']) }}"
|
||
min="0"
|
||
class="w-full rounded-lg border border-gray-300 p-2 text-sm focus:border-indigo-400">
|
||
</div>
|
||
<div>
|
||
<label class="mb-1 block text-xs font-bold text-gray-600">答对奖励经验</label>
|
||
<input type="number" name="params[reward_exp]"
|
||
value="{{ old('params.reward_exp', $sharedConfig['reward_exp']) }}"
|
||
min="0"
|
||
class="w-full rounded-lg border border-gray-300 p-2 text-sm focus:border-indigo-400">
|
||
</div>
|
||
<div>
|
||
<label class="mb-1 block text-xs font-bold text-gray-600">题目过期时间</label>
|
||
<input type="number" name="params[expire_minutes]"
|
||
value="{{ old('params.expire_minutes', $sharedConfig['expire_minutes']) }}"
|
||
min="0"
|
||
class="w-full rounded-lg border border-gray-300 p-2 text-sm focus:border-indigo-400">
|
||
<p class="mt-1 text-xs text-gray-400">分钟,0 表示不过期。</p>
|
||
</div>
|
||
<div>
|
||
<label class="mb-1 block text-xs font-bold text-gray-600">自动出题间隔</label>
|
||
<input type="number" name="params[auto_start_interval]"
|
||
value="{{ old('params.auto_start_interval', $sharedConfig['auto_start_interval']) }}"
|
||
min="0"
|
||
class="w-full rounded-lg border border-gray-300 p-2 text-sm focus:border-indigo-400">
|
||
<p class="mt-1 text-xs text-gray-400">分钟,0 表示仅手动出题。</p>
|
||
</div>
|
||
<div>
|
||
<label class="mb-1 block text-xs font-bold text-gray-600">参与房间模式</label>
|
||
<select name="params[room_scope_mode]"
|
||
data-idiom-room-mode
|
||
class="w-full rounded-lg border border-gray-300 p-2 text-sm focus:border-indigo-400">
|
||
<option value="all" @selected($sharedConfig['room_mode'] === 'all')>全部房间</option>
|
||
<option value="single" @selected($sharedConfig['room_mode'] === 'single')>单选房间</option>
|
||
<option value="multiple" @selected($sharedConfig['room_mode'] === 'multiple')>多选房间</option>
|
||
</select>
|
||
</div>
|
||
<div class="xl:col-span-2">
|
||
<label class="mb-2 block text-xs font-bold text-gray-600" data-idiom-room-label>参与房间</label>
|
||
<div class="grid grid-cols-2 gap-2 rounded-xl border border-dashed border-slate-200 bg-white p-3 lg:grid-cols-3">
|
||
@foreach ($availableRooms as $room)
|
||
<label class="flex items-center gap-2 rounded-lg border border-slate-100 px-3 py-2 text-sm text-slate-600">
|
||
<input type="checkbox"
|
||
name="params[room_ids][]"
|
||
value="{{ $room->id }}"
|
||
@checked(in_array((int) $room->id, $checkedRoomIds, true))
|
||
data-idiom-room-checkbox
|
||
class="rounded border-slate-300 text-indigo-600">
|
||
<span>#{{ $room->id }} {{ $room->name }}</span>
|
||
</label>
|
||
@endforeach
|
||
</div>
|
||
<p class="mt-2 text-xs text-gray-400">单选模式下只保留一个房间,多选模式可同时勾选多个房间。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@else
|
||
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
|
||
@foreach ($paramKeys as $paramKey)
|
||
@php
|
||
$paramValue = $params[$paramKey] ?? ($labels[$paramKey]['default'] ?? '');
|
||
|
||
if ($game->game_key === 'mystery_box') {
|
||
$legacyFallbackMap = [
|
||
'normal_reward_min' => 'min_reward',
|
||
'normal_reward_max' => 'max_reward',
|
||
'rare_reward_min' => 'rare_min_reward',
|
||
'rare_reward_max' => 'rare_max_reward',
|
||
];
|
||
|
||
if (($paramValue === '' || $paramValue === null) && isset($legacyFallbackMap[$paramKey])) {
|
||
$paramValue = $params[$legacyFallbackMap[$paramKey]] ?? $paramValue;
|
||
}
|
||
}
|
||
@endphp
|
||
@php $meta = $labels[$paramKey] ?? ['label' => $paramKey, 'type' => 'number', 'unit' => ''] @endphp
|
||
<div>
|
||
<label class="mb-1 block text-xs font-bold text-gray-600">
|
||
{{ $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 rounded-lg border border-gray-300 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 rounded-lg border border-gray-300 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 rounded-lg border border-gray-300 p-2 text-sm focus:border-indigo-400">
|
||
@endif
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
@endif
|
||
|
||
<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 === 'idiom')
|
||
<div class="mt-4 border-t border-gray-100 pt-4">
|
||
<div class="mb-3 text-xs font-bold text-gray-600">🧩 手动出题</div>
|
||
<div class="flex flex-wrap items-center gap-3">
|
||
<select id="idiom-manual-room"
|
||
class="rounded-lg border border-gray-300 px-3 py-2 text-sm text-slate-700">
|
||
@foreach ($availableRooms as $room)
|
||
<option value="{{ $room->id }}">#{{ $room->id }} {{ $room->name }}</option>
|
||
@endforeach
|
||
</select>
|
||
<select id="idiom-manual-type"
|
||
class="rounded-lg border border-gray-300 px-3 py-2 text-sm text-slate-700">
|
||
@foreach ($riddleTypeOptions as $typeKey => $typeLabel)
|
||
<option value="{{ $typeKey }}">{{ $typeLabel }}</option>
|
||
@endforeach
|
||
</select>
|
||
<button type="button" id="idiom-manual-start-btn"
|
||
data-idiom-start-url="{{ route('riddle-quiz.start') }}"
|
||
class="rounded-lg bg-gradient-to-r from-purple-600 to-indigo-600 px-4 py-2 text-sm font-bold text-white transition hover:opacity-90">
|
||
立即出题
|
||
</button>
|
||
<span class="text-xs text-gray-400">先选房间,再选题型,后台会按对应题型配置发题。</span>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
{{-- 神秘箱子:手动投放区域 --}}
|
||
@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 type="button" data-game-drop-box-type="normal"
|
||
data-game-drop-box-url="{{ route('admin.mystery-box.drop') }}"
|
||
data-game-hover-opacity=".85"
|
||
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;">
|
||
📦 投放普通箱
|
||
</button>
|
||
<button type="button" data-game-drop-box-type="rare"
|
||
data-game-drop-box-url="{{ route('admin.mystery-box.drop') }}"
|
||
data-game-hover-opacity=".85"
|
||
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;">
|
||
💎 投放稀有箱
|
||
</button>
|
||
<button type="button" data-game-drop-box-type="trap"
|
||
data-game-drop-box-url="{{ route('admin.mystery-box.drop') }}"
|
||
data-game-hover-opacity=".85"
|
||
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;">
|
||
☠️ 投放黑化箱
|
||
</button>
|
||
<span class="text-xs text-gray-400">直接向 #1 房间投放,立即广播暗号</span>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
{{-- 双色球彩票:手动操作区域 --}}
|
||
@if ($game->game_key === 'lottery')
|
||
<div class="mt-4 pt-4 border-t border-gray-100">
|
||
<div class="text-xs font-bold text-gray-600 mb-3">🎟️ 手动操作</div>
|
||
{{-- 当前期次状态展示 --}}
|
||
<div id="lottery-issue-status"
|
||
class="mb-3 text-xs text-gray-500 bg-red-50 border border-red-100 rounded-lg p-3">
|
||
<span class="text-red-400">⏳ 点击下方「加载期次状态」查看当前状态</span>
|
||
</div>
|
||
<div class="flex items-center gap-3 flex-wrap">
|
||
<button type="button" data-game-lottery-current-url="{{ url('/lottery/current') }}"
|
||
data-game-hover-opacity=".85"
|
||
style="padding:8px 16px; background:linear-gradient(135deg,#475569,#64748b); color:#fff; border:none; border-radius:8px; font-size:13px; font-weight:700; cursor:pointer; transition:opacity .15s;">
|
||
🔄 加载期次状态
|
||
</button>
|
||
<button type="button" data-game-lottery-open-url="{{ route('admin.lottery.open-issue') }}"
|
||
data-game-hover-opacity=".85"
|
||
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;">
|
||
➕ 手动开新期
|
||
</button>
|
||
<button type="button" data-game-lottery-force-url="{{ route('admin.lottery.force-draw') }}"
|
||
data-game-hover-opacity=".85"
|
||
style="padding:8px 16px; background:linear-gradient(135deg,#dc2626,#ef4444); color:#fff; border:none; border-radius:8px; font-size:13px; font-weight:700; cursor:pointer; transition:opacity .15s;">
|
||
🎊 立即强制开奖
|
||
</button>
|
||
<span class="text-xs text-gray-400">开新期仅在无进行中期次时生效;强制开奖将提前结束当期</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>
|
||
|
||
@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],
|
||
'seed_pool' => ['label' => '系统初始化资金池', 'type' => 'number', 'unit' => '金币', 'min' => 0],
|
||
'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],
|
||
],
|
||
'lottery' => [
|
||
// ── 开奖时间 ──
|
||
'draw_hour' => [
|
||
'label' => '开奖时(24h制)',
|
||
'type' => 'number',
|
||
'unit' => '时',
|
||
'min' => 0,
|
||
'max' => 23,
|
||
],
|
||
'draw_minute' => ['label' => '开奖分', 'type' => 'number', 'unit' => '分', 'min' => 0, 'max' => 59],
|
||
'stop_sell_minutes' => ['label' => '停售提前', 'type' => 'number', 'unit' => '分钟', 'min' => 1],
|
||
// ── 购票限制 ──
|
||
'ticket_price' => ['label' => '每注金额', 'type' => 'number', 'unit' => '金币', 'min' => 1],
|
||
'max_tickets_per_user' => ['label' => '单人每期上限', 'type' => 'number', 'unit' => '注', 'min' => 1],
|
||
'max_tickets_per_buy' => ['label' => '单次购买上限', 'type' => 'number', 'unit' => '注', 'min' => 1],
|
||
// ── 奖池分配 ──
|
||
'pool_ratio' => [
|
||
'label' => '购票进奖池比例',
|
||
'type' => 'number',
|
||
'unit' => '%',
|
||
'min' => 1,
|
||
'max' => 100,
|
||
],
|
||
'prize_1st_ratio' => [
|
||
'label' => '一等奖占奖池',
|
||
'type' => 'number',
|
||
'unit' => '%',
|
||
'min' => 1,
|
||
'max' => 100,
|
||
],
|
||
'prize_2nd_ratio' => [
|
||
'label' => '二等奖占奖池',
|
||
'type' => 'number',
|
||
'unit' => '%',
|
||
'min' => 1,
|
||
'max' => 100,
|
||
],
|
||
'prize_3rd_ratio' => [
|
||
'label' => '三等奖占奖池',
|
||
'type' => 'number',
|
||
'unit' => '%',
|
||
'min' => 1,
|
||
'max' => 100,
|
||
],
|
||
'carry_ratio' => ['label' => '强制滚存', 'type' => 'number', 'unit' => '%', 'min' => 0, 'max' => 50],
|
||
// ── 固定小奖 ──
|
||
'prize_4th_fixed' => ['label' => '四等奖固定金额/注', 'type' => 'number', 'unit' => '金币', 'min' => 0],
|
||
'prize_5th_fixed' => ['label' => '五等奖固定金额/注', 'type' => 'number', 'unit' => '金币', 'min' => 0],
|
||
// ── 超级期 ──
|
||
'super_issue_threshold' => [
|
||
'label' => '超级期触发连续无一等奖',
|
||
'type' => 'number',
|
||
'unit' => '期',
|
||
'min' => 1,
|
||
],
|
||
'super_issue_inject' => [
|
||
'label' => '超级期系统注入上限',
|
||
'type' => 'number',
|
||
'unit' => '金币',
|
||
'min' => 0,
|
||
],
|
||
],
|
||
'gomoku' => [
|
||
// ── PvP 对战 ──
|
||
'pvp_reward' => ['label' => 'PvP 胜利奖励', 'type' => 'number', 'unit' => '金币', 'min' => 0],
|
||
'pvp_invite_timeout' => ['label' => 'PvP 邀请超时', 'type' => 'number', 'unit' => '秒', 'min' => 10],
|
||
'pvp_move_timeout' => ['label' => '每步落子超时', 'type' => 'number', 'unit' => '秒', 'min' => 10],
|
||
'pvp_ready_timeout' => ['label' => '对局准备超时', 'type' => 'number', 'unit' => '秒', 'min' => 10],
|
||
// ── 人机对战:简单 (Level 1) ──
|
||
'pve_fee_level_1' => ['label' => 'AI简单 入场费', 'type' => 'number', 'unit' => '金币', 'min' => 0],
|
||
'pve_reward_level_1' => ['label' => 'AI简单 胜利奖励', 'type' => 'number', 'unit' => '金币', 'min' => 0],
|
||
// ── 人机对战:普通 (Level 2) ──
|
||
'pve_fee_level_2' => ['label' => 'AI普通 入场费', 'type' => 'number', 'unit' => '金币', 'min' => 0],
|
||
'pve_reward_level_2' => ['label' => 'AI普通 胜利奖励', 'type' => 'number', 'unit' => '金币', 'min' => 0],
|
||
// ── 人机对战:困难 (Level 3) ──
|
||
'pve_fee_level_3' => ['label' => 'AI困难 入场费', 'type' => 'number', 'unit' => '金币', 'min' => 0],
|
||
'pve_reward_level_3' => ['label' => 'AI困难 胜利奖励', 'type' => 'number', 'unit' => '金币', 'min' => 0],
|
||
// ── 人机对战:专家 (Level 4) ──
|
||
'pve_fee_level_4' => ['label' => 'AI专家 入场费', 'type' => 'number', 'unit' => '金币', 'min' => 0],
|
||
'pve_reward_level_4' => ['label' => 'AI专家 胜利奖励', 'type' => 'number', 'unit' => '金币', 'min' => 0],
|
||
],
|
||
'idiom' => [
|
||
'reward_gold' => ['label' => '答对奖励金币', 'type' => 'number', 'unit' => '枚', 'min' => 0],
|
||
'reward_exp' => ['label' => '答对奖励经验', 'type' => 'number', 'unit' => '点', 'min' => 0],
|
||
'auto_start_interval' => ['label' => '自动出题间隔', 'type' => 'number', 'unit' => '分钟(0=仅手动)', 'min' => 0],
|
||
],
|
||
default => [],
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 解析猜谜活动公共配置,并兼容旧版题型拆分配置。
|
||
*
|
||
* @return array{reward_gold:int,reward_exp:int,expire_minutes:int,auto_start_interval:int,room_mode:string,room_ids:array<int, int>}
|
||
*/
|
||
function gameRiddleSharedConfig(array $params): array
|
||
{
|
||
$fallbackTypeConfig = collect((array) ($params['type_configs'] ?? []))
|
||
->first(fn ($typeConfig) => is_array($typeConfig) && $typeConfig !== [], []);
|
||
|
||
$roomMode = (string) ($params['room_scope_mode'] ?? ($fallbackTypeConfig['room_mode'] ?? 'single'));
|
||
|
||
if (! in_array($roomMode, ['all', 'single', 'multiple'], true)) {
|
||
$roomMode = 'single';
|
||
}
|
||
|
||
return [
|
||
'reward_gold' => max(0, (int) ($params['reward_gold'] ?? ($fallbackTypeConfig['reward_gold'] ?? 50))),
|
||
'reward_exp' => max(0, (int) ($params['reward_exp'] ?? ($fallbackTypeConfig['reward_exp'] ?? 30))),
|
||
'expire_minutes' => max(0, (int) ($params['expire_minutes'] ?? ($fallbackTypeConfig['expire_minutes'] ?? 5))),
|
||
'auto_start_interval' => max(0, (int) ($params['auto_start_interval'] ?? ($fallbackTypeConfig['auto_start_interval'] ?? 0))),
|
||
'room_mode' => $roomMode,
|
||
'room_ids' => collect((array) ($params['room_ids'] ?? ($fallbackTypeConfig['room_ids'] ?? [])))
|
||
->map(fn ($roomId) => (int) $roomId)
|
||
->filter(fn ($roomId) => $roomId > 0)
|
||
->unique()
|
||
->values()
|
||
->all(),
|
||
];
|
||
}
|
||
@endphp
|
||
|
||
@push('scripts')
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
function showAdminAlert(message, title = '提示', icon = 'ℹ️') {
|
||
if (window.adminDialog?.alert) {
|
||
window.adminDialog.alert(message, title, icon);
|
||
return;
|
||
}
|
||
|
||
window.alert(message);
|
||
}
|
||
|
||
document.querySelectorAll('[data-idiom-config-form]').forEach(function (form) {
|
||
form.addEventListener('submit', function (event) {
|
||
let hasValidationError = false;
|
||
const modeSelect = this.querySelector('[data-idiom-room-mode]');
|
||
const roomCheckboxes = Array.from(this.querySelectorAll('[data-idiom-room-checkbox]'));
|
||
const checkedRooms = roomCheckboxes.filter(function (checkbox) {
|
||
return checkbox.checked;
|
||
});
|
||
|
||
if (modeSelect) {
|
||
if (modeSelect.value === 'single' && checkedRooms.length > 1) {
|
||
showAdminAlert('猜谜活动处于单选房间模式时,只能勾选一个房间。', '房间选择有误', '⚠️');
|
||
hasValidationError = true;
|
||
}
|
||
|
||
if (modeSelect.value === 'single' && checkedRooms.length === 0) {
|
||
const firstRoomCheckbox = roomCheckboxes[0];
|
||
|
||
if (firstRoomCheckbox) {
|
||
firstRoomCheckbox.checked = true;
|
||
}
|
||
}
|
||
|
||
if (modeSelect.value === 'multiple' && checkedRooms.length === 0) {
|
||
showAdminAlert('猜谜活动处于多选房间模式时,请至少选择一个房间。', '房间选择有误', '⚠️');
|
||
hasValidationError = true;
|
||
}
|
||
}
|
||
|
||
if (hasValidationError) {
|
||
event.preventDefault();
|
||
}
|
||
});
|
||
});
|
||
|
||
const manualStartButton = document.getElementById('idiom-manual-start-btn');
|
||
if (!manualStartButton) {
|
||
return;
|
||
}
|
||
|
||
manualStartButton.addEventListener('click', function () {
|
||
const startUrl = this.getAttribute('data-idiom-start-url') || '';
|
||
const roomId = Number.parseInt(document.getElementById('idiom-manual-room')?.value || '0', 10);
|
||
const quizType = document.getElementById('idiom-manual-type')?.value || '';
|
||
|
||
if (!startUrl || roomId <= 0 || !quizType) {
|
||
showAdminAlert('请先选择房间和题型。', '手动出题', '⚠️');
|
||
return;
|
||
}
|
||
|
||
const button = this;
|
||
button.disabled = true;
|
||
button.textContent = '出题中...';
|
||
|
||
fetch(startUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
room_id: roomId,
|
||
quiz_type: quizType,
|
||
}),
|
||
})
|
||
.then(function (response) {
|
||
return response.json();
|
||
})
|
||
.then(function (response) {
|
||
if (response.status === 'success') {
|
||
showAdminAlert('题目已发送到目标房间。', '手动出题成功', '✅');
|
||
return;
|
||
}
|
||
|
||
showAdminAlert(response.message || '出题失败', '手动出题失败', '❌');
|
||
})
|
||
.catch(function () {
|
||
showAdminAlert('网络错误,出题失败', '手动出题失败', '🌐');
|
||
})
|
||
.finally(function () {
|
||
button.disabled = false;
|
||
button.textContent = '立即出题';
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
@endpush
|