index.blade.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  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 d-flex align-items-center justify-content-between">
  9. <h3 class="card-title mb-0">1234 权重配置 & 数据统计</h3>
  10. <small class="text-muted">配置 1/2/3/4 权重;带 <code>show</code> 只记曝光;每日明细 Redis 保留约 3 天</small>
  11. </div>
  12. <div class="card-body">
  13. <ul class="nav nav-tabs mb-3" role="tablist">
  14. <li class="nav-item">
  15. <a class="nav-link active" data-toggle="tab" href="#tab-config" 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="#tab-stats" role="tab">
  21. <i class="mdi mdi-chart-bar"></i> 数据统计
  22. </a>
  23. </li>
  24. </ul>
  25. <div class="tab-content">
  26. <div class="tab-pane fade show active" id="tab-config" role="tabpanel">
  27. @php
  28. $totalWeight = array_sum($config);
  29. @endphp
  30. <div class="alert alert-info">
  31. 当前总权重: <strong id="total-weight">{{ $totalWeight }}</strong>
  32. ,前端按权重比例随机返回 1/2/3/4
  33. </div>
  34. <div class="table-responsive">
  35. <table class="table table-striped table-hover align-middle mb-0" style="max-width: 600px;">
  36. <thead class="thead-light">
  37. <tr>
  38. <th style="width:80px;">ID</th>
  39. <th style="width:200px;">权重 (整数)</th>
  40. <th>占比</th>
  41. </tr>
  42. </thead>
  43. <tbody class="weight-form">
  44. @foreach([1,2,3,4] as $id)
  45. @php
  46. $w = intval($config[$id] ?? 0);
  47. $percent = $totalWeight > 0 ? round($w / $totalWeight * 100, 2) : 0;
  48. @endphp
  49. <tr>
  50. <td><span class="badge badge-primary">{{ $id }}</span></td>
  51. <td>
  52. <input type="number" min="0" class="form-control form-control-sm weight-input"
  53. data-id="{{ $id }}" name="weight[{{ $id }}]" value="{{ $w }}" />
  54. </td>
  55. <td>
  56. <span class="weight-percent" data-id="{{ $id }}">{{ $percent }}%</span>
  57. </td>
  58. </tr>
  59. @endforeach
  60. </tbody>
  61. </table>
  62. </div>
  63. <div class="d-flex align-items-center mt-4">
  64. <button class="btn btn-gradient-primary btn-sm" id="save-weights">
  65. <i class="mdi mdi-content-save"></i> 保存权重
  66. </button>
  67. <span class="save-status ml-3"></span>
  68. </div>
  69. </div>
  70. <div class="tab-pane fade" id="tab-stats" role="tabpanel">
  71. @php
  72. $chartIds = [1, 2, 3, 4];
  73. $chartTodayShows = array_map(fn ($id) => (int) ($todayShows[$id] ?? 0), $chartIds);
  74. $chartTodayClicks = array_map(fn ($id) => (int) ($todayClicks[$id] ?? 0), $chartIds);
  75. $chartTotalShows = array_map(fn ($id) => (int) ($totalShows[$id] ?? 0), $chartIds);
  76. $chartTotalClicks = array_map(fn ($id) => (int) ($totalClicks[$id] ?? 0), $chartIds);
  77. @endphp
  78. <div class="card border mb-4 shadow-sm">
  79. <div class="card-body">
  80. <div class="d-flex flex-wrap align-items-center justify-content-between mb-2">
  81. <h5 class="card-title mb-0">曝光 & 点击(按 ID)</h5>
  82. <div class="btn-group btn-group-sm" role="group" aria-label="统计维度">
  83. <button type="button" class="btn btn-outline-primary active weight-chart-mode" data-mode="today">当天</button>
  84. <button type="button" class="btn btn-outline-primary weight-chart-mode" data-mode="total">总计</button>
  85. </div>
  86. </div>
  87. <p class="text-muted small mb-3 mb-md-2">
  88. <span id="weight-chart-subtitle">当天数据 · {{ date('Y-m-d') }}</span>
  89. </p>
  90. <div id="weight-stats-chart" style="width:100%;height:340px;"></div>
  91. <div class="d-flex justify-content-center align-items-center mt-2 small text-muted">
  92. <span class="mr-3"><span class="weight-chart-legend-swatch" style="background:#c8bdb0;"></span> 曝光(show)</span>
  93. <span><span class="weight-chart-legend-swatch" style="background:#2563eb;"></span> 点击</span>
  94. </div>
  95. </div>
  96. </div>
  97. <h5 class="mb-3">总计(Redis 累计)</h5>
  98. @php
  99. $totalClicksAll = array_sum($totalClicks);
  100. $totalShowsAll = array_sum($totalShows);
  101. @endphp
  102. <div class="table-responsive mb-4">
  103. <table class="table table-bordered align-middle weight-stats-merge-table" style="max-width: 920px;">
  104. <thead class="thead-light">
  105. <tr>
  106. <th>ID</th>
  107. <th class="text-right">曝光</th>
  108. <th class="text-right">曝光%</th>
  109. <th class="text-right">点击</th>
  110. <th class="text-right">点击率(%)</th>
  111. <th class="text-center" style="min-width:140px;">操作</th>
  112. </tr>
  113. </thead>
  114. <tbody>
  115. @foreach([1,2,3,4] as $id)
  116. @php
  117. $c = intval($totalClicks[$id] ?? 0);
  118. $s = intval($totalShows[$id] ?? 0);
  119. $pShow = $totalShowsAll > 0 ? round($s / $totalShowsAll * 100, 2) : 0;
  120. $ctrPct = $s > 0 ? round($c / $s * 100, 2) : null;
  121. @endphp
  122. <tr>
  123. <td><span class="badge badge-primary">{{ $id }}</span></td>
  124. <td class="text-right">{{ $s }}</td>
  125. <td class="text-right">{{ $pShow }}%</td>
  126. <td class="text-right">{{ $c }}</td>
  127. <td class="text-right text-nowrap" title="点击÷曝光×100%">{{ $ctrPct !== null ? $ctrPct . '%' : '—' }}</td>
  128. <td class="text-center">
  129. <button type="button" class="btn btn-outline-secondary btn-sm py-0 px-2 mr-1 reset-stats-btn"
  130. data-type="show_id" data-id="{{ $id }}"
  131. data-confirm="确认清除 ID {{ $id }} 的曝光总计及每日明细中该 ID?">
  132. 清曝光
  133. </button>
  134. <button type="button" class="btn btn-outline-danger btn-sm py-0 px-2 reset-stats-btn"
  135. data-type="click_id" data-id="{{ $id }}"
  136. data-confirm="确认清除 ID {{ $id }} 的点击总计及每日明细中该 ID?">
  137. 清点击
  138. </button>
  139. </td>
  140. </tr>
  141. @endforeach
  142. <tr class="table-secondary">
  143. <td><strong>合计</strong></td>
  144. <td class="text-right"><strong>{{ $totalShowsAll }}</strong></td>
  145. <td class="text-right">—</td>
  146. <td class="text-right"><strong>{{ $totalClicksAll }}</strong></td>
  147. @php
  148. $ctrAll = $totalShowsAll > 0 ? round($totalClicksAll / $totalShowsAll * 100, 2) : null;
  149. @endphp
  150. <td class="text-right text-nowrap" title="点击÷曝光×100%"><strong>{{ $ctrAll !== null ? $ctrAll . '%' : '—' }}</strong></td>
  151. <td class="text-center text-muted small">—</td>
  152. </tr>
  153. </tbody>
  154. </table>
  155. </div>
  156. <h5 class="mb-3">最近 3 天每日(曝光 / 点击)</h5>
  157. <div class="table-responsive mb-4">
  158. <table class="table table-bordered align-middle table-sm weight-stats-merge-table" style="max-width: 920px;">
  159. <thead class="thead-light">
  160. <tr>
  161. <th class="align-middle">日期</th>
  162. @foreach([1,2,3,4] as $hid)
  163. <th class="text-center">{{ $hid }}</th>
  164. @endforeach
  165. <th class="text-right">曝光∑</th>
  166. <th class="text-right">点击∑</th>
  167. <th class="text-right">点击率(%)</th>
  168. </tr>
  169. </thead>
  170. <tbody>
  171. @foreach($dailyClicks as $idx => $row)
  172. @php
  173. $rowS = $dailyShows[$idx] ?? ['date' => $row['date'], 1=>0,2=>0,3=>0,4=>0];
  174. $sumShow = 0;
  175. $sumClick = 0;
  176. @endphp
  177. <tr>
  178. <td>{{ $row['date'] }}</td>
  179. @foreach([1,2,3,4] as $hid)
  180. @php
  181. $sv = intval($rowS[$hid] ?? 0);
  182. $cv = intval($row[$hid] ?? 0);
  183. $sumShow += $sv;
  184. $sumClick += $cv;
  185. $cellCtr = $sv > 0 ? round($cv / $sv * 100, 2) : null;
  186. @endphp
  187. <td class="text-center cell-exp-click">
  188. <span class="text-muted small">曝</span> {{ $sv }}
  189. <span class="mx-1 text-muted">/</span>
  190. <span class="text-muted small">点</span> {{ $cv }}
  191. @if($cellCtr !== null)
  192. <div class="text-muted small mt-1">CTR {{ $cellCtr }}%</div>
  193. @elseif($cv > 0)
  194. <div class="text-muted small mt-1">CTR —</div>
  195. @endif
  196. </td>
  197. @endforeach
  198. @php
  199. $dayCtr = $sumShow > 0 ? round($sumClick / $sumShow * 100, 2) : null;
  200. @endphp
  201. <td class="text-right"><strong>{{ $sumShow }}</strong></td>
  202. <td class="text-right"><strong>{{ $sumClick }}</strong></td>
  203. <td class="text-right text-nowrap" title="当日点击∑÷曝光∑×100%"><strong>{{ $dayCtr !== null ? $dayCtr . '%' : '—' }}</strong></td>
  204. </tr>
  205. @endforeach
  206. </tbody>
  207. </table>
  208. </div>
  209. <div class="alert alert-secondary mb-0">
  210. <button type="button" class="btn btn-sm btn-danger reset-stats-btn" data-type="all" data-confirm="确认清除点击与曝光的全部总计及每日明细?此操作不可恢复。">
  211. <i class="mdi mdi-delete-forever"></i> 清除全部统计(点击+曝光含明细)
  212. </button>
  213. </div>
  214. </div>
  215. </div>
  216. </div>
  217. </div>
  218. </div>
  219. </div>
  220. </div>
  221. <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
  222. <script>
  223. $(function() {
  224. var weightChartData = {
  225. today: {
  226. shows: @json($chartTodayShows),
  227. clicks: @json($chartTodayClicks),
  228. subtitle: @json('当天数据 · ' . date('Y-m-d'))
  229. },
  230. total: {
  231. shows: @json($chartTotalShows),
  232. clicks: @json($chartTotalClicks),
  233. subtitle: @json('总计(Redis 累计)')
  234. }
  235. };
  236. function initWeightStatsChart() {
  237. var chartDom = document.getElementById('weight-stats-chart');
  238. if (!chartDom || typeof echarts === 'undefined') {
  239. return null;
  240. }
  241. var chart = echarts.init(chartDom);
  242. function buildOption(pack) {
  243. return {
  244. backgroundColor: '#ffffff',
  245. tooltip: {
  246. trigger: 'axis',
  247. axisPointer: { type: 'shadow' }
  248. },
  249. legend: {
  250. data: ['曝光', '点击'],
  251. bottom: 6,
  252. itemGap: 28,
  253. textStyle: { color: '#555' }
  254. },
  255. grid: {
  256. left: '2%',
  257. right: '2%',
  258. bottom: '16%',
  259. top: '10%',
  260. containLabel: true
  261. },
  262. xAxis: {
  263. type: 'category',
  264. data: ['1', '2', '3', '4'],
  265. axisLine: { lineStyle: { color: '#ddd' } },
  266. axisTick: { alignWithLabel: true },
  267. axisLabel: { color: '#666', fontSize: 12 }
  268. },
  269. yAxis: {
  270. type: 'value',
  271. minInterval: 1,
  272. splitLine: { lineStyle: { color: '#eeeeee', width: 1 } },
  273. axisLabel: { color: '#666' }
  274. },
  275. series: [
  276. {
  277. name: '曝光',
  278. type: 'bar',
  279. barMaxWidth: 36,
  280. barGap: '18%',
  281. barCategoryGap: '40%',
  282. data: pack.shows,
  283. itemStyle: {
  284. color: '#c8bdb0',
  285. borderRadius: [4, 4, 0, 0]
  286. }
  287. },
  288. {
  289. name: '点击',
  290. type: 'bar',
  291. barMaxWidth: 36,
  292. data: pack.clicks,
  293. itemStyle: {
  294. color: '#2563eb',
  295. borderRadius: [4, 4, 0, 0]
  296. }
  297. }
  298. ]
  299. };
  300. }
  301. function applyMode(mode) {
  302. var pack = mode === 'today' ? weightChartData.today : weightChartData.total;
  303. chart.setOption(buildOption(pack), true);
  304. $('#weight-chart-subtitle').text(pack.subtitle);
  305. $('.weight-chart-mode').removeClass('active');
  306. $('.weight-chart-mode[data-mode="' + mode + '"]').addClass('active');
  307. }
  308. $('.weight-chart-mode').on('click', function () {
  309. applyMode($(this).data('mode'));
  310. });
  311. $('a[data-toggle="tab"][href="#tab-stats"]').on('shown.bs.tab', function () {
  312. chart.resize();
  313. });
  314. $(window).on('resize', function () {
  315. chart.resize();
  316. });
  317. applyMode('today');
  318. return chart;
  319. }
  320. initWeightStatsChart();
  321. function refreshPercents() {
  322. let total = 0;
  323. $('.weight-input').each(function(){
  324. total += parseInt($(this).val() || 0);
  325. });
  326. $('#total-weight').text(total);
  327. $('.weight-input').each(function(){
  328. const id = $(this).data('id');
  329. const w = parseInt($(this).val() || 0);
  330. const p = total > 0 ? (w / total * 100).toFixed(2) : 0;
  331. $('.weight-percent[data-id="' + id + '"]').text(p + '%');
  332. });
  333. }
  334. $('.weight-input').on('input change', refreshPercents);
  335. $('#save-weights').click(function() {
  336. const $btn = $(this);
  337. const $status = $btn.siblings('.save-status');
  338. const config = {};
  339. $('.weight-input').each(function(){
  340. const id = $(this).data('id');
  341. let v = parseInt($(this).val() || 0);
  342. if (isNaN(v) || v < 0) v = 0;
  343. config[id] = v;
  344. });
  345. $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 保存中...');
  346. $status.removeClass('text-success text-danger').text('');
  347. $.post("{{ url('/admin/weight-config/update') }}", {
  348. config: JSON.stringify(config),
  349. _token: "{{ csrf_token() }}"
  350. }).done(function(res){
  351. $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存权重');
  352. if (res.status === 'success') {
  353. $status.text('更新成功').addClass('text-success');
  354. } else {
  355. $status.text(res.message || '更新失败').addClass('text-danger');
  356. }
  357. setTimeout(function(){ $status.fadeOut(function(){ $(this).text('').show().removeClass('text-success text-danger'); }); }, 3000);
  358. }).fail(function(){
  359. $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存权重');
  360. $status.text('系统错误').addClass('text-danger');
  361. });
  362. });
  363. $('.reset-stats-btn').click(function(){
  364. const type = $(this).data('type');
  365. const id = $(this).data('id');
  366. const msg = $(this).data('confirm') || '确认清除?';
  367. if (!confirm(msg)) return;
  368. const payload = { type: type, _token: "{{ csrf_token() }}" };
  369. if (id !== undefined && id !== '') {
  370. payload.id = id;
  371. }
  372. $.post("{{ url('/admin/weight-config/reset-stats') }}", payload).done(function(res){
  373. if (res.status === 'success') {
  374. location.reload();
  375. } else {
  376. alert(res.message || '操作失败');
  377. }
  378. }).fail(function(){
  379. alert('系统错误');
  380. });
  381. });
  382. });
  383. </script>
  384. <style>
  385. .table thead th { white-space: nowrap; }
  386. .table tbody td { vertical-align: middle; }
  387. .cell-exp-click { font-size: 0.875rem; }
  388. .weight-stats-merge-table tbody td { vertical-align: middle; }
  389. display: inline-block;
  390. width: 12px;
  391. height: 12px;
  392. border-radius: 3px;
  393. vertical-align: -2px;
  394. margin-right: 6px;
  395. }
  396. </style>
  397. @endsection