Tree 1 день тому
батько
коміт
dfc1f2ffeb

+ 7 - 1
app/Console/Commands/ExemptReview.php

@@ -11,6 +11,7 @@ use App\Models\Cpf;
 use App\Models\SystemStatusInfo;
 use App\Models\Withdrawal;
 use App\Models\WithdrawalAgentRatioConfig;
+use App\Notification\TelegramBot;
 use App\Services\CashService;
 use App\Util;
 use Illuminate\Console\Command;
@@ -136,6 +137,10 @@ class ExemptReview extends Command
             if (true) {
 
                 $agent = $this->getWithdrawalAgent($value);
+                if(!$agent){
+                    TelegramBot::getDefault()->sendProgramNotify('auto withdraw error', '自动免审异常,请检查余额和后台配置');
+                    break;
+                }
 //                $agent = 105;
                 $redis = Redis::connection();
                 $order_sn = $value->OrderId;
@@ -204,7 +209,7 @@ class ExemptReview extends Command
 
     private function getWithdrawalAgentRuleKey($value)
     {
-        if ((int)($value->PixType ?? 0) == 1 && ($value->WithDraw / NumConfig::NUM_VALUE) < 100) {
+        if ((int)($value->PixType ?? 0) == 1 && ($value->WithDraw / NumConfig::NUM_VALUE) < 50) {
             return WithdrawalAgentRatioConfig::PIX_TYPE_CASH_SMALL;
         }
 
@@ -240,6 +245,7 @@ class ExemptReview extends Command
 
     private function defaultWithdrawalAgent($value)
     {
+        return 0;
         if ($value->PixType == 2) {
             return 100;
         }

+ 494 - 0
app/Http/Controllers/Admin/GameSiteBuilderController.php

@@ -0,0 +1,494 @@
+<?php
+
+namespace App\Http\Controllers\Admin;
+
+use App\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Validator;
+
+class GameSiteBuilderController extends Controller
+{
+    private const GAME_MODULE_TYPES = ['ModuleGameList', 'ModuleSmallGameList', 'ModuleRollSmallGameList'];
+
+    public function index(Request $request)
+    {
+        $pages = DB::connection('mysql')
+            ->table('webgame.routes')
+            ->select('id', 'path', 'title', 'state')
+            ->whereNull('parent_id')
+            ->orderBy('index')
+            ->get();
+
+        $channels = DB::connection('mysql')
+            ->table('webgame.WebChannelConfig')
+            ->select('ID', 'Channel', 'StateNo', 'RegionID', 'Remarks')
+            ->orderBy('Channel')
+            ->get();
+
+        $defaultPageId = (int)($request->input('page_id') ?: ($pages->first()->id ?? 1));
+
+        return view('admin.game_site.builder', [
+            'pages' => $pages,
+            'channels' => $channels,
+            'defaultPageId' => $defaultPageId,
+        ]);
+    }
+
+    public function data(Request $request)
+    {
+        $pageId = (int)$request->input('page_id', 1);
+
+        $routeRows = DB::connection('mysql')
+            ->table('webgame.routes')
+            ->select('id', 'parent_id', 'index', 'path', 'type', 'side', 'block',
+                'title', 'icon', 'fill', 'style', 'component', 'query', 'login', 'lpath', 'state')
+            ->orderBy('parent_id')
+            ->orderBy('index')
+            ->get();
+
+        $pageModules = DB::connection('mysql')
+            ->table('webgame.modules')
+            ->select('id', 'page_id', 'icon', 'pos_index', 'title', 'type', 'api',
+                'data_key', 'game_ids', 'tabtype', 'parent_id', 'state', 'link')
+            ->where('page_id', $pageId)
+            ->orderBy('pos_index')
+            ->get();
+
+        $pageModuleIds = $pageModules->pluck('id')->all();
+
+        $links = collect();
+        if (!empty($pageModuleIds)) {
+            $links = DB::connection('mysql')
+                ->table('webgame.module_parents')
+                ->select('module_id', 'parent_id')
+                ->whereIn('parent_id', $pageModuleIds)
+                ->get();
+        }
+
+        $linkedModuleIds = $links->pluck('module_id')->all();
+        $allModuleIds = array_values(array_unique(array_merge($pageModuleIds, $linkedModuleIds)));
+
+        $allModules = collect();
+        if (!empty($allModuleIds)) {
+            $allModules = DB::connection('mysql')
+                ->table('webgame.modules')
+                ->select('id', 'page_id', 'icon', 'pos_index', 'title', 'type', 'api',
+                    'data_key', 'game_ids', 'tabtype', 'parent_id', 'state', 'link')
+                ->whereIn('id', $allModuleIds)
+                ->orderBy('pos_index')
+                ->get();
+        }
+
+        $allGameIds = [];
+        foreach ($allModules as $module) {
+            $allGameIds = array_merge($allGameIds, $this->parseGameIds($module->game_ids));
+        }
+        $allGameIds = array_values(array_unique($allGameIds));
+
+        $gamesMap = [];
+        if (!empty($allGameIds)) {
+            $games = DB::connection('mysql')
+                ->table('webgame.games')
+                ->select('id', 'title', 'img', 'brand', 'link', 'state', 'gridNum')
+                ->whereIn('id', $allGameIds)
+                ->get();
+            foreach ($games as $game) {
+                $gamesMap[(int)$game->id] = $game;
+            }
+        }
+
+        $moduleGames = [];
+        foreach ($allModules as $module) {
+            $ids = $this->parseGameIds($module->game_ids);
+            $moduleGames[$module->id] = array_values(array_filter(array_map(
+                fn($gid) => $gamesMap[$gid] ?? null, $ids
+            )));
+        }
+
+        $styles = DB::connection('mysql')
+            ->table('webgame.styles')
+            ->select('styleid', 'style', 'remarks')
+            ->orderBy('styleid')
+            ->get();
+
+        $banners = DB::connection('mysql')
+            ->table('webgame.banners')
+            ->select('bid', 'img', 'alt', 'link', 'state', 'b_order', 'link_module', 'theme_key')
+            ->orderBy('b_order', 'desc')
+            ->get();
+
+        $channels = DB::connection('mysql')
+            ->table('webgame.WebChannelConfig')
+            ->select('ID', 'Channel', 'StateNo', 'RegionID', 'Remarks')
+            ->orderBy('Channel')
+            ->get();
+
+        return response()->json([
+            'code' => 200,
+            'msg' => 'ok',
+            'data' => [
+                'page_id' => $pageId,
+                'routes' => $routeRows,
+                'page_modules' => $pageModules,
+                'modules' => $allModules,
+                'module_parents' => $links,
+                'module_games' => $moduleGames,
+                'styles' => $styles,
+                'banners' => $banners,
+                'channels' => $channels,
+            ],
+        ]);
+    }
+
+    public function updateState(Request $request)
+    {
+        $validator = Validator::make($request->all(), [
+            'table' => 'required|in:routes,modules,games,banners',
+            'id' => 'required|integer',
+            'state' => 'required|integer|min:0',
+        ]);
+
+        if ($validator->fails()) {
+            return apiReturnFail($validator->errors()->first());
+        }
+
+        $tableMap = [
+            'routes' => ['webgame.routes', 'id'],
+            'modules' => ['webgame.modules', 'id'],
+            'games' => ['webgame.games', 'id'],
+            'banners' => ['webgame.banners', 'bid'],
+        ];
+
+        $table = $request->input('table');
+        [$tableName, $pk] = $tableMap[$table];
+
+        DB::connection('mysql')
+            ->table($tableName)
+            ->where($pk, $request->input('id'))
+            ->update(['state' => $request->input('state')]);
+
+        return apiReturnSuc('state 已更新');
+    }
+
+    public function reorderRoutes(Request $request)
+    {
+        $payload = $request->all();
+        if (array_key_exists('parent_id', $payload) && $payload['parent_id'] === '') {
+            $payload['parent_id'] = null;
+        }
+
+        $validator = Validator::make($payload, [
+            'parent_id' => 'nullable|integer',
+            'ordered_ids' => 'required|array|min:1',
+            'ordered_ids.*' => 'integer',
+        ]);
+
+        if ($validator->fails()) {
+            return apiReturnFail($validator->errors()->first());
+        }
+
+        $parentId = $payload['parent_id'] ?? null;
+        $orderedIds = array_values(array_unique(array_map('intval', $payload['ordered_ids'] ?? [])));
+
+        DB::connection('mysql')->transaction(function () use ($parentId, $orderedIds) {
+            foreach ($orderedIds as $i => $id) {
+                DB::connection('mysql')
+                    ->table('webgame.routes')
+                    ->where('id', $id)
+                    ->where('parent_id', $parentId)
+                    ->update(['index' => ($i + 1) * 10]);
+            }
+        });
+
+        return apiReturnSuc('路由排序已保存');
+    }
+
+    public function saveModuleLayout(Request $request)
+    {
+        $payload = $request->all();
+        if (isset($payload['items']) && is_array($payload['items'])) {
+            foreach ($payload['items'] as $idx => $item) {
+                if (array_key_exists('parent_id', $item) && $item['parent_id'] === '') {
+                    $payload['items'][$idx]['parent_id'] = null;
+                }
+            }
+        }
+
+        $validator = Validator::make($payload, [
+            'page_id' => 'required|integer',
+            'items' => 'required|array|min:1',
+            'items.*.module_id' => 'required|integer',
+            'items.*.parent_id' => 'nullable|integer',
+            'items.*.pos_index' => 'required|integer|min:1',
+        ]);
+
+        if ($validator->fails()) {
+            return apiReturnFail($validator->errors()->first());
+        }
+
+        $pageId = (int)$payload['page_id'];
+        $items = $payload['items'] ?? [];
+
+        $pageModuleIds = DB::connection('mysql')
+            ->table('webgame.modules')
+            ->where('page_id', $pageId)
+            ->pluck('id')
+            ->map(fn($v) => (int)$v)
+            ->all();
+
+        $pageModuleIdSet = array_flip($pageModuleIds);
+
+        DB::connection('mysql')->transaction(function () use ($items, $pageModuleIds, $pageModuleIdSet) {
+            foreach ($items as $item) {
+                $moduleId = (int)$item['module_id'];
+                $parentId = isset($item['parent_id']) ? (int)$item['parent_id'] : null;
+                $posIndex = (int)$item['pos_index'];
+
+                if ($parentId === $moduleId) {
+                    continue;
+                }
+
+                DB::connection('mysql')
+                    ->table('webgame.modules')
+                    ->where('id', $moduleId)
+                    ->update(['pos_index' => $posIndex, 'parent_id' => $parentId]);
+
+                DB::connection('mysql')
+                    ->table('webgame.module_parents')
+                    ->where('module_id', $moduleId)
+                    ->whereIn('parent_id', $pageModuleIds)
+                    ->delete();
+
+                if (!is_null($parentId) && isset($pageModuleIdSet[$parentId])) {
+                    DB::connection('mysql')
+                        ->table('webgame.module_parents')
+                        ->updateOrInsert(
+                            ['module_id' => $moduleId, 'parent_id' => $parentId],
+                            ['module_id' => $moduleId, 'parent_id' => $parentId]
+                        );
+                }
+            }
+        });
+
+        return apiReturnSuc('模块布局已保存');
+    }
+
+    public function saveModuleGames(Request $request, int $id)
+    {
+        $validator = Validator::make($request->all(), [
+            'game_ids' => 'present|array',
+            'game_ids.*' => 'integer',
+        ]);
+
+        if ($validator->fails()) {
+            return apiReturnFail($validator->errors()->first());
+        }
+
+        $module = DB::connection('mysql')->table('webgame.modules')->where('id', $id)->first();
+        if (!$module) {
+            return apiReturnFail('模块不存在');
+        }
+
+        if (!in_array($module->type, self::GAME_MODULE_TYPES, true)) {
+            return apiReturnFail('模块类型 [' . $module->type . '] 不支持编辑游戏列表');
+        }
+
+        $gameIds = array_values(array_unique(array_map('intval', $request->input('game_ids', []))));
+
+        DB::connection('mysql')
+            ->table('webgame.modules')
+            ->where('id', $id)
+            ->update(['game_ids' => implode(',', $gameIds)]);
+
+        return apiReturnSuc('模块游戏排序已保存');
+    }
+
+    public function searchGames(Request $request)
+    {
+        $q = trim((string)$request->input('q', ''));
+        $limit = min(max((int)$request->input('limit', 50), 1), 200);
+
+        $query = DB::connection('mysql')
+            ->table('webgame.games')
+            ->select('id', 'title', 'brand', 'img', 'link', 'state', 'gridNum')
+            ->orderBy('id', 'desc');
+
+        if ($q !== '') {
+            $query->where(function ($sub) use ($q) {
+                $sub->where('title', 'like', '%' . $q . '%')
+                    ->orWhere('brand', 'like', '%' . $q . '%')
+                    ->orWhere('gid', 'like', '%' . $q . '%')
+                    ->orWhere('id', $q);
+            });
+        }
+
+        return response()->json([
+            'code' => 200, 'msg' => 'ok',
+            'data' => $query->limit($limit)->get(),
+        ]);
+    }
+
+    // ==================== GameTab CRUD ====================
+
+    public function createTab(Request $request)
+    {
+        $validator = Validator::make($request->all(), [
+            'parent_id' => 'required|integer',
+            'title' => 'required|string|max:255',
+            'icon' => 'nullable|string|max:100',
+            'tabtype' => 'required|integer',
+            'state' => 'nullable|integer|min:0',
+        ]);
+
+        if ($validator->fails()) {
+            return apiReturnFail($validator->errors()->first());
+        }
+
+        $parentId = (int)$request->input('parent_id');
+        $parent = DB::connection('mysql')->table('webgame.modules')->where('id', $parentId)->first();
+        if (!$parent || $parent->type !== 'ModuleGameTabs') {
+            return apiReturnFail('父模块不存在或不是 ModuleGameTabs 类型');
+        }
+
+        $maxPos = DB::connection('mysql')
+            ->table('webgame.module_parents')
+            ->where('parent_id', $parentId)
+            ->join('webgame.modules', 'webgame.modules.id', '=', 'webgame.module_parents.module_id')
+            ->where('webgame.modules.type', 'GameTab')
+            ->max('webgame.modules.pos_index');
+
+        $newId = DB::connection('mysql')->table('webgame.modules')->insertGetId([
+            'page_id' => $parent->page_id,
+            'icon' => $request->input('icon', ''),
+            'pos_index' => ($maxPos ?? 0) + 1,
+            'title' => $request->input('title'),
+            'type' => 'GameTab',
+            'api' => '',
+            'data' => null,
+            'data_key' => '',
+            'game_ids' => '',
+            'tabtype' => $request->input('tabtype'),
+            'parent_id' => $parent->parent_id,
+            'state' => $request->input('state', 16383),
+            'link' => '',
+        ]);
+
+        DB::connection('mysql')->table('webgame.module_parents')->insert([
+            'module_id' => $newId,
+            'parent_id' => $parentId,
+        ]);
+
+        return apiReturnSuc(['msg' => 'Tab 已创建', 'id' => $newId]);
+    }
+
+    public function updateTab(Request $request, int $id)
+    {
+        $validator = Validator::make($request->all(), [
+            'title' => 'nullable|string|max:255',
+            'icon' => 'nullable|string|max:100',
+            'tabtype' => 'nullable|integer',
+        ]);
+
+        if ($validator->fails()) {
+            return apiReturnFail($validator->errors()->first());
+        }
+
+        $module = DB::connection('mysql')->table('webgame.modules')->where('id', $id)->first();
+        if (!$module || $module->type !== 'GameTab') {
+            return apiReturnFail('模块不存在或不是 GameTab 类型');
+        }
+
+        $update = [];
+        if ($request->has('title')) $update['title'] = $request->input('title');
+        if ($request->has('icon')) $update['icon'] = $request->input('icon');
+        if ($request->has('tabtype')) $update['tabtype'] = (int)$request->input('tabtype');
+
+        if (!empty($update)) {
+            DB::connection('mysql')->table('webgame.modules')->where('id', $id)->update($update);
+        }
+
+        return apiReturnSuc('Tab 已更新');
+    }
+
+    public function deleteTab(Request $request, int $id)
+    {
+        $module = DB::connection('mysql')->table('webgame.modules')->where('id', $id)->first();
+        if (!$module || $module->type !== 'GameTab') {
+            return apiReturnFail('模块不存在或不是 GameTab 类型');
+        }
+
+        DB::connection('mysql')->transaction(function () use ($id) {
+            DB::connection('mysql')->table('webgame.module_parents')->where('module_id', $id)->delete();
+            DB::connection('mysql')->table('webgame.module_parents')->where('parent_id', $id)->delete();
+            DB::connection('mysql')->table('webgame.modules')->where('id', $id)->delete();
+        });
+
+        return apiReturnSuc('Tab 已删除');
+    }
+
+    public function reorderTabs(Request $request)
+    {
+        $validator = Validator::make($request->all(), [
+            'parent_id' => 'required|integer',
+            'ordered_ids' => 'required|array|min:1',
+            'ordered_ids.*' => 'integer',
+        ]);
+
+        if ($validator->fails()) {
+            return apiReturnFail($validator->errors()->first());
+        }
+
+        $parentId = (int)$request->input('parent_id');
+        $orderedIds = array_values(array_unique(array_map('intval', $request->input('ordered_ids', []))));
+
+        DB::connection('mysql')->transaction(function () use ($orderedIds) {
+            foreach ($orderedIds as $i => $id) {
+                DB::connection('mysql')
+                    ->table('webgame.modules')
+                    ->where('id', $id)
+                    ->where('type', 'GameTab')
+                    ->update(['pos_index' => $i + 1]);
+            }
+        });
+
+        return apiReturnSuc('Tab 排序已保存');
+    }
+
+    public function updateStyle(Request $request, int $id)
+    {
+        $validator = Validator::make($request->all(), [
+            'style' => 'required|string',
+            'remarks' => 'nullable|string|max:200',
+        ]);
+
+        if ($validator->fails()) {
+            return apiReturnFail($validator->errors()->first());
+        }
+
+        DB::connection('mysql')
+            ->table('webgame.styles')
+            ->where('styleid', $id)
+            ->update([
+                'style' => $request->input('style'),
+                'remarks' => $request->input('remarks', ''),
+            ]);
+
+        return apiReturnSuc('样式已保存');
+    }
+
+    private function parseGameIds(?string $gameIds): array
+    {
+        if (!$gameIds) {
+            return [];
+        }
+        $parts = array_filter(array_map('trim', explode(',', $gameIds)));
+        $ids = [];
+        foreach ($parts as $part) {
+            if ($part !== '' && is_numeric($part)) {
+                $ids[] = (int)$part;
+            }
+        }
+        return $ids;
+    }
+}

+ 2 - 0
app/Http/Controllers/Admin/GameSomeConfigController.php

@@ -24,6 +24,8 @@ class GameSomeConfigController
             0 => '通用配置',
             '921219001' => 'IGT-双砖',
             '1519119693' => 'Jokers Jewels',
+            '9135' => 'JILI-CRAZY 777',
+            '91302' => 'JILI-MONEY COMING EXPENDS',
         ];
 
         // 查询指定GameID和StockMode的配置

+ 642 - 0
resources/views/admin/game_site/builder.blade.php

@@ -0,0 +1,642 @@
+@extends('base.base')
+@section('base')
+<div class="main-panel">
+<div class="content-wrapper">
+    <div class="page-header">
+        <h3 class="page-title">
+            <span class="page-title-icon bg-gradient-primary text-white mr-2"><i class="mdi mdi-view-dashboard"></i></span>
+            页面编排管理
+        </h3>
+    </div>
+
+    <div class="row">
+    <div class="col-lg-12 grid-margin stretch-card">
+    <div class="card"><div class="card-body">
+
+    {{-- ===== 顶部工具栏 ===== --}}
+    <div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-bottom:12px;">
+        <label>页面</label>
+        <select id="pageIdSelect" class="form-control" style="max-width:260px;">
+            @foreach($pages as $p)
+            <option value="{{ $p->id }}" {{ $defaultPageId == $p->id ? 'selected' : '' }}>
+                #{{ $p->id }} {{ $p->path }} {{ $p->title ? ('- '.$p->title) : '' }}
+            </option>
+            @endforeach
+        </select>
+
+        <label>模版预览</label>
+        <select id="stateNoSelect" class="form-control" style="max-width:260px;">
+            <option value="0">全部显示(忽略state)</option>
+            @foreach($channels as $ch)
+            <option value="{{ $ch->StateNo }}">
+                StateNo={{ $ch->StateNo }} Ch={{ $ch->Channel }} {{ $ch->Remarks ? ('('.$ch->Remarks.')') : '' }}
+            </option>
+            @endforeach
+        </select>
+
+        <button class="btn btn-sm btn-gradient-primary" id="reloadBtn">加载</button>
+        <span id="statusHint" style="color:#666;"></span>
+    </div>
+
+    {{-- ===== Tabs ===== --}}
+    <ul class="nav nav-tabs" role="tablist">
+        <li class="nav-item"><a class="nav-link active" data-toggle="tab" href="#tab-preview">页面预览编排</a></li>
+        <li class="nav-item"><a class="nav-link" data-toggle="tab" href="#tab-games">游戏卡片编辑</a></li>
+        <li class="nav-item"><a class="nav-link" data-toggle="tab" href="#tab-route">路由排序</a></li>
+        <li class="nav-item"><a class="nav-link" data-toggle="tab" href="#tab-style">样式管理</a></li>
+    </ul>
+
+    <div class="tab-content" style="padding-top:16px;">
+
+    {{-- ==================== 页面预览编排 ==================== --}}
+    <div class="tab-pane fade show active" id="tab-preview">
+        <div style="display:flex; gap:8px; margin-bottom:12px; flex-wrap:wrap; align-items:center;">
+            <button class="btn btn-sm btn-gradient-info" id="saveLayoutBtn">保存模块排序</button>
+            <span style="color:#999; font-size:13px;">
+                上下拖动模块调整顺序。灰色半透明=当前模版下不显示(state不匹配)。红色虚线=state=0(已弃用)。
+            </span>
+        </div>
+        <div class="site-preview-wrap">
+            {{-- 左侧导航 --}}
+            <div class="sp-sidebar" id="sidebarPreview"></div>
+            {{-- 主内容区 --}}
+            <div class="sp-main" id="mainPreview"></div>
+        </div>
+    </div>
+
+    {{-- ==================== 游戏卡片编辑 ==================== --}}
+    <div class="tab-pane fade" id="tab-games">
+        <div style="display:flex; gap:8px; margin-bottom:10px; flex-wrap:wrap; align-items:center;">
+            <label>选择游戏模块</label>
+            <select id="gameModuleSelect" class="form-control" style="max-width:360px;"></select>
+            <button class="btn btn-sm btn-gradient-success" id="saveGamesBtn">保存游戏排序</button>
+        </div>
+        <div style="display:flex; gap:8px; margin-bottom:10px; flex-wrap:wrap;">
+            <input id="gameSearchInput" class="form-control" placeholder="搜索: id / title / brand" style="max-width:240px;">
+            <button class="btn btn-sm btn-light" id="gameSearchBtn">搜索</button>
+            <button class="btn btn-sm btn-light pv-switch active" data-cols="3">手机3列</button>
+            <button class="btn btn-sm btn-light pv-switch" data-cols="5">Pad5列</button>
+            <button class="btn btn-sm btn-light pv-switch" data-cols="7">电脑7列</button>
+        </div>
+        <div style="margin-bottom:6px; font-weight:600;">当前模块游戏 <span id="gameCountHint"></span></div>
+        <div id="moduleGameCards" class="gg cols-3"></div>
+        <hr>
+        <div style="margin-bottom:6px; font-weight:600;">搜索结果 (点击添加)</div>
+        <div id="gameSearchResult" class="gg cols-3"></div>
+    </div>
+
+    {{-- ==================== 路由排序 ==================== --}}
+    <div class="tab-pane fade" id="tab-route">
+        <div style="display:flex; gap:8px; align-items:center; margin-bottom:10px;">
+            <label>父级路由</label>
+            <select id="routeParentSelect" class="form-control" style="max-width:360px;"></select>
+            <button class="btn btn-sm btn-gradient-info" id="saveRouteBtn">保存排序</button>
+        </div>
+        <div id="routeSortList" class="list-group"></div>
+    </div>
+
+    {{-- ==================== 样式管理 ==================== --}}
+    <div class="tab-pane fade" id="tab-style">
+        <table class="table table-bordered">
+            <thead><tr><th style="width:70px;">ID</th><th style="width:150px;">备注</th><th>style JSON</th><th style="width:80px;">操作</th></tr></thead>
+            <tbody id="styleBody"></tbody>
+        </table>
+    </div>
+
+    </div>{{-- /tab-content --}}
+    </div></div>
+    </div></div>
+</div>
+</div>
+
+<style>
+/* ===== 模拟网站布局 ===== */
+.site-preview-wrap { display:flex; gap:0; border:1px solid #333; border-radius:10px; overflow:hidden; background:#1a1a2e; min-height:500px; }
+.sp-sidebar { width:180px; min-width:180px; background:#12122a; padding:10px 0; overflow-y:auto; max-height:700px; }
+.sp-main { flex:1; padding:16px; overflow-y:auto; max-height:700px; background:#1a1a2e; }
+.sp-sidebar .sb-item { display:flex; align-items:center; gap:6px; padding:8px 14px; color:rgba(255,255,255,0.7); font-size:13px; cursor:pointer; border-left:3px solid transparent; }
+.sp-sidebar .sb-item:hover { background:rgba(255,255,255,0.06); }
+.sp-sidebar .sb-item.active { border-left-color:#b66dff; color:#fff; }
+.sp-sidebar .sb-item .sb-icon { width:18px; text-align:center; font-size:14px; }
+
+/* 模块预览卡片 */
+.mp-module { background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.08); border-radius:8px; padding:10px 12px; margin-bottom:10px; cursor:move; position:relative; }
+.mp-module.state-off { opacity:0.35; }
+.mp-module.state-dead { opacity:0.25; border:2px dashed #d32f2f; }
+.mp-module .mp-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:6px; }
+.mp-module .mp-head .mp-type { font-size:11px; padding:1px 6px; border-radius:3px; font-weight:600; }
+.mp-module .mp-head .mp-title { color:#fff; font-size:13px; font-weight:500; }
+.mp-module .mp-head .mp-state-tag { font-size:10px; padding:1px 5px; border-radius:3px; cursor:pointer; }
+.mp-module .mp-body { color:rgba(255,255,255,0.5); font-size:11px; }
+
+/* 类型颜色 */
+.tp-gamelist { background:#2e7d32; color:#fff; }
+.tp-smallgamelist { background:#1565c0; color:#fff; }
+.tp-banner { background:#e65100; color:#fff; }
+.tp-gametabs { background:#7b1fa2; color:#fff; }
+.tp-gametab { background:#c62828; color:#fff; }
+.tp-func { background:#455a64; color:#fff; }
+
+/* banner 预览 */
+.banner-row { display:flex; gap:8px; overflow-x:auto; padding-bottom:6px; }
+.banner-row img { height:80px; border-radius:6px; flex-shrink:0; }
+
+/* game tabs 预览 */
+.tab-row { display:flex; gap:6px; flex-wrap:wrap; margin-bottom:6px; }
+.tab-pill { padding:4px 10px; border-radius:14px; font-size:11px; background:rgba(255,255,255,0.08); color:rgba(255,255,255,0.7); }
+.tab-pill.active { background:#b66dff; color:#fff; }
+
+/* 子模块容器 */
+.mp-children { margin-top:8px; padding:8px; background:rgba(0,0,0,0.15); border-radius:6px; min-height:30px; }
+
+/* 游戏缩略图行 */
+.game-thumb-row { display:flex; gap:6px; flex-wrap:wrap; }
+.game-thumb { width:60px; text-align:center; }
+.game-thumb img { width:56px; height:56px; object-fit:cover; border-radius:6px; background:#333; }
+.game-thumb .gt-name { font-size:9px; color:rgba(255,255,255,0.5); line-height:1.1; margin-top:2px; height:20px; overflow:hidden; }
+
+/* ===== state 编辑弹窗 ===== */
+.state-popup { position:absolute; right:0; top:30px; z-index:100; background:#fff; border:1px solid #ddd; border-radius:6px; padding:10px; min-width:240px; box-shadow:0 4px 16px rgba(0,0,0,0.15); }
+.state-popup label { display:flex; align-items:center; gap:6px; font-size:12px; color:#333; cursor:pointer; padding:2px 0; }
+.state-popup .sp-actions { display:flex; gap:6px; margin-top:8px; }
+
+/* ===== Tab 管理区 ===== */
+.tab-mgr { background:rgba(0,0,0,0.2); border-radius:6px; padding:8px 10px; margin-bottom:8px; }
+.tab-mgr-label { color:rgba(255,255,255,0.6); font-size:12px; margin-bottom:6px; }
+.tab-sortable { display:flex; gap:6px; flex-wrap:wrap; margin-bottom:8px; }
+.tab-editable { display:inline-flex; align-items:center; gap:5px; padding:4px 8px; border-radius:16px; background:rgba(255,255,255,0.08); color:rgba(255,255,255,0.8); font-size:12px; cursor:move; border:1px solid rgba(255,255,255,0.12); }
+.tab-editable:hover { background:rgba(255,255,255,0.14); border-color:rgba(255,255,255,0.25); }
+.tab-editable .te-icon { font-size:13px; }
+.tab-editable .te-title { cursor:pointer; text-decoration:underline; text-decoration-style:dotted; text-underline-offset:2px; }
+.tab-editable .te-title:hover { color:#fff; }
+.tab-editable .te-info { color:rgba(255,255,255,0.35); font-size:10px; }
+.tab-editable .te-del { cursor:pointer; color:rgba(255,100,100,0.6); font-size:13px; margin-left:2px; }
+.tab-editable .te-del:hover { color:#ff5252; }
+.te-add-btn { background:rgba(255,255,255,0.06) !important; color:rgba(255,255,255,0.5) !important; border:1px dashed rgba(255,255,255,0.2) !important; font-size:12px !important; margin-right:6px; }
+.te-add-btn:hover { background:rgba(255,255,255,0.12) !important; color:#fff !important; }
+.te-save-order-btn { background:rgba(76,175,80,0.15) !important; color:rgba(76,175,80,0.9) !important; border:1px solid rgba(76,175,80,0.3) !important; font-size:12px !important; }
+.te-save-order-btn:hover { background:rgba(76,175,80,0.3) !important; }
+
+/* ===== 游戏卡片网格 ===== */
+.gg { display:grid; gap:8px; }
+.gg.cols-3 { grid-template-columns:repeat(3,minmax(0,1fr)); }
+.gg.cols-5 { grid-template-columns:repeat(5,minmax(0,1fr)); }
+.gg.cols-7 { grid-template-columns:repeat(7,minmax(0,1fr)); }
+.gc { border:1px solid #eee; background:#fff; border-radius:6px; padding:6px; cursor:move; min-height:90px; }
+.gc img { width:100%; min-height:156px; object-fit:cover; border-radius:4px; background:#f3f3f3; }
+.gc .gc-t { font-size:11px; line-height:1.2; margin-top:3px; height:28px; overflow:hidden; }
+.pv-switch.active { border:1px solid #b66dff; color:#b66dff; }
+#routeSortList .list-group-item { cursor:move; }
+</style>
+
+<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
+<script>
+const GAME_TYPES = ['ModuleGameList','ModuleSmallGameList','ModuleRollSmallGameList'];
+const TYPE_MAP = {
+    'ModuleAutoBanner':       {cls:'tp-banner',       label:'Banner',      desc:'轮播Banner'},
+    'ModuleWinList':          {cls:'tp-func',          label:'WinList',     desc:'中奖榜单'},
+    'ModuleSearch':           {cls:'tp-func',          label:'Search',      desc:'搜索入口'},
+    'ModuleGameTabs':         {cls:'tp-gametabs',      label:'GameTabs',    desc:'游戏分类Tab容器'},
+    'GameTab':                {cls:'tp-gametab',       label:'Tab标签',     desc:'Tab标签'},
+    'ModuleHomeWithdraw':     {cls:'tp-func',          label:'Withdraw',    desc:'提现入口'},
+    'ModuleGameList':         {cls:'tp-gamelist',      label:'GameList',    desc:'完整游戏列表'},
+    'ModuleSmallGameList':    {cls:'tp-smallgamelist',  label:'SmallList',   desc:'小游戏预览'},
+    'ModuleRollSmallGameList':{cls:'tp-smallgamelist',  label:'RollSmall',   desc:'滚动小游戏'},
+};
+function tm(type) { return TYPE_MAP[type] || {cls:'tp-func', label:type, desc:type}; }
+function isGameType(type) { return GAME_TYPES.includes(type); }
+function esc(s) { return $('<div/>').text(s||'').html(); }
+function csrf() { return $('meta[name="csrf-token"]').attr('content'); }
+function hint(msg) { $('#statusHint').text(msg); setTimeout(()=>{ if($('#statusHint').text()===msg) $('#statusHint').text(''); },2500); }
+function pint(v,d) { const n=Number(v); return Number.isFinite(n)?n:d; }
+
+const S = {
+    pageId: {{ (int)$defaultPageId }},
+    stateNo: 0,
+    routes:[], modules:[], pageModules:[], moduleParents:[], moduleGames:{}, styles:[], banners:[], channels:[],
+    selGameModule: null,
+};
+
+function stateVisible(stateVal) {
+    if (S.stateNo === 0) return true;
+    return (stateVal & S.stateNo) === S.stateNo;
+}
+function stateDead(stateVal) { return stateVal === 0; }
+
+function stateTagHtml(table, id, stateVal) {
+    const dead = stateDead(stateVal);
+    const visible = stateVisible(stateVal);
+    const bg = dead ? '#d32f2f' : (visible ? '#4caf50' : '#ff9800');
+    const txt = dead ? '弃用' : (visible ? '显示' : '隐藏');
+    const bits = stateVal.toString(2);
+    return '<span class="mp-state-tag" style="background:'+bg+';color:#fff;" onclick="openStateEditor(event,\''+table+'\','+id+','+stateVal+')" title="state='+stateVal+' (0b'+bits+')">'+txt+' s:'+stateVal+'</span>';
+}
+
+// ==================== state 编辑器弹窗 ====================
+function openStateEditor(e, table, id, currentState) {
+    e.stopPropagation();
+    $('.state-popup').remove();
+
+    const channels = S.channels || [];
+    const uniqueStates = [];
+    const seen = {};
+    channels.forEach(ch => {
+        if (!seen[ch.StateNo]) { seen[ch.StateNo] = true; uniqueStates.push(ch); }
+    });
+
+    let checkboxes = uniqueStates.map(ch => {
+        const bit = Number(ch.StateNo);
+        const checked = (currentState & bit) === bit ? 'checked' : '';
+        return '<label><input type="checkbox" data-bit="'+bit+'" '+checked+'> StateNo='+bit+' (Ch '+ch.Channel + (ch.Remarks ? ' '+esc(ch.Remarks) : '') +')</label>';
+    }).join('');
+
+    const popup = $('<div class="state-popup">' +
+        '<div style="font-weight:600;margin-bottom:6px;">State 模版选择 <small style="color:#999;">位运算</small></div>' +
+        '<div style="margin-bottom:4px;"><label><input type="checkbox" id="stateAllCheck"> 全选/全不选</label></div>' +
+        checkboxes +
+        '<div style="margin-top:4px;"><label style="color:#d32f2f;"><input type="checkbox" id="stateDeadCheck" '+(currentState===0?'checked':'')+
+        '> 设为弃用(state=0)</label></div>' +
+        '<div class="sp-actions">' +
+        '<button class="btn btn-sm btn-gradient-primary" id="stateSaveBtn">保存</button>' +
+        '<button class="btn btn-sm btn-light" onclick="$(\'.state-popup\').remove()">取消</button>' +
+        '</div></div>');
+
+    $(e.target).closest('.mp-module,.list-group-item,tr').css('position','relative').append(popup);
+
+    popup.find('#stateAllCheck').on('change', function() {
+        popup.find('input[data-bit]').prop('checked', this.checked);
+    });
+
+    popup.find('#stateDeadCheck').on('change', function() {
+        if (this.checked) popup.find('input[data-bit]').prop('checked', false);
+    });
+
+    popup.find('input[data-bit]').on('change', function() {
+        popup.find('#stateDeadCheck').prop('checked', false);
+    });
+
+    popup.find('#stateSaveBtn').on('click', async function() {
+        let newState = 0;
+        if (popup.find('#stateDeadCheck').is(':checked')) {
+            newState = 0;
+        } else {
+            popup.find('input[data-bit]:checked').each(function() { newState |= Number($(this).attr('data-bit')); });
+        }
+        const res = await $.ajax({
+            url:'/admin/game_site/state', method:'POST',
+            headers:{'X-CSRF-TOKEN':csrf()},
+            data:{table, id, state:newState}
+        });
+        alert(res.msg||'已保存');
+        popup.remove();
+        await loadAll();
+    });
+}
+
+// ==================== 加载 ====================
+async function loadAll() {
+    const res = await $.get('/admin/game_site/data?page_id='+S.pageId);
+    if (res.code!==200) { alert(res.msg||'失败'); return; }
+    Object.assign(S, res.data);
+    renderSidebar(); renderMain(); renderGameModuleSelect(); renderRoutes(); renderStyles();
+    hint('已加载');
+}
+
+// ==================== 侧边栏(仿站点) ====================
+function renderSidebar() {
+    const sideRoutes = S.routes.filter(r => Number(r.side)===1 && r.parent_id===null)
+        .sort((a,b) => Number(a.index)-Number(b.index));
+
+    let html = '<div class="sb-item active"><div class="sb-icon">🏠</div> 首页</div>';
+    sideRoutes.forEach(r => {
+        const vis = stateVisible(r.state);
+        const dead = stateDead(r.state);
+        const opacity = dead ? 'opacity:0.2;' : (!vis ? 'opacity:0.4;' : '');
+        html += '<div class="sb-item" style="'+opacity+'" title="state='+r.state+'">' +
+            '<div class="sb-icon">📁</div>' + esc(r.title||r.path) +
+            '</div>';
+    });
+    $('#sidebarPreview').html(html);
+}
+
+// ==================== 主内容区(仿站点布局) ====================
+function renderMain() {
+    const byId = {}; S.modules.forEach(m => byId[m.id]=m);
+    const links = S.module_parents||[];
+    const p2c = {};
+    links.forEach(l => { const p=Number(l.parent_id), c=Number(l.module_id); if(!p2c[p])p2c[p]=[]; if(!p2c[p].includes(c))p2c[p].push(c); });
+
+    const linkedSet = new Set(links.map(l => Number(l.module_id)));
+    const roots = S.page_modules.filter(m => !linkedSet.has(Number(m.id)))
+        .sort((a,b) => Number(a.pos_index)-Number(b.pos_index));
+
+    let html = '<div class="module-sortable" data-container="root">';
+    roots.forEach(m => { html += renderModulePreview(m, byId, p2c); });
+    html += '</div>';
+
+    $('#mainPreview').html(html);
+
+    document.querySelectorAll('.module-sortable').forEach(el => {
+        Sortable.create(el, { group:'module-layout', animation:140, handle:'.mp-head' });
+    });
+
+    document.querySelectorAll('.tab-sortable').forEach(el => {
+        Sortable.create(el, { animation:120 });
+    });
+
+    bindTabEvents();
+}
+
+function renderModulePreview(m, byId, p2c) {
+    const meta = tm(m.type);
+    const vis = stateVisible(m.state);
+    const dead = stateDead(m.state);
+    const cls = dead ? 'state-dead' : (!vis ? 'state-off' : '');
+
+    let inner = '';
+
+    if (m.type === 'ModuleAutoBanner') {
+        const mBanners = (S.banners||[]).filter(b => Number(b.link_module)===Number(m.id) && stateVisible(b.state))
+            .sort((a,b) => Number(b.b_order)-Number(a.b_order));
+        if (mBanners.length) {
+            inner = '<div class="banner-row">' + mBanners.map(b =>
+                '<img src="'+esc(b.img)+'" alt="'+esc(b.alt)+'" title="'+esc(b.alt)+'">'
+            ).join('') + '</div>';
+        } else {
+            inner = '<div style="color:rgba(255,255,255,0.3);">无Banner</div>';
+        }
+    } else if (m.type === 'ModuleGameTabs') {
+        const childIds = (p2c[m.id]||[]).filter(id => !!byId[id]);
+        childIds.sort((a,b) => Number(byId[a].pos_index)-Number(byId[b].pos_index));
+        const tabs = childIds.map(id => byId[id]).filter(c => c.type==='GameTab');
+        const nonTabs = childIds.map(id => byId[id]).filter(c => c.type!=='GameTab');
+
+        inner = '<div class="tab-mgr" data-tabs-parent="'+m.id+'">';
+        inner += '<div class="tab-mgr-label">Tab标签管理 <small style="color:rgba(255,255,255,0.4);">(拖拽排序 | 点击编辑 | ✕删除)</small></div>';
+        inner += '<div class="tab-sortable" data-tabs-parent="'+m.id+'">';
+        tabs.forEach(t => {
+            const tVis = stateVisible(t.state); const tDead = stateDead(t.state);
+            const tabOpacity = tDead ? 'opacity:0.3;' : (!tVis ? 'opacity:0.5;' : '');
+            inner += '<div class="tab-editable" data-id="'+t.id+'" style="'+tabOpacity+'">' +
+                '<span class="te-icon" title="icon: '+esc(t.icon)+'">'+(t.icon ? '🎮' : '📑')+'</span>' +
+                '<span class="te-title" title="点击编辑">'+esc(t.title||t.icon||'Tab')+'</span>' +
+                '<span class="te-info">type='+t.tabtype+'</span>' +
+                stateTagHtml('modules', t.id, t.state) +
+                '<span class="te-del" data-id="'+t.id+'" title="删除此Tab">✕</span>' +
+                '</div>';
+        });
+        inner += '</div>';
+        inner += '<button class="btn btn-sm te-add-btn" data-parent="'+m.id+'">+ 新增Tab</button>';
+        inner += '<button class="btn btn-sm te-save-order-btn" data-parent="'+m.id+'">💾 保存Tab排序</button>';
+        inner += '</div>';
+
+        inner += '<div class="mp-children module-sortable" data-container="m_'+m.id+'">';
+        nonTabs.forEach(c => { inner += renderModulePreview(c, byId, p2c); });
+        inner += '</div>';
+    } else if (isGameType(m.type)) {
+        const games = S.module_games[m.id] || [];
+        const show = games.slice(0, 7);
+        inner = '<div class="game-thumb-row">' + show.map(g =>
+            '<div class="game-thumb"><img src="'+esc(g.img)+'"><div class="gt-name">'+esc(g.title)+'</div></div>'
+        ).join('') + (games.length>7 ? '<div class="game-thumb" style="display:flex;align-items:center;color:rgba(255,255,255,0.4);">+' + (games.length-7) + '</div>' : '') + '</div>';
+    } else if (m.type === 'ModuleHomeWithdraw') {
+        inner = '<div style="display:flex;align-items:center;gap:8px;"><span style="color:rgba(255,255,255,0.5);">💰 Total Free Bonus $0</span><span style="background:#4caf50;color:#fff;padding:3px 12px;border-radius:14px;font-size:12px;">Withdraw</span></div>';
+    } else if (m.type === 'ModuleSearch') {
+        inner = '<div style="background:rgba(255,255,255,0.06);border-radius:6px;padding:6px 12px;color:rgba(255,255,255,0.3);">🔍 Search games...</div>';
+    }
+
+    return '<div class="mp-module '+cls+'" data-id="'+m.id+'">' +
+        '<div class="mp-head">' +
+        '<div><span class="mp-type '+meta.cls+'">'+meta.label+'</span> ' +
+        '<span class="mp-title">#'+m.id+' '+esc(m.title||'-')+'</span></div>' +
+        '<div>'+stateTagHtml('modules', m.id, m.state)+'</div>' +
+        '</div>' +
+        '<div class="mp-body">'+inner+'</div>' +
+        '</div>';
+}
+
+// ==================== 游戏编辑器 ====================
+function renderGameModuleSelect() {
+    const gms = S.modules.filter(m => isGameType(m.type));
+    const opts = gms.map(m => '<option value="'+m.id+'">['+tm(m.type).label+'] #'+m.id+' '+esc(m.title||'-')+' ('+(S.module_games[m.id]||[]).length+'个游戏)</option>');
+    $('#gameModuleSelect').html(opts.length ? opts.join('') : '<option value="">无游戏模块</option>');
+    if (gms.length && (!S.selGameModule || !gms.some(m=>Number(m.id)===S.selGameModule))) S.selGameModule = Number(gms[0].id);
+    if (S.selGameModule) { $('#gameModuleSelect').val(String(S.selGameModule)); renderMG(S.selGameModule); }
+}
+
+function gcHtml(g, removable) {
+    const btn = removable
+        ? '<button class="btn btn-sm btn-light remove-gc" data-id="'+g.id+'" style="padding:0 4px;margin-top:3px;">移除</button>'
+        : '<button class="btn btn-sm btn-outline-success add-gc" data-id="'+g.id+'" style="padding:0 4px;margin-top:3px;">添加</button>';
+    return '<div class="gc" data-id="'+g.id+'"><img src="'+esc(g.img)+'"><div class="gc-t">#'+g.id+' '+esc(g.title)+'</div><small>'+esc(g.brand)+'</small><br>'+btn+'</div>';
+}
+
+function renderMG(mid) {
+    const gs = S.module_games[mid]||[];
+    $('#gameCountHint').text('('+gs.length+'个)');
+    const el = document.getElementById('moduleGameCards');
+    $(el).html(gs.map(g=>gcHtml(g,true)).join('')||'<div style="color:#999;padding:10px;">暂无游戏</div>');
+    Sortable.create(el, {animation:120, filter:'.remove-gc'});
+    $('#moduleGameCards .remove-gc').off('click').on('click', function(){
+        const gid = pint($(this).attr('data-id'),0);
+        S.module_games[mid] = (S.module_games[mid]||[]).filter(g=>Number(g.id)!==gid);
+        renderMG(mid);
+    });
+}
+
+async function searchGames() {
+    const q=$('#gameSearchInput').val()||'';
+    const res = await $.get('/admin/game_site/games/search?q='+encodeURIComponent(q)+'&limit=60');
+    if(res.code!==200){ alert(res.msg); return; }
+    const list = res.data||[];
+    $('#gameSearchResult').html(list.map(g=>gcHtml(g,false)).join(''));
+    $('#gameSearchResult .add-gc').off('click').on('click',function(){
+        const gid=pint($(this).attr('data-id'),0);
+        const hit=list.find(v=>Number(v.id)===gid);
+        if(!hit||!S.selGameModule) return;
+        const arr=S.module_games[S.selGameModule]||[];
+        if(arr.some(v=>Number(v.id)===gid)){ hint('已在列表中'); return; }
+        arr.push(hit); S.module_games[S.selGameModule]=arr;
+        renderMG(S.selGameModule); hint('#'+gid+' 已添加');
+    });
+}
+
+// ==================== 路由排序 ====================
+function renderRoutes() {
+    const opts = ['<option value="">顶级路由</option>'];
+    S.routes.forEach(r => { opts.push('<option value="'+r.id+'">#'+r.id+' '+esc(r.path)+(r.title?' - '+esc(r.title):'')+'</option>'); });
+    $('#routeParentSelect').html(opts.join(''));
+    renderRouteList();
+}
+function renderRouteList() {
+    const pv=$('#routeParentSelect').val(); const pid = pv===''?null:pint(pv,null);
+    const list = S.routes.filter(r=>{ const rp=r.parent_id===null?null:Number(r.parent_id); return rp===pid; })
+        .sort((a,b)=>Number(a.index)-Number(b.index));
+    const html = list.map(r => {
+        const vis=stateVisible(r.state); const dead=stateDead(r.state);
+        const bg = dead?'background:rgba(211,47,47,0.08);':(!vis?'background:rgba(0,0,0,0.03);':'');
+        return '<div class="list-group-item" data-id="'+r.id+'" style="position:relative;'+bg+'">' +
+            '<span>#'+r.id+' '+esc(r.path)+(r.title?' - '+esc(r.title):'')+'</span> '+
+            '<small style="color:#999;">idx='+r.index+' style='+r.style+'</small> '+
+            stateTagHtml('routes', r.id, r.state) +
+            '</div>';
+    }).join('');
+    $('#routeSortList').html(html||'<div class="text-muted">无路由</div>');
+    const el=document.getElementById('routeSortList');
+    if(el&&list.length) Sortable.create(el,{animation:120});
+}
+
+// ==================== 样式 ====================
+function renderStyles() {
+    const rows = (S.styles||[]).map(s =>
+        '<tr><td>'+s.styleid+'</td><td>'+esc(s.remarks)+'</td>' +
+        '<td><pre style="max-height:160px;overflow:auto;margin:0;white-space:pre-wrap;font-size:11px;">'+esc(s.style)+'</pre></td>' +
+        '<td><button class="btn btn-sm btn-gradient-info ed-style" data-id="'+s.styleid+'">编辑</button></td></tr>'
+    ).join('');
+    $('#styleBody').html(rows);
+    $('.ed-style').off('click').on('click', async function(){
+        const id=pint($(this).attr('data-id'),0);
+        const obj=S.styles.find(v=>Number(v.styleid)===id); if(!obj)return;
+        const sv=prompt('style JSON',obj.style||''); if(sv===null)return;
+        const rv=prompt('备注 remarks',obj.remarks||''); if(rv===null)return;
+        await $.ajax({url:'/admin/game_site/styles/'+id,method:'POST',headers:{'X-CSRF-TOKEN':csrf()},data:{style:sv,remarks:rv}});
+        await loadAll();
+    });
+}
+
+// ==================== Tab 管理事件 ====================
+function bindTabEvents() {
+    $('.te-title').off('click').on('click', function(e) {
+        e.stopPropagation();
+        const tabEl = $(this).closest('.tab-editable');
+        const tabId = pint(tabEl.attr('data-id'), 0);
+        const tabModule = S.modules.find(m => Number(m.id) === tabId);
+        if (!tabModule) return;
+
+        const newTitle = prompt('Tab 标题', tabModule.title || '');
+        if (newTitle === null) return;
+        const newIcon = prompt('Tab 图标 (icon名称)', tabModule.icon || '');
+        if (newIcon === null) return;
+        const newTabtype = prompt('Tab tabtype (关联游戏类目编号)', String(tabModule.tabtype || 0));
+        if (newTabtype === null) return;
+
+        $.ajax({
+            url: '/admin/game_site/tabs/' + tabId + '/update',
+            method: 'POST',
+            headers: {'X-CSRF-TOKEN': csrf()},
+            data: {title: newTitle, icon: newIcon, tabtype: Number(newTabtype)}
+        }).then(res => {
+            hint(res.msg || '已更新');
+            loadAll();
+        });
+    });
+
+    $('.te-del').off('click').on('click', function(e) {
+        e.stopPropagation();
+        const tabId = pint($(this).attr('data-id'), 0);
+        if (!confirm('确定删除 Tab #' + tabId + '?此操作不可撤销。')) return;
+
+        $.ajax({
+            url: '/admin/game_site/tabs/' + tabId + '/delete',
+            method: 'POST',
+            headers: {'X-CSRF-TOKEN': csrf()}
+        }).then(res => {
+            hint(res.msg || '已删除');
+            loadAll();
+        });
+    });
+
+    $('.te-add-btn').off('click').on('click', function(e) {
+        e.stopPropagation();
+        const parentId = pint($(this).attr('data-parent'), 0);
+        const title = prompt('新 Tab 标题');
+        if (!title) return;
+        const icon = prompt('图标 (icon名称,可留空)', '') || '';
+        const tabtype = prompt('tabtype (关联游戏类目编号)', '0');
+        if (tabtype === null) return;
+
+        $.ajax({
+            url: '/admin/game_site/tabs/create',
+            method: 'POST',
+            headers: {'X-CSRF-TOKEN': csrf()},
+            data: {parent_id: parentId, title, icon, tabtype: Number(tabtype), state: 16383}
+        }).then(res => {
+            hint(typeof res.msg === 'string' ? res.msg : '已创建');
+            loadAll();
+        });
+    });
+
+    $('.te-save-order-btn').off('click').on('click', function(e) {
+        e.stopPropagation();
+        const parentId = pint($(this).attr('data-parent'), 0);
+        const sortable = $(this).siblings('.tab-sortable');
+        const ids = [];
+        sortable.children('.tab-editable').each(function() {
+            ids.push(pint($(this).attr('data-id'), 0));
+        });
+
+        if (!ids.length) { alert('无Tab可排序'); return; }
+
+        $.ajax({
+            url: '/admin/game_site/tabs/reorder',
+            method: 'POST',
+            headers: {'X-CSRF-TOKEN': csrf()},
+            data: {parent_id: parentId, ordered_ids: ids}
+        }).then(res => {
+            hint(res.msg || '排序已保存');
+            loadAll();
+        });
+    });
+}
+
+// ==================== 保存动作 ====================
+async function saveLayout() {
+    const items=[];
+    $('.module-sortable').each(function(){
+        const c=$(this).attr('data-container');
+        const pid = c==='root'?null:pint(c.replace('m_',''),null);
+        $(this).children('.mp-module').each(function(idx){
+            items.push({module_id:pint($(this).attr('data-id'),0), parent_id:pid, pos_index:idx+1});
+        });
+    });
+    if(!items.length){alert('无内容');return;}
+    const res=await $.ajax({url:'/admin/game_site/modules/layout',method:'POST',headers:{'X-CSRF-TOKEN':csrf()},data:{page_id:S.pageId,items}});
+    alert(res.msg||'OK'); await loadAll();
+}
+
+async function saveGames() {
+    if(!S.selGameModule){alert('选择模块');return;}
+    const ids=[]; $('#moduleGameCards .gc').each(function(){ids.push(pint($(this).attr('data-id'),0));});
+    const res=await $.ajax({url:'/admin/game_site/modules/'+S.selGameModule+'/games',method:'POST',headers:{'X-CSRF-TOKEN':csrf()},data:{game_ids:ids.filter(v=>v>0)}});
+    alert(res.msg||'OK'); await loadAll();
+}
+
+async function saveRouteOrder() {
+    const pv=$('#routeParentSelect').val(); const pid=pv===''?null:pint(pv,null);
+    const ids=[]; $('#routeSortList .list-group-item').each(function(){ids.push(pint($(this).attr('data-id'),0));});
+    if(!ids.length){alert('无内容');return;}
+    const res=await $.ajax({url:'/admin/game_site/routes/reorder',method:'POST',headers:{'X-CSRF-TOKEN':csrf()},data:{parent_id:pid,ordered_ids:ids}});
+    alert(res.msg||'OK'); await loadAll();
+}
+
+// ==================== 事件 ====================
+$(function(){
+    $('#pageIdSelect').on('change',function(){ S.pageId=pint($(this).val(),S.pageId); });
+    $('#stateNoSelect').on('change',function(){ S.stateNo=pint($(this).val(),0); renderSidebar(); renderMain(); renderRouteList(); hint('模版视角切换: StateNo='+S.stateNo); });
+    $('#reloadBtn').on('click',()=>loadAll());
+    $('#saveLayoutBtn').on('click',()=>saveLayout());
+    $('#saveGamesBtn').on('click',()=>saveGames());
+    $('#saveRouteBtn').on('click',()=>saveRouteOrder());
+    $('#routeParentSelect').on('change',()=>renderRouteList());
+    $('#gameModuleSelect').on('change',function(){ S.selGameModule=pint($(this).val(),null); if(S.selGameModule)renderMG(S.selGameModule); });
+    $('#gameSearchBtn').on('click',()=>searchGames());
+    $('#gameSearchInput').on('keypress',function(e){ if(e.which===13) searchGames(); });
+    $('.pv-switch').on('click',function(){
+        const c=$(this).attr('data-cols');
+        ['#moduleGameCards','#gameSearchResult'].forEach(s=>{ $(s).removeClass('cols-3 cols-5 cols-7').addClass('cols-'+c); });
+        $('.pv-switch').removeClass('active'); $(this).addClass('active');
+    });
+    loadAll();
+});
+</script>
+@endsection

+ 13 - 0
routes/web.php

@@ -370,6 +370,19 @@ Route::group([
         $route->post('/banner/delete/{id}', 'Admin\BannerController@delete');
         $route->post('/banner/sync/{id}', 'Admin\BannerController@sync');
 
+        // 游戏网站页面编排管理
+        $route->get('/game_site/builder', 'Admin\GameSiteBuilderController@index');
+        $route->get('/game_site/data', 'Admin\GameSiteBuilderController@data');
+        $route->post('/game_site/routes/reorder', 'Admin\GameSiteBuilderController@reorderRoutes');
+        $route->post('/game_site/modules/layout', 'Admin\GameSiteBuilderController@saveModuleLayout');
+        $route->post('/game_site/modules/{id}/games', 'Admin\GameSiteBuilderController@saveModuleGames');
+        $route->get('/game_site/games/search', 'Admin\GameSiteBuilderController@searchGames');
+        $route->post('/game_site/styles/{id}', 'Admin\GameSiteBuilderController@updateStyle');
+        $route->post('/game_site/state', 'Admin\GameSiteBuilderController@updateState');
+        $route->post('/game_site/tabs/create', 'Admin\GameSiteBuilderController@createTab');
+        $route->post('/game_site/tabs/{id}/update', 'Admin\GameSiteBuilderController@updateTab');
+        $route->post('/game_site/tabs/{id}/delete', 'Admin\GameSiteBuilderController@deleteTab');
+        $route->post('/game_site/tabs/reorder', 'Admin\GameSiteBuilderController@reorderTabs');
 
 
         //实时数据