dashboard

This commit is contained in:
hantao 2025-08-29 16:56:03 +08:00
parent f0708b12aa
commit 41f470b5de
3 changed files with 730 additions and 68 deletions

View File

@ -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);
}
}

View File

@ -7,86 +7,377 @@
<input type="text" class="form-control datetimerange" value = "{$default_daterange|htmlentities}" data-locale='{"format":"YYYY-MM-DD"}'
placeholder="指定日期" name="filter[daterange]" id="daterange" autocomplete="off" style="width: 200px;">
</div>
<!-- 查询按钮 -->
<button class="btn btn-default" id="filter-btn" style="margin-left: 15px;">查询</button>
</div>
</div>
<div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card" >
<span class="stat-icon new-car">🚙</span>
<div class="stat-number">loading</div>
<div class="stat-label">新车库存</div>
</div>
<div class="stat-card">
<span class="stat-icon used-car">🚗</span>
<div class="stat-number">loading</div>
<div class="stat-label">二手车库存</div>
</div>
<div class="stat-card" >
<span class="stat-icon rental-car">🚐</span>
<div class="stat-number">loading</div>
<div class="stat-label">租车车辆</div>
</div>
<div class="stat-card">
<span class="stat-icon total-sales">💰</span>
<div class="stat-number">loading</div>
<div class="stat-label">本月销售额</div>
<!-- 新车业务 -->
<div class="stat-card new-car">
<div class="stat-header">
<div class="stat-title">新车业务</div>
<div class="stat-icon">
<i class="fa fa-car"></i>
</div>
</div>
<div class="stat-value" id="newCarCount">5 辆</div>
<div class="stat-details">
<div class="detail-item">
<span class="detail-label">销售数量</span>
<span class="detail-value" id="newSalesCount">1 辆</span>
</div>
<div class="detail-item">
<span class="detail-label">销售金额</span>
<span class="detail-value" id="newSalesAmount">¥230,000</span>
</div>
</div>
</div>
<!-- 租车业务 -->
<div class="stat-card rent-car">
<div class="stat-header">
<div class="stat-title">租车业务</div>
<div class="stat-icon">
<i class="fa fa-credit-card"></i>
</div>
</div>
<div class="stat-value" id="rentCarCount">29 辆</div>
<div class="stat-details">
<div class="detail-item">
<span class="detail-label">订单数量</span>
<span class="detail-value" id="rentOrderCount">1 单</span>
</div>
<div class="detail-item">
<span class="detail-label">租金收入</span>
<span class="detail-value" id="rentIncome">¥2,222</span>
</div>
</div>
</div>
<!-- 二手车业务 -->
<div class="stat-card old-car">
<div class="stat-header">
<div class="stat-title">二手车业务</div>
<div class="stat-icon">
<i class="fa fa-handshake-o"></i>
</div>
</div>
<div class="stat-value" id="oldCarCount">1 辆</div>
<div class="stat-details">
<div class="detail-item">
<span class="detail-label">销售数量</span>
<span class="detail-value" id="oldSalesCount">0 辆</span>
</div>
<div class="detail-item">
<span class="detail-label">销售金额</span>
<span class="detail-value" id="oldSalesAmount">¥0</span>
</div>
</div>
</div>
<!-- 总营收 -->
<div class="stat-card total">
<div class="stat-header">
<div class="stat-title">总营收概览</div>
<div class="stat-icon">
<i class="fa fa-money"></i>
</div>
</div>
<div class="stat-value" id="totalRevenue">¥232,222</div>
<div class="stat-details">
<div class="detail-item">
<span class="detail-label">新车占比</span>
<span class="detail-value" id="newCarPercent">99.04%</span>
</div>
<div class="detail-item">
<span class="detail-label">租车占比</span>
<span class="detail-value" id="rentCarPercent">0.96%</span>
</div>
</div>
</div>
</div>
<!-- 业务汇总 -->
<div class="summary-section">
<h2 class="summary-title"><i class="fas fa-chart-bar"></i> 业务数据汇总</h2>
<div class="summary-grid">
<div class="summary-item">
<div class="summary-number" id="totalCars">35</div>
<div class="summary-label">车辆总数</div>
</div>
<div class="summary-item">
<div class="summary-number" id="totalOrders">2</div>
<div class="summary-label">总订单数</div>
</div>
<div class="summary-item">
<div class="summary-number" id="avgOrderValue">¥116,111</div>
<div class="summary-label">平均订单金额</div>
</div>
<div class="summary-item">
<div class="summary-number" id="inventoryUtilization">5.7%</div>
<div class="summary-label">库存利用率</div>
</div>
</div>
</div>
</div>
<div class="lines">
<div id="money_line"></div>
</div>
<div class="update-time">
<i class="fas fa-clock"></i> 数据更新时间:<span id="updateTime"></span>
</div>
</div>
<style>
html {
font-size: 13px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: #f5f6fa;
color: #333;
line-height: 1.6;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 40px;
margin-bottom: 30px;
}
.stat-card {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 15px;
background: white;
padding: 25px;
text-align: center;
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
transition: all 0.3s ease;
cursor: pointer;
border-radius: 10px;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
border-left: 4px solid;
transition: transform 0.3s ease, box-shadow 0.3s ease;
position: relative;
overflow: hidden;
}
.stat-card:hover {
transform: translateY(-10px);
box-shadow: 0 15px 40px rgba(0,0,0,0.15);
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 60px;
height: 60px;
background: linear-gradient(45deg, rgba(255,255,255,0.1), rgba(255,255,255,0.3));
border-radius: 50%;
transform: translate(20px, -20px);
}
.stat-card.new-car {
border-left-color: #28a745;
}
.stat-card.rent-car {
border-left-color: #007bff;
}
.stat-card.old-car {
border-left-color: #ffc107;
}
.stat-card.total {
border-left-color: #dc3545;
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
color: white;
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.stat-icon {
font-size: 3rem;
margin-bottom: 15px;
display: block;
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: white;
}
.new-car { color: #4299e1; }
.used-car { color: #48bb78; }
.rental-car { color: #ed8936; }
.total-sales { color: #9f7aea; }
.new-car .stat-icon {
background: linear-gradient(135deg, #28a745, #20c997);
}
.stat-number {
font-size: 2.5rem;
font-weight: 700;
color: #2d3748;
.rent-car .stat-icon {
background: linear-gradient(135deg, #007bff, #0056b3);
}
.old-car .stat-icon {
background: linear-gradient(135deg, #ffc107, #fd7e14);
}
.total .stat-icon {
background: linear-gradient(135deg, #dc3545, #c82333);
}
.stat-title {
font-size: 1rem;
color: #666;
font-weight: 500;
}
.total .stat-title {
color: rgba(255,255,255,0.9);
}
.stat-value {
font-size: 2rem;
font-weight: bold;
margin-bottom: 10px;
}
.stat-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
font-size: 0.9rem;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
background: rgba(0,0,0,0.05);
border-radius: 5px;
}
.lines{
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
}
.total .detail-item {
background: rgba(255,255,255,0.2);
color: white;
}
.detail-label {
color: #666;
}
.total .detail-label {
color: rgba(255,255,255,0.8);
}
.detail-value {
font-weight: 600;
}
.summary-section {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.summary-title {
font-size: 1.5rem;
margin-bottom: 20px;
color: #333;
border-bottom: 3px solid #667eea;
padding-bottom: 10px;
display: inline-block;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.summary-item {
text-align: center;
padding: 20px;
border: 2px solid #eee;
border-radius: 8px;
transition: border-color 0.3s ease;
}
.summary-item:hover {
border-color: #667eea;
}
.summary-number {
font-size: 1.8rem;
font-weight: bold;
color: #667eea;
margin-bottom: 5px;
}
.stat-label {
color: #718096;
font-size: 1rem;
font-weight: 500;
.summary-label {
color: #666;
font-size: 0.9rem;
}
.update-time {
text-align: center;
margin-top: 30px;
color: #999;
font-size: 0.9rem;
}
.lines{
width: 100%;
height: 500px;
}
#money_line{
width: 100%;
height: 100%;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header h1 {
font-size: 2rem;
}
.stats-grid {
grid-template-columns: 1fr;
}
.stat-details {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -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 () {
},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');
},
}
};