index.blade.php 60 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210
  1. @extends('base.base')
  2. @section('base')
  3. <meta name="csrf-token" content="{{ csrf_token() }}">
  4. <div class="container-fluid">
  5. <div class="row">
  6. <div class="col-12">
  7. <div class="card">
  8. <div class="card-header">
  9. <h3 class="card-title mb-0">库存模式配置管理</h3>
  10. </div>
  11. <div class="card-body">
  12. <!-- 标签页导航 -->
  13. <ul class="nav nav-tabs mb-4" role="tablist">
  14. <li class="nav-item">
  15. <a class="nav-link active" data-toggle="tab" href="#config-tab" role="tab">
  16. <i class="mdi mdi-cog"></i> 参数配置
  17. </a>
  18. </li>
  19. <li class="nav-item">
  20. <a class="nav-link" data-toggle="tab" href="#snapshot-tab" role="tab">
  21. <i class="mdi mdi-chart-line"></i> 快照历史
  22. </a>
  23. </li>
  24. </ul>
  25. <!-- 标签页内容 -->
  26. <div class="tab-content">
  27. <!-- 参数配置标签页 -->
  28. <div class="tab-pane fade show active" id="config-tab" role="tabpanel">
  29. <!-- 系统参数配置 -->
  30. <div class="config-section mb-5">
  31. <h5 class="section-title mb-3">
  32. <i class="mdi mdi-cog-outline"></i> 系统参数配置
  33. </h5>
  34. <!-- 房间等级分割配置 -->
  35. <div class="card mb-3">
  36. <div class="card-header bg-light">
  37. <h6 class="mb-0">StockMode2BetLevels - 房间等级分割配置</h6>
  38. <small class="text-muted">配置低中高三个房间的下注上限和最低充值要求</small>
  39. </div>
  40. <div class="card-body">
  41. <div class="table-responsive">
  42. <table class="table table-bordered">
  43. <thead class="thead-light">
  44. <tr>
  45. <th width="120">房间</th>
  46. <th>最大下注额</th>
  47. <th>最低充值金额</th>
  48. </tr>
  49. </thead>
  50. <tbody>
  51. <tr>
  52. <td><span class="badge badge-success">低级房 (索引1)</span></td>
  53. <td>
  54. <div class="input-group input-group-sm">
  55. <input type="number" class="form-control" id="bet_max_1"
  56. value="{{ $betMaxLimits[1] ?? 2 }}" />
  57. <div class="input-group-append">
  58. <span class="input-group-text">下注 ≤ 此值</span>
  59. </div>
  60. </div>
  61. </td>
  62. <td>
  63. <div class="input-group input-group-sm">
  64. <input type="number" class="form-control" id="recharge_min_1"
  65. value="{{ $rechargeMinLimits[1] ?? 0 }}" />
  66. <div class="input-group-append">
  67. <span class="input-group-text">充值 ≥ 此值</span>
  68. </div>
  69. </div>
  70. </td>
  71. </tr>
  72. <tr>
  73. <td><span class="badge badge-info">中级房 (索引2)</span></td>
  74. <td>
  75. <div class="input-group input-group-sm">
  76. <input type="number" class="form-control" id="bet_max_2"
  77. value="{{ $betMaxLimits[2] ?? 10 }}" />
  78. <div class="input-group-append">
  79. <span class="input-group-text">下注 ≤ 此值</span>
  80. </div>
  81. </div>
  82. </td>
  83. <td>
  84. <div class="input-group input-group-sm">
  85. <input type="number" class="form-control" id="recharge_min_2"
  86. value="{{ $rechargeMinLimits[2] ?? 100 }}" />
  87. <div class="input-group-append">
  88. <span class="input-group-text">充值 ≥ 此值</span>
  89. </div>
  90. </div>
  91. </td>
  92. </tr>
  93. <tr>
  94. <td><span class="badge badge-warning">高级房 (索引3)</span></td>
  95. <td>
  96. <div class="input-group input-group-sm">
  97. <input type="number" class="form-control" id="bet_max_3"
  98. value="{{ $betMaxLimits[3] ?? 10000 }}" />
  99. <div class="input-group-append">
  100. <span class="input-group-text">下注 ≤ 此值</span>
  101. </div>
  102. </div>
  103. </td>
  104. <td>
  105. <div class="input-group input-group-sm">
  106. <input type="number" class="form-control" id="recharge_min_3"
  107. value="{{ $rechargeMinLimits[3] ?? 1000 }}" />
  108. <div class="input-group-append">
  109. <span class="input-group-text">充值 ≥ 此值</span>
  110. </div>
  111. </div>
  112. </td>
  113. </tr>
  114. </tbody>
  115. </table>
  116. </div>
  117. </div>
  118. </div>
  119. <!-- 其他系统参数 -->
  120. <div class="row">
  121. <div class="col-md-6">
  122. <div class="card">
  123. <div class="card-header bg-light">
  124. <h6 class="mb-0">StockMode2RevenueRatio - 税收比例</h6>
  125. </div>
  126. <div class="card-body">
  127. <div class="form-group">
  128. <label>税收千分比</label>
  129. <div class="input-group">
  130. <input type="number" class="form-control" id="revenue_ratio"
  131. value="{{ $systemConfig['StockMode2RevenueRatio']->StatusValue ?? 50 }}" />
  132. <div class="input-group-append">
  133. <span class="input-group-text">‰</span>
  134. </div>
  135. </div>
  136. <small class="form-text text-muted">
  137. 库存模式下的税收比例,计算公式:赢钱 × 值 / 1000<br>
  138. 例如:值为 50 时,税收 = 赢钱 × 50/1000 = 赢钱 × 5%
  139. </small>
  140. </div>
  141. </div>
  142. </div>
  143. </div>
  144. <div class="col-md-6">
  145. <div class="card">
  146. <div class="card-header bg-light">
  147. <h6 class="mb-0">StockMode2SwitchRecharge - 模式切换充值阈值</h6>
  148. </div>
  149. <div class="card-body">
  150. <div class="form-group">
  151. <label>切换充值金额</label>
  152. <input type="number" class="form-control" id="switch_recharge"
  153. value="{{ $systemConfig['StockMode2SwitchRecharge']->StatusValue ?? 100 }}" />
  154. <small class="form-text text-muted">
  155. 玩家充值达到此金额后,游戏模式切换为库存模式
  156. </small>
  157. </div>
  158. </div>
  159. </div>
  160. </div>
  161. </div>
  162. <div class="text-right mt-3">
  163. <button class="btn btn-primary" id="save-system-config">
  164. <i class="mdi mdi-content-save"></i> 保存系统配置
  165. </button>
  166. <span class="ml-2 system-status"></span>
  167. </div>
  168. </div>
  169. <hr class="my-5">
  170. <!-- 房间库存配置 -->
  171. <div class="config-section">
  172. <h5 class="section-title mb-3">
  173. <i class="mdi mdi-database"></i> 房间库存配置 (RoomStockStatic2)
  174. </h5>
  175. <p class="text-muted mb-3">
  176. LevelBase 基数配置说明:根据 LevelBase 的倍数区间计算对应的个控(Z)随机范围
  177. </p>
  178. <div class="card">
  179. <div class="card-body">
  180. <div class="alert alert-warning">
  181. <i class="mdi mdi-alert"></i> <strong>注意:</strong>所有数值显示时已除以100,保存时会自动乘以100还原到数据库
  182. </div>
  183. <div class="table-responsive">
  184. <table class="table table-bordered table-hover">
  185. <thead class="thead-light">
  186. <tr>
  187. <th width="120">房间等级</th>
  188. <th>当前库存 (Stock)</th>
  189. <th>基数 (LevelBase)</th>
  190. <th>累计税收 (Revenue)</th>
  191. <th>今日税收</th>
  192. <th>今日rtp</th>
  193. <th>中奖率</th>
  194. <th width="120">操作</th>
  195. </tr>
  196. </thead>
  197. <tbody>
  198. @foreach([1 => '低级房', 2 => '中级房', 3 => '高级房'] as $sortId => $roomName)
  199. @php
  200. $roomStock = $roomStocks->firstWhere('SortID', $sortId);
  201. $stock = ($roomStock->Stock ?? 0) / 100;
  202. $levelBase = ($roomStock->LevelBase ?? 10000) / 100;
  203. $revenue = ($roomStock->Revenue ?? 0) / 100;
  204. $revenueD = ($roomStock->RevenueD ?? 0) / 100;
  205. $todayRevenue = ($rst2[$sortId]->todayRevenue ?? 0);
  206. $todayRtp = ($rst2[$sortId]->todayRtp ?? 0);
  207. $todayWinRatio = ($rst2[$sortId]->todayWinRatio ?? 0) * 100;
  208. @endphp
  209. <tr>
  210. <td>
  211. <span class="badge badge-{{ $sortId == 1 ? 'success' : ($sortId == 2 ? 'info' : 'warning') }}">
  212. {{ $roomName }} (ID:{{ $sortId }})
  213. </span>
  214. </td>
  215. <td>
  216. <div class="input-group input-group-sm">
  217. <div class="input-group-prepend">
  218. <button class="btn btn-outline-danger stock-decrease" type="button" data-sort-id="{{ $sortId }}">
  219. <i class="mdi mdi-minus"></i>
  220. </button>
  221. </div>
  222. <input type="number" step="0.01" class="form-control text-center stock-adjust-input"
  223. data-sort-id="{{ $sortId }}" placeholder="增减数值" />
  224. <div class="input-group-append">
  225. <button class="btn btn-outline-success stock-increase" type="button" data-sort-id="{{ $sortId }}">
  226. <i class="mdi mdi-plus"></i>
  227. </button>
  228. </div>
  229. </div>
  230. <small class="text-muted d-block mt-1">
  231. 当前: <strong class="current-stock-display" data-sort-id="{{ $sortId }}">{{ number_format($stock, 2) }}</strong>
  232. </small>
  233. </td>
  234. <td>
  235. <input type="number" step="0.01" class="form-control form-control-sm level-base-input"
  236. data-sort-id="{{ $sortId }}"
  237. value="{{ number_format($levelBase, 2, '.', '') }}" />
  238. </td>
  239. <td class="text-right text-muted">
  240. {{ number_format($revenue, 2) }}
  241. </td>
  242. <td class="text-right text-muted">{{ $todayRevenue }}</td>
  243. <td class="text-right text-muted">{{ $todayRtp*100 }} %</td>
  244. <td class="text-right text-muted">{{ $todayWinRatio }} %</td>
  245. <td class="text-center">
  246. <button class="btn btn-sm btn-primary save-room-stock" data-sort-id="{{ $sortId }}">
  247. <i class="mdi mdi-content-save"></i> 保存
  248. </button>
  249. </td>
  250. </tr>
  251. @endforeach
  252. </tbody>
  253. </table>
  254. </div>
  255. <!-- 个控计算说明 -->
  256. <div class="alert alert-info mt-4">
  257. <h6><i class="mdi mdi-information"></i> 个控计算规则说明</h6>
  258. <p class="mb-2">根据当前库存(Stock)与基数(LevelBase)的比值,计算个控(Z)的随机范围:</p>
  259. <div class="table-responsive">
  260. <table class="table table-sm table-bordered bg-white">
  261. <thead>
  262. <tr>
  263. <th>库存区间</th>
  264. <th>个控范围(Z)</th>
  265. </tr>
  266. </thead>
  267. <tbody>
  268. <tr>
  269. <td>0 ~ 2×LevelBase</td>
  270. <td>0(固定)</td>
  271. </tr>
  272. <tr>
  273. <td>2×LevelBase ~ 4×LevelBase</td>
  274. <td>1 ~ 5(随机)</td>
  275. </tr>
  276. <tr>
  277. <td>4×LevelBase ~ 6×LevelBase</td>
  278. <td>6 ~ 10(随机)</td>
  279. </tr>
  280. <tr>
  281. <td>6×LevelBase ~ 8×LevelBase</td>
  282. <td>11 ~ 15(随机)</td>
  283. </tr>
  284. <tr>
  285. <td>8×LevelBase ~ 10×LevelBase</td>
  286. <td>16 ~ 20(随机)</td>
  287. </tr>
  288. <tr>
  289. <td>10×LevelBase 以上</td>
  290. <td>20(固定)</td>
  291. </tr>
  292. </tbody>
  293. </table>
  294. </div>
  295. <p class="mb-0 mt-2">
  296. <strong>示例:</strong>当 LevelBase = 100 时
  297. <ul class="mb-0">
  298. <li>Stock 在 0-200 时,个控为 0</li>
  299. <li>Stock 在 200-400 时,个控为 1-5 随机</li>
  300. <li>Stock 在 400-600 时,个控为 6-10 随机</li>
  301. <li>以此类推...</li>
  302. </ul>
  303. </p>
  304. </div>
  305. </div>
  306. </div>
  307. </div>
  308. </div>
  309. <!-- 参数配置标签页结束 -->
  310. <!-- 快照历史标签页 -->
  311. <div class="tab-pane fade" id="snapshot-tab" role="tabpanel">
  312. <!-- 时间范围选择器 -->
  313. <div class="card mb-3">
  314. <div class="card-body">
  315. <div class="row">
  316. <div class="col-md-12 mb-3">
  317. <label>快捷时间区间:</label>
  318. <div class="btn-group d-block" role="group">
  319. <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="5">5分钟</button>
  320. <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="15">15分钟</button>
  321. <button type="button" class="btn btn-sm btn-outline-primary time-range-btn active" data-minutes="60">1小时</button>
  322. <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="240">4小时</button>
  323. <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="720">12小时</button>
  324. <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="1440">24小时</button>
  325. <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="10080">一周</button>
  326. </div>
  327. </div>
  328. <div class="col-md-12">
  329. <label>自定义时间范围(拖动滑块调整):</label>
  330. <div id="time-range-slider" class="mb-2"></div>
  331. <div class="d-flex justify-content-between">
  332. <span id="range-start" class="text-muted small"></span>
  333. <span id="range-duration" class="badge badge-info"></span>
  334. <span id="range-end" class="text-muted small"></span>
  335. </div>
  336. </div>
  337. </div>
  338. </div>
  339. </div>
  340. <!-- 库存值图表 -->
  341. <div class="card mb-3">
  342. <div class="card-header">
  343. <div class="d-flex justify-content-between align-items-center mb-2">
  344. <h6 class="mb-0">库存变化</h6>
  345. <div class="btn-group btn-group-sm" role="group">
  346. <button type="button" class="btn btn-primary chart-type-btn active" data-type="stock">库存值</button>
  347. <button type="button" class="btn btn-outline-primary chart-type-btn" data-type="ratio">比值</button>
  348. <button type="button" class="btn btn-outline-primary chart-type-btn" data-type="both">都显示</button>
  349. </div>
  350. </div>
  351. <div class="d-flex align-items-center">
  352. <span class="mr-2 text-muted small">显示房间:</span>
  353. <div class="form-check form-check-inline mb-0">
  354. <input class="form-check-input room-checkbox" type="checkbox" id="room-1" value="1" checked>
  355. <label class="form-check-label" for="room-1">
  356. <span class="badge badge-success">低级房</span>
  357. </label>
  358. </div>
  359. <div class="form-check form-check-inline mb-0">
  360. <input class="form-check-input room-checkbox" type="checkbox" id="room-2" value="2" checked>
  361. <label class="form-check-label" for="room-2">
  362. <span class="badge badge-info">中级房</span>
  363. </label>
  364. </div>
  365. <div class="form-check form-check-inline mb-0">
  366. <input class="form-check-input room-checkbox" type="checkbox" id="room-3" value="3" checked>
  367. <label class="form-check-label" for="room-3">
  368. <span class="badge badge-warning">高级房</span>
  369. </label>
  370. </div>
  371. </div>
  372. </div>
  373. <div class="card-body">
  374. <div id="stock-ratio-chart" style="height: 500px;"></div>
  375. </div>
  376. </div>
  377. <!-- 房间数据表格 -->
  378. <div class="row">
  379. <div class="col-md-4">
  380. <div class="card">
  381. <div class="card-header bg-success text-white">
  382. <h6 class="mb-0">低级房快照数据</h6>
  383. </div>
  384. <div class="card-body p-0">
  385. <div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
  386. <table class="table table-sm table-hover mb-0" id="snapshot-table-1">
  387. <thead class="thead-light sticky-top">
  388. <tr>
  389. <th>时间</th>
  390. <th>比值</th>
  391. <th>库存</th>
  392. <th>基数</th>
  393. <th>税收</th>
  394. <th>Z值</th>
  395. </tr>
  396. </thead>
  397. <tbody>
  398. <tr><td colspan="6" class="text-center text-muted">加载中...</td></tr>
  399. </tbody>
  400. </table>
  401. </div>
  402. </div>
  403. </div>
  404. </div>
  405. <div class="col-md-4">
  406. <div class="card">
  407. <div class="card-header bg-info text-white">
  408. <h6 class="mb-0">中级房快照数据</h6>
  409. </div>
  410. <div class="card-body p-0">
  411. <div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
  412. <table class="table table-sm table-hover mb-0" id="snapshot-table-2">
  413. <thead class="thead-light sticky-top">
  414. <tr>
  415. <th>时间</th>
  416. <th>比值</th>
  417. <th>库存</th>
  418. <th>基数</th>
  419. <th>税收</th>
  420. <th>Z值</th>
  421. </tr>
  422. </thead>
  423. <tbody>
  424. <tr><td colspan="6" class="text-center text-muted">加载中...</td></tr>
  425. </tbody>
  426. </table>
  427. </div>
  428. </div>
  429. </div>
  430. </div>
  431. <div class="col-md-4">
  432. <div class="card">
  433. <div class="card-header bg-warning text-white">
  434. <h6 class="mb-0">高级房快照数据</h6>
  435. </div>
  436. <div class="card-body p-0">
  437. <div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
  438. <table class="table table-sm table-hover mb-0" id="snapshot-table-3">
  439. <thead class="thead-light sticky-top">
  440. <tr>
  441. <th>时间</th>
  442. <th>比值</th>
  443. <th>库存</th>
  444. <th>基数</th>
  445. <th>税收</th>
  446. <th>Z值</th>
  447. </tr>
  448. </thead>
  449. <tbody>
  450. <tr><td colspan="6" class="text-center text-muted">加载中...</td></tr>
  451. </tbody>
  452. </table>
  453. </div>
  454. </div>
  455. </div>
  456. </div>
  457. </div>
  458. </div>
  459. <!-- 快照历史标签页结束 -->
  460. </div>
  461. <!-- tab-content结束 -->
  462. </div>
  463. </div>
  464. </div>
  465. </div>
  466. </div>
  467. <!-- 引入 ECharts -->
  468. <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
  469. <!-- 引入 noUiSlider -->
  470. <link href="https://cdn.jsdelivr.net/npm/nouislider@15.7.1/dist/nouislider.min.css" rel="stylesheet">
  471. <script src="https://cdn.jsdelivr.net/npm/nouislider@15.7.1/dist/nouislider.min.js"></script>
  472. <script>
  473. $(function() {
  474. // 如果通过哈希访问快照历史,自动切换到对应标签页
  475. if (window.location.hash === '#snapshot-tab') {
  476. $('a[href="#snapshot-tab"]').tab('show');
  477. }
  478. // 保存系统配置
  479. $('#save-system-config').click(function() {
  480. const $btn = $(this);
  481. const $status = $('.system-status');
  482. $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 保存中...');
  483. $status.html('').removeClass('text-success text-danger');
  484. const data = {
  485. bet_max_limits: [
  486. 0, // 索引0轮空
  487. parseInt($('#bet_max_1').val()) || 0,
  488. parseInt($('#bet_max_2').val()) || 0,
  489. parseInt($('#bet_max_3').val()) || 0
  490. ],
  491. recharge_min_limits: [
  492. 0, // 索引0轮空
  493. parseInt($('#recharge_min_1').val()) || 0,
  494. parseInt($('#recharge_min_2').val()) || 0,
  495. parseInt($('#recharge_min_3').val()) || 0
  496. ],
  497. revenue_ratio: parseInt($('#revenue_ratio').val()) || 50,
  498. switch_recharge: parseInt($('#switch_recharge').val()) || 100,
  499. _token: "{{ csrf_token() }}"
  500. };
  501. $.post("{{ url('/admin/stock-mode/update-system-config') }}", data)
  502. .done(function(res) {
  503. $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存系统配置');
  504. if (res.status === 'success') {
  505. $status.text('更新成功').addClass('text-success');
  506. } else {
  507. $status.text(res.message || '更新失败').addClass('text-danger');
  508. }
  509. setTimeout(function() {
  510. $status.fadeOut(function() {
  511. $(this).text('').show().removeClass('text-success text-danger');
  512. });
  513. }, 3000);
  514. })
  515. .fail(function() {
  516. $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存系统配置');
  517. $status.text('系统错误').addClass('text-danger');
  518. });
  519. });
  520. // 库存增加
  521. $('.stock-increase').click(function() {
  522. const sortId = $(this).data('sort-id');
  523. const $input = $('.stock-adjust-input[data-sort-id="' + sortId + '"]');
  524. const adjustValue = parseFloat($input.val()) || 0;
  525. if (adjustValue === 0) {
  526. alert('请输入要增加的数值');
  527. return;
  528. }
  529. updateStock(sortId, adjustValue);
  530. });
  531. // 库存减少
  532. $('.stock-decrease').click(function() {
  533. const sortId = $(this).data('sort-id');
  534. const $input = $('.stock-adjust-input[data-sort-id="' + sortId + '"]');
  535. const adjustValue = parseFloat($input.val()) || 0;
  536. if (adjustValue === 0) {
  537. alert('请输入要减少的数值');
  538. return;
  539. }
  540. updateStock(sortId, -adjustValue);
  541. });
  542. // 格式化数字为千分位
  543. function formatNumber(num) {
  544. return num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  545. }
  546. // 更新库存函数
  547. function updateStock(sortId, adjustValue) {
  548. // 将显示的数值(已除以100)乘以100还原成数据库值
  549. const adjustValueDb = Math.round(adjustValue * 100);
  550. $.post("{{ url('/admin/stock-mode/update-stock') }}", {
  551. sort_id: sortId,
  552. adjust_value: adjustValueDb,
  553. _token: "{{ csrf_token() }}"
  554. })
  555. .done(function(res) {
  556. if (res.status === 'success') {
  557. // 更新显示的当前库存(已除以100)
  558. const newStock = res.new_stock / 100;
  559. $('.current-stock-display[data-sort-id="' + sortId + '"]').text(formatNumber(newStock));
  560. $('.stock-adjust-input[data-sort-id="' + sortId + '"]').val('');
  561. // 显示成功提示
  562. const $display = $('.current-stock-display[data-sort-id="' + sortId + '"]');
  563. $display.addClass('text-success');
  564. setTimeout(function() {
  565. $display.removeClass('text-success');
  566. }, 1000);
  567. } else {
  568. alert(res.message || '更新失败');
  569. }
  570. })
  571. .fail(function() {
  572. alert('系统错误');
  573. });
  574. }
  575. // 保存房间库存配置(LevelBase)
  576. $('.save-room-stock').click(function() {
  577. const $btn = $(this);
  578. const sortId = $btn.data('sort-id');
  579. const $input = $('.level-base-input[data-sort-id="' + sortId + '"]');
  580. const levelBaseDisplay = parseFloat($input.val()) || 100;
  581. // 将显示的数值(已除以100)乘以100还原成数据库值
  582. const levelBase = Math.round(levelBaseDisplay * 100);
  583. $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i>');
  584. $.post("{{ url('/admin/stock-mode/update-room-stock') }}", {
  585. sort_id: sortId,
  586. level_base: levelBase,
  587. _token: "{{ csrf_token() }}"
  588. })
  589. .done(function(res) {
  590. $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存');
  591. if (res.status === 'success') {
  592. $btn.removeClass('btn-primary btn-danger').addClass('btn-success');
  593. setTimeout(function() {
  594. $btn.removeClass('btn-success').addClass('btn-primary');
  595. }, 1500);
  596. } else {
  597. $btn.removeClass('btn-primary btn-success').addClass('btn-danger');
  598. alert(res.message || '更新失败');
  599. setTimeout(function() {
  600. $btn.removeClass('btn-danger').addClass('btn-primary');
  601. }, 2000);
  602. }
  603. })
  604. .fail(function() {
  605. $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存');
  606. $btn.removeClass('btn-primary btn-success').addClass('btn-danger');
  607. alert('系统错误');
  608. setTimeout(function() {
  609. $btn.removeClass('btn-danger').addClass('btn-primary');
  610. }, 2000);
  611. });
  612. });
  613. // ============ 快照历史相关功能 ============
  614. let ratioChart = null;
  615. let timeRangeSlider = null;
  616. let allRoomsData = {1: [], 2: [], 3: []}; // 三个房间的所有数据
  617. let filteredRoomsData = {1: [], 2: [], 3: []}; // 筛选后的数据
  618. let chartType = 'stock'; // 默认显示库存值,可选:stock, ratio, both
  619. let selectedRooms = [1, 2, 3]; // 默认显示所有房间
  620. const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
  621. const MINUTE_IN_MS = 60 * 1000;
  622. // 初始化图表
  623. function initCharts() {
  624. ratioChart = echarts.init(document.getElementById('stock-ratio-chart'));
  625. }
  626. // 补全数据:按固定间隔填充缺失的数据点,确保图表连续
  627. function fillDataGaps(data, minutes) {
  628. if (data.length === 0) return [];
  629. const ACTUAL_INTERVAL_MS = 3000; // 实际数据的间隔是3秒
  630. const FILL_INTERVAL_MS = 10000; // 填充数据使用10秒间隔,降低数据量
  631. const now = Date.now();
  632. const startTime = now - (minutes * 60 * 1000);
  633. const endTime = now;
  634. // 分离前置数据和正常数据
  635. let previousData = null;
  636. let normalData = [];
  637. data.forEach(item => {
  638. if (item.isPrevious) {
  639. previousData = item;
  640. } else {
  641. normalData.push(item);
  642. }
  643. });
  644. // 如果没有正常数据但有前置数据,使用前置数据填充整个时间范围
  645. if (normalData.length === 0 && previousData) {
  646. const filledData = [];
  647. for (let t = startTime; t <= endTime; t += FILL_INTERVAL_MS) {
  648. filledData.push({
  649. ...previousData,
  650. timestamp: t,
  651. createTime: new Date(t).toISOString().replace('T', ' ').substring(0, 19),
  652. isFilled: true
  653. });
  654. }
  655. console.log(`房间无新数据,使用前置数据填充: 填充=${filledData.length}`);
  656. return filledData;
  657. }
  658. // 如果完全没有数据,返回空
  659. if (normalData.length === 0) {
  660. console.log('房间无任何数据');
  661. return [];
  662. }
  663. const filledData = [];
  664. let fillCount = 0;
  665. const firstDataTime = normalData[0].timestamp;
  666. const lastDataTime = normalData[normalData.length - 1].timestamp;
  667. // 1. 填充起始时间到第一条数据之间的空白
  668. if (firstDataTime > startTime) {
  669. const fillSource = previousData || normalData[0];
  670. for (let t = startTime; t < firstDataTime; t += FILL_INTERVAL_MS) {
  671. filledData.push({
  672. ...fillSource,
  673. timestamp: t,
  674. createTime: new Date(t).toISOString().replace('T', ' ').substring(0, 19),
  675. isFilled: true
  676. });
  677. fillCount++;
  678. }
  679. }
  680. // 2. 处理实际数据和中间空白
  681. for (let i = 0; i < normalData.length; i++) {
  682. const currentItem = normalData[i];
  683. // 添加当前实际数据
  684. filledData.push({
  685. ...currentItem,
  686. isFilled: false
  687. });
  688. // 检查与下一条数据之间的间隔
  689. if (i < normalData.length - 1) {
  690. const nextItem = normalData[i + 1];
  691. const gap = nextItem.timestamp - currentItem.timestamp;
  692. // 如果间隔大于实际间隔(说明有数据缺失),填充中间
  693. if (gap > ACTUAL_INTERVAL_MS * 2) {
  694. for (let t = currentItem.timestamp + FILL_INTERVAL_MS; t < nextItem.timestamp; t += FILL_INTERVAL_MS) {
  695. filledData.push({
  696. ...currentItem,
  697. timestamp: t,
  698. createTime: new Date(t).toISOString().replace('T', ' ').substring(0, 19),
  699. isFilled: true
  700. });
  701. fillCount++;
  702. }
  703. }
  704. }
  705. }
  706. // 3. 填充最后一条数据到结束时间的空白
  707. if (lastDataTime < endTime) {
  708. const lastItem = normalData[normalData.length - 1];
  709. for (let t = lastDataTime + FILL_INTERVAL_MS; t <= endTime; t += FILL_INTERVAL_MS) {
  710. filledData.push({
  711. ...lastItem,
  712. timestamp: t,
  713. createTime: new Date(t).toISOString().replace('T', ' ').substring(0, 19),
  714. isFilled: true
  715. });
  716. fillCount++;
  717. }
  718. }
  719. console.log(`数据补全完成: 原始=${normalData.length}, 填充=${fillCount}, 总计=${filledData.length}`);
  720. return filledData;
  721. }
  722. // 加载所有房间的快照数据
  723. function loadAllRoomsData() {
  724. const minutes = $('.time-range-btn.active').data('minutes');
  725. const requests = [];
  726. // 并行请求三个房间的数据
  727. for (let sortId = 1; sortId <= 3; sortId++) {
  728. requests.push(
  729. $.get("{{ url('/admin/stock-mode/snapshot-history') }}", {
  730. sort_id: sortId,
  731. minutes: minutes
  732. })
  733. );
  734. }
  735. Promise.all(requests).then(function(responses) {
  736. console.log('收到响应数量:', responses.length);
  737. responses.forEach(function(res, index) {
  738. const sortId = index + 1;
  739. console.log(`房间${sortId} 响应:`, res.status, '原始数据量:', res.data ? res.data.length : 0);
  740. if (res.status === 'success') {
  741. // 补全数据
  742. console.log(`房间${sortId} 开始补全数据`);
  743. const filledData = fillDataGaps(res.data, minutes);
  744. console.log(`房间${sortId} 补全后数据量:`, filledData.length);
  745. allRoomsData[sortId] = filledData;
  746. filteredRoomsData[sortId] = filledData;
  747. } else {
  748. console.error(`房间${sortId} 加载失败`);
  749. allRoomsData[sortId] = [];
  750. filteredRoomsData[sortId] = [];
  751. }
  752. });
  753. console.log('所有房间数据加载完成:', {
  754. room1: allRoomsData[1].length,
  755. room2: allRoomsData[2].length,
  756. room3: allRoomsData[3].length
  757. });
  758. renderChart();
  759. renderTables();
  760. initTimeRangeSlider();
  761. }).catch(function(err) {
  762. console.error('加载数据失败:', err);
  763. alert('加载数据失败');
  764. });
  765. }
  766. // 渲染库存比值图表(三条线)
  767. function renderChart() {
  768. if (!ratioChart) return;
  769. // 合并选中房间的所有时间点并去重排序
  770. let allTimestamps = [];
  771. selectedRooms.forEach(sortId => {
  772. allTimestamps = allTimestamps.concat(filteredRoomsData[sortId].map(d => d.timestamp));
  773. });
  774. allTimestamps = [...new Set(allTimestamps)].sort((a, b) => a - b);
  775. const times = allTimestamps.map(ts => new Date(ts).toLocaleString('zh-CN', {
  776. month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
  777. }));
  778. // 为每个房间创建数据映射(保存完整数据用于 tooltip)
  779. const roomStockData = {};
  780. const roomRatioData = {};
  781. const roomDetailData = {};
  782. [1, 2, 3].forEach(sortId => {
  783. const stockMap = {};
  784. const ratioMap = {};
  785. const detailMap = {};
  786. filteredRoomsData[sortId].forEach(item => {
  787. stockMap[item.timestamp] = item.stock;
  788. ratioMap[item.timestamp] = item.stockRatio;
  789. detailMap[item.timestamp] = item;
  790. });
  791. roomStockData[sortId] = allTimestamps.map(ts => stockMap[ts] || null);
  792. roomRatioData[sortId] = allTimestamps.map(ts => ratioMap[ts] || null);
  793. roomDetailData[sortId] = detailMap;
  794. });
  795. // 房间配置
  796. const roomConfigs = [
  797. { id: 1, name: '低级房', color: '#67C23A', lightColor: '#95D475' },
  798. { id: 2, name: '中级房', color: '#409EFF', lightColor: '#79BBFF' },
  799. { id: 3, name: '高级房', color: '#E6A23C', lightColor: '#F3D19E' }
  800. ];
  801. // 根据图表类型确定标题和Y轴名称
  802. let chartTitle = '';
  803. let yAxisName = '';
  804. let legendData = [];
  805. let series = [];
  806. if (chartType === 'stock') {
  807. chartTitle = '库存值变化';
  808. yAxisName = '库存值';
  809. roomConfigs.forEach(room => {
  810. if (selectedRooms.includes(room.id)) {
  811. legendData.push(room.name);
  812. series.push({
  813. name: room.name,
  814. type: 'line',
  815. data: roomStockData[room.id],
  816. smooth: true,
  817. lineStyle: { width: 2 },
  818. itemStyle: { color: room.color },
  819. connectNulls: true
  820. });
  821. }
  822. });
  823. } else if (chartType === 'ratio') {
  824. chartTitle = '库存比值变化';
  825. yAxisName = '比值';
  826. roomConfigs.forEach(room => {
  827. if (selectedRooms.includes(room.id)) {
  828. legendData.push(room.name);
  829. series.push({
  830. name: room.name,
  831. type: 'line',
  832. data: roomRatioData[room.id],
  833. smooth: true,
  834. lineStyle: { width: 2 },
  835. itemStyle: { color: room.color },
  836. connectNulls: true
  837. });
  838. }
  839. });
  840. } else { // both - 使用双Y轴
  841. chartTitle = '库存值与比值变化(双Y轴)';
  842. yAxisName = ['库存值', '比值']; // 双Y轴
  843. roomConfigs.forEach(room => {
  844. if (selectedRooms.includes(room.id)) {
  845. legendData.push(`${room.name}-库存`);
  846. legendData.push(`${room.name}-比值`);
  847. series.push({
  848. name: `${room.name}-库存`,
  849. type: 'line',
  850. data: roomStockData[room.id],
  851. yAxisIndex: 0, // 使用左侧Y轴
  852. smooth: true,
  853. lineStyle: { width: 2 },
  854. itemStyle: { color: room.color },
  855. connectNulls: true
  856. });
  857. series.push({
  858. name: `${room.name}-比值`,
  859. type: 'line',
  860. data: roomRatioData[room.id],
  861. yAxisIndex: 1, // 使用右侧Y轴
  862. smooth: true,
  863. lineStyle: { width: 2, type: 'dashed' },
  864. itemStyle: { color: room.lightColor },
  865. connectNulls: true
  866. });
  867. }
  868. });
  869. }
  870. const option = {
  871. title: {
  872. text: chartTitle,
  873. left: 'center'
  874. },
  875. tooltip: {
  876. trigger: 'axis',
  877. axisPointer: { type: 'cross' },
  878. formatter: function(params) {
  879. let tooltip = `<div style="font-weight:bold;">${params[0].axisValue}</div>`;
  880. // 只显示选中房间的信息
  881. const roomConfigs = [
  882. { id: 1, name: '低级房', color: '#67C23A' },
  883. { id: 2, name: '中级房', color: '#409EFF' },
  884. { id: 3, name: '高级房', color: '#E6A23C' }
  885. ];
  886. roomConfigs.forEach(room => {
  887. if (selectedRooms.includes(room.id)) {
  888. const dataIndex = params[0].dataIndex;
  889. const timestamp = allTimestamps[dataIndex];
  890. const detail = roomDetailData[room.id][timestamp];
  891. if (detail) {
  892. tooltip += `<div style="margin-top:5px;">
  893. <span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${room.color};margin-right:5px;"></span>
  894. <strong>${room.name}</strong><br/>
  895. <span style="margin-left:15px;">比值: ${detail.stockRatio}</span><br/>
  896. <span style="margin-left:15px;">库存: ${formatNumber(detail.stock)}</span><br/>
  897. <span style="margin-left:15px;">基数: ${formatNumber(detail.levelBase)}</span><br/>
  898. <span style="margin-left:15px;">税收: ${formatNumber(detail.revenue)}</span>
  899. </div>`;
  900. }
  901. }
  902. });
  903. return tooltip;
  904. }
  905. },
  906. legend: {
  907. data: legendData,
  908. top: 35
  909. },
  910. grid: {
  911. left: '3%',
  912. right: '4%',
  913. bottom: '3%',
  914. containLabel: true,
  915. top: 70
  916. },
  917. xAxis: {
  918. type: 'category',
  919. data: times,
  920. axisLabel: {
  921. rotate: 45,
  922. fontSize: 10
  923. }
  924. },
  925. yAxis: Array.isArray(yAxisName) ? [
  926. {
  927. type: 'value',
  928. name: yAxisName[0],
  929. position: 'left',
  930. axisLabel: {
  931. formatter: '{value}'
  932. }
  933. },
  934. {
  935. type: 'value',
  936. name: yAxisName[1],
  937. position: 'right',
  938. axisLabel: {
  939. formatter: '{value}'
  940. }
  941. }
  942. ] : {
  943. type: 'value',
  944. name: yAxisName
  945. },
  946. series: series
  947. };
  948. // 使用 notMerge: true 完全替换配置,避免旧配置残留
  949. ratioChart.setOption(option, true);
  950. }
  951. // 渲染三个房间的数据表格
  952. function renderTables() {
  953. [1, 2, 3].forEach(sortId => {
  954. const $tbody = $(`#snapshot-table-${sortId} tbody`);
  955. $tbody.empty();
  956. const data = filteredRoomsData[sortId];
  957. if (data.length === 0) {
  958. $tbody.append('<tr><td colspan="6" class="text-center text-muted">暂无数据</td></tr>');
  959. return;
  960. }
  961. data.forEach(item => {
  962. const time = item.createTime.substring(11, 19);
  963. const row = `
  964. <tr>
  965. <td>${time}</td>
  966. <td>${item.stockRatio}</td>
  967. <td>${formatNumber(item.stock)}</td>
  968. <td>${formatNumber(item.levelBase)}</td>
  969. <td>${formatNumber(item.revenue)}</td>
  970. <td><span class="badge badge-${item.zValue <= 5 ? 'success' : (item.zValue <= 15 ? 'warning' : 'danger')}">${item.zValue}</span></td>
  971. </tr>
  972. `;
  973. $tbody.append(row);
  974. });
  975. });
  976. }
  977. // 初始化时间范围滑块
  978. function initTimeRangeSlider() {
  979. // 收集所有数据的时间戳
  980. let allTimestamps = [];
  981. [1, 2, 3].forEach(sortId => {
  982. allTimestamps = allTimestamps.concat(allRoomsData[sortId].map(d => d.timestamp));
  983. });
  984. if (allTimestamps.length === 0) return;
  985. const minTime = Math.min(...allTimestamps);
  986. const maxTime = Math.max(...allTimestamps);
  987. // 限制最小范围为1分钟,最大为一周
  988. const totalRange = maxTime - minTime;
  989. const limitedMaxTime = Math.min(maxTime, minTime + WEEK_IN_MS);
  990. if (timeRangeSlider) {
  991. timeRangeSlider.destroy();
  992. }
  993. const sliderElement = document.getElementById('time-range-slider');
  994. timeRangeSlider = noUiSlider.create(sliderElement, {
  995. start: [minTime, limitedMaxTime],
  996. connect: true,
  997. range: {
  998. 'min': minTime,
  999. 'max': limitedMaxTime
  1000. },
  1001. step: 1000
  1002. });
  1003. // 更新时间显示和时长
  1004. function updateTimeLabels(values) {
  1005. const start = parseInt(values[0]);
  1006. const end = parseInt(values[1]);
  1007. const duration = end - start;
  1008. $('#range-start').text(new Date(start).toLocaleString());
  1009. $('#range-end').text(new Date(end).toLocaleString());
  1010. // 计算时长
  1011. const minutes = Math.floor(duration / MINUTE_IN_MS);
  1012. const hours = Math.floor(minutes / 60);
  1013. const days = Math.floor(hours / 24);
  1014. let durationText = '';
  1015. if (days > 0) {
  1016. durationText = `${days}天${hours % 24}小时`;
  1017. } else if (hours > 0) {
  1018. durationText = `${hours}小时${minutes % 60}分钟`;
  1019. } else {
  1020. durationText = `${minutes}分钟`;
  1021. }
  1022. $('#range-duration').text(durationText);
  1023. // 验证最小1分钟限制
  1024. if (duration < MINUTE_IN_MS) {
  1025. timeRangeSlider.set([start, start + MINUTE_IN_MS]);
  1026. return;
  1027. }
  1028. // 验证最大一周限制
  1029. if (duration > WEEK_IN_MS) {
  1030. timeRangeSlider.set([start, start + WEEK_IN_MS]);
  1031. return;
  1032. }
  1033. }
  1034. updateTimeLabels(timeRangeSlider.get());
  1035. // 滑块变化事件
  1036. timeRangeSlider.on('update', function(values) {
  1037. updateTimeLabels(values);
  1038. });
  1039. timeRangeSlider.on('change', function(values) {
  1040. const startTime = parseInt(values[0]);
  1041. const endTime = parseInt(values[1]);
  1042. // 筛选数据
  1043. [1, 2, 3].forEach(sortId => {
  1044. filteredRoomsData[sortId] = allRoomsData[sortId].filter(d =>
  1045. d.timestamp >= startTime && d.timestamp <= endTime
  1046. );
  1047. });
  1048. renderChart();
  1049. renderTables();
  1050. });
  1051. }
  1052. // 时间区间按钮点击
  1053. $('.time-range-btn').on('click', function() {
  1054. $('.time-range-btn').removeClass('active');
  1055. $(this).addClass('active');
  1056. loadAllRoomsData();
  1057. });
  1058. // 图表类型切换按钮点击
  1059. $('.chart-type-btn').on('click', function() {
  1060. $('.chart-type-btn').removeClass('active btn-primary').addClass('btn-outline-primary');
  1061. $(this).removeClass('btn-outline-primary').addClass('active btn-primary');
  1062. chartType = $(this).data('type');
  1063. renderChart();
  1064. });
  1065. // 房间复选框变化事件
  1066. $('.room-checkbox').on('change', function() {
  1067. selectedRooms = [];
  1068. $('.room-checkbox:checked').each(function() {
  1069. selectedRooms.push(parseInt($(this).val()));
  1070. });
  1071. // 至少选择一个房间
  1072. if (selectedRooms.length === 0) {
  1073. $(this).prop('checked', true);
  1074. selectedRooms.push(parseInt($(this).val()));
  1075. alert('至少需要选择一个房间');
  1076. return;
  1077. }
  1078. renderChart();
  1079. });
  1080. // 切换到快照历史标签页时初始化
  1081. $('a[href="#snapshot-tab"]').on('shown.bs.tab', function() {
  1082. if (!ratioChart) {
  1083. initCharts();
  1084. }
  1085. loadAllRoomsData();
  1086. });
  1087. // 窗口大小改变时重新渲染图表
  1088. $(window).on('resize', function() {
  1089. if (ratioChart) ratioChart.resize();
  1090. });
  1091. });
  1092. </script>
  1093. <style>
  1094. .section-title {
  1095. font-weight: 600;
  1096. color: #495057;
  1097. padding-bottom: 10px;
  1098. border-bottom: 2px solid #e9ecef;
  1099. }
  1100. .card-header h6 {
  1101. font-weight: 600;
  1102. }
  1103. .table th {
  1104. font-weight: 600;
  1105. }
  1106. .alert-info {
  1107. background-color: #e7f3ff;
  1108. border-color: #b3d9ff;
  1109. }
  1110. </style>
  1111. @endsection