| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200 |
- @extends('base.base')
- @section('base')
- <meta name="csrf-token" content="{{ csrf_token() }}">
- <div class="container-fluid">
- <div class="row">
- <div class="col-12">
- <div class="card">
- <div class="card-header">
- <h3 class="card-title mb-0">库存模式配置管理</h3>
- </div>
- <div class="card-body">
- <!-- 标签页导航 -->
- <ul class="nav nav-tabs mb-4" role="tablist">
- <li class="nav-item">
- <a class="nav-link active" data-toggle="tab" href="#config-tab" role="tab">
- <i class="mdi mdi-cog"></i> 参数配置
- </a>
- </li>
- <li class="nav-item">
- <a class="nav-link" data-toggle="tab" href="#snapshot-tab" role="tab">
- <i class="mdi mdi-chart-line"></i> 快照历史
- </a>
- </li>
- </ul>
- <!-- 标签页内容 -->
- <div class="tab-content">
- <!-- 参数配置标签页 -->
- <div class="tab-pane fade show active" id="config-tab" role="tabpanel">
- <!-- 系统参数配置 -->
- <div class="config-section mb-5">
- <h5 class="section-title mb-3">
- <i class="mdi mdi-cog-outline"></i> 系统参数配置
- </h5>
- <!-- 房间等级分割配置 -->
- <div class="card mb-3">
- <div class="card-header bg-light">
- <h6 class="mb-0">StockMode2BetLevels - 房间等级分割配置</h6>
- <small class="text-muted">配置低中高三个房间的下注上限和最低充值要求</small>
- </div>
- <div class="card-body">
- <div class="table-responsive">
- <table class="table table-bordered">
- <thead class="thead-light">
- <tr>
- <th width="120">房间</th>
- <th>最大下注额</th>
- <th>最低充值金额</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td><span class="badge badge-success">低级房 (索引1)</span></td>
- <td>
- <div class="input-group input-group-sm">
- <input type="number" class="form-control" id="bet_max_1"
- value="{{ $betMaxLimits[1] ?? 2 }}" />
- <div class="input-group-append">
- <span class="input-group-text">下注 ≤ 此值</span>
- </div>
- </div>
- </td>
- <td>
- <div class="input-group input-group-sm">
- <input type="number" class="form-control" id="recharge_min_1"
- value="{{ $rechargeMinLimits[1] ?? 0 }}" />
- <div class="input-group-append">
- <span class="input-group-text">充值 ≥ 此值</span>
- </div>
- </div>
- </td>
- </tr>
- <tr>
- <td><span class="badge badge-info">中级房 (索引2)</span></td>
- <td>
- <div class="input-group input-group-sm">
- <input type="number" class="form-control" id="bet_max_2"
- value="{{ $betMaxLimits[2] ?? 10 }}" />
- <div class="input-group-append">
- <span class="input-group-text">下注 ≤ 此值</span>
- </div>
- </div>
- </td>
- <td>
- <div class="input-group input-group-sm">
- <input type="number" class="form-control" id="recharge_min_2"
- value="{{ $rechargeMinLimits[2] ?? 100 }}" />
- <div class="input-group-append">
- <span class="input-group-text">充值 ≥ 此值</span>
- </div>
- </div>
- </td>
- </tr>
- <tr>
- <td><span class="badge badge-warning">高级房 (索引3)</span></td>
- <td>
- <div class="input-group input-group-sm">
- <input type="number" class="form-control" id="bet_max_3"
- value="{{ $betMaxLimits[3] ?? 10000 }}" />
- <div class="input-group-append">
- <span class="input-group-text">下注 ≤ 此值</span>
- </div>
- </div>
- </td>
- <td>
- <div class="input-group input-group-sm">
- <input type="number" class="form-control" id="recharge_min_3"
- value="{{ $rechargeMinLimits[3] ?? 1000 }}" />
- <div class="input-group-append">
- <span class="input-group-text">充值 ≥ 此值</span>
- </div>
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
- </div>
- <!-- 其他系统参数 -->
- <div class="row">
- <div class="col-md-6">
- <div class="card">
- <div class="card-header bg-light">
- <h6 class="mb-0">StockMode2RevenueRatio - 税收比例</h6>
- </div>
- <div class="card-body">
- <div class="form-group">
- <label>税收千分比</label>
- <div class="input-group">
- <input type="number" class="form-control" id="revenue_ratio"
- value="{{ $systemConfig['StockMode2RevenueRatio']->StatusValue ?? 50 }}" />
- <div class="input-group-append">
- <span class="input-group-text">‰</span>
- </div>
- </div>
- <small class="form-text text-muted">
- 库存模式下的税收比例,计算公式:赢钱 × 值 / 1000<br>
- 例如:值为 50 时,税收 = 赢钱 × 50/1000 = 赢钱 × 5%
- </small>
- </div>
- </div>
- </div>
- </div>
- <div class="col-md-6">
- <div class="card">
- <div class="card-header bg-light">
- <h6 class="mb-0">StockMode2SwitchRecharge - 模式切换充值阈值</h6>
- </div>
- <div class="card-body">
- <div class="form-group">
- <label>切换充值金额</label>
- <input type="number" class="form-control" id="switch_recharge"
- value="{{ $systemConfig['StockMode2SwitchRecharge']->StatusValue ?? 100 }}" />
- <small class="form-text text-muted">
- 玩家充值达到此金额后,游戏模式切换为库存模式
- </small>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="text-right mt-3">
- <button class="btn btn-primary" id="save-system-config">
- <i class="mdi mdi-content-save"></i> 保存系统配置
- </button>
- <span class="ml-2 system-status"></span>
- </div>
- </div>
- <hr class="my-5">
- <!-- 房间库存配置 -->
- <div class="config-section">
- <h5 class="section-title mb-3">
- <i class="mdi mdi-database"></i> 房间库存配置 (RoomStockStatic2)
- </h5>
- <p class="text-muted mb-3">
- LevelBase 基数配置说明:根据 LevelBase 的倍数区间计算对应的个控(Z)随机范围
- </p>
- <div class="card">
- <div class="card-body">
- <div class="alert alert-warning">
- <i class="mdi mdi-alert"></i> <strong>注意:</strong>所有数值显示时已除以100,保存时会自动乘以100还原到数据库
- </div>
- <div class="table-responsive">
- <table class="table table-bordered table-hover">
- <thead class="thead-light">
- <tr>
- <th width="120">房间等级</th>
- <th>当前库存 (Stock)</th>
- <th>基数 (LevelBase)</th>
- <th>累计税收 (Revenue)</th>
- <th>累计暗税 (RevenueD)</th>
- <th width="120">操作</th>
- </tr>
- </thead>
- <tbody>
- @foreach([1 => '低级房', 2 => '中级房', 3 => '高级房'] as $sortId => $roomName)
- @php
- $roomStock = $roomStocks->firstWhere('SortID', $sortId);
- $stock = ($roomStock->Stock ?? 0) / 100;
- $levelBase = ($roomStock->LevelBase ?? 10000) / 100;
- $revenue = ($roomStock->Revenue ?? 0) / 100;
- $revenueD = ($roomStock->RevenueD ?? 0) / 100;
- @endphp
- <tr>
- <td>
- <span class="badge badge-{{ $sortId == 1 ? 'success' : ($sortId == 2 ? 'info' : 'warning') }}">
- {{ $roomName }} (ID:{{ $sortId }})
- </span>
- </td>
- <td>
- <div class="input-group input-group-sm">
- <div class="input-group-prepend">
- <button class="btn btn-outline-danger stock-decrease" type="button" data-sort-id="{{ $sortId }}">
- <i class="mdi mdi-minus"></i>
- </button>
- </div>
- <input type="number" step="0.01" class="form-control text-center stock-adjust-input"
- data-sort-id="{{ $sortId }}" placeholder="增减数值" />
- <div class="input-group-append">
- <button class="btn btn-outline-success stock-increase" type="button" data-sort-id="{{ $sortId }}">
- <i class="mdi mdi-plus"></i>
- </button>
- </div>
- </div>
- <small class="text-muted d-block mt-1">
- 当前: <strong class="current-stock-display" data-sort-id="{{ $sortId }}">{{ number_format($stock, 2) }}</strong>
- </small>
- </td>
- <td>
- <input type="number" step="0.01" class="form-control form-control-sm level-base-input"
- data-sort-id="{{ $sortId }}"
- value="{{ number_format($levelBase, 2, '.', '') }}" />
- </td>
- <td class="text-right text-muted">
- {{ number_format($revenue, 2) }}
- </td>
- <td class="text-right text-muted">
- {{ number_format($revenueD, 2) }}
- </td>
- <td class="text-center">
- <button class="btn btn-sm btn-primary save-room-stock" data-sort-id="{{ $sortId }}">
- <i class="mdi mdi-content-save"></i> 保存
- </button>
- </td>
- </tr>
- @endforeach
- </tbody>
- </table>
- </div>
- <!-- 个控计算说明 -->
- <div class="alert alert-info mt-4">
- <h6><i class="mdi mdi-information"></i> 个控计算规则说明</h6>
- <p class="mb-2">根据当前库存(Stock)与基数(LevelBase)的比值,计算个控(Z)的随机范围:</p>
- <div class="table-responsive">
- <table class="table table-sm table-bordered bg-white">
- <thead>
- <tr>
- <th>库存区间</th>
- <th>个控范围(Z)</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>0 ~ 2×LevelBase</td>
- <td>0(固定)</td>
- </tr>
- <tr>
- <td>2×LevelBase ~ 4×LevelBase</td>
- <td>1 ~ 5(随机)</td>
- </tr>
- <tr>
- <td>4×LevelBase ~ 6×LevelBase</td>
- <td>6 ~ 10(随机)</td>
- </tr>
- <tr>
- <td>6×LevelBase ~ 8×LevelBase</td>
- <td>11 ~ 15(随机)</td>
- </tr>
- <tr>
- <td>8×LevelBase ~ 10×LevelBase</td>
- <td>16 ~ 20(随机)</td>
- </tr>
- <tr>
- <td>10×LevelBase 以上</td>
- <td>20(固定)</td>
- </tr>
- </tbody>
- </table>
- </div>
- <p class="mb-0 mt-2">
- <strong>示例:</strong>当 LevelBase = 100 时
- <ul class="mb-0">
- <li>Stock 在 0-200 时,个控为 0</li>
- <li>Stock 在 200-400 时,个控为 1-5 随机</li>
- <li>Stock 在 400-600 时,个控为 6-10 随机</li>
- <li>以此类推...</li>
- </ul>
- </p>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 参数配置标签页结束 -->
- <!-- 快照历史标签页 -->
- <div class="tab-pane fade" id="snapshot-tab" role="tabpanel">
- <!-- 时间范围选择器 -->
- <div class="card mb-3">
- <div class="card-body">
- <div class="row">
- <div class="col-md-12 mb-3">
- <label>快捷时间区间:</label>
- <div class="btn-group d-block" role="group">
- <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="5">5分钟</button>
- <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="15">15分钟</button>
- <button type="button" class="btn btn-sm btn-outline-primary time-range-btn active" data-minutes="60">1小时</button>
- <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="240">4小时</button>
- <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="720">12小时</button>
- <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="1440">24小时</button>
- <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="10080">一周</button>
- </div>
- </div>
- <div class="col-md-12">
- <label>自定义时间范围(拖动滑块调整):</label>
- <div id="time-range-slider" class="mb-2"></div>
- <div class="d-flex justify-content-between">
- <span id="range-start" class="text-muted small"></span>
- <span id="range-duration" class="badge badge-info"></span>
- <span id="range-end" class="text-muted small"></span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 库存值图表 -->
- <div class="card mb-3">
- <div class="card-header">
- <div class="d-flex justify-content-between align-items-center mb-2">
- <h6 class="mb-0">库存变化</h6>
- <div class="btn-group btn-group-sm" role="group">
- <button type="button" class="btn btn-primary chart-type-btn active" data-type="stock">库存值</button>
- <button type="button" class="btn btn-outline-primary chart-type-btn" data-type="ratio">比值</button>
- <button type="button" class="btn btn-outline-primary chart-type-btn" data-type="both">都显示</button>
- </div>
- </div>
- <div class="d-flex align-items-center">
- <span class="mr-2 text-muted small">显示房间:</span>
- <div class="form-check form-check-inline mb-0">
- <input class="form-check-input room-checkbox" type="checkbox" id="room-1" value="1" checked>
- <label class="form-check-label" for="room-1">
- <span class="badge badge-success">低级房</span>
- </label>
- </div>
- <div class="form-check form-check-inline mb-0">
- <input class="form-check-input room-checkbox" type="checkbox" id="room-2" value="2" checked>
- <label class="form-check-label" for="room-2">
- <span class="badge badge-info">中级房</span>
- </label>
- </div>
- <div class="form-check form-check-inline mb-0">
- <input class="form-check-input room-checkbox" type="checkbox" id="room-3" value="3" checked>
- <label class="form-check-label" for="room-3">
- <span class="badge badge-warning">高级房</span>
- </label>
- </div>
- </div>
- </div>
- <div class="card-body">
- <div id="stock-ratio-chart" style="height: 500px;"></div>
- </div>
- </div>
- <!-- 房间数据表格 -->
- <div class="row">
- <div class="col-md-4">
- <div class="card">
- <div class="card-header bg-success text-white">
- <h6 class="mb-0">低级房快照数据</h6>
- </div>
- <div class="card-body p-0">
- <div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
- <table class="table table-sm table-hover mb-0" id="snapshot-table-1">
- <thead class="thead-light sticky-top">
- <tr>
- <th>时间</th>
- <th>比值</th>
- <th>库存</th>
- <th>基数</th>
- <th>税收</th>
- <th>Z值</th>
- </tr>
- </thead>
- <tbody>
- <tr><td colspan="6" class="text-center text-muted">加载中...</td></tr>
- </tbody>
- </table>
- </div>
- </div>
- </div>
- </div>
- <div class="col-md-4">
- <div class="card">
- <div class="card-header bg-info text-white">
- <h6 class="mb-0">中级房快照数据</h6>
- </div>
- <div class="card-body p-0">
- <div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
- <table class="table table-sm table-hover mb-0" id="snapshot-table-2">
- <thead class="thead-light sticky-top">
- <tr>
- <th>时间</th>
- <th>比值</th>
- <th>库存</th>
- <th>基数</th>
- <th>税收</th>
- <th>Z值</th>
- </tr>
- </thead>
- <tbody>
- <tr><td colspan="6" class="text-center text-muted">加载中...</td></tr>
- </tbody>
- </table>
- </div>
- </div>
- </div>
- </div>
- <div class="col-md-4">
- <div class="card">
- <div class="card-header bg-warning text-white">
- <h6 class="mb-0">高级房快照数据</h6>
- </div>
- <div class="card-body p-0">
- <div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
- <table class="table table-sm table-hover mb-0" id="snapshot-table-3">
- <thead class="thead-light sticky-top">
- <tr>
- <th>时间</th>
- <th>比值</th>
- <th>库存</th>
- <th>基数</th>
- <th>税收</th>
- <th>Z值</th>
- </tr>
- </thead>
- <tbody>
- <tr><td colspan="6" class="text-center text-muted">加载中...</td></tr>
- </tbody>
- </table>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 快照历史标签页结束 -->
- </div>
- <!-- tab-content结束 -->
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 引入 ECharts -->
- <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
- <!-- 引入 noUiSlider -->
- <link href="https://cdn.jsdelivr.net/npm/nouislider@15.7.1/dist/nouislider.min.css" rel="stylesheet">
- <script src="https://cdn.jsdelivr.net/npm/nouislider@15.7.1/dist/nouislider.min.js"></script>
- <script>
- $(function() {
- // 保存系统配置
- $('#save-system-config').click(function() {
- const $btn = $(this);
- const $status = $('.system-status');
- $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 保存中...');
- $status.html('').removeClass('text-success text-danger');
- const data = {
- bet_max_limits: [
- 0, // 索引0轮空
- parseInt($('#bet_max_1').val()) || 0,
- parseInt($('#bet_max_2').val()) || 0,
- parseInt($('#bet_max_3').val()) || 0
- ],
- recharge_min_limits: [
- 0, // 索引0轮空
- parseInt($('#recharge_min_1').val()) || 0,
- parseInt($('#recharge_min_2').val()) || 0,
- parseInt($('#recharge_min_3').val()) || 0
- ],
- revenue_ratio: parseInt($('#revenue_ratio').val()) || 50,
- switch_recharge: parseInt($('#switch_recharge').val()) || 100,
- _token: "{{ csrf_token() }}"
- };
- $.post("{{ url('/admin/stock-mode/update-system-config') }}", data)
- .done(function(res) {
- $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存系统配置');
- if (res.status === 'success') {
- $status.text('更新成功').addClass('text-success');
- } else {
- $status.text(res.message || '更新失败').addClass('text-danger');
- }
- setTimeout(function() {
- $status.fadeOut(function() {
- $(this).text('').show().removeClass('text-success text-danger');
- });
- }, 3000);
- })
- .fail(function() {
- $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存系统配置');
- $status.text('系统错误').addClass('text-danger');
- });
- });
- // 库存增加
- $('.stock-increase').click(function() {
- const sortId = $(this).data('sort-id');
- const $input = $('.stock-adjust-input[data-sort-id="' + sortId + '"]');
- const adjustValue = parseFloat($input.val()) || 0;
- if (adjustValue === 0) {
- alert('请输入要增加的数值');
- return;
- }
- updateStock(sortId, adjustValue);
- });
- // 库存减少
- $('.stock-decrease').click(function() {
- const sortId = $(this).data('sort-id');
- const $input = $('.stock-adjust-input[data-sort-id="' + sortId + '"]');
- const adjustValue = parseFloat($input.val()) || 0;
- if (adjustValue === 0) {
- alert('请输入要减少的数值');
- return;
- }
- updateStock(sortId, -adjustValue);
- });
- // 格式化数字为千分位
- function formatNumber(num) {
- return num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
- }
- // 更新库存函数
- function updateStock(sortId, adjustValue) {
- // 将显示的数值(已除以100)乘以100还原成数据库值
- const adjustValueDb = Math.round(adjustValue * 100);
- $.post("{{ url('/admin/stock-mode/update-stock') }}", {
- sort_id: sortId,
- adjust_value: adjustValueDb,
- _token: "{{ csrf_token() }}"
- })
- .done(function(res) {
- if (res.status === 'success') {
- // 更新显示的当前库存(已除以100)
- const newStock = res.new_stock / 100;
- $('.current-stock-display[data-sort-id="' + sortId + '"]').text(formatNumber(newStock));
- $('.stock-adjust-input[data-sort-id="' + sortId + '"]').val('');
- // 显示成功提示
- const $display = $('.current-stock-display[data-sort-id="' + sortId + '"]');
- $display.addClass('text-success');
- setTimeout(function() {
- $display.removeClass('text-success');
- }, 1000);
- } else {
- alert(res.message || '更新失败');
- }
- })
- .fail(function() {
- alert('系统错误');
- });
- }
- // 保存房间库存配置(LevelBase)
- $('.save-room-stock').click(function() {
- const $btn = $(this);
- const sortId = $btn.data('sort-id');
- const $input = $('.level-base-input[data-sort-id="' + sortId + '"]');
- const levelBaseDisplay = parseFloat($input.val()) || 100;
- // 将显示的数值(已除以100)乘以100还原成数据库值
- const levelBase = Math.round(levelBaseDisplay * 100);
- $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i>');
- $.post("{{ url('/admin/stock-mode/update-room-stock') }}", {
- sort_id: sortId,
- level_base: levelBase,
- _token: "{{ csrf_token() }}"
- })
- .done(function(res) {
- $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存');
- if (res.status === 'success') {
- $btn.removeClass('btn-primary btn-danger').addClass('btn-success');
- setTimeout(function() {
- $btn.removeClass('btn-success').addClass('btn-primary');
- }, 1500);
- } else {
- $btn.removeClass('btn-primary btn-success').addClass('btn-danger');
- alert(res.message || '更新失败');
- setTimeout(function() {
- $btn.removeClass('btn-danger').addClass('btn-primary');
- }, 2000);
- }
- })
- .fail(function() {
- $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存');
- $btn.removeClass('btn-primary btn-success').addClass('btn-danger');
- alert('系统错误');
- setTimeout(function() {
- $btn.removeClass('btn-danger').addClass('btn-primary');
- }, 2000);
- });
- });
- // ============ 快照历史相关功能 ============
- let ratioChart = null;
- let timeRangeSlider = null;
- let allRoomsData = {1: [], 2: [], 3: []}; // 三个房间的所有数据
- let filteredRoomsData = {1: [], 2: [], 3: []}; // 筛选后的数据
- let chartType = 'stock'; // 默认显示库存值,可选:stock, ratio, both
- let selectedRooms = [1, 2, 3]; // 默认显示所有房间
- const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
- const MINUTE_IN_MS = 60 * 1000;
- // 初始化图表
- function initCharts() {
- ratioChart = echarts.init(document.getElementById('stock-ratio-chart'));
- }
- // 补全数据:按固定间隔填充缺失的数据点,确保图表连续
- function fillDataGaps(data, minutes) {
- if (data.length === 0) return [];
- const ACTUAL_INTERVAL_MS = 3000; // 实际数据的间隔是3秒
- const FILL_INTERVAL_MS = 10000; // 填充数据使用10秒间隔,降低数据量
- const now = Date.now();
- const startTime = now - (minutes * 60 * 1000);
- const endTime = now;
- // 分离前置数据和正常数据
- let previousData = null;
- let normalData = [];
- data.forEach(item => {
- if (item.isPrevious) {
- previousData = item;
- } else {
- normalData.push(item);
- }
- });
- // 如果没有正常数据但有前置数据,使用前置数据填充整个时间范围
- if (normalData.length === 0 && previousData) {
- const filledData = [];
- for (let t = startTime; t <= endTime; t += FILL_INTERVAL_MS) {
- filledData.push({
- ...previousData,
- timestamp: t,
- createTime: new Date(t).toISOString().replace('T', ' ').substring(0, 19),
- isFilled: true
- });
- }
- console.log(`房间无新数据,使用前置数据填充: 填充=${filledData.length}`);
- return filledData;
- }
- // 如果完全没有数据,返回空
- if (normalData.length === 0) {
- console.log('房间无任何数据');
- return [];
- }
- const filledData = [];
- let fillCount = 0;
- const firstDataTime = normalData[0].timestamp;
- const lastDataTime = normalData[normalData.length - 1].timestamp;
- // 1. 填充起始时间到第一条数据之间的空白
- if (firstDataTime > startTime) {
- const fillSource = previousData || normalData[0];
- for (let t = startTime; t < firstDataTime; t += FILL_INTERVAL_MS) {
- filledData.push({
- ...fillSource,
- timestamp: t,
- createTime: new Date(t).toISOString().replace('T', ' ').substring(0, 19),
- isFilled: true
- });
- fillCount++;
- }
- }
- // 2. 处理实际数据和中间空白
- for (let i = 0; i < normalData.length; i++) {
- const currentItem = normalData[i];
- // 添加当前实际数据
- filledData.push({
- ...currentItem,
- isFilled: false
- });
- // 检查与下一条数据之间的间隔
- if (i < normalData.length - 1) {
- const nextItem = normalData[i + 1];
- const gap = nextItem.timestamp - currentItem.timestamp;
- // 如果间隔大于实际间隔(说明有数据缺失),填充中间
- if (gap > ACTUAL_INTERVAL_MS * 2) {
- for (let t = currentItem.timestamp + FILL_INTERVAL_MS; t < nextItem.timestamp; t += FILL_INTERVAL_MS) {
- filledData.push({
- ...currentItem,
- timestamp: t,
- createTime: new Date(t).toISOString().replace('T', ' ').substring(0, 19),
- isFilled: true
- });
- fillCount++;
- }
- }
- }
- }
- // 3. 填充最后一条数据到结束时间的空白
- if (lastDataTime < endTime) {
- const lastItem = normalData[normalData.length - 1];
- for (let t = lastDataTime + FILL_INTERVAL_MS; t <= endTime; t += FILL_INTERVAL_MS) {
- filledData.push({
- ...lastItem,
- timestamp: t,
- createTime: new Date(t).toISOString().replace('T', ' ').substring(0, 19),
- isFilled: true
- });
- fillCount++;
- }
- }
- console.log(`数据补全完成: 原始=${normalData.length}, 填充=${fillCount}, 总计=${filledData.length}`);
- return filledData;
- }
- // 加载所有房间的快照数据
- function loadAllRoomsData() {
- const minutes = $('.time-range-btn.active').data('minutes');
- const requests = [];
- // 并行请求三个房间的数据
- for (let sortId = 1; sortId <= 3; sortId++) {
- requests.push(
- $.get("{{ url('/admin/stock-mode/snapshot-history') }}", {
- sort_id: sortId,
- minutes: minutes
- })
- );
- }
- Promise.all(requests).then(function(responses) {
- console.log('收到响应数量:', responses.length);
- responses.forEach(function(res, index) {
- const sortId = index + 1;
- console.log(`房间${sortId} 响应:`, res.status, '原始数据量:', res.data ? res.data.length : 0);
- if (res.status === 'success') {
- // 补全数据
- console.log(`房间${sortId} 开始补全数据`);
- const filledData = fillDataGaps(res.data, minutes);
- console.log(`房间${sortId} 补全后数据量:`, filledData.length);
- allRoomsData[sortId] = filledData;
- filteredRoomsData[sortId] = filledData;
- } else {
- console.error(`房间${sortId} 加载失败`);
- allRoomsData[sortId] = [];
- filteredRoomsData[sortId] = [];
- }
- });
- console.log('所有房间数据加载完成:', {
- room1: allRoomsData[1].length,
- room2: allRoomsData[2].length,
- room3: allRoomsData[3].length
- });
- renderChart();
- renderTables();
- initTimeRangeSlider();
- }).catch(function(err) {
- console.error('加载数据失败:', err);
- alert('加载数据失败');
- });
- }
- // 渲染库存比值图表(三条线)
- function renderChart() {
- if (!ratioChart) return;
- // 合并选中房间的所有时间点并去重排序
- let allTimestamps = [];
- selectedRooms.forEach(sortId => {
- allTimestamps = allTimestamps.concat(filteredRoomsData[sortId].map(d => d.timestamp));
- });
- allTimestamps = [...new Set(allTimestamps)].sort((a, b) => a - b);
- const times = allTimestamps.map(ts => new Date(ts).toLocaleString('zh-CN', {
- month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
- }));
- // 为每个房间创建数据映射(保存完整数据用于 tooltip)
- const roomStockData = {};
- const roomRatioData = {};
- const roomDetailData = {};
- [1, 2, 3].forEach(sortId => {
- const stockMap = {};
- const ratioMap = {};
- const detailMap = {};
- filteredRoomsData[sortId].forEach(item => {
- stockMap[item.timestamp] = item.stock;
- ratioMap[item.timestamp] = item.stockRatio;
- detailMap[item.timestamp] = item;
- });
- roomStockData[sortId] = allTimestamps.map(ts => stockMap[ts] || null);
- roomRatioData[sortId] = allTimestamps.map(ts => ratioMap[ts] || null);
- roomDetailData[sortId] = detailMap;
- });
- // 房间配置
- const roomConfigs = [
- { id: 1, name: '低级房', color: '#67C23A', lightColor: '#95D475' },
- { id: 2, name: '中级房', color: '#409EFF', lightColor: '#79BBFF' },
- { id: 3, name: '高级房', color: '#E6A23C', lightColor: '#F3D19E' }
- ];
- // 根据图表类型确定标题和Y轴名称
- let chartTitle = '';
- let yAxisName = '';
- let legendData = [];
- let series = [];
- if (chartType === 'stock') {
- chartTitle = '库存值变化';
- yAxisName = '库存值';
- roomConfigs.forEach(room => {
- if (selectedRooms.includes(room.id)) {
- legendData.push(room.name);
- series.push({
- name: room.name,
- type: 'line',
- data: roomStockData[room.id],
- smooth: true,
- lineStyle: { width: 2 },
- itemStyle: { color: room.color },
- connectNulls: true
- });
- }
- });
- } else if (chartType === 'ratio') {
- chartTitle = '库存比值变化';
- yAxisName = '比值';
- roomConfigs.forEach(room => {
- if (selectedRooms.includes(room.id)) {
- legendData.push(room.name);
- series.push({
- name: room.name,
- type: 'line',
- data: roomRatioData[room.id],
- smooth: true,
- lineStyle: { width: 2 },
- itemStyle: { color: room.color },
- connectNulls: true
- });
- }
- });
- } else { // both - 使用双Y轴
- chartTitle = '库存值与比值变化(双Y轴)';
- yAxisName = ['库存值', '比值']; // 双Y轴
- roomConfigs.forEach(room => {
- if (selectedRooms.includes(room.id)) {
- legendData.push(`${room.name}-库存`);
- legendData.push(`${room.name}-比值`);
- series.push({
- name: `${room.name}-库存`,
- type: 'line',
- data: roomStockData[room.id],
- yAxisIndex: 0, // 使用左侧Y轴
- smooth: true,
- lineStyle: { width: 2 },
- itemStyle: { color: room.color },
- connectNulls: true
- });
- series.push({
- name: `${room.name}-比值`,
- type: 'line',
- data: roomRatioData[room.id],
- yAxisIndex: 1, // 使用右侧Y轴
- smooth: true,
- lineStyle: { width: 2, type: 'dashed' },
- itemStyle: { color: room.lightColor },
- connectNulls: true
- });
- }
- });
- }
- const option = {
- title: {
- text: chartTitle,
- left: 'center'
- },
- tooltip: {
- trigger: 'axis',
- axisPointer: { type: 'cross' },
- formatter: function(params) {
- let tooltip = `<div style="font-weight:bold;">${params[0].axisValue}</div>`;
- // 只显示选中房间的信息
- const roomConfigs = [
- { id: 1, name: '低级房', color: '#67C23A' },
- { id: 2, name: '中级房', color: '#409EFF' },
- { id: 3, name: '高级房', color: '#E6A23C' }
- ];
- roomConfigs.forEach(room => {
- if (selectedRooms.includes(room.id)) {
- const dataIndex = params[0].dataIndex;
- const timestamp = allTimestamps[dataIndex];
- const detail = roomDetailData[room.id][timestamp];
- if (detail) {
- tooltip += `<div style="margin-top:5px;">
- <span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${room.color};margin-right:5px;"></span>
- <strong>${room.name}</strong><br/>
- <span style="margin-left:15px;">比值: ${detail.stockRatio}</span><br/>
- <span style="margin-left:15px;">库存: ${formatNumber(detail.stock)}</span><br/>
- <span style="margin-left:15px;">基数: ${formatNumber(detail.levelBase)}</span><br/>
- <span style="margin-left:15px;">税收: ${formatNumber(detail.revenue)}</span>
- </div>`;
- }
- }
- });
- return tooltip;
- }
- },
- legend: {
- data: legendData,
- top: 35
- },
- grid: {
- left: '3%',
- right: '4%',
- bottom: '3%',
- containLabel: true,
- top: 70
- },
- xAxis: {
- type: 'category',
- data: times,
- axisLabel: {
- rotate: 45,
- fontSize: 10
- }
- },
- yAxis: Array.isArray(yAxisName) ? [
- {
- type: 'value',
- name: yAxisName[0],
- position: 'left',
- axisLabel: {
- formatter: '{value}'
- }
- },
- {
- type: 'value',
- name: yAxisName[1],
- position: 'right',
- axisLabel: {
- formatter: '{value}'
- }
- }
- ] : {
- type: 'value',
- name: yAxisName
- },
- series: series
- };
- // 使用 notMerge: true 完全替换配置,避免旧配置残留
- ratioChart.setOption(option, true);
- }
- // 渲染三个房间的数据表格
- function renderTables() {
- [1, 2, 3].forEach(sortId => {
- const $tbody = $(`#snapshot-table-${sortId} tbody`);
- $tbody.empty();
- const data = filteredRoomsData[sortId];
- if (data.length === 0) {
- $tbody.append('<tr><td colspan="6" class="text-center text-muted">暂无数据</td></tr>');
- return;
- }
- data.forEach(item => {
- const time = item.createTime.substring(11, 19);
- const row = `
- <tr>
- <td>${time}</td>
- <td>${item.stockRatio}</td>
- <td>${formatNumber(item.stock)}</td>
- <td>${formatNumber(item.levelBase)}</td>
- <td>${formatNumber(item.revenue)}</td>
- <td><span class="badge badge-${item.zValue <= 5 ? 'success' : (item.zValue <= 15 ? 'warning' : 'danger')}">${item.zValue}</span></td>
- </tr>
- `;
- $tbody.append(row);
- });
- });
- }
- // 初始化时间范围滑块
- function initTimeRangeSlider() {
- // 收集所有数据的时间戳
- let allTimestamps = [];
- [1, 2, 3].forEach(sortId => {
- allTimestamps = allTimestamps.concat(allRoomsData[sortId].map(d => d.timestamp));
- });
- if (allTimestamps.length === 0) return;
- const minTime = Math.min(...allTimestamps);
- const maxTime = Math.max(...allTimestamps);
- // 限制最小范围为1分钟,最大为一周
- const totalRange = maxTime - minTime;
- const limitedMaxTime = Math.min(maxTime, minTime + WEEK_IN_MS);
- if (timeRangeSlider) {
- timeRangeSlider.destroy();
- }
- const sliderElement = document.getElementById('time-range-slider');
- timeRangeSlider = noUiSlider.create(sliderElement, {
- start: [minTime, limitedMaxTime],
- connect: true,
- range: {
- 'min': minTime,
- 'max': limitedMaxTime
- },
- step: 1000
- });
- // 更新时间显示和时长
- function updateTimeLabels(values) {
- const start = parseInt(values[0]);
- const end = parseInt(values[1]);
- const duration = end - start;
- $('#range-start').text(new Date(start).toLocaleString());
- $('#range-end').text(new Date(end).toLocaleString());
- // 计算时长
- const minutes = Math.floor(duration / MINUTE_IN_MS);
- const hours = Math.floor(minutes / 60);
- const days = Math.floor(hours / 24);
- let durationText = '';
- if (days > 0) {
- durationText = `${days}天${hours % 24}小时`;
- } else if (hours > 0) {
- durationText = `${hours}小时${minutes % 60}分钟`;
- } else {
- durationText = `${minutes}分钟`;
- }
- $('#range-duration').text(durationText);
- // 验证最小1分钟限制
- if (duration < MINUTE_IN_MS) {
- timeRangeSlider.set([start, start + MINUTE_IN_MS]);
- return;
- }
- // 验证最大一周限制
- if (duration > WEEK_IN_MS) {
- timeRangeSlider.set([start, start + WEEK_IN_MS]);
- return;
- }
- }
- updateTimeLabels(timeRangeSlider.get());
- // 滑块变化事件
- timeRangeSlider.on('update', function(values) {
- updateTimeLabels(values);
- });
- timeRangeSlider.on('change', function(values) {
- const startTime = parseInt(values[0]);
- const endTime = parseInt(values[1]);
- // 筛选数据
- [1, 2, 3].forEach(sortId => {
- filteredRoomsData[sortId] = allRoomsData[sortId].filter(d =>
- d.timestamp >= startTime && d.timestamp <= endTime
- );
- });
- renderChart();
- renderTables();
- });
- }
- // 时间区间按钮点击
- $('.time-range-btn').on('click', function() {
- $('.time-range-btn').removeClass('active');
- $(this).addClass('active');
- loadAllRoomsData();
- });
- // 图表类型切换按钮点击
- $('.chart-type-btn').on('click', function() {
- $('.chart-type-btn').removeClass('active btn-primary').addClass('btn-outline-primary');
- $(this).removeClass('btn-outline-primary').addClass('active btn-primary');
- chartType = $(this).data('type');
- renderChart();
- });
- // 房间复选框变化事件
- $('.room-checkbox').on('change', function() {
- selectedRooms = [];
- $('.room-checkbox:checked').each(function() {
- selectedRooms.push(parseInt($(this).val()));
- });
- // 至少选择一个房间
- if (selectedRooms.length === 0) {
- $(this).prop('checked', true);
- selectedRooms.push(parseInt($(this).val()));
- alert('至少需要选择一个房间');
- return;
- }
- renderChart();
- });
- // 切换到快照历史标签页时初始化
- $('a[href="#snapshot-tab"]').on('shown.bs.tab', function() {
- if (!ratioChart) {
- initCharts();
- }
- loadAllRoomsData();
- });
- // 窗口大小改变时重新渲染图表
- $(window).on('resize', function() {
- if (ratioChart) ratioChart.resize();
- });
- });
- </script>
- <style>
- .section-title {
- font-weight: 600;
- color: #495057;
- padding-bottom: 10px;
- border-bottom: 2px solid #e9ecef;
- }
- .card-header h6 {
- font-weight: 600;
- }
- .table th {
- font-weight: 600;
- }
- .alert-info {
- background-color: #e7f3ff;
- border-color: #b3d9ff;
- }
- </style>
- @endsection
|