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