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

数据中心优化

parent 32f0f5e6
......@@ -36,24 +36,6 @@ class Dashboard extends Common
*/
public function index()
{
// $cardList = [
// [
// 'name' => 'total',
// 'card' => 'shopro/dashboard/total',
// 'status' => $this->auth->check('shopro/dashboard/total')
// ],
// [
// 'name' => 'chart',
// 'card' => 'shopro/dashboard/chart',
// 'status' => $this->auth->check('shopro/dashboard/chart')
// ],
// [
// 'name' => 'ranking',
// 'card' => 'shopro/dashboard/ranking',
// 'status' => $this->auth->check('shopro/dashboard/ranking')
// ],
// ];
// $this->assignconfig('cardList', $cardList);
return $this->view->fetch();
}
......@@ -90,10 +72,20 @@ class Dashboard extends Common
$cacheKey = 'shopro:data_stats_center:' . $timeRange;
$cachedData = cache($cacheKey);
// 获取最新统计记录的更新时间
$updateTime = Db::name('shopro_data_stats_center')
->order('created_at', 'desc')
->value('created_at');
if ($cachedData !== false && isset($cachedData['summary']) && isset($cachedData['trend'])) {
$summary = $cachedData['summary'];
$trendData = $this->buildTrendDataForChart($cachedData['trend'], $period);
// 较上周期 效果计算
// - 上一周期和当前周期数据都是0时 → 显示"持平"
// - 上一周期是0但当前周期有数据 → 显示"+100%"
// - 上一周期有数据但当前周期是0 → 显示"-100%"
// - 两个周期都有数据时 → 显示实际百分比
$result = [
'total_amount' => $summary['total_amount']['value'],
'total_amount_compare_rate' => $summary['total_amount']['compare_rate'],
......@@ -110,7 +102,8 @@ class Dashboard extends Common
'register_users' => $summary['register_users']['value'],
'register_users_compare_rate' => $summary['register_users']['compare_rate'],
'trend_data' => $trendData,
'from_cache' => true
'from_cache' => true,
'update_time' => $updateTime ? date('Y-m-d H:i:s', $updateTime) : date('Y-m-d H:i:s')
];
$this->success('获取成功', null, $result);
......@@ -121,14 +114,22 @@ class Dashboard extends Common
$result = [
'total_amount' => $trendData['total_amount'],
'total_amount_compare_rate' => $trendData['total_amount_compare_rate'] ?? 0,
'profit_rate' => $trendData['profit_rate'],
'profit_rate_compare_rate' => $trendData['profit_rate_compare_rate'] ?? 0,
'avg_amount' => $trendData['avg_amount'],
'avg_amount_compare_rate' => $trendData['avg_amount_compare_rate'] ?? 0,
'order_count' => $trendData['order_count'],
'order_count_compare_rate' => $trendData['order_count_compare_rate'] ?? 0,
'visitor_count' => $trendData['visitor_count'],
'visitor_count_compare_rate' => $trendData['visitor_count_compare_rate'] ?? 0,
'conversion_rate' => $trendData['conversion_rate'],
'conversion_rate_compare_rate' => $trendData['conversion_rate_compare_rate'] ?? 0,
'register_users' => $trendData['register_users'],
'register_users_compare_rate' => $trendData['register_users_compare_rate'] ?? 0,
'trend_data' => $trendData['trend_data'],
'from_cache' => false
'from_cache' => false,
'update_time' => $updateTime ? date('Y-m-d H:i:s', $updateTime) : date('Y-m-d H:i:s')
];
$this->success('获取成功', null, $result);
......@@ -327,6 +328,7 @@ class Dashboard extends Common
$orderCount = 0;
$registerUsers = 0;
$visitorCount = 0;
$buyerCount = 0;
$format = $this->getDateFormat($period);
......@@ -343,13 +345,16 @@ class Dashboard extends Common
->whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED])
->count();
$buyerNum = Order::whereBetween('createtime', [$startTime, $endTime])
->whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED])
->where('user_id', '>', 0)
->distinct('user_id')
->count('user_id');
$registerNum = User::whereBetween('createtime', [$startTime, $endTime])->count();
try {
$visitorNum = Db::name('shopro_user_goods_log')
->whereBetween('createtime', [$startTime, $endTime])
->distinct(true)
->count('user_id');
$visitorNum = $this->getVisitorCountFromTrack($startTime, $endTime);
} catch (\Exception $e) {
$visitorNum = 0;
}
......@@ -365,20 +370,63 @@ class Dashboard extends Common
$orderCount += $orderNum;
$registerUsers += $registerNum;
$visitorCount += $visitorNum;
$buyerCount += $buyerNum;
}
$previousStartTime = $startTime - ($days * 86400);
$previousEndTime = $startTime - 1;
$previousAmount = Order::whereBetween('createtime', [$previousStartTime, $previousEndTime])
->whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED])
->sum('pay_fee') ?: 0;
$previousOrderNum = Order::whereBetween('createtime', [$previousStartTime, $previousEndTime])
->whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED])
->count();
$previousBuyerNum = Order::whereBetween('createtime', [$previousStartTime, $previousEndTime])
->whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED])
->where('user_id', '>', 0)
->distinct('user_id')
->count('user_id');
$previousRegisterNum = User::whereBetween('createtime', [$previousStartTime, $previousEndTime])->count();
try {
$previousVisitorNum = $this->getVisitorCountFromTrack($previousStartTime, $previousEndTime);
} catch (\Exception $e) {
$previousVisitorNum = 0;
}
$previousAvgAmount = $previousOrderNum > 0 ? bcdiv($previousAmount, $previousOrderNum, 2) : 0;
$previousConversionRate = $previousVisitorNum > 0 ? bcdiv($previousBuyerNum * 100, $previousVisitorNum, 2) : 0;
$avgAmount = $orderCount > 0 ? bcdiv($totalAmount, $orderCount, 2) : 0;
$profitRate = $totalAmount > 0 ? '18.6%' : '0%';
$conversionRate = $visitorCount > 0 ? bcdiv($orderCount * 100, $visitorCount, 2) . '%' : '0%';
$conversionRate = $visitorCount > 0 ? bcdiv($buyerCount * 100, $visitorCount, 2) . '%' : '0%';
$totalAmountCompareRate = $previousAmount > 0 ? round((($totalAmount - $previousAmount) / $previousAmount) * 100, 2) : ($totalAmount > 0 ? 100 : ($previousAmount == 0 ? null : -100));
$avgAmountCompareRate = $previousAvgAmount > 0 ? round((($avgAmount - $previousAvgAmount) / $previousAvgAmount) * 100, 2) : ($avgAmount > 0 ? 100 : ($previousAvgAmount == 0 ? null : -100));
$orderCountCompareRate = $previousOrderNum > 0 ? round((($orderCount - $previousOrderNum) / $previousOrderNum) * 100, 2) : ($orderCount > 0 ? 100 : ($previousOrderNum == 0 ? null : -100));
$visitorCountCompareRate = $previousVisitorNum > 0 ? round((($visitorCount - $previousVisitorNum) / $previousVisitorNum) * 100, 2) : ($visitorCount > 0 ? 100 : ($previousVisitorNum == 0 ? null : -100));
$conversionRateCompareRate = $previousConversionRate > 0 ? round((($conversionRate - $previousConversionRate) / $previousConversionRate) * 100, 2) : ($conversionRate > 0 ? 100 : ($previousConversionRate == 0 ? null : -100));
$registerUsersCompareRate = $previousRegisterNum > 0 ? round((($registerUsers - $previousRegisterNum) / $previousRegisterNum) * 100, 2) : ($registerUsers > 0 ? 100 : ($previousRegisterNum == 0 ? null : -100));
return [
'total_amount' => $totalAmount,
'total_amount_compare_rate' => $totalAmountCompareRate,
'order_count' => $orderCount,
'order_count_compare_rate' => $orderCountCompareRate,
'avg_amount' => $avgAmount,
'avg_amount_compare_rate' => $avgAmountCompareRate,
'profit_rate' => $profitRate,
'profit_rate_compare_rate' => 0,
'visitor_count' => $visitorCount,
'visitor_count_compare_rate' => $visitorCountCompareRate,
'conversion_rate' => $conversionRate,
'conversion_rate_compare_rate' => $conversionRateCompareRate,
'register_users' => $registerUsers,
'register_users_compare_rate' => $registerUsersCompareRate,
'trend_data' => $data
];
}
......@@ -630,4 +678,29 @@ class Dashboard extends Common
}
}
/**
* 从埋点表获取访客数
*
* 使用与定时任务相同的逻辑统计去重访客数
* 用户识别优先级:user_id > session_id > ip
*
* @param int $startTime 开始时间戳
* @param int $endTime 结束时间戳
* @return int 去重后的访客数量
*/
private function getVisitorCountFromTrack($startTime, $endTime)
{
$table = config('database.prefix') . 'shopro_miniprogram_track';
$sql = "SELECT COUNT(DISTINCT CASE
WHEN user_id > 0 THEN CONCAT('u_', user_id)
WHEN session_id <> '' THEN CONCAT('s_', session_id)
ELSE CONCAT('i_', ip)
END) AS total
FROM `{$table}`
WHERE create_time BETWEEN ? AND ?";
$result = Db::query($sql, [$startTime, $endTime]);
return isset($result[0]['total']) ? (int)$result[0]['total'] : 0;
}
}
\ No newline at end of file
......@@ -367,6 +367,7 @@
<el-container class="panel-block">
<el-main class="dashboard-index-main">
<el-scrollbar height="100%">
<div class="stats-center">
<div class="stats-center-header">
<div>
......@@ -377,23 +378,25 @@
<div class="stats-center-time">实时业务数据监控 · 更新于 <span id="update-time">{:date('Y-m-d H:i:s')}</span></div>
</div>
<div class="header-right">
<div class="data-status">
<!-- <div class="data-status">
<span class="status-dot"></span>
<span>数据同步中</span>
</div>
</div> -->
<div class="time-select-wrapper">
<select id="time_range" class="time-select" onchange="loadStatistics(this.value)">
<select id="time_range" class="time-select">
<option value="7days">近7天</option>
<option value="30days" selected>近30天</option>
<option value="90days">近90天</option>
</select>
</div>
<button class="refresh-btn" id="refreshBtn" onclick="refreshAllData()">
<button class="refresh-btn" id="refreshBtn">
<i class="fa fa-refresh"></i>
<span>刷新数据</span>
</button>
</div>
</div>
</div>
<div class="stats-center">
<div class="stats-card-row">
<div class="stats-card card-1">
<div class="stats-card-header">
......@@ -401,7 +404,7 @@
<span class="stats-card-icon"><i class="fa fa-money"></i></span>
</div>
<div class="stats-card-value" id="stat-total-amount">¥0.00</div>
<div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-total-trend">+0%</span> 较上</div>
<div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-total-trend">+0%</span> 较上周期</div>
</div>
<div class="stats-card card-2">
<div class="stats-card-header">
......@@ -409,7 +412,7 @@
<span class="stats-card-icon"><i class="fa fa-percent"></i></span>
</div>
<div class="stats-card-value" id="stat-profit-rate">0%</div>
<div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-profit-trend">+0%</span> 较上</div>
<div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-profit-trend">+0%</span> 较上周期</div>
</div>
<div class="stats-card card-3">
<div class="stats-card-header">
......@@ -417,7 +420,7 @@
<span class="stats-card-icon"><i class="fa fa-shopping-cart"></i></span>
</div>
<div class="stats-card-value" id="stat-avg-amount">¥0.00</div>
<div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-avg-trend">+0%</span> 较上</div>
<div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-avg-trend">+0%</span> 较上周期</div>
</div>
<div class="stats-card card-4">
<div class="stats-card-header">
......@@ -425,7 +428,7 @@
<span class="stats-card-icon"><i class="fa fa-file-text"></i></span>
</div>
<div class="stats-card-value" id="stat-order-count">0</div>
<div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-order-trend">+0%</span> 较上</div>
<div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-order-trend">+0%</span> 较上周期</div>
</div>
<div class="stats-card card-5">
<div class="stats-card-header">
......@@ -433,7 +436,7 @@
<span class="stats-card-icon"><i class="fa fa-users"></i></span>
</div>
<div class="stats-card-value" id="stat-visitor-count">0</div>
<div class="stats-card-trend" id="stat-visitor-trend"><i class="fa fa-arrow-down"></i> <span>-0%</span> 较上月</div>
<div class="stats-card-trend up" id="stat-visitor-trend"><i class="fa fa-arrow-up"></i> <span>+0%</span> 较上周期</div>
</div>
<div class="stats-card card-6">
<div class="stats-card-header">
......@@ -441,15 +444,7 @@
<span class="stats-card-icon"><i class="fa fa-line-chart"></i></span>
</div>
<div class="stats-card-value" id="stat-conversion-rate">0%</div>
<div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-conversion-trend">+0%</span> 较上月</div>
</div>
<div class="stats-card card-7">
<div class="stats-card-header">
<span class="stats-card-label">新增用户</span>
<span class="stats-card-icon"><i class="fa fa-user-plus"></i></span>
</div>
<div class="stats-card-value" id="stat-new-users">0</div>
<div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-new-trend">+0(+0%)</span></div>
<div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-conversion-trend">+0%</span> 较上周期</div>
</div>
<div class="stats-card card-8">
<div class="stats-card-header">
......@@ -457,7 +452,7 @@
<span class="stats-card-icon"><i class="fa fa-users"></i></span>
</div>
<div class="stats-card-value" id="stat-register-users">0</div>
<div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-register-trend">+0(+0%)</span></div>
<div class="stats-card-trend up"><i class="fa fa-arrow-up"></i> <span id="stat-register-trend">+0%</span> 较上周期</div>
</div>
</div>
</div>
......@@ -470,11 +465,11 @@
<div class="chart-panel-title">成交总额趋势</div>
<div class="chart-panel-subtitle" id="gmv-subtitle">近30天每日GMV走势</div>
</div>
<div class="chart-tabs">
<button class="chart-tab active" id="gmv-day" onclick="switchPeriod('gmv', 'day')"></button>
<button class="chart-tab" id="gmv-week" onclick="switchPeriod('gmv', 'week')"></button>
<button class="chart-tab" id="gmv-month" onclick="switchPeriod('gmv', 'month')"></button>
</div>
<!-- <div class="chart-tabs">
<button class="chart-tab active" id="gmv-day" data-chart="gmv" data-period="day">日</button>
<button class="chart-tab" id="gmv-week" data-chart="gmv" data-period="week">周</button>
<button class="chart-tab" id="gmv-month" data-chart="gmv" data-period="month">月</button>
</div> -->
</div>
<div id="gmv-chart" class="chart-container"></div>
</div>
......@@ -486,11 +481,11 @@
<div class="chart-panel-title">订单数趋势</div>
<div class="chart-panel-subtitle" id="order-subtitle">近30天订单量走势</div>
</div>
<div class="chart-tabs">
<button class="chart-tab active" id="order-day" onclick="switchPeriod('order', 'day')"></button>
<button class="chart-tab" id="order-week" onclick="switchPeriod('order', 'week')"></button>
<button class="chart-tab" id="order-month" onclick="switchPeriod('order', 'month')"></button>
</div>
<!-- <div class="chart-tabs">
<button class="chart-tab active" id="order-day" data-chart="order" data-period="day">日</button>
<button class="chart-tab" id="order-week" data-chart="order" data-period="week">周</button>
<button class="chart-tab" id="order-month" data-chart="order" data-period="month">月</button>
</div> -->
</div>
<div id="order-chart" class="chart-container"></div>
</div>
......@@ -548,7 +543,7 @@
</div>
<div>
<span class="growth-badge" id="growth-badge">+0</span>
<p class="growth-badge-text">累计注册用户</p>
<p class="chart-panel-subtitle">累计注册用户</p>
</div>
</div>
<div id="user-growth-chart" class="chart-container"></div>
......
......@@ -195,10 +195,10 @@ class DataStatsCenter extends Command
$currentRegisterTotal = $this->getRegisterCount(0, $currentRange['end']);
$previousRegisterTotal = $this->getRegisterCount(0, $previousRange['end']);
// 计算转化率(订单数/访客数)
// 计算转化率(购买用户数/访客数)
$this->customLog('info', '开始计算当前&上一期统计周期转化率');
$currentConversion = $currentVisitors > 0 ? round($currentRevenue['order_count'] / $currentVisitors * 100, 2) : 0;
$previousConversion = $previousVisitors > 0 ? round($previousRevenue['order_count'] / $previousVisitors * 100, 2) : 0;
$currentConversion = $currentVisitors > 0 ? round($currentRevenue['buyer_count'] / $currentVisitors * 100, 2) : 0;
$previousConversion = $previousVisitors > 0 ? round($previousRevenue['buyer_count'] / $previousVisitors * 100, 2) : 0;
// 构建趋势数据
$this->customLog('info', '开始构建当前统计周期趋势数据');
......@@ -332,6 +332,13 @@ class DataStatsCenter extends Command
->where('paid_time', 'between', [$startTime, $endTime])
->count();
// 统计购买用户数(去重)
$buyerCount = (int)Db::name('shopro_order')
->whereIn('status', ['paid', 'completed'])
->where('paid_time', 'between', [$startTime, $endTime])
->where('user_id', '>', 0)
->count('DISTINCT user_id');
// 计算客单价(成交额/订单数)
$avgAmount = $orderCount > 0 ? round($totalAmount / $orderCount, 2) : 0;
......@@ -355,6 +362,7 @@ class DataStatsCenter extends Command
return [
'total_amount' => $totalAmount,
'order_count' => $orderCount,
'buyer_count' => $buyerCount,
'avg_amount' => $avgAmount,
'cost_amount' => $costAmount,
'refund_amount' => $refundAmount,
......
......@@ -50,6 +50,9 @@ if (Config.modulename == 'admin' && Config.controllername == 'index' && Config.a
'ElementPlus': {
deps: ['css!../addons/shopro/libs/element-plus/index.css']
},
'ElementPlusIconsVue': {
deps: ['ElementPlusIconsVue3']
}
},
});
require(['vue3', 'ElementPlusIconsVue3'], function (Vue3, ElementPlusIconsVue3) {
......@@ -75,8 +78,11 @@ if (Config.modulename == 'admin' && Config.controllername == 'index' && Config.a
const app = createApp({})
app.use(ElementPlus)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
if (ElementPlusIconsVue && typeof ElementPlusIconsVue === 'object') {
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
}
app.component('sa-chat', SaChat)
......
......@@ -4,54 +4,32 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
index: () => {
const { reactive, onMounted } = Vue;
// ECharts图表实例对象
// gmvChart: 成交总额趋势图表
// orderChart: 订单数趋势图表
// userGrowthChart: 用户增长图表(活跃用户/新增用户)
var gmvChart, orderChart, userGrowthChart;
// 当前时间范围筛选条件,默认近30天
var currentTimeRange = '30days';
// 成交总额图表的周期(day/week/month)
var currentGmvPeriod = 'day';
// 订单数图表的周期(day/week/month)
var currentOrderPeriod = 'day';
const index = {
setup() {
const state = reactive({
total: {
user: {
data: {},
color: '#806AF6',
color1: 'rgba(128, 106, 246, 0.4)',
color2: 'rgba(128, 106, 246, 0)',
title: '用户数量',
tip: '今日新增用户',
footer: '本周新增用户人数',
},
agent: {
data: {},
color: '#409EFF',
color1: 'rgba(64, 158, 255, 0.4)',
color2: 'rgba(64, 158, 255, 0)',
title: '分销商数量',
tip: '今日新增人数',
footer: '本周新增分销商人数',
},
share: {
data: {},
color: '#21C732',
color1: 'rgba(33, 199, 50, 0.4)',
color2: 'rgba(33, 199, 50, 0)',
title: '分享次数',
tip: '今日分享次数',
footer: '本周新增分享次数',
},
},
});
// 图表配置响应式对象
// 用于底部统计图表的Tab切换(订单数/支付订单/支付金额)
const chart = reactive({
tabsData: {
order: '订单数',
payOrder: '支付订单',
payAmount: '支付金额',
},
tabActive: 'order',
dateTime: getTimeSlot(),
shortcuts: [
tabActive: 'order', // 当前激活的Tab
dateTime: getTimeSlot(), // 时间选择器的时间范围
shortcuts: [ // 时间快捷选项
{
text: '今天',
value: getTimeSlot(),
......@@ -70,31 +48,34 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
},
],
data: {
payAmountArr: [],
payOrderArr: [],
orderArr: [],
payAmountArr: [], // 支付金额数据数组
payOrderArr: [], // 支付订单数据数组
orderArr: [], // 订单数数据数组
},
});
// 统计数据响应式对象
// 存储仪表盘顶部统计卡片的数据
const statistics = reactive({
total_amount: 0,
total_amount_compare_rate: 0,
profit_rate: '0%',
profit_rate_compare_rate: 0,
avg_amount: 0,
avg_amount_compare_rate: 0,
order_count: 0,
order_count_compare_rate: 0,
visitor_count: 0,
visitor_count_compare_rate: 0,
conversion_rate: '0%',
conversion_rate_compare_rate: 0,
register_users: 0,
register_users_compare_rate: 0,
trend_data: [],
from_cache: false
total_amount: 0, // 成交总额
total_amount_compare_rate: 0, // 成交总额环比增长率
profit_rate: '0%', // 净利润率
profit_rate_compare_rate: 0, // 净利润率环比增长率
avg_amount: 0, // 客单价
avg_amount_compare_rate: 0, // 客单价环比增长率
order_count: 0, // 订单数
order_count_compare_rate: 0, // 订单数环比增长率
visitor_count: 0, // 访客数
visitor_count_compare_rate: 0, // 访客数环比增长率
conversion_rate: '0%', // 转化率
conversion_rate_compare_rate: 0, // 转化率环比增长率
register_users: 0, // 注册用户数
register_users_compare_rate: 0, // 注册用户环比增长率
trend_data: [], // 趋势数据数组
from_cache: false // 数据是否来自缓存
});
// 底部统计图表的配置选项
const chartOption = reactive({
grid: {
left: '10px',
......@@ -161,6 +142,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
],
});
/**
* 生成日期标签数组
* @param {number} days - 需要生成的天数
* @returns {Array} - 日期标签数组,格式如 ['1月1日', '1月2日', ...]
*/
function generateDateLabels(days) {
var labels = [];
for (var i = days - 1; i >= 0; i--) {
......@@ -171,6 +157,12 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
return labels;
}
/**
* 格式化数字显示
* @param {number} num - 需要格式化的数字
* @param {number} decimals - 小数位数(可选)
* @returns {string} - 格式化后的数字字符串
*/
function formatNumber(num, decimals) {
if (num === null || num === undefined) num = 0;
if (decimals !== undefined) {
......@@ -179,39 +171,60 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
return parseInt(num).toLocaleString();
}
/**
* 更新趋势元素显示
* @param {HTMLElement} element - 趋势元素DOM节点
* @param {number|null} compareRate - 环比增长率,null表示持平
*/
function updateTrendElement(element, compareRate) {
if (!element) return;
var spanElement = element.querySelector('span');
if (!spanElement) return;
if (compareRate === null || compareRate === undefined) {
spanElement.textContent = '持平';
element.classList.remove('up', 'down');
var icon = element.querySelector('i');
if (icon) {
icon.classList.remove('fa-arrow-up', 'fa-arrow-down');
}
return;
}
compareRate = parseFloat(compareRate) || 0;
var trendText = '';
var trendClass = '';
if (compareRate > 0) {
trendText = '+' + compareRate.toFixed(2) + '%';
var roundedRate = Math.round(compareRate);
if (roundedRate > 0) {
trendText = '+' + roundedRate + '%';
trendClass = 'up';
} else if (compareRate < 0) {
trendText = compareRate.toFixed(2) + '%';
} else if (roundedRate < 0) {
trendText = roundedRate + '%';
trendClass = 'down';
} else {
trendText = '+0%';
trendClass = 'up';
}
element.textContent = trendText;
var parentDiv = element.parentElement;
if (parentDiv) {
parentDiv.classList.remove('up', 'down');
parentDiv.classList.add(trendClass);
var icon = parentDiv.querySelector('i');
if (icon) {
icon.classList.remove('fa-arrow-up', 'fa-arrow-down');
icon.classList.add(trendClass === 'up' ? 'fa-arrow-up' : 'fa-arrow-down');
}
spanElement.textContent = trendText;
element.classList.remove('up', 'down');
element.classList.add(trendClass);
var icon = element.querySelector('i');
if (icon) {
icon.classList.remove('fa-arrow-up', 'fa-arrow-down');
icon.classList.add(trendClass === 'up' ? 'fa-arrow-up' : 'fa-arrow-down');
}
}
/**
* 初始化ECharts图表实例
* 创建成交总额趋势、订单数趋势和用户增长图表
*/
function initCharts() {
gmvChart = echarts.init(document.getElementById('gmv-chart'));
var gmvOption = {
......@@ -336,6 +349,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
};
userGrowthChart.setOption(userGrowthOption);
// 监听窗口大小变化,自动调整图表尺寸
window.addEventListener('resize', function() {
gmvChart.resize();
orderChart.resize();
......@@ -343,6 +357,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
});
}
/**
* 切换图表周期(日/周/月)
* @param {string} chartType - 图表类型:'gmv' 或 'order'
* @param {string} period - 周期类型:'day'、'week' 或 'month'
*/
function switchPeriod(chartType, period) {
if (chartType === 'gmv') {
currentGmvPeriod = period;
......@@ -381,6 +400,10 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
loadStatistics(currentTimeRange);
}
/**
* 加载统计数据
* @param {string} timeRange - 时间范围,如 '7days'、'30days'、'90days'
*/
function loadStatistics(timeRange) {
currentTimeRange = timeRange;
Fast.api.ajax({
......@@ -410,6 +433,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
statistics.trend_data = data.trend_data;
statistics.from_cache = data.from_cache;
// 更新页面显示的更新时间
var updateTimeElement = document.getElementById('update-time');
if (updateTimeElement && data.update_time) {
updateTimeElement.textContent = data.update_time;
console.log(data.update_time);
}
// 更新统计卡片显示
var statTotalAmount = document.getElementById('stat-total-amount');
var statProfitRate = document.getElementById('stat-profit-rate');
var statAvgAmount = document.getElementById('stat-avg-amount');
......@@ -442,9 +473,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
updateGmvChart(data.trend_data);
// 如果GMV和订单图表周期相同,复用同一数据
if (currentGmvPeriod === currentOrderPeriod) {
updateOrderChart(data.trend_data);
} else {
// 周期不同时单独请求订单数据
Fast.api.ajax({
url: 'shopro/dashboard/statistics',
type: 'GET',
......@@ -463,11 +496,16 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
return false;
});
// 同时加载其他数据
loadFunnel(timeRange);
loadPriceDistribution(timeRange);
loadRecentOrders();
}
/**
* 更新成交总额趋势图表和用户增长图表
* @param {Array} trendData - 趋势数据数组
*/
function updateGmvChart(trendData) {
var labels = trendData.map(function(item) { return item.date; });
var amounts = trendData.map(function(item) { return item.amount; });
......@@ -479,6 +517,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
xAxis: { data: labels },
series: [{ data: amounts }]
});
gmvChart.resize();
}
if (userGrowthChart) {
......@@ -489,13 +528,19 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
{ data: registers }
]
});
userGrowthChart.resize();
}
// 更新用户增长徽章
var totalRegister = registers.reduce(function(a, b) { return a + b; }, 0);
var growthBadge = document.getElementById('growth-badge');
if (growthBadge) growthBadge.textContent = '+' + formatNumber(totalRegister);
}
/**
* 更新订单数趋势图表
* @param {Array} trendData - 趋势数据数组
*/
function updateOrderChart(trendData) {
var labels = trendData.map(function(item) { return item.date; });
var orders = trendData.map(function(item) { return item.order_num; });
......@@ -505,9 +550,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
xAxis: { data: labels },
series: [{ data: orders }]
});
orderChart.resize();
}
}
/**
* 加载转化漏斗数据
* @param {string} timeRange - 时间范围
*/
function loadFunnel(timeRange) {
Fast.api.ajax({
url: 'shopro/dashboard/funnel',
......@@ -553,6 +603,10 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
});
}
/**
* 加载价格分布数据
* @param {string} timeRange - 时间范围
*/
function loadPriceDistribution(timeRange) {
Fast.api.ajax({
url: 'shopro/dashboard/priceDistribution',
......@@ -590,6 +644,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
});
}
/**
* 加载最近订单列表
*/
function loadRecentOrders() {
Fast.api.ajax({
url: 'shopro/dashboard/recentOrders',
......@@ -615,6 +672,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
});
}
/**
* 刷新所有数据
*/
function refreshAllData() {
var refreshBtn = document.getElementById('refreshBtn');
var updateTime = document.getElementById('update-time');
......@@ -634,134 +694,134 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
}
function getTotal() {
Fast.api.ajax({
url: 'shopro/dashboard/total',
type: 'GET',
}, function (ret, res) {
state.total.agent.data = res.data.agent_data;
state.total.share.data = res.data.share_data;
state.total.user.data = res.data.user_data;
for (var key in state.total) {
if (state.total[key].data) {
initChartTotal(key);
}
}
return false;
}, function (ret, res) { });
// Fast.api.ajax({
// url: 'shopro/dashboard/total',
// type: 'GET',
// }, function (ret, res) {
// state.total.agent.data = res.data.agent_data;
// state.total.share.data = res.data.share_data;
// state.total.user.data = res.data.user_data;
// for (var key in state.total) {
// if (state.total[key].data) {
// initChartTotal(key);
// }
// }
// return false;
// }, function (ret, res) { });
}
function initChartTotal(key) {
var chartEl = document.getElementById(key + 'Total');
if (!chartEl) return;
var myChart = echarts.init(chartEl);
window.onresize = () => {
myChart.resize();
};
var option = {
grid: {
left: 6,
top: 6,
right: 6,
bottom: 6,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'none',
},
},
xAxis: {
type: 'category',
data: ['分', 20, 36, 10, 10, 20],
boundaryGap: false,
axisTick: {
show: false,
},
axisLabel: {
show: false,
},
axisLine: {
show: false,
},
},
yAxis: {
type: 'value',
splitLine: {
show: false,
},
axisLabel: {
show: false,
},
},
series: [{
data: [5, 20, 36, 10, 10, 20],
type: 'line',
smooth: true,
showSymbol: false,
symbol: 'circle',
symbolSize: 6,
itemStyle: {
color: state.total[key].color,
},
areaStyle: {
color: new echarts.graphic.LinearGradient(
0,
0,
0,
1,
[
{
offset: 0,
color: state.total[key].color1,
},
{
offset: 1,
color: state.total[key].color2,
},
],
false,
),
},
lineStyle: {
width: 2,
},
}]
};
initIntervalTotal(24, 'hours', option, key);
myChart.setOption(option);
// var chartEl = document.getElementById(key + 'Total');
// if (!chartEl) return;
// var myChart = echarts.init(chartEl);
// window.onresize = () => {
// myChart.resize();
// };
// var option = {
// grid: {
// left: 6,
// top: 6,
// right: 6,
// bottom: 6,
// },
// tooltip: {
// trigger: 'axis',
// axisPointer: {
// type: 'none',
// },
// },
// xAxis: {
// type: 'category',
// data: ['分', 20, 36, 10, 10, 20],
// boundaryGap: false,
// axisTick: {
// show: false,
// },
// axisLabel: {
// show: false,
// },
// axisLine: {
// show: false,
// },
// },
// yAxis: {
// type: 'value',
// splitLine: {
// show: false,
// },
// axisLabel: {
// show: false,
// },
// },
// series: [{
// data: [5, 20, 36, 10, 10, 20],
// type: 'line',
// smooth: true,
// showSymbol: false,
// symbol: 'circle',
// symbolSize: 6,
// itemStyle: {
// color: state.total[key].color,
// },
// areaStyle: {
// color: new echarts.graphic.LinearGradient(
// 0,
// 0,
// 0,
// 1,
// [
// {
// offset: 0,
// color: state.total[key].color1,
// },
// {
// offset: 1,
// color: state.total[key].color2,
// },
// ],
// false,
// ),
// },
// lineStyle: {
// width: 2,
// },
// }]
// };
// initIntervalTotal(24, 'hours', option, key);
// myChart.setOption(option);
}
function initIntervalTotal(interval, kld, option, key) {
let dateTime = Moment().format('YYYY-MM-DD') + ' 00:00:00';
let x = [];
let y = [];
let timeStamp = [];
for (let i = 0; i <= interval; i++) {
x.push(Moment(dateTime).add(i, kld).format('HH:mm'));
y.push(0);
timeStamp.push(Moment(dateTime).add(i, kld).valueOf());
}
x.forEach((item, index) => {
state.total[key].data.array.forEach((item) => {
if (
timeStamp[index + 1] &&
item.createtime_unix > timeStamp[index] &&
item.createtime_unix <= timeStamp[index + 1]
) {
y[index]++;
}
});
});
option.xAxis.data = x;
option.series[0].data = y;
// let dateTime = Moment().format('YYYY-MM-DD') + ' 00:00:00';
// let x = [];
// let y = [];
// let timeStamp = [];
// for (let i = 0; i <= interval; i++) {
// x.push(Moment(dateTime).add(i, kld).format('HH:mm'));
// y.push(0);
// timeStamp.push(Moment(dateTime).add(i, kld).valueOf());
// }
// x.forEach((item, index) => {
// state.total[key].data.array.forEach((item) => {
// if (
// timeStamp[index + 1] &&
// item.createtime_unix > timeStamp[index] &&
// item.createtime_unix <= timeStamp[index + 1]
// ) {
// y[index]++;
// }
// });
// });
// option.xAxis.data = x;
// option.series[0].data = y;
}
function onChangeTabActive(type) {
chart.tabActive = type;
chartOption.series[0].name = chart.tabsData[chart.tabActive];
initChart();
}
// function onChangeTabActive(type) {
// chart.tabActive = type;
// chartOption.series[0].name = chart.tabsData[chart.tabActive];
// initChart();
// }
function onChangeDateTime(e) {
e && getChart();
......@@ -887,100 +947,94 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
return timeSlot;
}
const ranking = reactive({
goods: [],
hot_search: []
});
const pieOption = reactive({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)',
},
legend: {
show: false,
},
series: [
{
name: '热搜榜',
type: 'pie',
radius: ['52%', '90%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
zlevel: 1,
z: 1,
emphasis: {
label: {
show: true,
fontSize: '16',
fontWeight: 'normal',
},
},
labelLine: {
show: false,
},
data: [],
},
],
});
// function getRanking() {
// Fast.api.ajax({
// url: 'shopro/dashboard/ranking',
// type: 'GET',
// }, function (ret, res) {
// ranking.goods = res.data.goods;
// ranking.hot_search = res.data.hot_search;
// pieOption.series[0].data = [];
// ranking.hot_search.forEach(item => {
// pieOption.series[0].data.push({
// name: item.keyword,
// value: item.num,
// });
// });
// var rankingContentEl = document.getElementById('rankingContent');
// if (rankingContentEl) {
// var myChart3 = echarts.init(rankingContentEl);
// window.onresize = () => {
// myChart3.resize();
// };
// myChart3.setOption(pieOption);
// const ranking = reactive({
// goods: [],
// hot_search: []
// });
// const pieOption = reactive({
// tooltip: {
// trigger: 'item',
// formatter: '{a} <br/>{b}: {c} ({d}%)',
// },
// legend: {
// show: false,
// },
// series: [
// {
// name: '热搜榜',
// type: 'pie',
// radius: ['52%', '90%'],
// avoidLabelOverlap: false,
// label: {
// show: false,
// position: 'center',
// },
// zlevel: 1,
// z: 1,
// emphasis: {
// label: {
// show: true,
// fontSize: '16',
// fontWeight: 'normal',
// },
// },
// labelLine: {
// show: false,
// },
// data: [],
// },
// ],
// });
// function onOpen(status) {
// Fast.api.open('shopro/order/order/index?status=' + status + '&createtime=' + encodeURI(JSON.stringify(chart.dateTime)), "订单", {
// callback() {
// getChart();
// }
// return false;
// }, function (ret, res) { });
// });
// }
function onOpen(status) {
Fast.api.open('shopro/order/order/index?status=' + status + '&createtime=' + encodeURI(JSON.stringify(chart.dateTime)), "订单", {
callback() {
getChart();
}
});
}
onMounted(() => {
initCharts();
setTimeout(function() {
if (gmvChart) gmvChart.resize();
if (orderChart) orderChart.resize();
if (userGrowthChart) userGrowthChart.resize();
}, 100);
loadStatistics('30days');
var timeRangeSelect = document.getElementById('time_range');
if (timeRangeSelect) {
timeRangeSelect.addEventListener('change', function(e) {
loadStatistics(e.target.value);
});
}
document.querySelectorAll('.chart-tab').forEach(btn => {
btn.addEventListener('click', function(e) {
var chartType = this.dataset.chart;
var period = this.dataset.period;
switchPeriod(chartType, period);
});
});
var refreshBtn = document.getElementById('refreshBtn');
if (refreshBtn) {
refreshBtn.addEventListener('click', refreshAllData);
}
});
return {
state,
getTotal,
chart,
onChangeTabActive,
onChangeDateTime,
statistics,
getChart,
chartOption,
initChart,
drawX,
getTimeSlot,
ranking,
onOpen,
initCharts,
loadStatistics,
updateGmvChart,
......
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