- 拆分 HomeController → TransactionController, ReportWebController, FollowPlanController - 新增 Service 层: TransactionService, ReportService, FollowPlanService - pk10.php JS 抽离为 4 个独立文件 (sound/race/bet/poll) - 前台新增报表查询页面 (/report) + 跟单计划页面 (/follow-plan) - 后台新增跟单计划管理 + 用户输赢明细统计 - 封盘状态显示倒计时 (x:xx) - 音效仅在开奖弹窗打开时播放 - 路由按模块分组整理 - autoload 支持 App\Services 命名空间
404 lines
23 KiB
PHP
404 lines
23 KiB
PHP
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
|
||
<i class="fas fa-chart-bar text-primary mr-3"></i>
|
||
数据报表
|
||
</h1>
|
||
|
||
<!-- 日期筛选栏 -->
|
||
<div class="bg-white rounded-xl p-5 card-shadow mb-6">
|
||
<div class="flex flex-wrap items-center gap-4">
|
||
<div class="flex items-center gap-2">
|
||
<label class="text-sm text-gray-500">起始日期</label>
|
||
<input type="text" id="dateFrom" value="<?= htmlspecialchars($dateFrom) ?>" class="border border-gray-300 rounded-lg px-3 py-2 text-sm w-36" readonly placeholder="选择日期">
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<label class="text-sm text-gray-500">截止日期</label>
|
||
<input type="text" id="dateTo" value="<?= htmlspecialchars($dateTo) ?>" class="border border-gray-300 rounded-lg px-3 py-2 text-sm w-36" readonly placeholder="选择日期">
|
||
</div>
|
||
<button onclick="doQuery()" class="px-5 py-2 bg-primary text-white rounded-lg text-sm hover:opacity-90 transition">
|
||
<i class="fas fa-search mr-1"></i> 查询
|
||
</button>
|
||
<button onclick="doExport()" class="px-5 py-2 bg-success text-white rounded-lg text-sm hover:opacity-90 transition">
|
||
<i class="fas fa-file-csv mr-1"></i> 导出CSV
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 总体统计卡片 -->
|
||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-4 mb-6">
|
||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<p class="text-gray-500 text-sm">总投注</p>
|
||
<h3 class="text-xl font-bold mt-1 text-warning"><?= number_format((float)$totalBet, 2) ?></h3>
|
||
</div>
|
||
<div class="w-10 h-10 rounded-full bg-warning/10 flex items-center justify-center">
|
||
<i class="fas fa-coins text-warning"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<p class="text-gray-500 text-sm">总派彩</p>
|
||
<h3 class="text-xl font-bold mt-1 text-primary"><?= number_format((float)$totalWin, 2) ?></h3>
|
||
</div>
|
||
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||
<i class="fas fa-trophy text-primary"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<p class="text-gray-500 text-sm">总利润</p>
|
||
<h3 class="text-xl font-bold mt-1 <?= $profit >= 0 ? 'text-success' : 'text-danger' ?>"><?= number_format((float)$profit, 2) ?></h3>
|
||
</div>
|
||
<div class="w-10 h-10 rounded-full <?= $profit >= 0 ? 'bg-success/10' : 'bg-danger/10' ?> flex items-center justify-center">
|
||
<i class="fas fa-chart-line <?= $profit >= 0 ? 'text-success' : 'text-danger' ?>"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<p class="text-gray-500 text-sm">利润率</p>
|
||
<?php
|
||
$prColor = 'text-warning';
|
||
if ($profitRate > 10) $prColor = 'text-success';
|
||
elseif ($profitRate < 0) $prColor = 'text-danger';
|
||
?>
|
||
<h3 class="text-xl font-bold mt-1 <?= $prColor ?>"><?= $profitRate ?>%</h3>
|
||
</div>
|
||
<div class="w-10 h-10 rounded-full bg-warning/10 flex items-center justify-center">
|
||
<i class="fas fa-percentage text-warning"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<p class="text-gray-500 text-sm">总充值</p>
|
||
<h3 class="text-xl font-bold mt-1 text-success"><?= number_format((float)$totalDeposit, 2) ?></h3>
|
||
</div>
|
||
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
|
||
<i class="fas fa-arrow-down text-success"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<p class="text-gray-500 text-sm">总提现</p>
|
||
<h3 class="text-xl font-bold mt-1 text-danger"><?= number_format((float)$totalWithdraw, 2) ?></h3>
|
||
</div>
|
||
<div class="w-10 h-10 rounded-full bg-danger/10 flex items-center justify-center">
|
||
<i class="fas fa-arrow-up text-danger"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<p class="text-gray-500 text-sm">活跃用户</p>
|
||
<h3 class="text-xl font-bold mt-1 text-primary"><?= $activeUsers ?></h3>
|
||
</div>
|
||
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||
<i class="fas fa-user-check text-primary"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<p class="text-gray-500 text-sm">新增用户</p>
|
||
<h3 class="text-xl font-bold mt-1 text-secondary"><?= $newUsers ?></h3>
|
||
</div>
|
||
<div class="w-10 h-10 rounded-full bg-secondary/10 flex items-center justify-center">
|
||
<i class="fas fa-user-plus text-secondary"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 日明细表 -->
|
||
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
|
||
<h2 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
|
||
<i class="fas fa-calendar-alt text-primary mr-2"></i> 日明细统计
|
||
</h2>
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full bg-white rounded-xl overflow-hidden">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">日期</th>
|
||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">投注额</th>
|
||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">派彩额</th>
|
||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">利润</th>
|
||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">充值</th>
|
||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">提现</th>
|
||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">笔数</th>
|
||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">胜率</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-200">
|
||
<?php if (!empty($dailyStats)): ?>
|
||
<?php foreach ($dailyStats as $d): ?>
|
||
<tr class="hover:bg-gray-50 transition-colors">
|
||
<td class="px-4 py-3 text-sm font-medium text-gray-900"><?= $d['date'] ?></td>
|
||
<td class="px-4 py-3 text-sm text-right"><?= number_format($d['total_bet'], 2) ?></td>
|
||
<td class="px-4 py-3 text-sm text-right"><?= number_format($d['total_win'], 2) ?></td>
|
||
<td class="px-4 py-3 text-sm text-right font-semibold <?= $d['profit'] >= 0 ? 'text-success' : 'text-danger' ?>">
|
||
<?= $d['profit'] >= 0 ? '+' : '' ?><?= number_format($d['profit'], 2) ?>
|
||
</td>
|
||
<td class="px-4 py-3 text-sm text-right text-success"><?= number_format($d['deposits'], 2) ?></td>
|
||
<td class="px-4 py-3 text-sm text-right text-danger"><?= number_format($d['withdraws'], 2) ?></td>
|
||
<td class="px-4 py-3 text-sm text-center"><?= $d['bet_count'] ?></td>
|
||
<td class="px-4 py-3 text-sm text-center">
|
||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $d['win_rate'] > 50 ? 'bg-danger/10 text-danger' : 'bg-success/10 text-success' ?>">
|
||
<?= $d['win_rate'] ?>%
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php else: ?>
|
||
<tr>
|
||
<td colspan="8" class="px-6 py-12 text-center">
|
||
<div class="flex flex-col items-center">
|
||
<i class="fas fa-chart-bar text-gray-300 text-5xl mb-4"></i>
|
||
<h3 class="text-lg font-medium text-gray-900">暂无数据</h3>
|
||
<p class="mt-1 text-gray-500 text-sm">选定日期范围内没有投注记录</p>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 用户投注排行 -->
|
||
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
|
||
<h2 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
|
||
<i class="fas fa-ranking-star text-warning mr-2"></i> 用户投注排行(前20)
|
||
</h2>
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full bg-white rounded-xl overflow-hidden">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase w-16">排名</th>
|
||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">用户名</th>
|
||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">投注额</th>
|
||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">派彩额</th>
|
||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">平台盈亏</th>
|
||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">笔数</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-200">
|
||
<?php if (!empty($topUsers)): ?>
|
||
<?php $rank = 1; foreach ($topUsers as $u): ?>
|
||
<tr class="hover:bg-gray-50 transition-colors">
|
||
<td class="px-4 py-3 text-center">
|
||
<?php if ($rank <= 3): ?>
|
||
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full text-white text-xs font-bold
|
||
<?= $rank === 1 ? 'bg-yellow-500' : ($rank === 2 ? 'bg-gray-400' : 'bg-amber-600') ?>">
|
||
<?= $rank ?>
|
||
</span>
|
||
<?php else: ?>
|
||
<span class="text-sm text-gray-500"><?= $rank ?></span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td class="px-4 py-3 text-sm font-medium text-gray-900"><?= htmlspecialchars($u['username'] ?? '') ?></td>
|
||
<td class="px-4 py-3 text-sm text-right"><?= number_format(floatval($u['total_bet']), 2) ?></td>
|
||
<td class="px-4 py-3 text-sm text-right"><?= number_format(floatval($u['total_win']), 2) ?></td>
|
||
<td class="px-4 py-3 text-sm text-right font-semibold <?= floatval($u['profit']) >= 0 ? 'text-success' : 'text-danger' ?>">
|
||
<?= floatval($u['profit']) >= 0 ? '+' : '' ?><?= number_format(floatval($u['profit']), 2) ?>
|
||
</td>
|
||
<td class="px-4 py-3 text-sm text-center"><?= intval($u['bet_count']) ?></td>
|
||
</tr>
|
||
<?php $rank++; endforeach; ?>
|
||
<?php else: ?>
|
||
<tr>
|
||
<td colspan="6" class="px-6 py-12 text-center">
|
||
<div class="flex flex-col items-center">
|
||
<i class="fas fa-users text-gray-300 text-5xl mb-4"></i>
|
||
<h3 class="text-lg font-medium text-gray-900">暂无数据</h3>
|
||
<p class="mt-1 text-gray-500 text-sm">选定日期范围内没有用户投注</p>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 代理报表 -->
|
||
<?php if (!empty($agentStats)): ?>
|
||
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
|
||
<h2 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
|
||
<i class="fas fa-user-tie text-secondary mr-2"></i> 代理报表
|
||
</h2>
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full bg-white rounded-xl overflow-hidden">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">代理</th>
|
||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">玩家数</th>
|
||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">投注额</th>
|
||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">中奖额</th>
|
||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">佣金</th>
|
||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">盈利</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-200">
|
||
<?php foreach ($agentStats as $as): ?>
|
||
<tr class="hover:bg-gray-50 transition-colors">
|
||
<td class="px-4 py-3 text-sm font-medium text-gray-900"><?= htmlspecialchars($as['user']['username'] ?? '') ?></td>
|
||
<td class="px-4 py-3 text-sm text-center"><?= $as['players'] ?></td>
|
||
<td class="px-4 py-3 text-sm text-right"><?= number_format($as['bets'], 2) ?></td>
|
||
<td class="px-4 py-3 text-sm text-right"><?= number_format($as['wins'], 2) ?></td>
|
||
<td class="px-4 py-3 text-sm text-right text-warning"><?= number_format($as['commission'], 2) ?></td>
|
||
<td class="px-4 py-3 text-sm text-right font-semibold <?= $as['profit'] >= 0 ? 'text-success' : 'text-danger' ?>">
|
||
<?= $as['profit'] >= 0 ? '+' : '' ?><?= number_format($as['profit'], 2) ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- 用户输赢明细 -->
|
||
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
|
||
<h2 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
|
||
<i class="fas fa-money-bill-trend-up text-danger mr-2"></i> 用户输赢明细
|
||
</h2>
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full bg-white rounded-xl overflow-hidden" id="userWinLossTable">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">用户名</th>
|
||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">当前余额</th>
|
||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">总投注</th>
|
||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">总派彩</th>
|
||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">平台盈亏</th>
|
||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">笔数</th>
|
||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">胜率</th>
|
||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">日明细</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-200">
|
||
<?php if (!empty($userSummary)): ?>
|
||
<?php foreach ($userSummary as $uid => $us):
|
||
$uProfit = $us['total_bet'] - $us['total_win'];
|
||
$uWinRate = $us['bet_count'] > 0 ? round($us['win_count'] / $us['bet_count'] * 100, 1) : 0;
|
||
?>
|
||
<tr class="hover:bg-gray-50 transition-colors">
|
||
<td class="px-4 py-3 text-sm font-medium text-gray-900"><?= htmlspecialchars($us['username']) ?></td>
|
||
<td class="px-4 py-3 text-sm text-right"><?= number_format($us['balance'], 2) ?></td>
|
||
<td class="px-4 py-3 text-sm text-right"><?= number_format($us['total_bet'], 2) ?></td>
|
||
<td class="px-4 py-3 text-sm text-right"><?= number_format($us['total_win'], 2) ?></td>
|
||
<td class="px-4 py-3 text-sm text-right font-semibold <?= $uProfit >= 0 ? 'text-success' : 'text-danger' ?>">
|
||
<?= $uProfit >= 0 ? '+' : '' ?><?= number_format($uProfit, 2) ?>
|
||
</td>
|
||
<td class="px-4 py-3 text-sm text-center"><?= $us['bet_count'] ?></td>
|
||
<td class="px-4 py-3 text-sm text-center">
|
||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $uWinRate > 50 ? 'bg-danger/10 text-danger' : 'bg-success/10 text-success' ?>">
|
||
<?= $uWinRate ?>%
|
||
</span>
|
||
</td>
|
||
<td class="px-4 py-3 text-sm text-center">
|
||
<button onclick="toggleDays('days_<?= $uid ?>')" class="text-primary hover:underline text-xs">
|
||
<i class="fas fa-chevron-down"></i> 展开
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
<tr id="days_<?= $uid ?>" style="display:none" class="bg-gray-50/50">
|
||
<td colspan="8" class="px-6 py-2">
|
||
<table class="w-full text-xs">
|
||
<thead><tr class="text-gray-400">
|
||
<th class="py-1 text-left">日期</th>
|
||
<th class="py-1 text-right">投注</th>
|
||
<th class="py-1 text-right">派彩</th>
|
||
<th class="py-1 text-right">盈亏</th>
|
||
<th class="py-1 text-center">笔数</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
<?php foreach ($us['days'] as $day): ?>
|
||
<tr class="border-t border-gray-100">
|
||
<td class="py-1 text-gray-600"><?= $day['date'] ?></td>
|
||
<td class="py-1 text-right"><?= number_format($day['bet'], 2) ?></td>
|
||
<td class="py-1 text-right"><?= number_format($day['win'], 2) ?></td>
|
||
<td class="py-1 text-right font-semibold <?= $day['profit'] >= 0 ? 'text-success' : 'text-danger' ?>">
|
||
<?= $day['profit'] >= 0 ? '+' : '' ?><?= number_format($day['profit'], 2) ?>
|
||
</td>
|
||
<td class="py-1 text-center"><?= $day['count'] ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php else: ?>
|
||
<tr>
|
||
<td colspan="8" class="px-6 py-12 text-center">
|
||
<div class="flex flex-col items-center">
|
||
<i class="fas fa-money-bill text-gray-300 text-5xl mb-4"></i>
|
||
<h3 class="text-lg font-medium text-gray-900">暂无数据</h3>
|
||
<p class="mt-1 text-gray-500 text-sm">选定日期范围内没有用户投注记录</p>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/Static/js/admin.js"></script>
|
||
<script>
|
||
layui.use(['laydate', 'layer'], function() {
|
||
var laydate = layui.laydate;
|
||
|
||
laydate.render({
|
||
elem: '#dateFrom',
|
||
type: 'date',
|
||
format: 'yyyy-MM-dd',
|
||
value: '<?= htmlspecialchars($dateFrom) ?>',
|
||
max: '<?= htmlspecialchars($dateTo) ?>'
|
||
});
|
||
|
||
laydate.render({
|
||
elem: '#dateTo',
|
||
type: 'date',
|
||
format: 'yyyy-MM-dd',
|
||
value: '<?= htmlspecialchars($dateTo) ?>',
|
||
max: 0
|
||
});
|
||
});
|
||
|
||
function doQuery() {
|
||
var from = document.getElementById('dateFrom').value;
|
||
var to = document.getElementById('dateTo').value;
|
||
window.location.href = '/admin/reports?from=' + from + '&to=' + to;
|
||
}
|
||
|
||
function toggleDays(id) {
|
||
var el = document.getElementById(id);
|
||
if (!el) return;
|
||
el.style.display = el.style.display === 'none' ? '' : 'none';
|
||
var btn = el.previousElementSibling.querySelector('button');
|
||
if (btn) {
|
||
var isOpen = el.style.display !== 'none';
|
||
btn.innerHTML = isOpen ? '<i class="fas fa-chevron-up"></i> 收起' : '<i class="fas fa-chevron-down"></i> 展开';
|
||
}
|
||
}
|
||
|
||
function doExport() {
|
||
var from = document.getElementById('dateFrom').value;
|
||
var to = document.getElementById('dateTo').value;
|
||
window.location.href = '/admin/reports/export?from=' + from + '&to=' + to;
|
||
}
|
||
</script>
|