index.blade.php 59 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205
  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. if (window.location.hash === '#snapshot-tab') {
  471. $('a[href="#snapshot-tab"]').tab('show');
  472. }
  473. // 保存系统配置
  474. $('#save-system-config').click(function() {
  475. const $btn = $(this);
  476. const $status = $('.system-status');
  477. $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 保存中...');
  478. $status.html('').removeClass('text-success text-danger');
  479. const data = {
  480. bet_max_limits: [
  481. 0, // 索引0轮空
  482. parseInt($('#bet_max_1').val()) || 0,
  483. parseInt($('#bet_max_2').val()) || 0,
  484. parseInt($('#bet_max_3').val()) || 0
  485. ],
  486. recharge_min_limits: [
  487. 0, // 索引0轮空
  488. parseInt($('#recharge_min_1').val()) || 0,
  489. parseInt($('#recharge_min_2').val()) || 0,
  490. parseInt($('#recharge_min_3').val()) || 0
  491. ],
  492. revenue_ratio: parseInt($('#revenue_ratio').val()) || 50,
  493. switch_recharge: parseInt($('#switch_recharge').val()) || 100,
  494. _token: "{{ csrf_token() }}"
  495. };
  496. $.post("{{ url('/admin/stock-mode/update-system-config') }}", data)
  497. .done(function(res) {
  498. $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存系统配置');
  499. if (res.status === 'success') {
  500. $status.text('更新成功').addClass('text-success');
  501. } else {
  502. $status.text(res.message || '更新失败').addClass('text-danger');
  503. }
  504. setTimeout(function() {
  505. $status.fadeOut(function() {
  506. $(this).text('').show().removeClass('text-success text-danger');
  507. });
  508. }, 3000);
  509. })
  510. .fail(function() {
  511. $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存系统配置');
  512. $status.text('系统错误').addClass('text-danger');
  513. });
  514. });
  515. // 库存增加
  516. $('.stock-increase').click(function() {
  517. const sortId = $(this).data('sort-id');
  518. const $input = $('.stock-adjust-input[data-sort-id="' + sortId + '"]');
  519. const adjustValue = parseFloat($input.val()) || 0;
  520. if (adjustValue === 0) {
  521. alert('请输入要增加的数值');
  522. return;
  523. }
  524. updateStock(sortId, adjustValue);
  525. });
  526. // 库存减少
  527. $('.stock-decrease').click(function() {
  528. const sortId = $(this).data('sort-id');
  529. const $input = $('.stock-adjust-input[data-sort-id="' + sortId + '"]');
  530. const adjustValue = parseFloat($input.val()) || 0;
  531. if (adjustValue === 0) {
  532. alert('请输入要减少的数值');
  533. return;
  534. }
  535. updateStock(sortId, -adjustValue);
  536. });
  537. // 格式化数字为千分位
  538. function formatNumber(num) {
  539. return num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  540. }
  541. // 更新库存函数
  542. function updateStock(sortId, adjustValue) {
  543. // 将显示的数值(已除以100)乘以100还原成数据库值
  544. const adjustValueDb = Math.round(adjustValue * 100);
  545. $.post("{{ url('/admin/stock-mode/update-stock') }}", {
  546. sort_id: sortId,
  547. adjust_value: adjustValueDb,
  548. _token: "{{ csrf_token() }}"
  549. })
  550. .done(function(res) {
  551. if (res.status === 'success') {
  552. // 更新显示的当前库存(已除以100)
  553. const newStock = res.new_stock / 100;
  554. $('.current-stock-display[data-sort-id="' + sortId + '"]').text(formatNumber(newStock));
  555. $('.stock-adjust-input[data-sort-id="' + sortId + '"]').val('');
  556. // 显示成功提示
  557. const $display = $('.current-stock-display[data-sort-id="' + sortId + '"]');
  558. $display.addClass('text-success');
  559. setTimeout(function() {
  560. $display.removeClass('text-success');
  561. }, 1000);
  562. } else {
  563. alert(res.message || '更新失败');
  564. }
  565. })
  566. .fail(function() {
  567. alert('系统错误');
  568. });
  569. }
  570. // 保存房间库存配置(LevelBase)
  571. $('.save-room-stock').click(function() {
  572. const $btn = $(this);
  573. const sortId = $btn.data('sort-id');
  574. const $input = $('.level-base-input[data-sort-id="' + sortId + '"]');
  575. const levelBaseDisplay = parseFloat($input.val()) || 100;
  576. // 将显示的数值(已除以100)乘以100还原成数据库值
  577. const levelBase = Math.round(levelBaseDisplay * 100);
  578. $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i>');
  579. $.post("{{ url('/admin/stock-mode/update-room-stock') }}", {
  580. sort_id: sortId,
  581. level_base: levelBase,
  582. _token: "{{ csrf_token() }}"
  583. })
  584. .done(function(res) {
  585. $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存');
  586. if (res.status === 'success') {
  587. $btn.removeClass('btn-primary btn-danger').addClass('btn-success');
  588. setTimeout(function() {
  589. $btn.removeClass('btn-success').addClass('btn-primary');
  590. }, 1500);
  591. } else {
  592. $btn.removeClass('btn-primary btn-success').addClass('btn-danger');
  593. alert(res.message || '更新失败');
  594. setTimeout(function() {
  595. $btn.removeClass('btn-danger').addClass('btn-primary');
  596. }, 2000);
  597. }
  598. })
  599. .fail(function() {
  600. $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存');
  601. $btn.removeClass('btn-primary btn-success').addClass('btn-danger');
  602. alert('系统错误');
  603. setTimeout(function() {
  604. $btn.removeClass('btn-danger').addClass('btn-primary');
  605. }, 2000);
  606. });
  607. });
  608. // ============ 快照历史相关功能 ============
  609. let ratioChart = null;
  610. let timeRangeSlider = null;
  611. let allRoomsData = {1: [], 2: [], 3: []}; // 三个房间的所有数据
  612. let filteredRoomsData = {1: [], 2: [], 3: []}; // 筛选后的数据
  613. let chartType = 'stock'; // 默认显示库存值,可选:stock, ratio, both
  614. let selectedRooms = [1, 2, 3]; // 默认显示所有房间
  615. const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
  616. const MINUTE_IN_MS = 60 * 1000;
  617. // 初始化图表
  618. function initCharts() {
  619. ratioChart = echarts.init(document.getElementById('stock-ratio-chart'));
  620. }
  621. // 补全数据:按固定间隔填充缺失的数据点,确保图表连续
  622. function fillDataGaps(data, minutes) {
  623. if (data.length === 0) return [];
  624. const ACTUAL_INTERVAL_MS = 3000; // 实际数据的间隔是3秒
  625. const FILL_INTERVAL_MS = 10000; // 填充数据使用10秒间隔,降低数据量
  626. const now = Date.now();
  627. const startTime = now - (minutes * 60 * 1000);
  628. const endTime = now;
  629. // 分离前置数据和正常数据
  630. let previousData = null;
  631. let normalData = [];
  632. data.forEach(item => {
  633. if (item.isPrevious) {
  634. previousData = item;
  635. } else {
  636. normalData.push(item);
  637. }
  638. });
  639. // 如果没有正常数据但有前置数据,使用前置数据填充整个时间范围
  640. if (normalData.length === 0 && previousData) {
  641. const filledData = [];
  642. for (let t = startTime; t <= endTime; t += FILL_INTERVAL_MS) {
  643. filledData.push({
  644. ...previousData,
  645. timestamp: t,
  646. createTime: new Date(t).toISOString().replace('T', ' ').substring(0, 19),
  647. isFilled: true
  648. });
  649. }
  650. console.log(`房间无新数据,使用前置数据填充: 填充=${filledData.length}`);
  651. return filledData;
  652. }
  653. // 如果完全没有数据,返回空
  654. if (normalData.length === 0) {
  655. console.log('房间无任何数据');
  656. return [];
  657. }
  658. const filledData = [];
  659. let fillCount = 0;
  660. const firstDataTime = normalData[0].timestamp;
  661. const lastDataTime = normalData[normalData.length - 1].timestamp;
  662. // 1. 填充起始时间到第一条数据之间的空白
  663. if (firstDataTime > startTime) {
  664. const fillSource = previousData || normalData[0];
  665. for (let t = startTime; t < firstDataTime; t += FILL_INTERVAL_MS) {
  666. filledData.push({
  667. ...fillSource,
  668. timestamp: t,
  669. createTime: new Date(t).toISOString().replace('T', ' ').substring(0, 19),
  670. isFilled: true
  671. });
  672. fillCount++;
  673. }
  674. }
  675. // 2. 处理实际数据和中间空白
  676. for (let i = 0; i < normalData.length; i++) {
  677. const currentItem = normalData[i];
  678. // 添加当前实际数据
  679. filledData.push({
  680. ...currentItem,
  681. isFilled: false
  682. });
  683. // 检查与下一条数据之间的间隔
  684. if (i < normalData.length - 1) {
  685. const nextItem = normalData[i + 1];
  686. const gap = nextItem.timestamp - currentItem.timestamp;
  687. // 如果间隔大于实际间隔(说明有数据缺失),填充中间
  688. if (gap > ACTUAL_INTERVAL_MS * 2) {
  689. for (let t = currentItem.timestamp + FILL_INTERVAL_MS; t < nextItem.timestamp; t += FILL_INTERVAL_MS) {
  690. filledData.push({
  691. ...currentItem,
  692. timestamp: t,
  693. createTime: new Date(t).toISOString().replace('T', ' ').substring(0, 19),
  694. isFilled: true
  695. });
  696. fillCount++;
  697. }
  698. }
  699. }
  700. }
  701. // 3. 填充最后一条数据到结束时间的空白
  702. if (lastDataTime < endTime) {
  703. const lastItem = normalData[normalData.length - 1];
  704. for (let t = lastDataTime + FILL_INTERVAL_MS; t <= endTime; t += FILL_INTERVAL_MS) {
  705. filledData.push({
  706. ...lastItem,
  707. timestamp: t,
  708. createTime: new Date(t).toISOString().replace('T', ' ').substring(0, 19),
  709. isFilled: true
  710. });
  711. fillCount++;
  712. }
  713. }
  714. console.log(`数据补全完成: 原始=${normalData.length}, 填充=${fillCount}, 总计=${filledData.length}`);
  715. return filledData;
  716. }
  717. // 加载所有房间的快照数据
  718. function loadAllRoomsData() {
  719. const minutes = $('.time-range-btn.active').data('minutes');
  720. const requests = [];
  721. // 并行请求三个房间的数据
  722. for (let sortId = 1; sortId <= 3; sortId++) {
  723. requests.push(
  724. $.get("{{ url('/admin/stock-mode/snapshot-history') }}", {
  725. sort_id: sortId,
  726. minutes: minutes
  727. })
  728. );
  729. }
  730. Promise.all(requests).then(function(responses) {
  731. console.log('收到响应数量:', responses.length);
  732. responses.forEach(function(res, index) {
  733. const sortId = index + 1;
  734. console.log(`房间${sortId} 响应:`, res.status, '原始数据量:', res.data ? res.data.length : 0);
  735. if (res.status === 'success') {
  736. // 补全数据
  737. console.log(`房间${sortId} 开始补全数据`);
  738. const filledData = fillDataGaps(res.data, minutes);
  739. console.log(`房间${sortId} 补全后数据量:`, filledData.length);
  740. allRoomsData[sortId] = filledData;
  741. filteredRoomsData[sortId] = filledData;
  742. } else {
  743. console.error(`房间${sortId} 加载失败`);
  744. allRoomsData[sortId] = [];
  745. filteredRoomsData[sortId] = [];
  746. }
  747. });
  748. console.log('所有房间数据加载完成:', {
  749. room1: allRoomsData[1].length,
  750. room2: allRoomsData[2].length,
  751. room3: allRoomsData[3].length
  752. });
  753. renderChart();
  754. renderTables();
  755. initTimeRangeSlider();
  756. }).catch(function(err) {
  757. console.error('加载数据失败:', err);
  758. alert('加载数据失败');
  759. });
  760. }
  761. // 渲染库存比值图表(三条线)
  762. function renderChart() {
  763. if (!ratioChart) return;
  764. // 合并选中房间的所有时间点并去重排序
  765. let allTimestamps = [];
  766. selectedRooms.forEach(sortId => {
  767. allTimestamps = allTimestamps.concat(filteredRoomsData[sortId].map(d => d.timestamp));
  768. });
  769. allTimestamps = [...new Set(allTimestamps)].sort((a, b) => a - b);
  770. const times = allTimestamps.map(ts => new Date(ts).toLocaleString('zh-CN', {
  771. month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
  772. }));
  773. // 为每个房间创建数据映射(保存完整数据用于 tooltip)
  774. const roomStockData = {};
  775. const roomRatioData = {};
  776. const roomDetailData = {};
  777. [1, 2, 3].forEach(sortId => {
  778. const stockMap = {};
  779. const ratioMap = {};
  780. const detailMap = {};
  781. filteredRoomsData[sortId].forEach(item => {
  782. stockMap[item.timestamp] = item.stock;
  783. ratioMap[item.timestamp] = item.stockRatio;
  784. detailMap[item.timestamp] = item;
  785. });
  786. roomStockData[sortId] = allTimestamps.map(ts => stockMap[ts] || null);
  787. roomRatioData[sortId] = allTimestamps.map(ts => ratioMap[ts] || null);
  788. roomDetailData[sortId] = detailMap;
  789. });
  790. // 房间配置
  791. const roomConfigs = [
  792. { id: 1, name: '低级房', color: '#67C23A', lightColor: '#95D475' },
  793. { id: 2, name: '中级房', color: '#409EFF', lightColor: '#79BBFF' },
  794. { id: 3, name: '高级房', color: '#E6A23C', lightColor: '#F3D19E' }
  795. ];
  796. // 根据图表类型确定标题和Y轴名称
  797. let chartTitle = '';
  798. let yAxisName = '';
  799. let legendData = [];
  800. let series = [];
  801. if (chartType === 'stock') {
  802. chartTitle = '库存值变化';
  803. yAxisName = '库存值';
  804. roomConfigs.forEach(room => {
  805. if (selectedRooms.includes(room.id)) {
  806. legendData.push(room.name);
  807. series.push({
  808. name: room.name,
  809. type: 'line',
  810. data: roomStockData[room.id],
  811. smooth: true,
  812. lineStyle: { width: 2 },
  813. itemStyle: { color: room.color },
  814. connectNulls: true
  815. });
  816. }
  817. });
  818. } else if (chartType === 'ratio') {
  819. chartTitle = '库存比值变化';
  820. yAxisName = '比值';
  821. roomConfigs.forEach(room => {
  822. if (selectedRooms.includes(room.id)) {
  823. legendData.push(room.name);
  824. series.push({
  825. name: room.name,
  826. type: 'line',
  827. data: roomRatioData[room.id],
  828. smooth: true,
  829. lineStyle: { width: 2 },
  830. itemStyle: { color: room.color },
  831. connectNulls: true
  832. });
  833. }
  834. });
  835. } else { // both - 使用双Y轴
  836. chartTitle = '库存值与比值变化(双Y轴)';
  837. yAxisName = ['库存值', '比值']; // 双Y轴
  838. roomConfigs.forEach(room => {
  839. if (selectedRooms.includes(room.id)) {
  840. legendData.push(`${room.name}-库存`);
  841. legendData.push(`${room.name}-比值`);
  842. series.push({
  843. name: `${room.name}-库存`,
  844. type: 'line',
  845. data: roomStockData[room.id],
  846. yAxisIndex: 0, // 使用左侧Y轴
  847. smooth: true,
  848. lineStyle: { width: 2 },
  849. itemStyle: { color: room.color },
  850. connectNulls: true
  851. });
  852. series.push({
  853. name: `${room.name}-比值`,
  854. type: 'line',
  855. data: roomRatioData[room.id],
  856. yAxisIndex: 1, // 使用右侧Y轴
  857. smooth: true,
  858. lineStyle: { width: 2, type: 'dashed' },
  859. itemStyle: { color: room.lightColor },
  860. connectNulls: true
  861. });
  862. }
  863. });
  864. }
  865. const option = {
  866. title: {
  867. text: chartTitle,
  868. left: 'center'
  869. },
  870. tooltip: {
  871. trigger: 'axis',
  872. axisPointer: { type: 'cross' },
  873. formatter: function(params) {
  874. let tooltip = `<div style="font-weight:bold;">${params[0].axisValue}</div>`;
  875. // 只显示选中房间的信息
  876. const roomConfigs = [
  877. { id: 1, name: '低级房', color: '#67C23A' },
  878. { id: 2, name: '中级房', color: '#409EFF' },
  879. { id: 3, name: '高级房', color: '#E6A23C' }
  880. ];
  881. roomConfigs.forEach(room => {
  882. if (selectedRooms.includes(room.id)) {
  883. const dataIndex = params[0].dataIndex;
  884. const timestamp = allTimestamps[dataIndex];
  885. const detail = roomDetailData[room.id][timestamp];
  886. if (detail) {
  887. tooltip += `<div style="margin-top:5px;">
  888. <span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${room.color};margin-right:5px;"></span>
  889. <strong>${room.name}</strong><br/>
  890. <span style="margin-left:15px;">比值: ${detail.stockRatio}</span><br/>
  891. <span style="margin-left:15px;">库存: ${formatNumber(detail.stock)}</span><br/>
  892. <span style="margin-left:15px;">基数: ${formatNumber(detail.levelBase)}</span><br/>
  893. <span style="margin-left:15px;">税收: ${formatNumber(detail.revenue)}</span>
  894. </div>`;
  895. }
  896. }
  897. });
  898. return tooltip;
  899. }
  900. },
  901. legend: {
  902. data: legendData,
  903. top: 35
  904. },
  905. grid: {
  906. left: '3%',
  907. right: '4%',
  908. bottom: '3%',
  909. containLabel: true,
  910. top: 70
  911. },
  912. xAxis: {
  913. type: 'category',
  914. data: times,
  915. axisLabel: {
  916. rotate: 45,
  917. fontSize: 10
  918. }
  919. },
  920. yAxis: Array.isArray(yAxisName) ? [
  921. {
  922. type: 'value',
  923. name: yAxisName[0],
  924. position: 'left',
  925. axisLabel: {
  926. formatter: '{value}'
  927. }
  928. },
  929. {
  930. type: 'value',
  931. name: yAxisName[1],
  932. position: 'right',
  933. axisLabel: {
  934. formatter: '{value}'
  935. }
  936. }
  937. ] : {
  938. type: 'value',
  939. name: yAxisName
  940. },
  941. series: series
  942. };
  943. // 使用 notMerge: true 完全替换配置,避免旧配置残留
  944. ratioChart.setOption(option, true);
  945. }
  946. // 渲染三个房间的数据表格
  947. function renderTables() {
  948. [1, 2, 3].forEach(sortId => {
  949. const $tbody = $(`#snapshot-table-${sortId} tbody`);
  950. $tbody.empty();
  951. const data = filteredRoomsData[sortId];
  952. if (data.length === 0) {
  953. $tbody.append('<tr><td colspan="6" class="text-center text-muted">暂无数据</td></tr>');
  954. return;
  955. }
  956. data.forEach(item => {
  957. const time = item.createTime.substring(11, 19);
  958. const row = `
  959. <tr>
  960. <td>${time}</td>
  961. <td>${item.stockRatio}</td>
  962. <td>${formatNumber(item.stock)}</td>
  963. <td>${formatNumber(item.levelBase)}</td>
  964. <td>${formatNumber(item.revenue)}</td>
  965. <td><span class="badge badge-${item.zValue <= 5 ? 'success' : (item.zValue <= 15 ? 'warning' : 'danger')}">${item.zValue}</span></td>
  966. </tr>
  967. `;
  968. $tbody.append(row);
  969. });
  970. });
  971. }
  972. // 初始化时间范围滑块
  973. function initTimeRangeSlider() {
  974. // 收集所有数据的时间戳
  975. let allTimestamps = [];
  976. [1, 2, 3].forEach(sortId => {
  977. allTimestamps = allTimestamps.concat(allRoomsData[sortId].map(d => d.timestamp));
  978. });
  979. if (allTimestamps.length === 0) return;
  980. const minTime = Math.min(...allTimestamps);
  981. const maxTime = Math.max(...allTimestamps);
  982. // 限制最小范围为1分钟,最大为一周
  983. const totalRange = maxTime - minTime;
  984. const limitedMaxTime = Math.min(maxTime, minTime + WEEK_IN_MS);
  985. if (timeRangeSlider) {
  986. timeRangeSlider.destroy();
  987. }
  988. const sliderElement = document.getElementById('time-range-slider');
  989. timeRangeSlider = noUiSlider.create(sliderElement, {
  990. start: [minTime, limitedMaxTime],
  991. connect: true,
  992. range: {
  993. 'min': minTime,
  994. 'max': limitedMaxTime
  995. },
  996. step: 1000
  997. });
  998. // 更新时间显示和时长
  999. function updateTimeLabels(values) {
  1000. const start = parseInt(values[0]);
  1001. const end = parseInt(values[1]);
  1002. const duration = end - start;
  1003. $('#range-start').text(new Date(start).toLocaleString());
  1004. $('#range-end').text(new Date(end).toLocaleString());
  1005. // 计算时长
  1006. const minutes = Math.floor(duration / MINUTE_IN_MS);
  1007. const hours = Math.floor(minutes / 60);
  1008. const days = Math.floor(hours / 24);
  1009. let durationText = '';
  1010. if (days > 0) {
  1011. durationText = `${days}天${hours % 24}小时`;
  1012. } else if (hours > 0) {
  1013. durationText = `${hours}小时${minutes % 60}分钟`;
  1014. } else {
  1015. durationText = `${minutes}分钟`;
  1016. }
  1017. $('#range-duration').text(durationText);
  1018. // 验证最小1分钟限制
  1019. if (duration < MINUTE_IN_MS) {
  1020. timeRangeSlider.set([start, start + MINUTE_IN_MS]);
  1021. return;
  1022. }
  1023. // 验证最大一周限制
  1024. if (duration > WEEK_IN_MS) {
  1025. timeRangeSlider.set([start, start + WEEK_IN_MS]);
  1026. return;
  1027. }
  1028. }
  1029. updateTimeLabels(timeRangeSlider.get());
  1030. // 滑块变化事件
  1031. timeRangeSlider.on('update', function(values) {
  1032. updateTimeLabels(values);
  1033. });
  1034. timeRangeSlider.on('change', function(values) {
  1035. const startTime = parseInt(values[0]);
  1036. const endTime = parseInt(values[1]);
  1037. // 筛选数据
  1038. [1, 2, 3].forEach(sortId => {
  1039. filteredRoomsData[sortId] = allRoomsData[sortId].filter(d =>
  1040. d.timestamp >= startTime && d.timestamp <= endTime
  1041. );
  1042. });
  1043. renderChart();
  1044. renderTables();
  1045. });
  1046. }
  1047. // 时间区间按钮点击
  1048. $('.time-range-btn').on('click', function() {
  1049. $('.time-range-btn').removeClass('active');
  1050. $(this).addClass('active');
  1051. loadAllRoomsData();
  1052. });
  1053. // 图表类型切换按钮点击
  1054. $('.chart-type-btn').on('click', function() {
  1055. $('.chart-type-btn').removeClass('active btn-primary').addClass('btn-outline-primary');
  1056. $(this).removeClass('btn-outline-primary').addClass('active btn-primary');
  1057. chartType = $(this).data('type');
  1058. renderChart();
  1059. });
  1060. // 房间复选框变化事件
  1061. $('.room-checkbox').on('change', function() {
  1062. selectedRooms = [];
  1063. $('.room-checkbox:checked').each(function() {
  1064. selectedRooms.push(parseInt($(this).val()));
  1065. });
  1066. // 至少选择一个房间
  1067. if (selectedRooms.length === 0) {
  1068. $(this).prop('checked', true);
  1069. selectedRooms.push(parseInt($(this).val()));
  1070. alert('至少需要选择一个房间');
  1071. return;
  1072. }
  1073. renderChart();
  1074. });
  1075. // 切换到快照历史标签页时初始化
  1076. $('a[href="#snapshot-tab"]').on('shown.bs.tab', function() {
  1077. if (!ratioChart) {
  1078. initCharts();
  1079. }
  1080. loadAllRoomsData();
  1081. });
  1082. // 窗口大小改变时重新渲染图表
  1083. $(window).on('resize', function() {
  1084. if (ratioChart) ratioChart.resize();
  1085. });
  1086. });
  1087. </script>
  1088. <style>
  1089. .section-title {
  1090. font-weight: 600;
  1091. color: #495057;
  1092. padding-bottom: 10px;
  1093. border-bottom: 2px solid #e9ecef;
  1094. }
  1095. .card-header h6 {
  1096. font-weight: 600;
  1097. }
  1098. .table th {
  1099. font-weight: 600;
  1100. }
  1101. .alert-info {
  1102. background-color: #e7f3ff;
  1103. border-color: #b3d9ff;
  1104. }
  1105. </style>
  1106. @endsection