index.blade.php 59 KB

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