|
|
@@ -0,0 +1,1200 @@
|
|
|
+@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
|