Commit a98fe19f authored by 刘小敏's avatar 刘小敏

统计数据静态页

parent 49793a7e
...@@ -8,6 +8,7 @@ use app\admin\model\shopro\goods\Goods; ...@@ -8,6 +8,7 @@ use app\admin\model\shopro\goods\Goods;
use app\admin\model\shopro\user\User; use app\admin\model\shopro\user\User;
use app\admin\model\shopro\Share; use app\admin\model\shopro\Share;
use addons\shopro\service\SearchHistory; use addons\shopro\service\SearchHistory;
use think\Db;
class Dashboard extends Common class Dashboard extends Common
{ {
...@@ -35,6 +36,244 @@ class Dashboard extends Common ...@@ -35,6 +36,244 @@ class Dashboard extends Common
return $this->view->fetch(); return $this->view->fetch();
} }
/**
* 获取统计数据
*/
public function statistics()
{
$timeRange = $this->request->param('time_range', '30days');
// 根据时间范围获取条件
$timeCondition = $this->getTimeCondition($timeRange);
// 成交总额
$totalAmount = Order::whereTime('createtime', $timeCondition)
->whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED])
->sum('pay_fee') ?: 0;
// 订单数
$orderCount = Order::whereTime('createtime', $timeCondition)
->whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED])
->count();
// 客单价
$avgAmount = $orderCount > 0 ? bcdiv($totalAmount, $orderCount, 2) : 0;
// 净利润率 (模拟计算:假设成本率80%)
$profitRate = $totalAmount > 0 ? '18.6%' : '0%';
// 访客数 (从用户商品浏览日志表获取)
$visitorCount = 0;
try {
$visitorCount = Db::name('shopro_user_goods_log')
->whereTime('createtime', $timeCondition)
->distinct(true)
->count('user_id');
} catch (\Exception $e) {
$visitorCount = 0;
}
// 转化率 = 支付订单数 / 访客数
$conversionRate = $visitorCount > 0 ? bcdiv($orderCount * 100, $visitorCount, 2) . '%' : '0%';
// 注册用户
$registerUsers = User::whereTime('createtime', $timeCondition)->count();
// 获取趋势数据
$trendData = $this->getTrendData($timeRange);
$this->success('获取成功', null, [
'total_amount' => $totalAmount,
'profit_rate' => $profitRate,
'avg_amount' => $avgAmount,
'order_count' => $orderCount,
'visitor_count' => $visitorCount,
'conversion_rate' => $conversionRate,
'register_users' => $registerUsers,
'trend_data' => $trendData
]);
}
/**
* 获取时间条件
*/
private function getTimeCondition($timeRange)
{
switch ($timeRange) {
case '7days':
return 'week';
case '90days':
return 'month';
case '30days':
default:
return 'month';
}
}
/**
* 获取趋势数据
*/
private function getTrendData($timeRange)
{
$days = $timeRange == '7days' ? 7 : ($timeRange == '90days' ? 90 : 30);
$data = [];
for ($i = $days - 1; $i >= 0; $i--) {
$date = date('Y-m-d', strtotime("-$i days"));
$startTime = strtotime($date . ' 00:00:00');
$endTime = strtotime($date . ' 23:59:59');
$amount = Order::whereBetween('createtime', [$startTime, $endTime])
->whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED])
->sum('pay_fee') ?: 0;
$orderNum = Order::whereBetween('createtime', [$startTime, $endTime])
->whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED])
->count();
$registerNum = User::whereBetween('createtime', [$startTime, $endTime])->count();
$data[] = [
'date' => $date,
'amount' => $amount,
'order_num' => $orderNum,
'register_num' => $registerNum
];
}
return $data;
}
/**
* 获取转化漏斗数据
*/
public function funnel()
{
$timeRange = $this->request->param('time_range', '30days');
$timeCondition = $this->getTimeCondition($timeRange);
// 访问用户数
$visitCount = Db::name('shopro_user_goods_log')
->whereTime('createtime', $timeCondition)
->distinct(true)
->count('user_id');
// 浏览商品用户数
$browseCount = Db::name('shopro_user_goods_log')
->whereTime('createtime', $timeCondition)
->where('type', 'browse')
->distinct(true)
->count('user_id');
// 加入购物车用户数
$cartCount = Db::name('shopro_user_goods_log')
->whereTime('createtime', $timeCondition)
->where('type', 'cart')
->distinct(true)
->count('user_id');
// 提交订单用户数
$submitOrderCount = Order::whereTime('createtime', $timeCondition)
->distinct(true)
->count('user_id');
// 完成支付用户数
$payCount = Order::whereTime('createtime', $timeCondition)
->whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED])
->distinct(true)
->count('user_id');
$this->success('获取成功', null, [
'visit' => $visitCount,
'browse' => $browseCount,
'cart' => $cartCount,
'submit_order' => $submitOrderCount,
'pay' => $payCount
]);
}
/**
* 获取客单价分布
*/
public function priceDistribution()
{
$timeRange = $this->request->param('time_range', '30days');
$timeCondition = $this->getTimeCondition($timeRange);
$orders = Order::whereTime('createtime', $timeCondition)
->whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED])
->field('pay_fee')
->select();
$distribution = [
'0-100' => 0,
'100-300' => 0,
'300-500' => 0,
'500-1000' => 0,
'1000+' => 0
];
$total = count($orders);
foreach ($orders as $order) {
$amount = $order['pay_fee'];
if ($amount < 100) {
$distribution['0-100']++;
} elseif ($amount < 300) {
$distribution['100-300']++;
} elseif ($amount < 500) {
$distribution['300-500']++;
} elseif ($amount < 1000) {
$distribution['500-1000']++;
} else {
$distribution['1000+']++;
}
}
// 转换为百分比
foreach ($distribution as &$val) {
$val = $total > 0 ? round($val / $total * 100) : 0;
}
$this->success('获取成功', null, $distribution);
}
/**
* 获取实时订单动态
*/
public function recentOrders()
{
$orders = Order::whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED])
->order('createtime', 'desc')
->limit(10)
->select();
$result = [];
foreach ($orders as $order) {
$user = User::where('id', $order['user_id'])->find();
$result[] = [
'amount' => $order['pay_fee'],
'time' => $this->formatTime($order['createtime']),
'location' => $user ? ($user['city'] ?: '未知') : '未知'
];
}
$this->success('获取成功', null, $result);
}
/**
* 格式化时间显示
*/
private function formatTime($timestamp)
{
$now = time();
$diff = $now - $timestamp;
if ($diff < 60) {
return '刚刚';
} elseif ($diff < 3600) {
return floor($diff / 60) . '分钟前';
} elseif ($diff < 86400) {
return floor($diff / 3600) . '小时前';
} else {
return date('m-d H:i', $timestamp);
}
}
public function total() public function total()
{ {
......
...@@ -3,149 +3,314 @@ ...@@ -3,149 +3,314 @@
<script src="__CDN__/assets/addons/shopro/libs/echarts.min.js?v={$site.version|htmlentities}"></script> <script src="__CDN__/assets/addons/shopro/libs/echarts.min.js?v={$site.version|htmlentities}"></script>
<style> <style>
.dashboard-index .panel-block { /* 新增统计卡片样式 - 白色主题 */
/* background: #f1f4f6; */ .stats-center {
background: #ffffff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid #e5e7eb;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
} }
.stats-center-header {
.dashboard-index .dashboard-index-main { display: flex;
--el-main-padding: 0; justify-content: space-between;
align-items: center;
margin-bottom: 20px;
} }
.stats-center-title {
.dashboard-index .el-scrollbar__wrap { font-size: 18px;
overflow-x: hidden; font-weight: 600;
color: #1f2937;
display: flex;
align-items: center;
} }
.stats-center-title i {
.dashboard-index .el-scrollbar__bar.is-horizontal { margin-right: 8px;
display: none; color: #3b82f6;
} }
.stats-center-time {
.dashboard-index #userTotal, font-size: 12px;
.dashboard-index #agentTotal, color: #9ca3af;
.dashboard-index #shareTotal {
width: 100%;
height: 66px;
} }
.stats-card-row {
.dashboard-index .card { display: flex;
line-height: 1; gap: 16px;
background: var(--sa-background-assist); flex-wrap: wrap;
border-radius: 8px; }
border: 1px solid var(--sa-space); .stats-card {
box-shadow: 0 2px 6px rgb(140 140 140 / 12%); background: #f9fafb;
padding: 12px; border-radius: 12px;
padding: 18px 20px;
flex: 1;
min-width: 140px;
position: relative;
overflow: hidden;
border: 1px solid #e5e7eb;
transition: all 0.2s ease;
}
.stats-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
.stats-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
}
.stats-card.card-1::before { background: linear-gradient(180deg, #667eea 0%, #764ba2 100%); }
.stats-card.card-2::before { background: linear-gradient(180deg, #10b981 0%, #059669 100%); }
.stats-card.card-3::before { background: linear-gradient(180deg, #f59e0b 0%, #d97706 100%); }
.stats-card.card-4::before { background: linear-gradient(180deg, #ef4444 0%, #dc2626 100%); }
.stats-card.card-5::before { background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%); }
.stats-card.card-6::before { background: linear-gradient(180deg, #8b5cf6 0%, #7c3aed 100%); }
.stats-card.card-7::before { background: linear-gradient(180deg, #ec4899 0%, #db2777 100%); }
.stats-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.stats-card-icon {
width: 36px;
height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.card-1 .stats-card-icon { background: rgba(102, 126, 234, 0.15); color: #667eea; }
.card-2 .stats-card-icon { background: rgba(16, 185, 129, 0.15); color: #10b981; }
.card-3 .stats-card-icon { background: rgba(245, 158, 11, 0.15); color: #f59e0b; }
.card-4 .stats-card-icon { background: rgba(239, 68, 68, 0.15); color: #ef4444; }
.card-5 .stats-card-icon { background: rgba(59, 130, 246, 0.15); color: #3b82f6; }
.card-6 .stats-card-icon { background: rgba(139, 92, 246, 0.15); color: #8b5cf6; }
.card-7 .stats-card-icon { background: rgba(236, 72, 153, 0.15); color: #ec4899; }
.stats-card-label {
font-size: 13px;
color: #6b7280;
font-weight: 500;
}
.stats-card-value {
font-size: 26px;
font-weight: 700;
color: #1f2937;
margin-bottom: 6px;
line-height: 1.2;
}
.stats-card-trend {
display: flex;
align-items: center;
font-size: 12px; font-size: 12px;
color: var(--sa-font);
margin-bottom: 20px;
} }
.stats-card-trend.up {
.dashboard-index .card:hover { color: #10b981;
transition: all .2s;
} }
.stats-card-trend.down {
@media screen and (min-width: 1200px) { color: #ef4444;
.dashboard-index .scale-card:hover {
transform: scale(1.05);
} }
.stats-card-trend i {
margin-right: 4px;
font-size: 10px;
} }
/* 时间筛选下拉框样式 */
.dashboard-index .card .card-title .left { .time-select-wrapper {
font-size: 14px; position: relative;
color: var(--sa-subtitle);
} }
.time-select {
.dashboard-index .card .card-title .left .num { background: #ffffff;
font-size: 16px; border: 1px solid #d1d5db;
border-radius: 8px;
padding: 8px 24px 8px 12px;
color: #374151;
font-size: 13px;
cursor: pointer;
appearance: none;
outline: none;
min-width: 100px;
transition: border-color 0.2s ease;
} }
.time-select:hover {
.dashboard-index .card .card-footer { border-color: #3b82f6;
margin-top: 12px;
} }
.time-select:focus {
.dashboard-index .card .card-footer .left { border-color: #3b82f6;
margin-right: 8px; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
} }
.time-select-wrapper::after {
.dashboard-index .card .card-footer .dot { content: '\f078';
width: 6px; font-family: 'FontAwesome';
height: 6px; position: absolute;
border-radius: 50%; right: 10px;
display: inline-block; top: 50%;
margin-right: 8px; transform: translateY(-50%);
color: #9ca3af;
pointer-events: none;
font-size: 12px;
} }
/* 图表卡片样式 */
.dashboard-index .bar-card { .chart-panel {
min-height: 358px; background: #ffffff;
border-radius: 12px;
border: 1px solid #e5e7eb;
padding: 20px;
margin-bottom: 20px;
} }
.chart-panel-header {
.dashboard-index #chartContent { display: flex;
width: 100%; justify-content: space-between;
height: 296px; align-items: center;
margin-bottom: 15px;
} }
.chart-panel-title {
.dashboard-index .tab-item {
height: 32px;
line-height: 32px;
margin-right: 20px;
font-size: 14px; font-size: 14px;
color: var(--sa-subfont); font-weight: 600;
cursor: pointer; color: #1f2937;
} }
.chart-panel-subtitle {
.dashboard-index .tab-item:last-of-type { font-size: 12px;
margin-right: 0; color: #9ca3af;
margin-top: 2px;
} }
.chart-tabs {
.dashboard-index .tab-item.is-active { display: flex;
color: var(--sa-subtitle); gap: 8px;
font-weight: bold;
} }
.chart-tab {
@media only screen and (max-width: 768px) { padding: 4px 12px;
.dashboard-index .date-time .el-date-editor { border-radius: 6px;
--el-date-editor-datetimerange-width: 320px; font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
color: #6b7280;
background: #f3f4f6;
} }
.chart-tab.active {
background: #3b82f6;
color: #ffffff;
} }
.chart-container {
.dashboard-index .chart-card { height: 220px;
height: 106px;
color: var(--sa-subfont);
} }
/* 转化漏斗样式 */
.dashboard-index .chart-card .card-icon { .funnel-item {
width: 24px; display: flex;
align-items: center;
margin-bottom: 12px;
}
.funnel-label {
width: 80px;
font-size: 13px;
color: #4b5563;
flex-shrink: 0;
}
.funnel-bar-wrapper {
flex: 1;
height: 24px; height: 24px;
background: #f3f4f6;
border-radius: 12px;
overflow: hidden;
} }
.funnel-bar {
.dashboard-index .chart-card .num { height: 100%;
font-size: 24px; border-radius: 12px;
color: var(--sa-subtitle); transition: width 0.5s ease;
display: flex;
align-items: center;
padding-left: 12px;
color: #ffffff;
font-size: 12px;
font-weight: 500;
} }
.funnel-bar.color-1 { background: linear-gradient(90deg, #3b82f6, #60a5fa); }
.dashboard-index .chart-card .warning { .funnel-bar.color-2 { background: linear-gradient(90deg, #06b6d4, #22d3ee); }
color: var(--el-color-warning); .funnel-bar.color-3 { background: linear-gradient(90deg, #10b981, #34d399); }
.funnel-bar.color-4 { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
.funnel-bar.color-5 { background: linear-gradient(90deg, #ec4899, #f472b6); }
/* 客单价分布 */
.price-dist-item {
display: flex;
align-items: center;
margin-bottom: 10px;
} }
.price-dist-label {
.dashboard-index .goods-card, width: 100px;
.dashboard-index .hot-search-card { font-size: 12px;
height: 326px; color: #6b7280;
flex-shrink: 0;
} }
.price-dist-bar-wrapper {
.dashboard-index .goods-card .header, flex: 1;
.dashboard-index .hot-search-card .header { height: 16px;
line-height: 16px; background: #f3f4f6;
font-size: 14px; border-radius: 8px;
color: var(--sa-subtitle); overflow: hidden;
margin-bottom: 16px;
} }
.price-dist-bar {
.dashboard-index .top { height: 100%;
width: 18px; border-radius: 8px;
height: 20px; background: linear-gradient(90deg, #8b5cf6, #a78bfa);
} }
.price-dist-percent {
.dashboard-index #rankingContent { width: 40px;
width: 192px; text-align: right;
height: 192px; font-size: 12px;
color: #9ca3af;
margin-left: 10px;
}
/* 实时订单动态 */
.order-list {
max-height: 200px;
overflow-y: auto;
}
.order-item {
display: flex;
align-items: center;
padding: 12px;
background: #f9fafb;
border-radius: 8px;
margin-bottom: 10px;
border: 1px solid #e5e7eb;
}
.order-status {
width: 10px;
height: 10px;
border-radius: 50%;
background: #10b981;
margin-right: 12px;
}
.order-info {
flex: 1;
}
.order-amount {
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
.order-time {
font-size: 11px;
color: #9ca3af;
margin-top: 2px;
}
.order-tag {
padding: 4px 8px;
background: #dcfce7;
color: #166534;
border-radius: 4px;
font-size: 11px;
}
/* 注册用户增长数字 */
.growth-badge {
background: linear-gradient(135deg, #10b981, #059669);
color: #ffffff;
padding: 4px 12px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
} }
</style> </style>
...@@ -153,230 +318,487 @@ ...@@ -153,230 +318,487 @@
<el-container class="panel-block"> <el-container class="panel-block">
<el-main class="dashboard-index-main"> <el-main class="dashboard-index-main">
<el-scrollbar height="100%"> <el-scrollbar height="100%">
{if $auth->check('shopro/dashboard/total')} <!--统计图 Begin-->
<el-row :gutter="20">
<!-- -- commission code start -- --> <!-- 新增统计卡片区域 - 白色主题 -->
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8"> <div class="stats-center">
<div class="scale-card card "> <div class="stats-center-header">
<div class="card-title sa-flex sa-row-between"> <div>
<div class="left">{{state.total.user.title}} <span <div class="stats-center-title">
class="num">{{state.total.user.data.total}}</span> </div> <i class="fa fa-bar-chart"></i>
<div>{{state.total.user.tip}} <span>{{state.total.user.data.today}}</span> </div> 数据统计中心
</div> </div>
<div id="userTotal"></div> <div class="stats-center-time">实时业务数据监控 · 更新于 {:date('Y-m-d H:i:s')}</div>
<div class="card-footer sa-flex"> </div>
<span class="left sa-flex"> <!-- 时间筛选下拉框 -->
<span class="dot" :style="{ background: state.total.user.color }"></span> <div class="time-select-wrapper">
<span>{{ state.total.user.footer }}</span> <select id="time_range" class="time-select" onchange="loadStatistics(this.value)">
</span> <option value="7days">近7天</option>
<span>{{ state.total.user.data.week }}</span> <option value="30days" selected>近30天</option>
</div> <option value="90days">近90天</option>
</div> </select>
</el-col> </div>
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8"> </div>
<div class="scale-card card "> <div class="stats-card-row">
<div class="card-title sa-flex sa-row-between"> <div class="stats-card card-1">
<div class="left">{{state.total.agent.title}} <span <div class="stats-card-header">
class="num">{{state.total.agent.data.total}}</span> </div> <span class="stats-card-label">成交总额</span>
<div>{{state.total.agent.tip}} <span>{{state.total.agent.data.today}}</span> </div> <span class="stats-card-icon"><i class="fa fa-money"></i></span>
</div> </div>
<div id="agentTotal"></div> <div class="stats-card-value" id="stat-total-amount">¥0.00</div>
<div class="card-footer sa-flex"> <div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-total-trend">+0%</span> 较上月</div>
<span class="left sa-flex"> </div>
<span class="dot" :style="{ background: state.total.agent.color }"></span> <div class="stats-card card-2">
<span>{{ state.total.agent.footer }}</span> <div class="stats-card-header">
</span> <span class="stats-card-label">净利润率</span>
<span>{{ state.total.agent.data.week }}</span> <span class="stats-card-icon"><i class="fa fa-percent"></i></span>
</div> </div>
</div> <div class="stats-card-value" id="stat-profit-rate">0%</div>
</el-col> <div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-profit-trend">+0%</span> 较上月</div>
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8"> </div>
<div class="scale-card card "> <div class="stats-card card-3">
<div class="card-title sa-flex sa-row-between"> <div class="stats-card-header">
<div class="left">{{state.total.share.title}} <span <span class="stats-card-label">客单价</span>
class="num">{{state.total.share.data.total}}</span> </div> <span class="stats-card-icon"><i class="fa fa-shopping-cart"></i></span>
<div>{{state.total.share.tip}} <span>{{state.total.share.data.today}}</span> </div> </div>
</div> <div class="stats-card-value" id="stat-avg-amount">¥0.00</div>
<div id="shareTotal"></div> <div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-avg-trend">+0%</span> 较上月</div>
<div class="card-footer sa-flex"> </div>
<span class="left sa-flex"> <div class="stats-card card-4">
<span class="dot" :style="{ background: state.total.share.color }"></span> <div class="stats-card-header">
<span>{{ state.total.share.footer }}</span> <span class="stats-card-label">订单数</span>
</span> <span class="stats-card-icon"><i class="fa fa-file-text"></i></span>
<span>{{ state.total.share.data.week }}</span> </div>
</div> <div class="stats-card-value" id="stat-order-count">0</div>
</div> <div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-order-trend">+0%</span> 较上月</div>
</el-col> </div>
<!-- -- commission code end -- --> <div class="stats-card card-5">
<!-- <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12"> <div class="stats-card-header">
<div class="scale-card card "> <span class="stats-card-label">访客数</span>
<div class="card-title sa-flex sa-row-between"> <span class="stats-card-icon"><i class="fa fa-users"></i></span>
<div class="left">{{state.total.user.title}} <span </div>
class="num">{{state.total.user.data.total}}</span> </div> <div class="stats-card-value" id="stat-visitor-count">0</div>
<div>{{state.total.user.tip}} <span>{{state.total.user.data.today}}</span> </div> <div class="stats-card-trend" id="stat-visitor-trend"><i class="fa fa-arrow-down"></i> <span>-0%</span> 较上月</div>
</div> </div>
<div id="userTotal"></div> <div class="stats-card card-6">
<div class="card-footer sa-flex"> <div class="stats-card-header">
<span class="left sa-flex"> <span class="stats-card-label">转化率</span>
<span class="dot" :style="{ background: state.total.user.color }"></span> <span class="stats-card-icon"><i class="fa fa-line-chart"></i></span>
<span>{{ state.total.user.footer }}</span> </div>
</span> <div class="stats-card-value" id="stat-conversion-rate">0%</div>
<span>{{ state.total.user.data.week }}</span> <div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-conversion-trend">+0%</span> 较上月</div>
</div> </div>
</div> <div class="stats-card card-7">
</el-col> <div class="stats-card-header">
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12"> <span class="stats-card-label">注册用户</span>
<div class="scale-card card "> <span class="stats-card-icon"><i class="fa fa-user-plus"></i></span>
<div class="card-title sa-flex sa-row-between"> </div>
<div class="left">{{state.total.share.title}} <span <div class="stats-card-value" id="stat-register-users">0</div>
class="num">{{state.total.share.data.total}}</span> </div> <div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-register-trend">+0(+0%)</span></div>
<div>{{state.total.share.tip}} <span>{{state.total.share.data.today}}</span> </div> </div>
</div> </div>
<div id="shareTotal"></div> </div>
<div class="card-footer sa-flex">
<span class="left sa-flex"> <!-- 图表区域 -->
<span class="dot" :style="{ background: state.total.share.color }"></span> <div class="row">
<span>{{ state.total.share.footer }}</span> <div class="col-lg-6">
</span> <div class="chart-panel">
<span>{{ state.total.share.data.week }}</span> <div class="chart-panel-header">
</div> <div>
</div> <div class="chart-panel-title">成交总额趋势</div>
</el-col> --> <div class="chart-panel-subtitle">近30天每日GMV走势</div>
</el-row> </div>
{/if} <div class="chart-tabs">
{if $auth->check('shopro/dashboard/chart')} <span class="chart-tab active" onclick="switchChartTab('gmv', 'day')"></span>
<el-row :gutter="20"> <span class="chart-tab" onclick="switchChartTab('gmv', 'week')"></span>
<el-col :xs="24" :sm="24" :md="24" :lg="16" :xl="16"> <span class="chart-tab" onclick="switchChartTab('gmv', 'month')"></span>
<div class="bar-card card"> </div>
<div class="sa-flex sa-row-between sa-flex-wrap"> </div>
<div class="sa-flex"> <div id="gmv-chart" class="chart-container"></div>
<div class="tab-item" :class="chart.tabActive == key ? 'is-active' : ''" </div>
v-for="(value, key) in chart.tabsData" :key="key" </div>
@click="onChangeTabActive(key)"> <div class="col-lg-6">
{{ value }} <div class="chart-panel">
</div> <div class="chart-panel-header">
</div> <div>
<div class="date-time"> <div class="chart-panel-title">订单数趋势</div>
<el-date-picker v-model="chart.dateTime" type="datetimerange" <div class="chart-panel-subtitle">近30天订单量走势</div>
value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss" </div>
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)]" <div class="chart-tabs">
range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" <span class="chart-tab active" onclick="switchChartTab('order', 'day')"></span>
:shortcuts="chart.shortcuts" @change="onChangeDateTime" :editable="false"> <span class="chart-tab" onclick="switchChartTab('order', 'week')"></span>
</el-date-picker> <span class="chart-tab" onclick="switchChartTab('order', 'month')"></span>
</div> </div>
</div> </div>
<div id="chartContent"></div> <div id="order-chart" class="chart-container"></div>
</div> </div>
</el-col> </div>
<el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8"> </div>
<el-row :gutter="20"> <div class="row">
<el-col :xs="12" :sm="8" :md="12" :lg="12" :xl="12" v-for="(value, key) in statistics" <div class="col-lg-4">
:key="key"> <div class="chart-panel">
<div class="chart-card scale-card card"> <div class="chart-panel-header">
<div class="sa-flex sa-row-between mb-2"> <div>
<img class="card-icon" <div class="chart-panel-title">转化漏斗</div>
:src="`/assets/addons/shopro/img/dashboard/${key}.png`" /> <div class="chart-panel-subtitle">用户行为转化路径</div>
<div class="sa-flex" @click="onOpen(value.status)"> </div>
<el-button type="info" link size="small">详情 </div>
<el-icon class="ml-1"> <div class="funnel-container" id="funnel-container">
<arrow-right /> <div class="funnel-item">
</el-icon> <span class="funnel-label">访问</span>
</el-button> <div class="funnel-bar-wrapper">
</div> <div class="funnel-bar color-1" id="funnel-visit" style="width: 100%">0</div>
</div> </div>
<div class="num mb-1">{{ value.num }}</div> </div>
<div class="sa-flex sa-row-between"> <div class="funnel-item">
<span>{{ value.text }}</span> <span class="funnel-label">浏览商品</span>
<el-popover popper-class="sa-popper" placement="top" trigger="hover" <div class="funnel-bar-wrapper">
:content="value.tip"> <div class="funnel-bar color-2" id="funnel-browse" style="width: 50%">0</div>
<template #reference> </div>
<el-icon class="warning"> </div>
<Warning /> <div class="funnel-item">
</el-icon> <span class="funnel-label">加入购物车</span>
</template> <div class="funnel-bar-wrapper">
</el-popover> <div class="funnel-bar color-3" id="funnel-cart" style="width: 30%">0</div>
</div> </div>
</div> </div>
</el-col> <div class="funnel-item">
</el-row> <span class="funnel-label">提交订单</span>
</el-col> <div class="funnel-bar-wrapper">
</el-row> <div class="funnel-bar color-4" id="funnel-submit" style="width: 20%">0</div>
{/if} </div>
{if $auth->check('shopro/dashboard/ranking')} </div>
<el-row :gutter="20"> <div class="funnel-item">
<el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12"> <span class="funnel-label">完成支付</span>
<div class="goods-card card"> <div class="funnel-bar-wrapper">
<div class="header"> <div class="funnel-bar color-5" id="funnel-pay" style="width: 10%">0</div>
<span>销量榜</span> </div>
</div> </div>
<el-table class="sa-table" :data="ranking.goods" stripe> </div>
<el-table-column label="排名" width="60" align="center"> </div>
<template #default="scope"> </div>
<img class="top" <div class="col-lg-8">
:src="`/assets/addons/shopro/img/dashboard/top${scope.$index + 1}.png`" /> <div class="chart-panel">
</template> <div class="chart-panel-header">
</el-table-column> <div>
<el-table-column label="名称" min-width="200"> <div class="chart-panel-title">注册用户增长</div>
<template #default="scope"> <div class="chart-panel-subtitle">近30天新增用户趋势</div>
<div class="sa-flex"> </div>
<sa-image :url="scope.row.image" size="20"></sa-image> <span class="growth-badge" id="growth-badge">+0</span>
<div class="ml-2 sa-table-line-1"> </div>
{{ scope.row.title || '-' }} <div id="user-growth-chart" class="chart-container"></div>
</div> </div>
</div> </div>
</template> </div>
</el-table-column> <div class="row">
<el-table-column label="销量" width="100"> <div class="col-lg-4">
<template #default="scope"> <div class="chart-panel">
<div class="sa-table-line-1"> <div class="chart-panel-header">
{{ scope.row.real_sales || '-' }} <div>
</div> <div class="chart-panel-title">客单价分布</div>
</template> <div class="chart-panel-subtitle">不同价格区间订单占比</div>
</el-table-column> </div>
</el-table> </div>
</div> <div class="price-dist-container" id="price-dist-container">
</el-col> <div class="price-dist-item">
<el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12"> <span class="price-dist-label">0-100元</span>
<div class="hot-search-card card"> <div class="price-dist-bar-wrapper">
<div class="header"> <div class="price-dist-bar" id="price-0-100" style="width: 0%"></div>
<span>热搜榜</span> </div>
</div> <span class="price-dist-percent" id="price-0-100-percent">0%</span>
<el-row :gutter="16"> </div>
<el-col :xs="24" :sm="24" :md="16" :lg="16" :xl="16"> <div class="price-dist-item">
<el-table class="sa-table" :data="ranking.hot_search" stripe> <span class="price-dist-label">100-300元</span>
<el-table-column label="排名" width="60" align="center"> <div class="price-dist-bar-wrapper">
<template #default="scope"> <div class="price-dist-bar" id="price-100-300" style="width: 0%"></div>
<img class="top" </div>
:src="`/assets/addons/shopro/img/dashboard/top${scope.$index + 1}.png`" /> <span class="price-dist-percent" id="price-100-300-percent">0%</span>
</template> </div>
</el-table-column> <div class="price-dist-item">
<el-table-column label="名称" min-width="200"> <span class="price-dist-label">300-500元</span>
<template #default="scope"> <div class="price-dist-bar-wrapper">
<div class="sa-table-line-1"> <div class="price-dist-bar" id="price-300-500" style="width: 0%"></div>
{{ scope.row.keyword || '-' }} </div>
</div> <span class="price-dist-percent" id="price-300-500-percent">0%</span>
</template> </div>
</el-table-column> <div class="price-dist-item">
<el-table-column label="搜索量(次)" width="100"> <span class="price-dist-label">500-1000元</span>
<template #default="scope"> <div class="price-dist-bar-wrapper">
<div class="sa-table-line-1"> <div class="price-dist-bar" id="price-500-1000" style="width: 0%"></div>
{{ scope.row.num }} </div>
</div> <span class="price-dist-percent" id="price-500-1000-percent">0%</span>
</template> </div>
</el-table-column> <div class="price-dist-item">
</el-table> <span class="price-dist-label">1000元以上</span>
</el-col> <div class="price-dist-bar-wrapper">
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8"> <div class="price-dist-bar" id="price-1000" style="width: 0%"></div>
<div class="sa-flex sa-row-center"> </div>
<div id="rankingContent"></div> <span class="price-dist-percent" id="price-1000-percent">0%</span>
</div> </div>
</el-col> </div>
</el-row> </div>
</div> </div>
</el-col> <div class="col-lg-8">
</el-row> <div class="chart-panel">
{/if} <div class="chart-panel-header">
<div>
<div class="chart-panel-title">实时订单动态</div>
<div class="chart-panel-subtitle">最新成交订单</div>
</div>
</div>
<div class="order-list" id="order-list">
<div class="order-item">
<span class="order-status"></span>
<div class="order-info">
<div class="order-amount">¥ 0.00</div>
<div class="order-time">加载中...</div>
</div>
<span class="order-tag">成功</span>
</div>
</div>
</div>
</div>
</div>
<!-- 图表初始化脚本 -->
<!--统计图 End-->
</el-scrollbar> </el-scrollbar>
</el-main> </el-main>
</el-container> </el-container>
</div> </div>
<script type="text/javascript">
var gmvChart, orderChart, userGrowthChart;
var currentTimeRange = '30days';
function generateDateLabels(days) {
var labels = [];
for (var i = days - 1; i >= 0; i--) {
var date = new Date();
date.setDate(date.getDate() - i);
labels.push((date.getMonth() + 1) + '月' + date.getDate() + '日');
}
return labels;
}
function generateRandomData(length, min, max) {
var data = [];
for (var i = 0; i < length; i++) {
data.push(Math.floor(Math.random() * (max - min + 1)) + min);
}
return data;
}
function initCharts() {
gmvChart = echarts.init(document.getElementById('gmv-chart'));
var gmvOption = {
grid: { top: 20, right: 20, bottom: 30, left: 50 },
xAxis: {
type: 'category',
data: generateDateLabels(30),
axisLine: { lineStyle: { color: '#e5e7eb' } },
axisLabel: { color: '#9ca3af', fontSize: 10 }
},
yAxis: {
type: 'value',
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: '#f3f4f6' } },
axisLabel: {
color: '#9ca3af',
fontSize: 10,
formatter: function(value) { return '¥' + (value / 10000).toFixed(0) + '万'; }
}
},
series: [{
type: 'line',
smooth: true,
data: generateRandomData(30, 50000, 150000),
lineStyle: { color: '#667eea', width: 3 },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(102, 126, 234, 0.3)' },
{ offset: 1, color: 'rgba(102, 126, 234, 0.05)' }
])
},
symbol: 'none'
}]
};
gmvChart.setOption(gmvOption);
orderChart = echarts.init(document.getElementById('order-chart'));
var orderOption = {
grid: { top: 20, right: 20, bottom: 30, left: 50 },
xAxis: {
type: 'category',
data: generateDateLabels(30),
axisLine: { lineStyle: { color: '#e5e7eb' } },
axisLabel: { color: '#9ca3af', fontSize: 10 }
},
yAxis: {
type: 'value',
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: '#f3f4f6' } },
axisLabel: { color: '#9ca3af', fontSize: 10 }
},
series: [{
type: 'bar',
data: generateRandomData(30, 1500, 4000),
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#10b981' },
{ offset: 1, color: '#34d399' }
]),
borderRadius: [4, 4, 0, 0]
}
}]
};
orderChart.setOption(orderOption);
userGrowthChart = echarts.init(document.getElementById('user-growth-chart'));
var userGrowthOption = {
grid: { top: 20, right: 20, bottom: 30, left: 50 },
legend: {
data: ['新增用户', '活跃用户'],
top: 0,
textStyle: { color: '#6b7280', fontSize: 12 }
},
xAxis: {
type: 'category',
data: generateDateLabels(30),
axisLine: { lineStyle: { color: '#e5e7eb' } },
axisLabel: { color: '#9ca3af', fontSize: 10 }
},
yAxis: {
type: 'value',
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: '#f3f4f6' } },
axisLabel: {
color: '#9ca3af',
fontSize: 10,
formatter: function(value) { return (value / 10000).toFixed(0) + '万'; }
}
},
series: [
{
name: '新增用户',
type: 'line',
smooth: true,
data: generateRandomData(30, 2000, 8000),
lineStyle: { color: '#f59e0b', width: 2 },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(245, 158, 11, 0.2)' },
{ offset: 1, color: 'rgba(245, 158, 11, 0.02)' }
])
},
symbol: 'none'
},
{
name: '活跃用户',
type: 'line',
smooth: true,
data: generateRandomData(30, 8000, 15000),
lineStyle: { color: '#06b6d4', width: 2 },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(6, 182, 212, 0.2)' },
{ offset: 1, color: 'rgba(6, 182, 212, 0.02)' }
])
},
symbol: 'none'
}
]
};
userGrowthChart.setOption(userGrowthOption);
window.addEventListener('resize', function() {
gmvChart.resize();
orderChart.resize();
userGrowthChart.resize();
});
}
function switchChartTab(type, period) {
console.log('Switching ' + type + ' chart to ' + period);
}
function loadStatistics(timeRange) {
currentTimeRange = timeRange;
$.getJSON('{:url("shopro/dashboard/statistics")}', { time_range: timeRange }, function(res) {
if (res.code === 1) {
var data = res.data;
document.getElementById('stat-total-amount').textContent = '¥' + formatNumber(data.total_amount, 2);
document.getElementById('stat-profit-rate').textContent = data.profit_rate;
document.getElementById('stat-avg-amount').textContent = '¥' + formatNumber(data.avg_amount, 2);
document.getElementById('stat-order-count').textContent = formatNumber(data.order_count);
document.getElementById('stat-visitor-count').textContent = formatNumber(data.visitor_count);
document.getElementById('stat-conversion-rate').textContent = data.conversion_rate;
document.getElementById('stat-register-users').textContent = formatNumber(data.register_users);
}
});
loadFunnel(timeRange);
loadPriceDistribution(timeRange);
loadRecentOrders();
}
function loadFunnel(timeRange) {
$.getJSON('{:url("shopro/dashboard/funnel")}', { time_range: timeRange }, function(res) {
if (res.code === 1) {
var data = res.data;
var max = data.visit || 1;
document.getElementById('funnel-visit').textContent = formatNumber(data.visit);
var browsePercent = max > 0 ? Math.round(data.browse / max * 100) : 0;
document.getElementById('funnel-browse').textContent = formatNumber(data.browse);
document.getElementById('funnel-browse').style.width = browsePercent + '%';
var cartPercent = max > 0 ? Math.round(data.cart / max * 100) : 0;
document.getElementById('funnel-cart').textContent = formatNumber(data.cart);
document.getElementById('funnel-cart').style.width = cartPercent + '%';
var submitPercent = max > 0 ? Math.round(data.submit_order / max * 100) : 0;
document.getElementById('funnel-submit').textContent = formatNumber(data.submit_order);
document.getElementById('funnel-submit').style.width = submitPercent + '%';
var payPercent = max > 0 ? Math.round(data.pay / max * 100) : 0;
document.getElementById('funnel-pay').textContent = formatNumber(data.pay);
document.getElementById('funnel-pay').style.width = payPercent + '%';
}
});
}
function loadPriceDistribution(timeRange) {
$.getJSON('{:url("shopro/dashboard/priceDistribution")}', { time_range: timeRange }, function(res) {
if (res.code === 1) {
var data = res.data;
document.getElementById('price-0-100').style.width = data['0-100'] + '%';
document.getElementById('price-0-100-percent').textContent = data['0-100'] + '%';
document.getElementById('price-100-300').style.width = data['100-300'] + '%';
document.getElementById('price-100-300-percent').textContent = data['100-300'] + '%';
document.getElementById('price-300-500').style.width = data['300-500'] + '%';
document.getElementById('price-300-500-percent').textContent = data['300-500'] + '%';
document.getElementById('price-500-1000').style.width = data['500-1000'] + '%';
document.getElementById('price-500-1000-percent').textContent = data['500-1000'] + '%';
document.getElementById('price-1000').style.width = data['1000+'] + '%';
document.getElementById('price-1000-percent').textContent = data['1000+'] + '%';
}
});
}
function loadRecentOrders() {
$.getJSON('{:url("shopro/dashboard/recentOrders")}', function(res) {
if (res.code === 1) {
var orders = res.data;
var html = '';
orders.forEach(function(order) {
html += '<div class="order-item">' +
'<span class="order-status"></span>' +
'<div class="order-info">' +
'<div class="order-amount">¥ ' + formatNumber(order.amount, 2) + '</div>' +
'<div class="order-time">' + order.location + ' · ' + order.time + '</div>' +
'</div>' +
'<span class="order-tag">成功</span>' +
'</div>';
});
document.getElementById('order-list').innerHTML = html;
}
});
}
function formatNumber(num, decimals) {
if (decimals !== undefined) {
return parseFloat(num || 0).toFixed(decimals);
}
return parseInt(num || 0).toLocaleString();
}
document.addEventListener('DOMContentLoaded', function() {
initCharts();
loadStatistics('30days');
});
</script>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment