添加根据经纬度获取城市
添加后台统计看板
This commit is contained in:
639
application/admin/view/bulletin_board/index.html
Normal file
639
application/admin/view/bulletin_board/index.html
Normal file
@@ -0,0 +1,639 @@
|
||||
{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;">10万</span></div>
|
||||
<div class="progress-item">
|
||||
<div class="progress-label">
|
||||
<span>总体完成率</span>
|
||||
<span style="color: #4A70F4; font-weight: bold;">84%</span>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" style="width: 84%;"></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">
|
||||
<!-- 客服进度条列表 -->
|
||||
<div class="progress-item">
|
||||
<div class="progress-label">
|
||||
<span>啦啦</span>
|
||||
<span>销售额: XXX | 百分比: xx% | 同比: xx%</span>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" style="width: 0%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-item">
|
||||
<div class="progress-label">
|
||||
<span>小喵</span>
|
||||
<span>销售额: XXX | 百分比: xx% | 同比: xx%</span>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" style="width: 0%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-item">
|
||||
<div class="progress-label">
|
||||
<span>哆哆</span>
|
||||
<span>销售额: XXX | 百分比: xx% | 同比: xx%</span>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" style="width: 0%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-item">
|
||||
<div class="progress-label">
|
||||
<span>大猫</span>
|
||||
<span>销售额: XXX | 百分比: xx% | 同比: xx%</span>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" style="width: 0%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</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 style="text-align: right; margin-bottom: 10px; color: #f56c6c; font-size: 14px;">
|
||||
▼ 29.23% 上月同比
|
||||
</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>
|
||||
<tr>
|
||||
<td>总额</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>年卡</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>次卡</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>单次服务</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>其他服务</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
</tr>
|
||||
</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>
|
||||
<tr>
|
||||
<td>总额</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>年卡</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>次卡</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>单次服务</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>其他服务</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
<td class="empty-data">-</td>
|
||||
</tr>
|
||||
</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;
|
||||
|
||||
// 日期选择器
|
||||
laydate.render({
|
||||
elem: '#date-filter',
|
||||
type: 'date',
|
||||
format: 'yyyy-MM-dd'
|
||||
});
|
||||
|
||||
// 初始化所有图表(仅样式,不填充数据)
|
||||
initCharts();
|
||||
|
||||
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: 28, name: '老客', itemStyle: {color: '#FFB6C1'}},
|
||||
{value: 72, 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: [
|
||||
{value: 8.2, name: '年卡销售', itemStyle: {color: '#4A70F4'}},
|
||||
{value: 3.2, name: '次卡销售', itemStyle: {color: '#6DD047'}},
|
||||
{value: 1.4, name: '单次服务', itemStyle: {color: '#F6A23F'}},
|
||||
{value: 1.2, 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: [
|
||||
{value: 0, name: '美团'},
|
||||
{value: 24, name: '公众号-企业客户'},
|
||||
{value: 0, name: '抖音'},
|
||||
{value: 0, name: '渠道'},
|
||||
{value: 0, name: '员工'},
|
||||
{value: 0, name: '异业'},
|
||||
{value: 0, name: '普通客户'}
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
// 每日销售趋势 - 折线图
|
||||
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: []
|
||||
},
|
||||
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: []
|
||||
}]
|
||||
});
|
||||
|
||||
// 区域月销售额 - 柱状图
|
||||
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: ['区域1', '区域2', '区域3', '区域4', '区域5', '区域6', '区域7', '区域8']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '销量'
|
||||
},
|
||||
series: [{
|
||||
name: '销量',
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
color: '#4A70F4'
|
||||
},
|
||||
data: [4668, 3775, 2912, 2200, 1259, 700, 403, 0]
|
||||
}]
|
||||
});
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', function() {
|
||||
customerChart.resize();
|
||||
salesChart.resize();
|
||||
channelChart.resize();
|
||||
trendChart.resize();
|
||||
regionalChart.resize();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
645
application/admin/view/bulletin_board/map.html
Normal file
645
application/admin/view/bulletin_board/map.html
Normal file
@@ -0,0 +1,645 @@
|
||||
{layout name="layout1" /}
|
||||
<style>
|
||||
.map-container {
|
||||
padding: 15px;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
.map-header {
|
||||
background: #fff;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.map-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
.map-header-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.map-header-stats {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 15px;
|
||||
background: #f8f9ff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #4A70F4;
|
||||
}
|
||||
.map-search-box {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.map-search-input {
|
||||
width: 300px;
|
||||
}
|
||||
#maplocation {
|
||||
width: 100%;
|
||||
height: calc(100vh - 200px);
|
||||
min-height: 600px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
background: #f5f5f5;
|
||||
}
|
||||
/* 头像标记点样式 */
|
||||
.tmap-marker-avatar {
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
object-fit: cover;
|
||||
}
|
||||
.info-window {
|
||||
padding: 10px;
|
||||
min-width: 150px;
|
||||
}
|
||||
.info-window-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.info-window-item {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin: 5px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.info-window-item strong {
|
||||
color: #333;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.map-controls {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.map-control-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 15px;
|
||||
text-align: center;
|
||||
background: #4A70F4;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.map-control-btn:hover {
|
||||
background: #3a5fd4;
|
||||
}
|
||||
.map-control-btn:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.map-legend {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
z-index: 1000;
|
||||
background: rgba(255,255,255,0.95);
|
||||
padding: 12px 15px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
.legend-color {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
}
|
||||
</style>
|
||||
<div class="map-container">
|
||||
<!-- 地图头部 -->
|
||||
<div class="map-header">
|
||||
<div class="map-header-left">
|
||||
<div class="map-header-title">用户位置分布图</div>
|
||||
<div class="map-header-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">总用户数:</span>
|
||||
<span class="stat-value" id="total-users">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">已定位:</span>
|
||||
<span class="stat-value" id="located-users">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="map-search-box">
|
||||
<input type="text" id="search_key" class="layui-input map-search-input" placeholder="请输入地名搜索">
|
||||
<!-- <button class="layui-btn layui-btn-normal searchKey">搜索</button>-->
|
||||
<button class="layui-btn layui-btn-primary" id="reset-map">重置视图</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 地图容器 -->
|
||||
<div style="position: relative;">
|
||||
<div id="maplocation"></div>
|
||||
|
||||
<!-- 地图控制按钮 -->
|
||||
<div class="map-controls">
|
||||
<button class="map-control-btn" id="zoom-in">放大</button>
|
||||
<button class="map-control-btn" id="zoom-out">缩小</button>
|
||||
<button class="map-control-btn" id="fit-bounds">适应所有标记</button>
|
||||
<button class="map-control-btn" id="clear-markers">清除标记</button>
|
||||
</div>
|
||||
|
||||
<!-- 图例 -->
|
||||
<div class="map-legend">
|
||||
<div style="font-weight: bold; margin-bottom: 8px; color: #333;">图例</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #4A70F4;"></div>
|
||||
<span>用户位置</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/webjs/jquery.min.js"></script>
|
||||
<script charset="utf-8" src="https://map.qq.com/api/gljs?v=1.exp&libraries=service,tools&key=EVOBZ-VX7YU-QKJVR-BVESA-AVFT3-7JBWG" onload="console.log('腾讯地图API脚本加载完成')" onerror="console.error('腾讯地图API脚本加载失败')"></script>
|
||||
|
||||
<script>
|
||||
layui.config({
|
||||
version:"{$front_version}",
|
||||
base: '/static/plug/layui-admin/dist/layuiadmin/'
|
||||
}).extend({
|
||||
index: 'lib/index'
|
||||
}).use(['index'], function(){
|
||||
var $ = layui.$;
|
||||
|
||||
// 用户数据(将从后端获取)
|
||||
var users = [];
|
||||
var markers = null;
|
||||
var map = null;
|
||||
var geocoder = null;
|
||||
var infoWindows = [];
|
||||
var defaultCenter = null; // 默认中心点(贵阳)
|
||||
var defaultZoom = 12;
|
||||
var markerClickHandler = null; // 保存点击事件处理函数
|
||||
|
||||
// 初始化地图
|
||||
function initMap() {
|
||||
try {
|
||||
// 检查TMap是否已加载
|
||||
if (typeof TMap === 'undefined') {
|
||||
console.error('腾讯地图API未加载');
|
||||
layer.msg('地图加载失败,请刷新页面重试', {icon: 2, time: 3000});
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化默认中心点
|
||||
if (!defaultCenter) {
|
||||
defaultCenter = new TMap.LatLng(26.647, 106.63); // 默认中心点(贵阳)
|
||||
}
|
||||
|
||||
console.log('开始初始化地图...');
|
||||
map = new TMap.Map('maplocation', {
|
||||
zoom: defaultZoom,
|
||||
center: defaultCenter
|
||||
});
|
||||
|
||||
console.log('地图对象创建成功');
|
||||
|
||||
// 初始化标记点(样式将在渲染时动态创建)
|
||||
markers = new TMap.MultiMarker({
|
||||
map: map,
|
||||
styles: {},
|
||||
geometries: []
|
||||
});
|
||||
|
||||
console.log('标记点对象创建成功');
|
||||
|
||||
// 初始化地理编码服务
|
||||
geocoder = new TMap.service.Geocoder();
|
||||
|
||||
// 等待地图加载完成
|
||||
map.on('complete', function() {
|
||||
console.log('地图加载完成');
|
||||
|
||||
// 加载用户数据
|
||||
setTimeout(function() {
|
||||
if (typeof loadUsers === 'function') {
|
||||
loadUsers();
|
||||
} else {
|
||||
console.error('loadUsers函数未定义');
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// 备用方案:如果complete事件没有触发,延迟后直接初始化
|
||||
setTimeout(function() {
|
||||
console.log('备用方案:延迟调用loadUsers()');
|
||||
if (typeof loadUsers === 'function') {
|
||||
loadUsers();
|
||||
} else {
|
||||
console.error('loadUsers函数未定义');
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
// 监听地图错误
|
||||
map.on('error', function(e) {
|
||||
console.error('地图错误:', e);
|
||||
layer.msg('地图加载出错,请刷新页面重试', {icon: 2, time: 3000});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('地图初始化失败:', error);
|
||||
layer.msg('地图初始化失败:' + error.message, {icon: 2, time: 3000});
|
||||
}
|
||||
}
|
||||
|
||||
// 加载用户数据
|
||||
function loadUsers() {
|
||||
if (typeof $ === 'undefined') {
|
||||
console.error('jQuery未加载');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{:url("BulletinBoard/getUserMap")}',
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
console.log('用户数据响应:', res);
|
||||
if (res.code === 1 && res.data) {
|
||||
users = res.data;
|
||||
console.log('获取到用户数据:', users.length, '条');
|
||||
updateStats();
|
||||
renderMarkers();
|
||||
} else {
|
||||
console.warn('获取用户数据失败:', res.msg);
|
||||
layer.msg(res.msg || '获取用户数据失败', {icon: 2});
|
||||
users = [];
|
||||
updateStats();
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('请求失败:', xhr, status, error);
|
||||
console.error('响应内容:', xhr.responseText);
|
||||
layer.msg('请求失败,请稍后重试', {icon: 2});
|
||||
users = [];
|
||||
updateStats();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 将loadUsers暴露到全局作用域,方便调试
|
||||
window.loadUsers = loadUsers;
|
||||
|
||||
// 更新统计信息
|
||||
function updateStats() {
|
||||
var totalUsers = users.length;
|
||||
var locatedUsers = users.filter(function(u) {
|
||||
return u.lat && u.lng && u.lat != 0 && u.lng != 0;
|
||||
}).length;
|
||||
|
||||
$('#total-users').text(totalUsers);
|
||||
$('#located-users').text(locatedUsers);
|
||||
}
|
||||
|
||||
// 渲染标记点
|
||||
function renderMarkers() {
|
||||
if (!markers || !map) {
|
||||
console.warn('标记点或地图未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除现有标记和信息窗口
|
||||
clearMarkers();
|
||||
|
||||
var geometries = [];
|
||||
var validUsers = [];
|
||||
var markerStyles = {};
|
||||
|
||||
// 过滤有效坐标的用户
|
||||
users.forEach(function(user, index) {
|
||||
if (!user.lat || !user.lng || user.lat == 0 || user.lng == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var lat = parseFloat(user.lat);
|
||||
var lng = parseFloat(user.lng);
|
||||
|
||||
if (isNaN(lat) || isNaN(lng)) {
|
||||
console.warn('无效的经纬度:', user);
|
||||
return;
|
||||
}
|
||||
|
||||
validUsers.push(user);
|
||||
|
||||
// 为每个用户创建使用头像的标记点样式
|
||||
var styleId = 'avatar_' + (user.id || index);
|
||||
// 如果没有头像,使用默认标记图标
|
||||
var avatarUrl = user.avatar && user.avatar.trim() !== ''
|
||||
? user.avatar
|
||||
: 'https://mapapi.qq.com/web/lbs/javascriptGL/demo/img/markerDefault.png';
|
||||
|
||||
// 创建头像标记样式(圆形头像,带边框)
|
||||
markerStyles[styleId] = new TMap.MarkerStyle({
|
||||
width: 40,
|
||||
height: 40,
|
||||
anchor: {x: 20, y: 20},
|
||||
src: avatarUrl
|
||||
});
|
||||
|
||||
// 创建标记点
|
||||
geometries.push({
|
||||
id: 'user_' + (user.id || index),
|
||||
position: new TMap.LatLng(lat, lng),
|
||||
styleId: styleId,
|
||||
properties: {
|
||||
title: user.contact || user.name || '用户' + (user.id || index),
|
||||
user: user
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log('有效用户数:', validUsers.length, '标记点数:', geometries.length);
|
||||
|
||||
// 更新标记样式和几何图形
|
||||
if (geometries.length > 0) {
|
||||
// 先更新样式
|
||||
markers.setStyles(markerStyles);
|
||||
// 再更新几何图形
|
||||
markers.updateGeometries(geometries);
|
||||
|
||||
// 创建信息窗口(但不自动显示)
|
||||
createInfoWindows(validUsers);
|
||||
|
||||
// 如果只有一个用户,居中显示
|
||||
if (validUsers.length === 1) {
|
||||
map.setCenter(new TMap.LatLng(validUsers[0].lat, validUsers[0].lng));
|
||||
map.setZoom(15);
|
||||
} else if (validUsers.length > 1) {
|
||||
// 适应所有标记
|
||||
fitBounds(validUsers);
|
||||
}
|
||||
} else {
|
||||
console.warn('没有有效的用户位置数据');
|
||||
}
|
||||
}
|
||||
|
||||
// 创建信息窗口(但不立即添加到地图,只在点击时创建)
|
||||
function createInfoWindows(users) {
|
||||
// 先清除之前的事件监听器(如果存在)
|
||||
if (markers && markerClickHandler) {
|
||||
try {
|
||||
markers.off('click', markerClickHandler);
|
||||
} catch (e) {
|
||||
console.warn('移除事件监听器失败:', e);
|
||||
}
|
||||
markerClickHandler = null;
|
||||
}
|
||||
|
||||
// 点击标记显示信息窗口
|
||||
markerClickHandler = function(evt) {
|
||||
var geometry = evt.geometry;
|
||||
if (geometry && geometry.properties) {
|
||||
// 关闭所有信息窗口
|
||||
infoWindows.forEach(function(item) {
|
||||
if (item.window) {
|
||||
try {
|
||||
item.window.close();
|
||||
if (item.window.destroy) {
|
||||
item.window.destroy();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('关闭信息窗口失败:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
infoWindows = [];
|
||||
|
||||
// 从 properties 中直接获取用户数据
|
||||
var user = geometry.properties.user;
|
||||
|
||||
// 如果没有,尝试从全局 users 数组查找
|
||||
if (!user) {
|
||||
var userId = geometry.id.replace('user_', '');
|
||||
user = users.find(function(u) {
|
||||
return (u.id || '').toString() === userId || users.indexOf(u).toString() === userId;
|
||||
});
|
||||
}
|
||||
|
||||
if (user) {
|
||||
var lat = parseFloat(user.lat);
|
||||
var lng = parseFloat(user.lng);
|
||||
|
||||
if (isNaN(lat) || isNaN(lng)) {
|
||||
console.error('无效的经纬度:', user);
|
||||
return;
|
||||
}
|
||||
|
||||
var content = '<div class="info-window">' +
|
||||
'<div class="info-window-title">' + (user.contact || user.name || '用户') + '</div>' +
|
||||
'<div class="info-window-item"><strong>电话:</strong>' + (user.telephone || user.phone || '-') + '</div>' +
|
||||
'<div class="info-window-item"><strong>地址:</strong>' + (user.address || '-') + '</div>';
|
||||
content += '</div>';
|
||||
|
||||
// 只在点击时创建信息窗口
|
||||
try {
|
||||
var infoWindow = new TMap.InfoWindow({
|
||||
map: map,
|
||||
position: new TMap.LatLng(lat, lng),
|
||||
content: content,
|
||||
visible: true,
|
||||
zIndex: 100
|
||||
});
|
||||
|
||||
infoWindows.push({
|
||||
window: infoWindow,
|
||||
markerId: geometry.id
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('创建信息窗口失败:', e);
|
||||
layer.msg('显示信息失败', {icon: 2});
|
||||
}
|
||||
} else {
|
||||
console.warn('未找到用户数据:', geometry.id);
|
||||
}
|
||||
} else {
|
||||
console.warn('标记点数据不完整:', geometry);
|
||||
}
|
||||
};
|
||||
|
||||
// 绑定事件监听器
|
||||
if (markers && markerClickHandler) {
|
||||
markers.on('click', markerClickHandler);
|
||||
}
|
||||
}
|
||||
|
||||
// 适应所有标记
|
||||
function fitBounds(users) {
|
||||
if (users.length === 0) return;
|
||||
|
||||
var bounds = new TMap.LatLngBounds();
|
||||
users.forEach(function(user) {
|
||||
bounds.extend(new TMap.LatLng(parseFloat(user.lat), parseFloat(user.lng)));
|
||||
});
|
||||
|
||||
map.fitBounds(bounds);
|
||||
}
|
||||
|
||||
// 清除标记
|
||||
function clearMarkers() {
|
||||
if (markers) {
|
||||
markers.updateGeometries([]);
|
||||
}
|
||||
infoWindows.forEach(function(item) {
|
||||
if (item.window) {
|
||||
try {
|
||||
item.window.close();
|
||||
if (item.window.destroy) {
|
||||
item.window.destroy();
|
||||
} else if (item.window.setMap) {
|
||||
item.window.setMap(null);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('清除信息窗口失败:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
infoWindows = [];
|
||||
}
|
||||
|
||||
// 搜索功能
|
||||
var searchService = {
|
||||
search: function(name) {
|
||||
if (!name || !name.trim()) {
|
||||
layer.msg('请输入搜索关键词', {icon: 0});
|
||||
return;
|
||||
}
|
||||
|
||||
geocoder.getLocation({ address: name })
|
||||
.then(function(result) {
|
||||
if (result && result.result && result.result.location) {
|
||||
var location = result.result.location;
|
||||
map.setCenter(location);
|
||||
map.setZoom(15);
|
||||
|
||||
// 添加临时标记
|
||||
markers.updateGeometries([{
|
||||
id: 'search_result',
|
||||
position: location,
|
||||
styleId: 'marker'
|
||||
}]);
|
||||
} else {
|
||||
layer.msg('未找到该地点', {icon: 0});
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
layer.msg('搜索失败:' + (error.message || '未知错误'), {icon: 2});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 事件绑定
|
||||
$(document).on('click', '.searchKey', function() {
|
||||
var searchKey = $("#search_key").val();
|
||||
searchService.search(searchKey);
|
||||
});
|
||||
|
||||
$('#search_key').on('keypress', function(e) {
|
||||
if (e.which === 13) {
|
||||
$('.searchKey').trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
$('#reset-map').on('click', function() {
|
||||
map.setCenter(defaultCenter);
|
||||
map.setZoom(defaultZoom);
|
||||
renderMarkers();
|
||||
});
|
||||
|
||||
$('#zoom-in').on('click', function() {
|
||||
map.setZoom(map.getZoom() + 1);
|
||||
});
|
||||
|
||||
$('#zoom-out').on('click', function() {
|
||||
map.setZoom(map.getZoom() - 1);
|
||||
});
|
||||
|
||||
$('#fit-bounds').on('click', function() {
|
||||
var validUsers = users.filter(function(u) {
|
||||
return u.lat && u.lng && u.lat != 0 && u.lng != 0;
|
||||
});
|
||||
if (validUsers.length > 0) {
|
||||
fitBounds(validUsers);
|
||||
} else {
|
||||
layer.msg('没有可显示的用户位置', {icon: 0});
|
||||
}
|
||||
});
|
||||
|
||||
$('#clear-markers').on('click', function() {
|
||||
clearMarkers();
|
||||
layer.msg('已清除所有标记', {icon: 1});
|
||||
});
|
||||
|
||||
// 等待腾讯地图API加载完成后再初始化
|
||||
var retryCount = 0;
|
||||
var maxRetries = 100; // 10秒
|
||||
|
||||
function waitForTMap() {
|
||||
if (typeof TMap !== 'undefined' && typeof TMap.Map !== 'undefined') {
|
||||
// 延迟一下确保API完全加载
|
||||
setTimeout(function() {
|
||||
initMap();
|
||||
}, 300);
|
||||
} else {
|
||||
// 如果还没加载,等待一段时间后重试(最多等待10秒)
|
||||
if (retryCount < maxRetries) {
|
||||
retryCount++;
|
||||
setTimeout(waitForTMap, 100);
|
||||
} else {
|
||||
console.error('腾讯地图API加载超时');
|
||||
layer.msg('地图API加载超时,请刷新页面重试', {icon: 2, time: 3000});
|
||||
}
|
||||
}
|
||||
}
|
||||
// 初始化
|
||||
$(function() {
|
||||
// 等待地图API加载
|
||||
waitForTMap();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user