Files
2026-01-30 18:48:13 +08:00

674 lines
24 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{layout name="layout1" /}
<style>
.dashboard-container {
padding: 15px;
}
.kpi-card {
background: #fff;
padding: 20px;
border-radius: 4px;
margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.kpi-title {
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
.kpi-value {
font-size: 28px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.kpi-compare {
font-size: 12px;
color: #999;
}
.chart-card {
background: #fff;
padding: 20px;
border-radius: 4px;
margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.chart-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
}
.chart-container {
height: 300px;
}
.chart-container-large {
height: 400px;
}
.progress-item {
margin-bottom: 15px;
}
.progress-label {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 14px;
}
.progress-bar-container {
height: 20px;
background: #f0f0f0;
border-radius: 10px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #4A70F4 0%, #6DD047 100%);
transition: width 0.3s;
}
.table-card {
background: #fff;
padding: 20px;
border-radius: 8px;
margin-bottom: 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
overflow: hidden;
}
.date-filter {
display: flex;
align-items: center;
gap: 10px;
}
/* 优化表格样式 */
.table-card .layui-table {
margin-top: 0;
border: none;
}
.table-card .layui-table thead tr {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.table-card .layui-table thead th {
color: #fff;
font-weight: 600;
text-align: center;
padding: 15px 10px;
border: none;
font-size: 14px;
}
.table-card .layui-table tbody tr {
transition: all 0.3s ease;
border-bottom: 1px solid #f0f0f0;
}
.table-card .layui-table tbody tr:hover {
background-color: #f8f9ff;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.table-card .layui-table tbody td {
text-align: center;
padding: 12px 10px;
border: none;
color: #333;
font-size: 13px;
}
.table-card .layui-table tbody td:first-child {
font-weight: 600;
color: #4A70F4;
background-color: #f8f9ff;
}
.table-card .layui-table tbody td.empty-data {
color: #999;
font-style: italic;
}
.table-card .chart-title {
display: flex;
align-items: center;
padding-bottom: 15px;
border-bottom: 2px solid #f0f0f0;
margin-bottom: 20px;
}
.table-card .chart-title::before {
content: '';
display: inline-block;
width: 4px;
height: 18px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin-right: 10px;
border-radius: 2px;
}
</style>
<div class="layui-fluid dashboard-container">
<!-- 顶部KPI指标 -->
<div class="layui-row layui-col-space15">
<div class="layui-col-md3">
<div class="kpi-card">
<div class="kpi-title">月销售总额</div>
<div class="kpi-value" id="monthly-sales">--</div>
<div class="kpi-compare">同比: <span id="sales-compare">--</span></div>
</div>
</div>
<div class="layui-col-md3">
<div class="kpi-card">
<div class="kpi-title">月订单量</div>
<div class="kpi-value" id="monthly-orders">--</div>
<div class="kpi-compare">同比: <span id="orders-compare">--</span></div>
</div>
</div>
<div class="layui-col-md3">
<div class="kpi-card">
<div class="kpi-title">月客户数</div>
<div class="kpi-value" id="monthly-customers">--</div>
<div class="kpi-compare">同比: <span id="customers-compare">--</span></div>
</div>
</div>
<div class="layui-col-md3">
<div class="kpi-card">
<div class="kpi-title">日期筛选</div>
<div class="date-filter">
<input type="text" class="layui-input" id="date-filter" placeholder="年月日(可筛选)">
</div>
</div>
</div>
</div>
<!-- 第一行图表 -->
<div class="layui-row layui-col-space15">
<!-- 月客户分析 -->
<div class="layui-col-md4">
<div class="chart-card">
<div class="chart-title">月客户分析</div>
<div class="chart-container" id="customer-analysis-chart"></div>
</div>
</div>
<!-- 月销售情况 -->
<div class="layui-col-md4">
<div class="chart-card">
<div class="chart-title">月销售情况</div>
<div class="chart-container" id="sales-situation-chart"></div>
</div>
</div>
<!-- 当月客户渠道分析 -->
<div class="layui-col-md4">
<div class="chart-card">
<div class="chart-title">当月客户渠道分析</div>
<div class="chart-container" id="channel-analysis-chart"></div>
</div>
</div>
</div>
<!-- 第二行:目标完成率 -->
<div class="layui-row layui-col-space15">
<div class="layui-col-md12">
<div class="chart-card">
<div class="chart-title">当月目标完成率</div>
<div style="margin-bottom: 20px;">
<div style="font-size: 14px; color: #666; margin-bottom: 10px;">
本月销售目标:
<span style="color: #4A70F4; font-weight: bold;" id="monthly-target-text">--</span>
</div>
<div class="progress-item">
<div class="progress-label">
<span>总体完成率</span>
<span style="color: #4A70F4; font-weight: bold;" id="overall-completion-text">--</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar" id="overall-completion-bar" style="width: 0%;"></div>
</div>
</div>
</div>
<div style="margin-top: 30px;">
<div class="chart-title" style="font-size: 14px; margin-bottom: 15px;">客服目标完成率(N个客服的目标陈列)</div>
<div id="staff-progress-list">
<!-- 客服进度条列表JS 根据数据库数据渲染) -->
</div>
</div>
</div>
</div>
</div>
<!-- 第三行:趋势图和区域图 -->
<div class="layui-row layui-col-space15">
<!-- 每日销售趋势 -->
<div class="layui-col-md6">
<div class="chart-card">
<div class="chart-title">每日销售趋势</div>
<div id="trend-compare-text" style="text-align: right; margin-bottom: 10px; color: #f56c6c; font-size: 14px;">
--
</div>
<div class="chart-container-large" id="daily-sales-trend-chart"></div>
</div>
</div>
<!-- 区域月销售额 -->
<div class="layui-col-md6">
<div class="chart-card">
<div class="chart-title">区域月销售额</div>
<div class="chart-container-large" id="regional-sales-chart"></div>
</div>
</div>
</div>
<!-- 第四行:数据表格 -->
<div class="layui-row layui-col-space15">
<!-- 渠道月销售详情 -->
<div class="layui-col-md12">
<div class="table-card">
<div class="chart-title">渠道月销售详情</div>
<table class="layui-table" lay-size="sm">
<thead>
<tr>
<th>渠道</th>
<th>美团</th>
<th>公众号</th>
<th>抖音</th>
<th>员工</th>
<th>异业</th>
<th>渠道</th>
</tr>
</thead>
<tbody id="channel-month-table-body">
<!-- JS 动态渲染渠道月销售详情 -->
</tbody>
</table>
</div>
</div>
</div>
<!-- 第五行:区域月销售详情 -->
<div class="layui-row layui-col-space15">
<div class="layui-col-md12">
<div class="table-card">
<div class="chart-title">区域月销售详情</div>
<table class="layui-table" lay-size="sm">
<thead>
<tr>
<th>服务类型</th>
<th>南明区</th>
<th>云岩区</th>
<th>白云区</th>
<th>乌当区</th>
<th>花溪区</th>
<th>龙里</th>
</tr>
</thead>
<tbody id="region-month-table-body">
<!-- JS 动态渲染区域月销售详情 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<script>
layui.config({
version:"{$front_version}",
base: '/static/plug/layui-admin/dist/layuiadmin/'
}).extend({
index: 'lib/index'
}).use(['index', 'laydate'], function(){
var $ = layui.$
,laydate = layui.laydate;
// 统计数据(从后端传递)
var statisticsData = {
monthlySales: parseFloat('{$monthlySales|default=0}') || 0,
monthlyOrders: parseInt('{$monthlyOrders|default=0}') || 0,
monthlyCustomers: parseInt('{$monthlyCustomers|default=0}') || 0,
salesCompare: parseFloat('{$salesCompare|default=0}') || 0,
ordersCompare: parseFloat('{$ordersCompare|default=0}') || 0,
customersCompare: parseFloat('{$customersCompare|default=0}') || 0,
oldCustomers: parseInt('{$oldCustomers|default=0}') || 0,
newCustomers: parseInt('{$newCustomers|default=0}') || 0
};
// 仪表盘其它统计数据(目标、客服进度、渠道、趋势、区域等)
// 这里直接输出后端已经 json_encode 好的字符串,避免模板语法冲突
var dashboardData = {$dashboardData|raw} || {};
// 渲染统计数据
renderStatistics();
// 日期选择器
laydate.render({
elem: '#date-filter',
type: 'date',
format: 'yyyy-MM-dd'
});
// 初始化所有图表(使用数据库数据渲染)
initCharts();
// 渲染目标完成率和客服进度条
renderTargetsAndStaff();
// 渲染渠道与区域明细表格
renderChannelAndRegionTables();
// 渲染统计数据
function renderStatistics() {
// 格式化金额(保留两位小数,添加千分位)
function formatMoney(amount) {
if (!amount) return '0.00';
return parseFloat(amount).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
// 格式化同比显示
function formatCompare(compare) {
if (compare > 0) {
return '<span style="color: #52c41a;">↑ ' + Math.abs(compare).toFixed(2) + '%</span>';
} else if (compare < 0) {
return '<span style="color: #ff4d4f;">↓ ' + Math.abs(compare).toFixed(2) + '%</span>';
} else {
return '<span style="color: #999;">持平</span>';
}
}
// 渲染月销售总额
$('#monthly-sales').text('¥' + formatMoney(statisticsData.monthlySales));
$('#sales-compare').html(formatCompare(statisticsData.salesCompare));
// 渲染月订单量
$('#monthly-orders').text(statisticsData.monthlyOrders);
$('#orders-compare').html(formatCompare(statisticsData.ordersCompare));
// 渲染月客户数
$('#monthly-customers').text(statisticsData.monthlyCustomers);
$('#customers-compare').html(formatCompare(statisticsData.customersCompare));
}
// 渲染销售目标、总体完成率和客服进度条
function renderTargetsAndStaff() {
if (!dashboardData) {
return;
}
// 销售目标与总体完成率
var target = dashboardData.monthlyTarget || 0;
var completion = dashboardData.overallCompletion || 0;
$('#monthly-target-text').text(target > 0 ? (target / 10000).toFixed(2) + ' 万' : '--');
$('#overall-completion-text').text(completion.toFixed ? completion.toFixed(2) + '%' : (completion + '%'));
$('#overall-completion-bar').css('width', (completion > 100 ? 100 : completion) + '%');
// 客服进度条
var list = $('#staff-progress-list');
list.empty();
if (dashboardData.staffProgress && dashboardData.staffProgress.length) {
dashboardData.staffProgress.forEach(function (item) {
var percentText = (item.percent || 0).toFixed(2) + '%';
var yoy = item.yoy || 0;
var yoyHtml;
if (yoy > 0) {
yoyHtml = '同比: <span style="color:#52c41a;">↑ ' + yoy.toFixed(2) + '%</span>';
} else if (yoy < 0) {
yoyHtml = '同比: <span style="color:#ff4d4f;">↓ ' + Math.abs(yoy).toFixed(2) + '%</span>';
} else {
yoyHtml = '同比: <span style="color:#999;">持平</span>';
}
var html = ''
+ '<div class="progress-item">'
+ ' <div class="progress-label">'
+ ' <span>' + (item.name || '未分配') + '</span>'
+ ' <span>销售额: ' + (item.sales || 0) + ' | 百分比: ' + percentText + ' | ' + yoyHtml + '</span>'
+ ' </div>'
+ ' <div class="progress-bar-container">'
+ ' <div class="progress-bar" style="width:' + (item.percent || 0) + '%;"></div>'
+ ' </div>'
+ '</div>';
list.append(html);
});
} else {
list.append('<div style="color:#999;font-size:12px;">暂无客服统计数据</div>');
}
}
// 渠道&区域月销售明细表格渲染(只处理“总额”这一行)
function renderChannelAndRegionTables() {
if (!dashboardData) {
return;
}
// ===== 渠道月销售详情(总额 / 年卡 / 次卡 / 单次 / 其它) =====
var channelBody = $('#channel-month-table-body');
channelBody.empty();
var channelHeaders = ['美团','公众号','抖音','员工','异业','渠道'];
var rowLabels = [
{key: 'total', text: '总额'},
{key: 'year', text: '年卡'},
{key: 'times', text: '次卡'},
{key: 'single',text: '单次服务'},
{key: 'other', text: '其他服务'}
];
var keyToCol = ['meituan','gzh','douyin','staff','yeye','other'];
rowLabels.forEach(function (row) {
var data = (dashboardData.channelDetail && dashboardData.channelDetail[row.key]) || {};
var tr = '<tr><td>' + row.text + '</td>';
keyToCol.forEach(function (colKey) {
var v = parseFloat(data[colKey] || 0).toFixed(2);
tr += '<td>' + v + '</td>';
});
tr += '</tr>';
channelBody.append(tr);
});
// ===== 区域月销售详情(总额 / 年卡 / 次卡 / 单次 / 其它) =====
var regionBody = $('#region-month-table-body');
regionBody.empty();
if (dashboardData.regions && dashboardData.regions.length) {
var regionNames = dashboardData.regions;
rowLabels.forEach(function (row) {
var rowData = (dashboardData.regionDetail && dashboardData.regionDetail[row.key]) || {};
var tr2 = '<tr><td>' + row.text + '</td>';
regionNames.forEach(function (rName) {
var val = parseFloat(rowData[rName] || 0).toFixed(2);
tr2 += '<td>' + val + '</td>';
});
tr2 += '</tr>';
regionBody.append(tr2);
});
}
}
function initCharts() {
// 月客户分析 - 饼图
var customerChart = echarts.init(document.getElementById('customer-analysis-chart'));
customerChart.setOption({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [{
name: '客户分析',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}: {d}%'
},
data: [
{value: statisticsData.oldCustomers, name: '老客', itemStyle: {color: '#FFB6C1'}},
{value: statisticsData.newCustomers, name: '新客', itemStyle: {color: '#FF69B4'}}
]
}]
});
// 月销售情况 - 饼图(使用数据库汇总结果)
var salesChart = echarts.init(document.getElementById('sales-situation-chart'));
salesChart.setOption({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
bottom: '5%',
left: 'center'
},
series: [{
name: '销售情况',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}: {c}'
},
data: (dashboardData.salesSituation && dashboardData.salesSituation.length)
? dashboardData.salesSituation
: [
{value: 0, name: '单次服务', itemStyle: {color: '#F6A23F'}},
{value: 0, name: '年卡销量', itemStyle: {color: '#4A70F4'}},
{value: 0, name: '次卡销量', itemStyle: {color: '#6DD047'}},
{value: 0, name: '其它服务', itemStyle: {color: '#87CEEB'}}
]
}]
});
// 当月客户渠道分析 - 环形图(使用数据库统计的渠道订单数)
var channelChart = echarts.init(document.getElementById('channel-analysis-chart'));
channelChart.setOption({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [{
name: '客户渠道',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}: {d}%'
},
data: (dashboardData.channelPie && dashboardData.channelPie.length)
? dashboardData.channelPie
: []
}]
});
// 每日销售趋势 - 折线图(使用数据库按天汇总的数据)
var trendChart = echarts.init(document.getElementById('daily-sales-trend-chart'));
trendChart.setOption({
tooltip: {
trigger: 'axis'
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: dashboardData.trendDates || []
},
yAxis: {
type: 'value',
name: '销售额',
axisLabel: {
formatter: '{value}K'
}
},
series: [{
name: '销售额',
type: 'line',
smooth: true,
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: 'rgba(74, 112, 244, 0.3)'
}, {
offset: 1, color: 'rgba(74, 112, 244, 0.1)'
}]
}
},
itemStyle: {
color: '#4A70F4'
},
data: dashboardData.trendValues || []
}]
});
// 渲染“上月同比”文字
var trendCompare = dashboardData.trendCompare || 0;
var trendTextDom = $('#trend-compare-text');
if (trendCompare > 0) {
trendTextDom.css('color', '#52c41a').html('▲ ' + trendCompare.toFixed(2) + '% 上月同比');
} else if (trendCompare < 0) {
trendTextDom.css('color', '#f56c6c').html('▼ ' + Math.abs(trendCompare).toFixed(2) + '% 上月同比');
} else {
trendTextDom.css('color', '#999').html('持平 上月同比');
}
// 区域月销售额 - 柱状图(根据地址简单按区域统计)
var regionalChart = echarts.init(document.getElementById('regional-sales-chart'));
regionalChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: dashboardData.regions || []
},
yAxis: {
type: 'value',
name: '销量'
},
series: [{
name: '销量',
type: 'bar',
itemStyle: {
color: '#4A70F4'
},
data: dashboardData.regionTotals || []
}]
});
// 响应式调整
window.addEventListener('resize', function() {
customerChart.resize();
salesChart.resize();
channelChart.resize();
trendChart.resize();
regionalChart.resize();
});
}
});
</script>