builder.blade.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. @extends('base.base')
  2. @section('base')
  3. <div class="main-panel">
  4. <div class="content-wrapper">
  5. <div class="page-header">
  6. <h3 class="page-title">
  7. <span class="page-title-icon bg-gradient-primary text-white mr-2"><i class="mdi mdi-view-dashboard"></i></span>
  8. 页面编排管理
  9. </h3>
  10. </div>
  11. <div class="row">
  12. <div class="col-lg-12 grid-margin stretch-card">
  13. <div class="card"><div class="card-body">
  14. {{-- ===== 顶部工具栏 ===== --}}
  15. <div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-bottom:12px;">
  16. <label>页面</label>
  17. <select id="pageIdSelect" class="form-control" style="max-width:260px;">
  18. @foreach($pages as $p)
  19. <option value="{{ $p->id }}" {{ $defaultPageId == $p->id ? 'selected' : '' }}>
  20. #{{ $p->id }} {{ $p->path }} {{ $p->title ? ('- '.$p->title) : '' }}
  21. </option>
  22. @endforeach
  23. </select>
  24. <label>模版预览</label>
  25. <select id="stateNoSelect" class="form-control" style="max-width:260px;">
  26. <option value="0">全部显示(忽略state)</option>
  27. @foreach($channels as $ch)
  28. <option value="{{ $ch->StateNo }}">
  29. StateNo={{ $ch->StateNo }} Ch={{ $ch->Channel }} {{ $ch->Remarks ? ('('.$ch->Remarks.')') : '' }}
  30. </option>
  31. @endforeach
  32. </select>
  33. <button class="btn btn-sm btn-gradient-primary" id="reloadBtn">加载</button>
  34. <span id="statusHint" style="color:#666;"></span>
  35. </div>
  36. {{-- ===== Tabs ===== --}}
  37. <ul class="nav nav-tabs" role="tablist">
  38. <li class="nav-item"><a class="nav-link active" data-toggle="tab" href="#tab-preview">页面预览编排</a></li>
  39. <li class="nav-item"><a class="nav-link" data-toggle="tab" href="#tab-games">游戏卡片编辑</a></li>
  40. <li class="nav-item"><a class="nav-link" data-toggle="tab" href="#tab-route">路由排序</a></li>
  41. <li class="nav-item"><a class="nav-link" data-toggle="tab" href="#tab-style">样式管理</a></li>
  42. </ul>
  43. <div class="tab-content" style="padding-top:16px;">
  44. {{-- ==================== 页面预览编排 ==================== --}}
  45. <div class="tab-pane fade show active" id="tab-preview">
  46. <div style="display:flex; gap:8px; margin-bottom:12px; flex-wrap:wrap; align-items:center;">
  47. <button class="btn btn-sm btn-gradient-info" id="saveLayoutBtn">保存模块排序</button>
  48. <span style="color:#999; font-size:13px;">
  49. 上下拖动模块调整顺序。灰色半透明=当前模版下不显示(state不匹配)。红色虚线=state=0(已弃用)。
  50. </span>
  51. </div>
  52. <div class="site-preview-wrap">
  53. {{-- 左侧导航 --}}
  54. <div class="sp-sidebar" id="sidebarPreview"></div>
  55. {{-- 主内容区 --}}
  56. <div class="sp-main" id="mainPreview"></div>
  57. </div>
  58. </div>
  59. {{-- ==================== 游戏卡片编辑 ==================== --}}
  60. <div class="tab-pane fade" id="tab-games">
  61. <div style="display:flex; gap:8px; margin-bottom:10px; flex-wrap:wrap; align-items:center;">
  62. <label>选择游戏模块</label>
  63. <select id="gameModuleSelect" class="form-control" style="max-width:360px;"></select>
  64. <button class="btn btn-sm btn-gradient-success" id="saveGamesBtn">保存游戏排序</button>
  65. </div>
  66. <div style="display:flex; gap:8px; margin-bottom:10px; flex-wrap:wrap;">
  67. <input id="gameSearchInput" class="form-control" placeholder="搜索: id / title / brand" style="max-width:240px;">
  68. <button class="btn btn-sm btn-light" id="gameSearchBtn">搜索</button>
  69. <button class="btn btn-sm btn-light pv-switch active" data-cols="3">手机3列</button>
  70. <button class="btn btn-sm btn-light pv-switch" data-cols="5">Pad5列</button>
  71. <button class="btn btn-sm btn-light pv-switch" data-cols="7">电脑7列</button>
  72. </div>
  73. <div style="margin-bottom:6px; font-weight:600;">当前模块游戏 <span id="gameCountHint"></span></div>
  74. <div id="moduleGameCards" class="gg cols-3"></div>
  75. <hr>
  76. <div style="margin-bottom:6px; font-weight:600;">搜索结果 (点击添加)</div>
  77. <div id="gameSearchResult" class="gg cols-3"></div>
  78. </div>
  79. {{-- ==================== 路由排序 ==================== --}}
  80. <div class="tab-pane fade" id="tab-route">
  81. <div style="display:flex; gap:8px; align-items:center; margin-bottom:10px;">
  82. <label>父级路由</label>
  83. <select id="routeParentSelect" class="form-control" style="max-width:360px;"></select>
  84. <button class="btn btn-sm btn-gradient-info" id="saveRouteBtn">保存排序</button>
  85. </div>
  86. <div id="routeSortList" class="list-group"></div>
  87. </div>
  88. {{-- ==================== 样式管理 ==================== --}}
  89. <div class="tab-pane fade" id="tab-style">
  90. <table class="table table-bordered">
  91. <thead><tr><th style="width:70px;">ID</th><th style="width:150px;">备注</th><th>style JSON</th><th style="width:80px;">操作</th></tr></thead>
  92. <tbody id="styleBody"></tbody>
  93. </table>
  94. </div>
  95. </div>{{-- /tab-content --}}
  96. </div></div>
  97. </div></div>
  98. </div>
  99. </div>
  100. <style>
  101. /* ===== 模拟网站布局 ===== */
  102. .site-preview-wrap { display:flex; gap:0; border:1px solid #333; border-radius:10px; overflow:hidden; background:#1a1a2e; min-height:500px; }
  103. .sp-sidebar { width:180px; min-width:180px; background:#12122a; padding:10px 0; overflow-y:auto; max-height:700px; }
  104. .sp-main { flex:1; padding:16px; overflow-y:auto; max-height:700px; background:#1a1a2e; }
  105. .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; }
  106. .sp-sidebar .sb-item:hover { background:rgba(255,255,255,0.06); }
  107. .sp-sidebar .sb-item.active { border-left-color:#b66dff; color:#fff; }
  108. .sp-sidebar .sb-item .sb-icon { width:18px; text-align:center; font-size:14px; }
  109. /* 模块预览卡片 */
  110. .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; }
  111. .mp-module.state-off { opacity:0.35; }
  112. .mp-module.state-dead { opacity:0.25; border:2px dashed #d32f2f; }
  113. .mp-module .mp-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:6px; }
  114. .mp-module .mp-head .mp-type { font-size:11px; padding:1px 6px; border-radius:3px; font-weight:600; }
  115. .mp-module .mp-head .mp-title { color:#fff; font-size:13px; font-weight:500; }
  116. .mp-module .mp-head .mp-state-tag { font-size:10px; padding:1px 5px; border-radius:3px; cursor:pointer; }
  117. .mp-module .mp-body { color:rgba(255,255,255,0.5); font-size:11px; }
  118. /* 类型颜色 */
  119. .tp-gamelist { background:#2e7d32; color:#fff; }
  120. .tp-smallgamelist { background:#1565c0; color:#fff; }
  121. .tp-banner { background:#e65100; color:#fff; }
  122. .tp-gametabs { background:#7b1fa2; color:#fff; }
  123. .tp-gametab { background:#c62828; color:#fff; }
  124. .tp-func { background:#455a64; color:#fff; }
  125. /* banner 预览 */
  126. .banner-row { display:flex; gap:8px; overflow-x:auto; padding-bottom:6px; }
  127. .banner-row img { height:80px; border-radius:6px; flex-shrink:0; }
  128. /* game tabs 预览 */
  129. .tab-row { display:flex; gap:6px; flex-wrap:wrap; margin-bottom:6px; }
  130. .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); }
  131. .tab-pill.active { background:#b66dff; color:#fff; }
  132. /* 子模块容器 */
  133. .mp-children { margin-top:8px; padding:8px; background:rgba(0,0,0,0.15); border-radius:6px; min-height:30px; }
  134. /* 游戏缩略图行 */
  135. .game-thumb-row { display:flex; gap:6px; flex-wrap:wrap; }
  136. .game-thumb { width:60px; text-align:center; }
  137. .game-thumb img { width:56px; height:56px; object-fit:cover; border-radius:6px; background:#333; }
  138. .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; }
  139. /* ===== state 编辑弹窗 ===== */
  140. .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); }
  141. .state-popup label { display:flex; align-items:center; gap:6px; font-size:12px; color:#333; cursor:pointer; padding:2px 0; }
  142. .state-popup .sp-actions { display:flex; gap:6px; margin-top:8px; }
  143. /* ===== Tab 管理区 ===== */
  144. .tab-mgr { background:rgba(0,0,0,0.2); border-radius:6px; padding:8px 10px; margin-bottom:8px; }
  145. .tab-mgr-label { color:rgba(255,255,255,0.6); font-size:12px; margin-bottom:6px; }
  146. .tab-sortable { display:flex; gap:6px; flex-wrap:wrap; margin-bottom:8px; }
  147. .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); }
  148. .tab-editable:hover { background:rgba(255,255,255,0.14); border-color:rgba(255,255,255,0.25); }
  149. .tab-editable .te-icon { font-size:13px; }
  150. .tab-editable .te-title { cursor:pointer; text-decoration:underline; text-decoration-style:dotted; text-underline-offset:2px; }
  151. .tab-editable .te-title:hover { color:#fff; }
  152. .tab-editable .te-info { color:rgba(255,255,255,0.35); font-size:10px; }
  153. .tab-editable .te-del { cursor:pointer; color:rgba(255,100,100,0.6); font-size:13px; margin-left:2px; }
  154. .tab-editable .te-del:hover { color:#ff5252; }
  155. .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; }
  156. .te-add-btn:hover { background:rgba(255,255,255,0.12) !important; color:#fff !important; }
  157. .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; }
  158. .te-save-order-btn:hover { background:rgba(76,175,80,0.3) !important; }
  159. /* ===== 游戏卡片网格 ===== */
  160. .gg { display:grid; gap:6px; }
  161. .gg.cols-3 { grid-template-columns:repeat(3,minmax(0,1fr)); }
  162. .gg.cols-5 { grid-template-columns:repeat(5,minmax(0,1fr)); }
  163. .gg.cols-7 { grid-template-columns:repeat(7,minmax(0,1fr)); }
  164. .gc { display:grid; grid-template-columns:72px minmax(0,1fr); column-gap:6px; align-items:start; border:1px solid #eee; background:#fff; border-radius:6px; padding:5px; cursor:move; min-height:0; }
  165. .gc img { grid-row:1 / span 3; width:72px; height:54px; object-fit:contain; border-radius:4px; background:#f3f3f3; }
  166. .gc .gc-t { font-size:11px; line-height:1.15; margin-top:0; height:26px; overflow:hidden; }
  167. .gc small { display:block; font-size:10px; line-height:1.1; height:12px; overflow:hidden; }
  168. .gc br { display:none; }
  169. .gc .remove-gc, .gc .add-gc { justify-self:start; line-height:1.2; }
  170. .pv-switch.active { border:1px solid #b66dff; color:#b66dff; }
  171. #routeSortList .list-group-item { cursor:move; }
  172. </style>
  173. <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
  174. <script>
  175. const GAME_TYPES = ['ModuleGameList','ModuleSmallGameList','ModuleRollSmallGameList'];
  176. const TYPE_MAP = {
  177. 'ModuleAutoBanner': {cls:'tp-banner', label:'Banner', desc:'轮播Banner'},
  178. 'ModuleWinList': {cls:'tp-func', label:'WinList', desc:'中奖榜单'},
  179. 'ModuleSearch': {cls:'tp-func', label:'Search', desc:'搜索入口'},
  180. 'ModuleGameTabs': {cls:'tp-gametabs', label:'GameTabs', desc:'游戏分类Tab容器'},
  181. 'GameTab': {cls:'tp-gametab', label:'Tab标签', desc:'Tab标签'},
  182. 'ModuleHomeWithdraw': {cls:'tp-func', label:'Withdraw', desc:'提现入口'},
  183. 'ModuleGameList': {cls:'tp-gamelist', label:'GameList', desc:'完整游戏列表'},
  184. 'ModuleSmallGameList': {cls:'tp-smallgamelist', label:'SmallList', desc:'小游戏预览'},
  185. 'ModuleRollSmallGameList':{cls:'tp-smallgamelist', label:'RollSmall', desc:'滚动小游戏'},
  186. };
  187. function tm(type) { return TYPE_MAP[type] || {cls:'tp-func', label:type, desc:type}; }
  188. function isGameType(type) { return GAME_TYPES.includes(type); }
  189. function esc(s) { return $('<div/>').text(s||'').html(); }
  190. function csrf() { return $('meta[name="csrf-token"]').attr('content'); }
  191. function hint(msg) { $('#statusHint').text(msg); setTimeout(()=>{ if($('#statusHint').text()===msg) $('#statusHint').text(''); },2500); }
  192. function pint(v,d) { const n=Number(v); return Number.isFinite(n)?n:d; }
  193. const S = {
  194. pageId: {{ (int)$defaultPageId }},
  195. stateNo: 0,
  196. routes:[], modules:[], pageModules:[], moduleParents:[], moduleGames:{}, styles:[], banners:[], channels:[],
  197. selGameModule: null,
  198. };
  199. function stateVisible(stateVal) {
  200. if (S.stateNo === 0) return true;
  201. return (stateVal & S.stateNo) === S.stateNo;
  202. }
  203. function stateDead(stateVal) { return stateVal === 0; }
  204. function stateTagHtml(table, id, stateVal) {
  205. const dead = stateDead(stateVal);
  206. const visible = stateVisible(stateVal);
  207. const bg = dead ? '#d32f2f' : (visible ? '#4caf50' : '#ff9800');
  208. const txt = dead ? '弃用' : (visible ? '显示' : '隐藏');
  209. const bits = stateVal.toString(2);
  210. 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>';
  211. }
  212. // ==================== state 编辑器弹窗 ====================
  213. function openStateEditor(e, table, id, currentState) {
  214. e.stopPropagation();
  215. $('.state-popup').remove();
  216. const channels = S.channels || [];
  217. const uniqueStates = [];
  218. const seen = {};
  219. channels.forEach(ch => {
  220. if (!seen[ch.StateNo]) { seen[ch.StateNo] = true; uniqueStates.push(ch); }
  221. });
  222. let checkboxes = uniqueStates.map(ch => {
  223. const bit = Number(ch.StateNo);
  224. const checked = (currentState & bit) === bit ? 'checked' : '';
  225. return '<label><input type="checkbox" data-bit="'+bit+'" '+checked+'> StateNo='+bit+' (Ch '+ch.Channel + (ch.Remarks ? ' '+esc(ch.Remarks) : '') +')</label>';
  226. }).join('');
  227. const popup = $('<div class="state-popup">' +
  228. '<div style="font-weight:600;margin-bottom:6px;">State 模版选择 <small style="color:#999;">位运算</small></div>' +
  229. '<div style="margin-bottom:4px;"><label><input type="checkbox" id="stateAllCheck"> 全选/全不选</label></div>' +
  230. checkboxes +
  231. '<div style="margin-top:4px;"><label style="color:#d32f2f;"><input type="checkbox" id="stateDeadCheck" '+(currentState===0?'checked':'')+
  232. '> 设为弃用(state=0)</label></div>' +
  233. '<div class="sp-actions">' +
  234. '<button class="btn btn-sm btn-gradient-primary" id="stateSaveBtn">保存</button>' +
  235. '<button class="btn btn-sm btn-light" onclick="$(\'.state-popup\').remove()">取消</button>' +
  236. '</div></div>');
  237. $(e.target).closest('.mp-module,.list-group-item,tr').css('position','relative').append(popup);
  238. popup.find('#stateAllCheck').on('change', function() {
  239. popup.find('input[data-bit]').prop('checked', this.checked);
  240. });
  241. popup.find('#stateDeadCheck').on('change', function() {
  242. if (this.checked) popup.find('input[data-bit]').prop('checked', false);
  243. });
  244. popup.find('input[data-bit]').on('change', function() {
  245. popup.find('#stateDeadCheck').prop('checked', false);
  246. });
  247. popup.find('#stateSaveBtn').on('click', async function() {
  248. let newState = 0;
  249. if (popup.find('#stateDeadCheck').is(':checked')) {
  250. newState = 0;
  251. } else {
  252. popup.find('input[data-bit]:checked').each(function() { newState |= Number($(this).attr('data-bit')); });
  253. }
  254. const res = await $.ajax({
  255. url:'/admin/game_site/state', method:'POST',
  256. headers:{'X-CSRF-TOKEN':csrf()},
  257. data:{table, id, state:newState}
  258. });
  259. alert(res.msg||'已保存');
  260. popup.remove();
  261. await loadAll();
  262. });
  263. }
  264. // ==================== 加载 ====================
  265. async function loadAll() {
  266. const res = await $.get('/admin/game_site/data?page_id='+S.pageId);
  267. if (res.code!==200) { alert(res.msg||'失败'); return; }
  268. Object.assign(S, res.data);
  269. renderSidebar(); renderMain(); renderGameModuleSelect(); renderRoutes(); renderStyles();
  270. hint('已加载');
  271. }
  272. // ==================== 侧边栏(仿站点) ====================
  273. function renderSidebar() {
  274. const sideRoutes = S.routes.filter(r => Number(r.side)===1 && r.parent_id===null)
  275. .sort((a,b) => Number(a.index)-Number(b.index));
  276. let html = '<div class="sb-item active"><div class="sb-icon">🏠</div> 首页</div>';
  277. sideRoutes.forEach(r => {
  278. const vis = stateVisible(r.state);
  279. const dead = stateDead(r.state);
  280. const opacity = dead ? 'opacity:0.2;' : (!vis ? 'opacity:0.4;' : '');
  281. html += '<div class="sb-item" style="'+opacity+'" title="state='+r.state+'">' +
  282. '<div class="sb-icon">📁</div>' + esc(r.title||r.path) +
  283. '</div>';
  284. });
  285. $('#sidebarPreview').html(html);
  286. }
  287. // ==================== 主内容区(仿站点布局) ====================
  288. function renderMain() {
  289. const byId = {}; S.modules.forEach(m => byId[m.id]=m);
  290. const links = S.module_parents||[];
  291. const p2c = {};
  292. 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); });
  293. const linkedSet = new Set(links.map(l => Number(l.module_id)));
  294. const roots = S.page_modules.filter(m => !linkedSet.has(Number(m.id)))
  295. .sort((a,b) => Number(a.pos_index)-Number(b.pos_index));
  296. let html = '<div class="module-sortable" data-container="root">';
  297. roots.forEach(m => { html += renderModulePreview(m, byId, p2c); });
  298. html += '</div>';
  299. $('#mainPreview').html(html);
  300. document.querySelectorAll('.module-sortable').forEach(el => {
  301. Sortable.create(el, { group:'module-layout', animation:140, handle:'.mp-head' });
  302. });
  303. document.querySelectorAll('.tab-sortable').forEach(el => {
  304. Sortable.create(el, { animation:120 });
  305. });
  306. bindTabEvents();
  307. }
  308. function renderModulePreview(m, byId, p2c) {
  309. const meta = tm(m.type);
  310. const vis = stateVisible(m.state);
  311. const dead = stateDead(m.state);
  312. const cls = dead ? 'state-dead' : (!vis ? 'state-off' : '');
  313. let inner = '';
  314. if (m.type === 'ModuleAutoBanner') {
  315. const mBanners = (S.banners||[]).filter(b => Number(b.link_module)===Number(m.id) && stateVisible(b.state))
  316. .sort((a,b) => Number(b.b_order)-Number(a.b_order));
  317. if (mBanners.length) {
  318. inner = '<div class="banner-row">' + mBanners.map(b =>
  319. '<img src="'+esc(b.img)+'" alt="'+esc(b.alt)+'" title="'+esc(b.alt)+'">'
  320. ).join('') + '</div>';
  321. } else {
  322. inner = '<div style="color:rgba(255,255,255,0.3);">无Banner</div>';
  323. }
  324. } else if (m.type === 'ModuleGameTabs') {
  325. const childIds = (p2c[m.id]||[]).filter(id => !!byId[id]);
  326. childIds.sort((a,b) => Number(byId[a].pos_index)-Number(byId[b].pos_index));
  327. const tabs = childIds.map(id => byId[id]).filter(c => c.type==='GameTab');
  328. const nonTabs = childIds.map(id => byId[id]).filter(c => c.type!=='GameTab');
  329. inner = '<div class="tab-mgr" data-tabs-parent="'+m.id+'">';
  330. inner += '<div class="tab-mgr-label">Tab标签管理 <small style="color:rgba(255,255,255,0.4);">(拖拽排序 | 点击编辑 | ✕删除)</small></div>';
  331. inner += '<div class="tab-sortable" data-tabs-parent="'+m.id+'">';
  332. tabs.forEach(t => {
  333. const tVis = stateVisible(t.state); const tDead = stateDead(t.state);
  334. const tabOpacity = tDead ? 'opacity:0.3;' : (!tVis ? 'opacity:0.5;' : '');
  335. inner += '<div class="tab-editable" data-id="'+t.id+'" style="'+tabOpacity+'">' +
  336. '<span class="te-icon" title="icon: '+esc(t.icon)+'">'+(t.icon ? '🎮' : '📑')+'</span>' +
  337. '<span class="te-title" title="点击编辑">'+esc(t.title||t.icon||'Tab')+'</span>' +
  338. '<span class="te-info">type='+t.tabtype+'</span>' +
  339. stateTagHtml('modules', t.id, t.state) +
  340. '<span class="te-del" data-id="'+t.id+'" title="删除此Tab">✕</span>' +
  341. '</div>';
  342. });
  343. inner += '</div>';
  344. inner += '<button class="btn btn-sm te-add-btn" data-parent="'+m.id+'">+ 新增Tab</button>';
  345. inner += '<button class="btn btn-sm te-save-order-btn" data-parent="'+m.id+'">💾 保存Tab排序</button>';
  346. inner += '</div>';
  347. inner += '<div class="mp-children module-sortable" data-container="m_'+m.id+'">';
  348. nonTabs.forEach(c => { inner += renderModulePreview(c, byId, p2c); });
  349. inner += '</div>';
  350. } else if (isGameType(m.type)) {
  351. const games = S.module_games[m.id] || [];
  352. const show = games.slice(0, 7);
  353. inner = '<div class="game-thumb-row">' + show.map(g =>
  354. '<div class="game-thumb"><img src="'+esc(g.img)+'"><div class="gt-name">'+esc(g.title)+'</div></div>'
  355. ).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>';
  356. } else if (m.type === 'ModuleHomeWithdraw') {
  357. 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>';
  358. } else if (m.type === 'ModuleSearch') {
  359. 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>';
  360. }
  361. return '<div class="mp-module '+cls+'" data-id="'+m.id+'">' +
  362. '<div class="mp-head">' +
  363. '<div><span class="mp-type '+meta.cls+'">'+meta.label+'</span> ' +
  364. '<span class="mp-title">#'+m.id+' '+esc(m.title||'-')+'</span></div>' +
  365. '<div>'+stateTagHtml('modules', m.id, m.state)+'</div>' +
  366. '</div>' +
  367. '<div class="mp-body">'+inner+'</div>' +
  368. '</div>';
  369. }
  370. // ==================== 游戏编辑器 ====================
  371. function renderGameModuleSelect() {
  372. const gms = S.modules.filter(m => isGameType(m.type));
  373. 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>');
  374. $('#gameModuleSelect').html(opts.length ? opts.join('') : '<option value="">无游戏模块</option>');
  375. if (gms.length && (!S.selGameModule || !gms.some(m=>Number(m.id)===S.selGameModule))) S.selGameModule = Number(gms[0].id);
  376. if (S.selGameModule) { $('#gameModuleSelect').val(String(S.selGameModule)); renderMG(S.selGameModule); }
  377. }
  378. function gcHtml(g, removable) {
  379. const btn = removable
  380. ? '<button class="btn btn-sm btn-light remove-gc" data-id="'+g.id+'" style="padding:0 4px;margin-top:3px;">移除</button>'
  381. : '<button class="btn btn-sm btn-outline-success add-gc" data-id="'+g.id+'" style="padding:0 4px;margin-top:3px;">添加</button>';
  382. 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>';
  383. }
  384. function renderMG(mid) {
  385. const gs = S.module_games[mid]||[];
  386. $('#gameCountHint').text('('+gs.length+'个)');
  387. const el = document.getElementById('moduleGameCards');
  388. $(el).html(gs.map(g=>gcHtml(g,true)).join('')||'<div style="color:#999;padding:10px;">暂无游戏</div>');
  389. Sortable.create(el, {animation:120, filter:'.remove-gc'});
  390. $('#moduleGameCards .remove-gc').off('click').on('click', function(){
  391. const gid = pint($(this).attr('data-id'),0);
  392. S.module_games[mid] = (S.module_games[mid]||[]).filter(g=>Number(g.id)!==gid);
  393. renderMG(mid);
  394. });
  395. }
  396. async function searchGames() {
  397. const q=$('#gameSearchInput').val()||'';
  398. const res = await $.get('/admin/game_site/games/search?q='+encodeURIComponent(q)+'&limit=60');
  399. if(res.code!==200){ alert(res.msg); return; }
  400. const list = res.data||[];
  401. $('#gameSearchResult').html(list.map(g=>gcHtml(g,false)).join(''));
  402. $('#gameSearchResult .add-gc').off('click').on('click',function(){
  403. const gid=pint($(this).attr('data-id'),0);
  404. const hit=list.find(v=>Number(v.id)===gid);
  405. if(!hit||!S.selGameModule) return;
  406. const arr=S.module_games[S.selGameModule]||[];
  407. if(arr.some(v=>Number(v.id)===gid)){ hint('已在列表中'); return; }
  408. arr.push(hit); S.module_games[S.selGameModule]=arr;
  409. renderMG(S.selGameModule); hint('#'+gid+' 已添加');
  410. });
  411. }
  412. // ==================== 路由排序 ====================
  413. function renderRoutes() {
  414. const opts = ['<option value="">顶级路由</option>'];
  415. S.routes.forEach(r => { opts.push('<option value="'+r.id+'">#'+r.id+' '+esc(r.path)+(r.title?' - '+esc(r.title):'')+'</option>'); });
  416. $('#routeParentSelect').html(opts.join(''));
  417. renderRouteList();
  418. }
  419. function renderRouteList() {
  420. const pv=$('#routeParentSelect').val(); const pid = pv===''?null:pint(pv,null);
  421. const list = S.routes.filter(r=>{ const rp=r.parent_id===null?null:Number(r.parent_id); return rp===pid; })
  422. .sort((a,b)=>Number(a.index)-Number(b.index));
  423. const html = list.map(r => {
  424. const vis=stateVisible(r.state); const dead=stateDead(r.state);
  425. const bg = dead?'background:rgba(211,47,47,0.08);':(!vis?'background:rgba(0,0,0,0.03);':'');
  426. return '<div class="list-group-item" data-id="'+r.id+'" style="position:relative;'+bg+'">' +
  427. '<span>#'+r.id+' '+esc(r.path)+(r.title?' - '+esc(r.title):'')+'</span> '+
  428. '<small style="color:#999;">idx='+r.index+' style='+r.style+'</small> '+
  429. stateTagHtml('routes', r.id, r.state) +
  430. '</div>';
  431. }).join('');
  432. $('#routeSortList').html(html||'<div class="text-muted">无路由</div>');
  433. const el=document.getElementById('routeSortList');
  434. if(el&&list.length) Sortable.create(el,{animation:120});
  435. }
  436. // ==================== 样式 ====================
  437. function renderStyles() {
  438. const rows = (S.styles||[]).map(s =>
  439. '<tr><td>'+s.styleid+'</td><td>'+esc(s.remarks)+'</td>' +
  440. '<td><pre style="max-height:160px;overflow:auto;margin:0;white-space:pre-wrap;font-size:11px;">'+esc(s.style)+'</pre></td>' +
  441. '<td><button class="btn btn-sm btn-gradient-info ed-style" data-id="'+s.styleid+'">编辑</button></td></tr>'
  442. ).join('');
  443. $('#styleBody').html(rows);
  444. $('.ed-style').off('click').on('click', async function(){
  445. const id=pint($(this).attr('data-id'),0);
  446. const obj=S.styles.find(v=>Number(v.styleid)===id); if(!obj)return;
  447. const sv=prompt('style JSON',obj.style||''); if(sv===null)return;
  448. const rv=prompt('备注 remarks',obj.remarks||''); if(rv===null)return;
  449. await $.ajax({url:'/admin/game_site/styles/'+id,method:'POST',headers:{'X-CSRF-TOKEN':csrf()},data:{style:sv,remarks:rv}});
  450. await loadAll();
  451. });
  452. }
  453. // ==================== Tab 管理事件 ====================
  454. function bindTabEvents() {
  455. $('.te-title').off('click').on('click', function(e) {
  456. e.stopPropagation();
  457. const tabEl = $(this).closest('.tab-editable');
  458. const tabId = pint(tabEl.attr('data-id'), 0);
  459. const tabModule = S.modules.find(m => Number(m.id) === tabId);
  460. if (!tabModule) return;
  461. const newTitle = prompt('Tab 标题', tabModule.title || '');
  462. if (newTitle === null) return;
  463. const newIcon = prompt('Tab 图标 (icon名称)', tabModule.icon || '');
  464. if (newIcon === null) return;
  465. const newTabtype = prompt('Tab tabtype (关联游戏类目编号)', String(tabModule.tabtype || 0));
  466. if (newTabtype === null) return;
  467. $.ajax({
  468. url: '/admin/game_site/tabs/' + tabId + '/update',
  469. method: 'POST',
  470. headers: {'X-CSRF-TOKEN': csrf()},
  471. data: {title: newTitle, icon: newIcon, tabtype: Number(newTabtype)}
  472. }).then(res => {
  473. hint(res.msg || '已更新');
  474. loadAll();
  475. });
  476. });
  477. $('.te-del').off('click').on('click', function(e) {
  478. e.stopPropagation();
  479. const tabId = pint($(this).attr('data-id'), 0);
  480. if (!confirm('确定删除 Tab #' + tabId + '?此操作不可撤销。')) return;
  481. $.ajax({
  482. url: '/admin/game_site/tabs/' + tabId + '/delete',
  483. method: 'POST',
  484. headers: {'X-CSRF-TOKEN': csrf()}
  485. }).then(res => {
  486. hint(res.msg || '已删除');
  487. loadAll();
  488. });
  489. });
  490. $('.te-add-btn').off('click').on('click', function(e) {
  491. e.stopPropagation();
  492. const parentId = pint($(this).attr('data-parent'), 0);
  493. const title = prompt('新 Tab 标题');
  494. if (!title) return;
  495. const icon = prompt('图标 (icon名称,可留空)', '') || '';
  496. const tabtype = prompt('tabtype (关联游戏类目编号)', '0');
  497. if (tabtype === null) return;
  498. $.ajax({
  499. url: '/admin/game_site/tabs/create',
  500. method: 'POST',
  501. headers: {'X-CSRF-TOKEN': csrf()},
  502. data: {parent_id: parentId, title, icon, tabtype: Number(tabtype), state: 16383}
  503. }).then(res => {
  504. hint(typeof res.msg === 'string' ? res.msg : '已创建');
  505. loadAll();
  506. });
  507. });
  508. $('.te-save-order-btn').off('click').on('click', function(e) {
  509. e.stopPropagation();
  510. const parentId = pint($(this).attr('data-parent'), 0);
  511. const sortable = $(this).siblings('.tab-sortable');
  512. const ids = [];
  513. sortable.children('.tab-editable').each(function() {
  514. ids.push(pint($(this).attr('data-id'), 0));
  515. });
  516. if (!ids.length) { alert('无Tab可排序'); return; }
  517. $.ajax({
  518. url: '/admin/game_site/tabs/reorder',
  519. method: 'POST',
  520. headers: {'X-CSRF-TOKEN': csrf()},
  521. data: {parent_id: parentId, ordered_ids: ids}
  522. }).then(res => {
  523. hint(res.msg || '排序已保存');
  524. loadAll();
  525. });
  526. });
  527. }
  528. // ==================== 保存动作 ====================
  529. async function saveLayout() {
  530. const items=[];
  531. $('.module-sortable').each(function(){
  532. const c=$(this).attr('data-container');
  533. const pid = c==='root'?null:pint(c.replace('m_',''),null);
  534. $(this).children('.mp-module').each(function(idx){
  535. items.push({module_id:pint($(this).attr('data-id'),0), parent_id:pid, pos_index:idx+1});
  536. });
  537. });
  538. if(!items.length){alert('无内容');return;}
  539. const res=await $.ajax({url:'/admin/game_site/modules/layout',method:'POST',headers:{'X-CSRF-TOKEN':csrf()},data:{page_id:S.pageId,items}});
  540. alert(res.msg||'OK'); await loadAll();
  541. }
  542. async function saveGames() {
  543. if(!S.selGameModule){alert('选择模块');return;}
  544. const ids=[]; $('#moduleGameCards .gc').each(function(){ids.push(pint($(this).attr('data-id'),0));});
  545. 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)}});
  546. alert(res.msg||'OK'); await loadAll();
  547. }
  548. async function saveRouteOrder() {
  549. const pv=$('#routeParentSelect').val(); const pid=pv===''?null:pint(pv,null);
  550. const ids=[]; $('#routeSortList .list-group-item').each(function(){ids.push(pint($(this).attr('data-id'),0));});
  551. if(!ids.length){alert('无内容');return;}
  552. const res=await $.ajax({url:'/admin/game_site/routes/reorder',method:'POST',headers:{'X-CSRF-TOKEN':csrf()},data:{parent_id:pid,ordered_ids:ids}});
  553. alert(res.msg||'OK'); await loadAll();
  554. }
  555. // ==================== 事件 ====================
  556. $(function(){
  557. $('#pageIdSelect').on('change',function(){ S.pageId=pint($(this).val(),S.pageId); });
  558. $('#stateNoSelect').on('change',function(){ S.stateNo=pint($(this).val(),0); renderSidebar(); renderMain(); renderRouteList(); hint('模版视角切换: StateNo='+S.stateNo); });
  559. $('#reloadBtn').on('click',()=>loadAll());
  560. $('#saveLayoutBtn').on('click',()=>saveLayout());
  561. $('#saveGamesBtn').on('click',()=>saveGames());
  562. $('#saveRouteBtn').on('click',()=>saveRouteOrder());
  563. $('#routeParentSelect').on('change',()=>renderRouteList());
  564. $('#gameModuleSelect').on('change',function(){ S.selGameModule=pint($(this).val(),null); if(S.selGameModule)renderMG(S.selGameModule); });
  565. $('#gameSearchBtn').on('click',()=>searchGames());
  566. $('#gameSearchInput').on('keypress',function(e){ if(e.which===13) searchGames(); });
  567. $('.pv-switch').on('click',function(){
  568. const c=$(this).attr('data-cols');
  569. ['#moduleGameCards','#gameSearchResult'].forEach(s=>{ $(s).removeClass('cols-3 cols-5 cols-7').addClass('cols-'+c); });
  570. $('.pv-switch').removeClass('active'); $(this).addClass('active');
  571. });
  572. loadAll();
  573. });
  574. </script>
  575. @endsection