| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- @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 d-flex align-items-center justify-content-between">
- <h3 class="card-title mb-0">1234 权重配置 & 数据统计</h3>
- <small class="text-muted">配置 1/2/3/4 权重;带 <code>show</code> 只记曝光;每日明细 Redis 保留约 3 天</small>
- </div>
- <div class="card-body">
- <ul class="nav nav-tabs mb-3" role="tablist">
- <li class="nav-item">
- <a class="nav-link active" data-toggle="tab" href="#tab-config" role="tab">
- <i class="mdi mdi-cog"></i> 权重配置
- </a>
- </li>
- <li class="nav-item">
- <a class="nav-link" data-toggle="tab" href="#tab-stats" role="tab">
- <i class="mdi mdi-chart-bar"></i> 数据统计
- </a>
- </li>
- </ul>
- <div class="tab-content">
- <div class="tab-pane fade show active" id="tab-config" role="tabpanel">
- @php
- $totalWeight = array_sum($config);
- @endphp
- <div class="alert alert-info">
- 当前总权重: <strong id="total-weight">{{ $totalWeight }}</strong>
- ,前端按权重比例随机返回 1/2/3/4
- </div>
- <div class="table-responsive">
- <table class="table table-striped table-hover align-middle mb-0" style="max-width: 600px;">
- <thead class="thead-light">
- <tr>
- <th style="width:80px;">ID</th>
- <th style="width:200px;">权重 (整数)</th>
- <th>占比</th>
- </tr>
- </thead>
- <tbody class="weight-form">
- @foreach([1,2,3,4] as $id)
- @php
- $w = intval($config[$id] ?? 0);
- $percent = $totalWeight > 0 ? round($w / $totalWeight * 100, 2) : 0;
- @endphp
- <tr>
- <td><span class="badge badge-primary">{{ $id }}</span></td>
- <td>
- <input type="number" min="0" class="form-control form-control-sm weight-input"
- data-id="{{ $id }}" name="weight[{{ $id }}]" value="{{ $w }}" />
- </td>
- <td>
- <span class="weight-percent" data-id="{{ $id }}">{{ $percent }}%</span>
- </td>
- </tr>
- @endforeach
- </tbody>
- </table>
- </div>
- <div class="d-flex align-items-center mt-4">
- <button class="btn btn-gradient-primary btn-sm" id="save-weights">
- <i class="mdi mdi-content-save"></i> 保存权重
- </button>
- <span class="save-status ml-3"></span>
- </div>
- </div>
- <div class="tab-pane fade" id="tab-stats" role="tabpanel">
- @php
- $chartIds = [1, 2, 3, 4];
- $chartTodayShows = array_map(fn ($id) => (int) ($todayShows[$id] ?? 0), $chartIds);
- $chartTodayClicks = array_map(fn ($id) => (int) ($todayClicks[$id] ?? 0), $chartIds);
- $chartTotalShows = array_map(fn ($id) => (int) ($totalShows[$id] ?? 0), $chartIds);
- $chartTotalClicks = array_map(fn ($id) => (int) ($totalClicks[$id] ?? 0), $chartIds);
- @endphp
- <div class="card border mb-4 shadow-sm">
- <div class="card-body">
- <div class="d-flex flex-wrap align-items-center justify-content-between mb-2">
- <h5 class="card-title mb-0">曝光 & 点击(按 ID)</h5>
- <div class="btn-group btn-group-sm" role="group" aria-label="统计维度">
- <button type="button" class="btn btn-outline-primary active weight-chart-mode" data-mode="today">当天</button>
- <button type="button" class="btn btn-outline-primary weight-chart-mode" data-mode="total">总计</button>
- </div>
- </div>
- <p class="text-muted small mb-3 mb-md-2">
- <span id="weight-chart-subtitle">当天数据 · {{ date('Y-m-d') }}</span>
- </p>
- <div id="weight-stats-chart" style="width:100%;height:340px;"></div>
- <div class="d-flex justify-content-center align-items-center mt-2 small text-muted">
- <span class="mr-3"><span class="weight-chart-legend-swatch" style="background:#c8bdb0;"></span> 曝光(show)</span>
- <span><span class="weight-chart-legend-swatch" style="background:#2563eb;"></span> 点击</span>
- </div>
- </div>
- </div>
- <h5 class="mb-3">总计(Redis 累计)</h5>
- @php
- $totalClicksAll = array_sum($totalClicks);
- $totalShowsAll = array_sum($totalShows);
- @endphp
- <div class="table-responsive mb-4">
- <table class="table table-bordered align-middle weight-stats-merge-table" style="max-width: 920px;">
- <thead class="thead-light">
- <tr>
- <th>ID</th>
- <th class="text-right">曝光</th>
- <th class="text-right">曝光%</th>
- <th class="text-right">点击</th>
- <th class="text-right">点击率(%)</th>
- <th class="text-center" style="min-width:140px;">操作</th>
- </tr>
- </thead>
- <tbody>
- @foreach([1,2,3,4] as $id)
- @php
- $c = intval($totalClicks[$id] ?? 0);
- $s = intval($totalShows[$id] ?? 0);
- $pShow = $totalShowsAll > 0 ? round($s / $totalShowsAll * 100, 2) : 0;
- $ctrPct = $s > 0 ? round($c / $s * 100, 2) : null;
- @endphp
- <tr>
- <td><span class="badge badge-primary">{{ $id }}</span></td>
- <td class="text-right">{{ $s }}</td>
- <td class="text-right">{{ $pShow }}%</td>
- <td class="text-right">{{ $c }}</td>
- <td class="text-right text-nowrap" title="点击÷曝光×100%">{{ $ctrPct !== null ? $ctrPct . '%' : '—' }}</td>
- <td class="text-center">
- <button type="button" class="btn btn-outline-secondary btn-sm py-0 px-2 mr-1 reset-stats-btn"
- data-type="show_id" data-id="{{ $id }}"
- data-confirm="确认清除 ID {{ $id }} 的曝光总计及每日明细中该 ID?">
- 清曝光
- </button>
- <button type="button" class="btn btn-outline-danger btn-sm py-0 px-2 reset-stats-btn"
- data-type="click_id" data-id="{{ $id }}"
- data-confirm="确认清除 ID {{ $id }} 的点击总计及每日明细中该 ID?">
- 清点击
- </button>
- </td>
- </tr>
- @endforeach
- <tr class="table-secondary">
- <td><strong>合计</strong></td>
- <td class="text-right"><strong>{{ $totalShowsAll }}</strong></td>
- <td class="text-right">—</td>
- <td class="text-right"><strong>{{ $totalClicksAll }}</strong></td>
- @php
- $ctrAll = $totalShowsAll > 0 ? round($totalClicksAll / $totalShowsAll * 100, 2) : null;
- @endphp
- <td class="text-right text-nowrap" title="点击÷曝光×100%"><strong>{{ $ctrAll !== null ? $ctrAll . '%' : '—' }}</strong></td>
- <td class="text-center text-muted small">—</td>
- </tr>
- </tbody>
- </table>
- </div>
- <h5 class="mb-3">最近 3 天每日(曝光 / 点击)</h5>
- <div class="table-responsive mb-4">
- <table class="table table-bordered align-middle table-sm weight-stats-merge-table" style="max-width: 920px;">
- <thead class="thead-light">
- <tr>
- <th class="align-middle">日期</th>
- @foreach([1,2,3,4] as $hid)
- <th class="text-center">{{ $hid }}</th>
- @endforeach
- <th class="text-right">曝光∑</th>
- <th class="text-right">点击∑</th>
- <th class="text-right">点击率(%)</th>
- </tr>
- </thead>
- <tbody>
- @foreach($dailyClicks as $idx => $row)
- @php
- $rowS = $dailyShows[$idx] ?? ['date' => $row['date'], 1=>0,2=>0,3=>0,4=>0];
- $sumShow = 0;
- $sumClick = 0;
- @endphp
- <tr>
- <td>{{ $row['date'] }}</td>
- @foreach([1,2,3,4] as $hid)
- @php
- $sv = intval($rowS[$hid] ?? 0);
- $cv = intval($row[$hid] ?? 0);
- $sumShow += $sv;
- $sumClick += $cv;
- $cellCtr = $sv > 0 ? round($cv / $sv * 100, 2) : null;
- @endphp
- <td class="text-center cell-exp-click">
- <span class="text-muted small">曝</span> {{ $sv }}
- <span class="mx-1 text-muted">/</span>
- <span class="text-muted small">点</span> {{ $cv }}
- @if($cellCtr !== null)
- <div class="text-muted small mt-1">CTR {{ $cellCtr }}%</div>
- @elseif($cv > 0)
- <div class="text-muted small mt-1">CTR —</div>
- @endif
- </td>
- @endforeach
- @php
- $dayCtr = $sumShow > 0 ? round($sumClick / $sumShow * 100, 2) : null;
- @endphp
- <td class="text-right"><strong>{{ $sumShow }}</strong></td>
- <td class="text-right"><strong>{{ $sumClick }}</strong></td>
- <td class="text-right text-nowrap" title="当日点击∑÷曝光∑×100%"><strong>{{ $dayCtr !== null ? $dayCtr . '%' : '—' }}</strong></td>
- </tr>
- @endforeach
- </tbody>
- </table>
- </div>
- <div class="alert alert-secondary mb-0">
- <button type="button" class="btn btn-sm btn-danger reset-stats-btn" data-type="all" data-confirm="确认清除点击与曝光的全部总计及每日明细?此操作不可恢复。">
- <i class="mdi mdi-delete-forever"></i> 清除全部统计(点击+曝光含明细)
- </button>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
- <script>
- $(function() {
- var weightChartData = {
- today: {
- shows: @json($chartTodayShows),
- clicks: @json($chartTodayClicks),
- subtitle: @json('当天数据 · ' . date('Y-m-d'))
- },
- total: {
- shows: @json($chartTotalShows),
- clicks: @json($chartTotalClicks),
- subtitle: @json('总计(Redis 累计)')
- }
- };
- function initWeightStatsChart() {
- var chartDom = document.getElementById('weight-stats-chart');
- if (!chartDom || typeof echarts === 'undefined') {
- return null;
- }
- var chart = echarts.init(chartDom);
- function buildOption(pack) {
- return {
- backgroundColor: '#ffffff',
- tooltip: {
- trigger: 'axis',
- axisPointer: { type: 'shadow' }
- },
- legend: {
- data: ['曝光', '点击'],
- bottom: 6,
- itemGap: 28,
- textStyle: { color: '#555' }
- },
- grid: {
- left: '2%',
- right: '2%',
- bottom: '16%',
- top: '10%',
- containLabel: true
- },
- xAxis: {
- type: 'category',
- data: ['1', '2', '3', '4'],
- axisLine: { lineStyle: { color: '#ddd' } },
- axisTick: { alignWithLabel: true },
- axisLabel: { color: '#666', fontSize: 12 }
- },
- yAxis: {
- type: 'value',
- minInterval: 1,
- splitLine: { lineStyle: { color: '#eeeeee', width: 1 } },
- axisLabel: { color: '#666' }
- },
- series: [
- {
- name: '曝光',
- type: 'bar',
- barMaxWidth: 36,
- barGap: '18%',
- barCategoryGap: '40%',
- data: pack.shows,
- itemStyle: {
- color: '#c8bdb0',
- borderRadius: [4, 4, 0, 0]
- }
- },
- {
- name: '点击',
- type: 'bar',
- barMaxWidth: 36,
- data: pack.clicks,
- itemStyle: {
- color: '#2563eb',
- borderRadius: [4, 4, 0, 0]
- }
- }
- ]
- };
- }
- function applyMode(mode) {
- var pack = mode === 'today' ? weightChartData.today : weightChartData.total;
- chart.setOption(buildOption(pack), true);
- $('#weight-chart-subtitle').text(pack.subtitle);
- $('.weight-chart-mode').removeClass('active');
- $('.weight-chart-mode[data-mode="' + mode + '"]').addClass('active');
- }
- $('.weight-chart-mode').on('click', function () {
- applyMode($(this).data('mode'));
- });
- $('a[data-toggle="tab"][href="#tab-stats"]').on('shown.bs.tab', function () {
- chart.resize();
- });
- $(window).on('resize', function () {
- chart.resize();
- });
- applyMode('today');
- return chart;
- }
- initWeightStatsChart();
- function refreshPercents() {
- let total = 0;
- $('.weight-input').each(function(){
- total += parseInt($(this).val() || 0);
- });
- $('#total-weight').text(total);
- $('.weight-input').each(function(){
- const id = $(this).data('id');
- const w = parseInt($(this).val() || 0);
- const p = total > 0 ? (w / total * 100).toFixed(2) : 0;
- $('.weight-percent[data-id="' + id + '"]').text(p + '%');
- });
- }
- $('.weight-input').on('input change', refreshPercents);
- $('#save-weights').click(function() {
- const $btn = $(this);
- const $status = $btn.siblings('.save-status');
- const config = {};
- $('.weight-input').each(function(){
- const id = $(this).data('id');
- let v = parseInt($(this).val() || 0);
- if (isNaN(v) || v < 0) v = 0;
- config[id] = v;
- });
- $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 保存中...');
- $status.removeClass('text-success text-danger').text('');
- $.post("{{ url('/admin/weight-config/update') }}", {
- config: JSON.stringify(config),
- _token: "{{ csrf_token() }}"
- }).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');
- });
- });
- $('.reset-stats-btn').click(function(){
- const type = $(this).data('type');
- const id = $(this).data('id');
- const msg = $(this).data('confirm') || '确认清除?';
- if (!confirm(msg)) return;
- const payload = { type: type, _token: "{{ csrf_token() }}" };
- if (id !== undefined && id !== '') {
- payload.id = id;
- }
- $.post("{{ url('/admin/weight-config/reset-stats') }}", payload).done(function(res){
- if (res.status === 'success') {
- location.reload();
- } else {
- alert(res.message || '操作失败');
- }
- }).fail(function(){
- alert('系统错误');
- });
- });
- });
- </script>
- <style>
- .table thead th { white-space: nowrap; }
- .table tbody td { vertical-align: middle; }
- .cell-exp-click { font-size: 0.875rem; }
- .weight-stats-merge-table tbody td { vertical-align: middle; }
- display: inline-block;
- width: 12px;
- height: 12px;
- border-radius: 3px;
- vertical-align: -2px;
- margin-right: 6px;
- }
- </style>
- @endsection
|