From 41f470b5defe25d0ae125d5b4acc89b1d5af2c0a Mon Sep 17 00:00:00 2001 From: hantao Date: Fri, 29 Aug 2025 16:56:03 +0800 Subject: [PATCH] dashboard --- application/admin/controller/Dashboard.php | 180 ++++++++- application/admin/view/dashboard/index.html | 389 +++++++++++++++++--- public/assets/js/backend/dashboard.js | 229 +++++++++++- 3 files changed, 730 insertions(+), 68 deletions(-) diff --git a/application/admin/controller/Dashboard.php b/application/admin/controller/Dashboard.php index 564b2ba..fce2e21 100644 --- a/application/admin/controller/Dashboard.php +++ b/application/admin/controller/Dashboard.php @@ -30,11 +30,148 @@ class Dashboard extends Backend $end_at = now()->format('Y-m-d'); $default_daterange = $start . ' - ' . $end_at; - $this->view->assign('default_daterange',$default_daterange); + $this->view->assign('default_daterange', $default_daterange); return $this->view->fetch(); } public function getData() + { + + $start = now()->modify('-30 days')->format('Y-m-d'); + $end_at = now()->format('Y-m-d 23:29:59'); + + $filter = request()->get('range', ''); + if (!empty($filter)) { + $arr = explode(' - ', $filter); + if (trim($arr[0])) { + $start = trim($arr[0]) . ' 00:00:00'; + } + if (trim($arr[1])) { + $end_at = trim($arr[1]) . ' 23:29:59'; + } + } + + $top = $this->getTopTotal($start, $end_at); + $lines = $this->getLines($start, $end_at); + $this->success(data: [ + 'top' => $top, + 'lines' => $lines + ]); + } + + + private function prepareEchartsBarData(array $data, string $startDate, string $endDate): array + { + // 将原始数据用日期作为键索引 + $indexed = []; + foreach ($data as $item) { + $indexed[$item['day']] = $item; + } + + $start = strtotime($startDate); + $end = strtotime($endDate); + + $xAxis = []; + $series_total = []; + $series_count = []; + + $new_count_array = []; + $old_count_array = []; + $rent_count_array = []; + $new_total_array = []; + $old_total_array = []; + $rent_total_array = []; + + for ($date = $start; $date <= $end; $date += 86400) { + $day = date('Y-m-d', $date); + $xAxis[] = $day; + + $total = $indexed[$day]['total'] ?? 0; + $count = $indexed[$day]['count'] ?? 0; + $new_count = $indexed[$day]['new_count'] ?? 0; + $old_count = $indexed[$day]['old_count'] ?? 0; + $rent_count = $indexed[$day]['rent_count'] ?? 0; + $new_total = $indexed[$day]['new_total'] ?? 0; + $old_total = $indexed[$day]['old_total'] ?? 0; + $rent_total = $indexed[$day]['rent_total'] ?? 0; + + $series_total[] = $total; + $series_count[] = $count; + $new_count_array[] = $new_count; + $old_count_array[] = $old_count; + $rent_count_array[] = $rent_count; + $new_total_array[] = $new_total; + $old_total_array[] = $old_total; + $rent_total_array[] = $rent_total; + + } + + $data = [ + 'xAxis' => $xAxis, + 'series' => [ + 'total' => $series_total, + 'count' => $series_count, + 'new_count' => $new_count_array, + 'old_count' => $old_count_array, + 'rent_count' => $rent_count_array, + 'new_total' => $new_total_array, + 'old_total' => $old_total_array, + 'rent_total' => $rent_total_array, + ], + ]; + + + // 判断是否需要按月归档 + if (count($data['xAxis']) > 31) { + $monthly = []; + + foreach ($data['xAxis'] as $i => $date) { + $month = date('Y-m', strtotime($date)); + + if (!isset($monthly[$month])) { + $monthly[$month] = [ + 'total' => 0, + 'count' => 0, + 'performance' => 0, + 'rate_sum' => 0, + 'rate_count' => 0, + ]; + } + + $monthly[$month]['total'] += $data['series']['total'][$i]; + $monthly[$month]['count'] += $data['series']['count'][$i]; + $monthly[$month]['performance'] += $data['series']['performance'][$i]; + + // 处理 rate(字符串,转换为 float) + $rate = (float)$data['series']['rate'][$i]; + $monthly[$month]['rate_sum'] += $rate; + $monthly[$month]['rate_count'] += 1; + } + + // 重新生成 xAxis 和 series + $data['xAxis'] = array_keys($monthly); + $data['series'] = [ + 'total' => [], + 'count' => [], + 'performance' => [], + 'rate' => [] + ]; + + foreach ($monthly as $month => $values) { + $data['series']['total'][] = $values['total']; + $data['series']['count'][] = $values['count']; + $data['series']['performance'][] = $values['performance']; + + // 平均 rate,保留 2 位小数 + $averageRate = $values['rate_count'] > 0 ? $values['rate_sum'] / $values['rate_count'] : 0; + $data['series']['rate'][] = number_format($averageRate, 2); + } + } + + return $data; + } + + private function getTopTotal($start, $end_at) { $car_num = Db::query('SELECT COUNT(IF(car_type = 1,1,null)) new_car, @@ -46,11 +183,11 @@ COUNT(IF(car_type = 2,1,null)) old_car $start = now()->modify('-30 days')->format('Y-m-d'); $end_at = now()->format('Y-m-d 23:29:59'); - $filter = request()->get('range',''); - if (!empty($filter)){ + $filter = request()->get('range', ''); + if (!empty($filter)) { $arr = explode(' - ', $filter); if (trim($arr[0])) { - $start = trim($arr[0]). ' 00:00:00'; + $start = trim($arr[0]) . ' 00:00:00'; } if (trim($arr[1])) { $end_at = trim($arr[1]) . ' 23:29:59'; @@ -62,8 +199,8 @@ COUNT(IF(car_type = 2,1,null)) old_car $group = \model('auth_group_access')->where('uid', $this->auth->id)->find()->group_id ?? 0; - if (!in_array($group,[1,2])){ - $build->where('saler_id',$this->auth->id); + if (!in_array($group, [1, 2])) { + $build->where('saler_id', $this->auth->id); } $sale = $build->field([ "count(if(type=1,1,null)) new_count", @@ -75,9 +212,34 @@ COUNT(IF(car_type = 2,1,null)) old_car "sum(total_price) total_price", ])->select()[0]->toArray(); - $this->success(data:[ - ...$car_num,...$sale - ]); + return [ + ...$car_num, ...$sale + ]; + } + + private function getLines($start, $end_at) + { + $build = new Sales(); + $build->whereBetween('finish_at', [$start, $end_at]); + $res = $build->field([ + 'DATE(finish_at) day', + "count(if(type=1,1,null)) new_count", + "count(if(type=2,1,null)) old_count", + "count(if(type=3,1,null)) rent_count", + "sum(if(type=1,total_price,0)) new_total", + "sum(if(type=2,total_price,0)) old_total", + "sum(if(type=3,total_price,0)) rent_total", + 'sum(total_price) total', + 'count(id) count', + ])->group('DATE(finish_at)') + ->select(); + + $data = []; + foreach ($res as $re) { + $re = $re->getData(); + $data [] = $re; + } + return $this->prepareEchartsBarData($data, $start, $end_at); } } diff --git a/application/admin/view/dashboard/index.html b/application/admin/view/dashboard/index.html index 08fc3bd..de8e953 100644 --- a/application/admin/view/dashboard/index.html +++ b/application/admin/view/dashboard/index.html @@ -7,86 +7,377 @@ - - - - -
-
- 🚙 -
loading
-
新车库存
+ + +
+ +
+ +
+
+
新车业务
+
+ +
+
+
5 辆
+
+
+ 销售数量 + 1 辆 +
+
+ 销售金额 + ¥230,000 +
+
+
+ + +
+
+
租车业务
+
+ +
+
+
29 辆
+
+
+ 订单数量 + 1 单 +
+
+ 租金收入 + ¥2,222 +
+
+
+ + +
+
+
二手车业务
+
+ +
+
+
1 辆
+
+
+ 销售数量 + 0 辆 +
+
+ 销售金额 + ¥0 +
+
+
+ + +
+
+
总营收概览
+
+ +
+
+
¥232,222
+
+
+ 新车占比 + 99.04% +
+
+ 租车占比 + 0.96% +
+
+
-
- 🚗 -
loading
-
二手车库存
-
-
- 🚐 -
loading
-
租车车辆
-
-
- 💰 -
loading
-
本月销售额
+ + +
+

业务数据汇总

+
+
+
35
+
车辆总数
+
+
+
2
+
总订单数
+
+
+
¥116,111
+
平均订单金额
+
+
+
5.7%
+
库存利用率
+
+
+ +
+ +
+
+
+ +
+ 数据更新时间: +
+
+ + + diff --git a/public/assets/js/backend/dashboard.js b/public/assets/js/backend/dashboard.js index ea6e209..5a8aa41 100755 --- a/public/assets/js/backend/dashboard.js +++ b/public/assets/js/backend/dashboard.js @@ -1,4 +1,4 @@ -define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts','echarts-theme', 'template', 'addtabs', 'moment','citypicker'], function ($, undefined, Backend, Table, Form, echarts, undefined, Template, Datatable, Moment) { +define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-theme', 'template', 'addtabs', 'moment', 'citypicker'], function ($, undefined, Backend, Table, Form, echarts, undefined, Template, Datatable, Moment) { var Controller = { index: function () { @@ -8,9 +8,27 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts','echarts-th $('#filter-btn').on('click', function () { Controller.api.getChartData(); }); + + // 页面加载完成后更新数据 + document.addEventListener('DOMContentLoaded', function() { + // 模拟数据实时更新(可选) + setInterval(() => { + Controller.api.getChartData() + }, 60000); // 每分钟更新一次时间 + }); + + // 添加卡片点击效果 + document.querySelectorAll('.stat-card').forEach(card => { + card.addEventListener('click', function() { + this.style.transform = 'scale(0.95)'; + setTimeout(() => { + this.style.transform = ''; + }, 150); + }); + }); }, api: { - getChartData:function (){ + getChartData: function () { // 获取日期范围值 var daterange = $('#daterange').val(); /* var source = $('#source').val(); @@ -25,17 +43,18 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts','echarts-th 'item_id': item_id,*/ }; - $.ajax({ - url: "dashboard/getData", // + Fast.api.ajax({ + url: "dashboard/getData", type: "POST", dataType: "json", data: params, - success: function (response) { - Controller.api.chart(response); - }, - error: function () { - console.error("图表数据加载失败"); - } + },function (response) { + document.getElementById('updateTime').textContent = new Date().toLocaleString('zh-CN'); + Controller.api.updateDashboard(response.top); + Controller.api.money_line(response.lines); + return false; + },function () { + console.error("图表数据加载失败"); }); }, bindevent: function () { @@ -81,6 +100,196 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts','echarts-th }); }); }, + + // 格式化金额 + formatCurrency: function (amount) { + return new Intl.NumberFormat('zh-CN', { + style: 'currency', + currency: 'CNY', + minimumFractionDigits: 0, + maximumFractionDigits: 0 + }).format(parseFloat(amount)); + }, + + // 计算百分比 + calculatePercentage: function (part, total) { + return ((parseFloat(part) / parseFloat(total)) * 100).toFixed(2) + '%'; + }, + money_line: function (data){ + var myChart = echarts.init(document.getElementById('money_line')); + + var option = { + title: { + text: '总业绩', + subtext: '数据', + left: 'left' + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross', + crossStyle: { + color: '#999' + } + } + }, + color: [ + "#18d1b1", + "#3fb1e3", + "#626c91", + "#a0a7e6", + "#c4ebad", + "#96dee8" + ], + legend: { + data: ['订单量', '营销额', '新车单量','新车营销额','租车单量','租车营销额','二手车单量','二手车营销额',], + // left: 'left', + }, + xAxis: [ + { + type: 'category', + data: data.xAxis, + axisPointer: { + type: 'shadow' + } + } + ], + yAxis: [ + { + type: 'value', + name: '金额', + position: 'left' + }, + { + type: 'value', + name: '单量', + position: 'right' + } + ], + series: [ + { + name: '订单量', + type: 'line', + data: data.series.count, + emphasis: { + focus: 'series' + }, + yAxisIndex: 1, + barGap: '10%' // 调整柱状图1与柱状图2之间的间距 + }, + { + name: '营销额', + type: 'bar', + data: data.series.total, + emphasis: { + focus: 'series' + }, + barGap: '10%' // 调整柱状图1与柱状图2之间的间距 + }, + + { + name: '新车单量', + type: 'line', + data: data.series.new_count, + emphasis: { + focus: 'series' + }, + yAxisIndex: 1, + barGap: '10%' // 调整柱状图1与柱状图2之间的间距 + }, + { + name: '新车营销额', + type: 'bar', + data: data.series.new_total, + emphasis: { + focus: 'series' + }, + barGap: '10%' // 调整柱状图1与柱状图2之间的间距 + }, + { + name: '租车单量', + type: 'line', + data: data.series.rent_count, + emphasis: { + focus: 'series' + }, + yAxisIndex: 1, + barGap: '10%' // 调整柱状图1与柱状图2之间的间距 + }, + { + name: '租车营销额', + type: 'line', + data: data.series.rent_total, + emphasis: { + focus: 'series' + }, + barGap: '10%' // 调整柱状图1与柱状图2之间的间距 + }, + { + name: '二手车单量', + type: 'line', + data: data.series.old_count, + emphasis: { + focus: 'series' + }, + yAxisIndex: 1, + barGap: '10%' // 调整柱状图1与柱状图2之间的间距 + }, + { + name: '二手车营销额', + type: 'bar', + data: data.series.old_total, + emphasis: { + focus: 'series' + }, + barGap: '10%' // 调整柱状图1与柱状图2之间的间距 + }, + ] + }; + + myChart.setOption(option); + + // 监听窗口大小变化,自动重新绘制图表 + window.addEventListener('resize', function() { + myChart.resize(); + }); + }, + // 更新界面数据 + updateDashboard: function (data) { + // 基础数据更新 + document.getElementById('newCarCount').textContent = data.new_car + ' 辆'; + document.getElementById('rentCarCount').textContent = data.rent_car + ' 辆'; + document.getElementById('oldCarCount').textContent = data.old_car + ' 辆'; + + document.getElementById('newSalesCount').textContent = data.new_count + ' 辆'; + document.getElementById('rentOrderCount').textContent = data.rent_count + ' 单'; + document.getElementById('oldSalesCount').textContent = data.old_count + ' 辆'; + + document.getElementById('newSalesAmount').textContent = Controller.api.formatCurrency(data.new_sum); + document.getElementById('rentIncome').textContent = Controller.api.formatCurrency(data.rent_sum); + document.getElementById('oldSalesAmount').textContent = Controller.api.formatCurrency(data.old_sum); + document.getElementById('totalRevenue').textContent = Controller.api.formatCurrency(data.total_price); + + // 占比计算 + document.getElementById('newCarPercent').textContent = Controller.api.calculatePercentage(data.new_sum, data.total_price); + document.getElementById('rentCarPercent').textContent = Controller.api.calculatePercentage(data.rent_sum, data.total_price); + + // 汇总数据 + const totalCars = data.new_car + data.rent_car + data.old_car; + const totalOrders = data.new_count + data.rent_count + data.old_count; + const avgOrderValue = totalOrders > 0 ? parseFloat(data.total_price) / totalOrders : 0; + const inventoryUtilization = (totalOrders / totalCars * 100).toFixed(1); + + document.getElementById('totalCars').textContent = totalCars; + document.getElementById('totalOrders').textContent = totalOrders; + document.getElementById('avgOrderValue').textContent = Controller.api.formatCurrency(avgOrderValue); + document.getElementById('inventoryUtilization').textContent = inventoryUtilization + '%'; + + // 更新时间 + document.getElementById('updateTime').textContent = new Date().toLocaleString('zh-CN'); + }, + + } };