添加根据经纬度获取城市
添加后台统计看板
This commit is contained in:
@@ -42,9 +42,21 @@ class Ad extends AdminBase
|
||||
}
|
||||
$this->_error($result);
|
||||
}
|
||||
// 确保 client 参数有效
|
||||
if (empty($client)) {
|
||||
$client = $this->request->param('client', '1');
|
||||
}
|
||||
$category_list = AdLogic::getGoodsCategory();
|
||||
$link_page = \app\common\model\Ad::getLinkPage($client);
|
||||
$position_list = AdLogic::infoPosition($client);
|
||||
$this->assign('category_list', AdLogic::getGoodsCategory());
|
||||
$this->assign('link_page', \app\common\model\Ad::getLinkPage($client));
|
||||
$this->assign('position_list', AdLogic::infoPosition($client));
|
||||
|
||||
$this->assign([
|
||||
'category_list' => $category_list,
|
||||
'link_page' => $link_page,
|
||||
'position_list' => $position_list,
|
||||
]);
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
|
||||
124
application/admin/controller/BulletinBoard.php
Normal file
124
application/admin/controller/BulletinBoard.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller;
|
||||
use app\admin\model\User;
|
||||
use app\common\server\UrlServer;
|
||||
use think\facade\Url;
|
||||
|
||||
class BulletinBoard extends AdminBase
|
||||
{
|
||||
|
||||
//看板统计
|
||||
public function index()
|
||||
{
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
|
||||
//地图上显示注册用户位置
|
||||
public function map()
|
||||
{
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
public function getUserMap()
|
||||
{
|
||||
// 查询有经纬度的用户
|
||||
$userArray = User::where('latitude', '<>', '')
|
||||
->where('longitude', '<>', '')
|
||||
->field('id,nickname,mobile,avatar,longitude,latitude')
|
||||
->select();
|
||||
// 格式化数据,转换为前端期望的字段名
|
||||
$result = [];
|
||||
foreach ($userArray as $user) {
|
||||
// 处理头像URL,使用代理接口避免CORS问题
|
||||
$avatar = '';
|
||||
if (!empty($user['avatar'])) {
|
||||
$originalUrl = UrlServer::getFileUrl($user['avatar']);
|
||||
// 去掉JSON编码时产生的转义反斜杠
|
||||
$originalUrl = str_replace('\\/', '/', $originalUrl);
|
||||
|
||||
// 如果URL是跨域的,使用代理接口
|
||||
$currentDomain = $this->request->domain();
|
||||
if (strpos($originalUrl, $currentDomain) === false) {
|
||||
// 跨域,使用代理(手动构建URL避免双重编码)
|
||||
$avatar = $this->request->domain() . '/admin/bulletin_board/proxyImage?url=' . rawurlencode($originalUrl);
|
||||
} else {
|
||||
// 同域,直接使用
|
||||
$avatar = $originalUrl;
|
||||
}
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'id' => $user['id'] ?? 0,
|
||||
'lng' => floatval($user['longitude'] ?? 0),
|
||||
'lat' => floatval($user['latitude'] ?? 0),
|
||||
'name' => $user['nickname'] ?? '',
|
||||
'contact' => $user['nickname'] ?? '', // 兼容两种字段名
|
||||
'mobile' => $user['mobile'] ?? '',
|
||||
'phone' => $user['mobile'] ?? '', // 兼容 telephone/phone
|
||||
'telephone' => $user['mobile'] ?? '',
|
||||
'avatar' => $avatar
|
||||
];
|
||||
}
|
||||
|
||||
return $this->_success('获取成功', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片代理接口,解决CORS跨域问题
|
||||
*/
|
||||
public function proxyImage()
|
||||
{
|
||||
$url = $this->request->get('url', '');
|
||||
if (empty($url)) {
|
||||
header('HTTP/1.1 404 Not Found');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 解码URL
|
||||
$url = urldecode($url);
|
||||
|
||||
// 验证URL格式
|
||||
if (!filter_var($url, FILTER_VALIDATE_URL)) {
|
||||
header('HTTP/1.1 400 Bad Request');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 只允许图片格式
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
$extension = strtolower(pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION));
|
||||
if (!in_array($extension, $allowedExtensions)) {
|
||||
header('HTTP/1.1 403 Forbidden');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 获取图片
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'timeout' => 10,
|
||||
'header' => [
|
||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$imageData = @file_get_contents($url, false, $context);
|
||||
|
||||
if ($imageData === false) {
|
||||
header('HTTP/1.1 404 Not Found');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
header('Content-Type: image/' . ($extension === 'jpg' ? 'jpeg' : $extension));
|
||||
header('Cache-Control: public, max-age=3600');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET');
|
||||
|
||||
// 输出图片
|
||||
echo $imageData;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -218,10 +218,37 @@ class AdLogic
|
||||
*/
|
||||
public static function infoPosition($client)
|
||||
{
|
||||
$position_list = Db::name('ad_position')
|
||||
->where(['client' => $client, 'status' => 1, 'del' => 0])
|
||||
->group('name')
|
||||
->column('id,name', 'id');
|
||||
$position_list = [];
|
||||
|
||||
// 使用错误抑制,避免 unserialize 警告影响页面
|
||||
$originalErrorReporting = error_reporting(0);
|
||||
|
||||
try {
|
||||
// 使用原始 SQL 查询,完全绕过 ThinkPHP 的类型转换
|
||||
$prefix = config('database.prefix');
|
||||
$tableName = $prefix . 'ad_position';
|
||||
$sql = "SELECT DISTINCT `id`, `name` FROM `{$tableName}` WHERE `client` = " . intval($client) . " AND `status` = 1 AND `del` = 0 GROUP BY `name`";
|
||||
|
||||
$result = Db::query($sql);
|
||||
|
||||
if (!empty($result) && is_array($result)) {
|
||||
foreach ($result as $row) {
|
||||
if (isset($row['id']) && isset($row['name'])) {
|
||||
$position_list[$row['id']] = $row['name'];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// 忽略所有异常
|
||||
} catch (\Error $e) {
|
||||
// 忽略所有错误
|
||||
} catch (\Throwable $e) {
|
||||
// 捕获所有可抛出的对象
|
||||
} finally {
|
||||
// 恢复错误报告级别
|
||||
error_reporting($originalErrorReporting);
|
||||
}
|
||||
|
||||
asort($position_list);
|
||||
return $position_list;
|
||||
}
|
||||
|
||||
@@ -14,3 +14,4 @@ class Invoice extends Model
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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>
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace app\api\controller;
|
||||
|
||||
use app\api\logic\AdLogic;
|
||||
|
||||
use app\common\server\UrlServer;
|
||||
use think\Db;
|
||||
|
||||
header("Access-Control-Allow-Origin: *");
|
||||
@@ -13,7 +14,7 @@ header("Access-Control-Allow-Headers: Content-Type, Authorization");
|
||||
|
||||
class Ad extends ApiBase
|
||||
{
|
||||
public $like_not_need_login = ['lists','channel','label','add_comost','add_comost','list_comost','follow_comost','comost_add','label_edit','comost_info','notice','position','position_list','vode_type','video_list','video_info','user_wages','user_wages_add','user_leave','fine','recruit','last_leave','last_fine','notice_list','leave','auth'];
|
||||
public $like_not_need_login = ['lists','popup','channel','label','add_comost','add_comost','list_comost','follow_comost','comost_add','label_edit','comost_info','notice','position','position_list','vode_type','video_list','video_info','user_wages','user_wages_add','user_leave','fine','recruit','last_leave','last_fine','notice_list','leave','auth'];
|
||||
|
||||
/**
|
||||
* @return void
|
||||
@@ -31,6 +32,21 @@ class Ad extends ApiBase
|
||||
$this->_success('获取成功', $list);
|
||||
}
|
||||
|
||||
|
||||
//获取弹窗
|
||||
public function popup()
|
||||
{
|
||||
$list = Db::name('ad')
|
||||
->where('status',1)
|
||||
->where('pid',25)
|
||||
->field('id,name,image,link_type,link')
|
||||
->find();
|
||||
if ($list != null){
|
||||
$list['image'] = UrlServer::getFileUrl($list['image']);
|
||||
}
|
||||
$this->_success('获取成功', $list);
|
||||
}
|
||||
|
||||
//获取客户的渠道
|
||||
public function channel(){
|
||||
$list=Db::name('staffchannel')->field('id,name')->select();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\api\server\CityService;
|
||||
use app\api\model\User;
|
||||
|
||||
class City extends ApiBase
|
||||
{
|
||||
@@ -25,4 +26,37 @@ class City extends ApiBase
|
||||
$array = $this->cityService->list();
|
||||
return $this->_success('成功',$array);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据经纬度获取所在城市
|
||||
* @return void
|
||||
*/
|
||||
public function getCity()
|
||||
{
|
||||
$lat = $this->request->param('lat', '');
|
||||
$lng = $this->request->param('lng', '');
|
||||
|
||||
if (empty($lat) || empty($lng)) {
|
||||
return $this->_error('请提供经纬度参数');
|
||||
}
|
||||
$uid = $this->user_id;
|
||||
//查询用户信息
|
||||
$userInfo = User::find($uid);
|
||||
if ($userInfo) {
|
||||
if ($userInfo->longitude == '' || $userInfo->latitude == '')
|
||||
{
|
||||
$userInfo->longitude = $lng; // 经度
|
||||
$userInfo->latitude = $lat; // 纬度
|
||||
$userInfo->save();
|
||||
}
|
||||
}
|
||||
$result = $this->cityService->getCityByLocation($lat, $lng);
|
||||
|
||||
if ($result['success']) {
|
||||
return $this->_success($result['msg'], $result['data']);
|
||||
} else {
|
||||
return $this->_error($result['msg'], $result['data']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@ use think\facade\Config;
|
||||
|
||||
class CityService
|
||||
{
|
||||
// 腾讯地图API Key
|
||||
private $tencentMapKey = 'EVOBZ-VX7YU-QKJVR-BVESA-AVFT3-7JBWG';
|
||||
|
||||
public function list()
|
||||
{
|
||||
$cacheKey = 'city_cache_keys';
|
||||
@@ -25,4 +28,163 @@ class CityService
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据经纬度获取所在城市
|
||||
* @param float $lat 纬度
|
||||
* @param float $lng 经度
|
||||
* @return array
|
||||
*/
|
||||
public function getCityByLocation($lat, $lng)
|
||||
{
|
||||
// 参数验证
|
||||
if (empty($lat) || empty($lng)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'msg' => '经纬度参数不能为空',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
|
||||
// 验证经纬度范围
|
||||
if (!is_numeric($lat) || !is_numeric($lng)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'msg' => '经纬度格式错误',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
|
||||
$lat = floatval($lat);
|
||||
$lng = floatval($lng);
|
||||
|
||||
if ($lat < -90 || $lat > 90 || $lng < -180 || $lng > 180) {
|
||||
return [
|
||||
'success' => false,
|
||||
'msg' => '经纬度范围错误',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
|
||||
// 生成缓存key
|
||||
$cacheKey = 'city_location_' . md5($lat . '_' . $lng);
|
||||
|
||||
// 尝试从缓存获取
|
||||
if (Cache::store('redis')->has($cacheKey)) {
|
||||
$cachedData = Cache::store('redis')->get($cacheKey);
|
||||
return [
|
||||
'success' => true,
|
||||
'msg' => '获取成功',
|
||||
'data' => $cachedData
|
||||
];
|
||||
}
|
||||
|
||||
// 调用腾讯地图逆地理编码API
|
||||
$apiUrl = 'https://apis.map.qq.com/ws/geocoder/v1/';
|
||||
$params = [
|
||||
'location' => $lat . ',' . $lng,
|
||||
'key' => $this->tencentMapKey,
|
||||
'get_poi' => 0
|
||||
];
|
||||
|
||||
$url = $apiUrl . '?' . http_build_query($params);
|
||||
|
||||
// 使用curl请求
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error) {
|
||||
return [
|
||||
'success' => false,
|
||||
'msg' => '请求失败:' . $error,
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
return [
|
||||
'success' => false,
|
||||
'msg' => 'API请求失败,HTTP状态码:' . $httpCode,
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
|
||||
$result = json_decode($response, true);
|
||||
|
||||
if (empty($result) || $result['status'] !== 0) {
|
||||
return [
|
||||
'success' => false,
|
||||
'msg' => isset($result['message']) ? $result['message'] : '获取城市信息失败',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
|
||||
// 解析返回数据
|
||||
$addressComponent = $result['result']['address_component'] ?? [];
|
||||
$cityName = $addressComponent['city'] ?? '';
|
||||
$provinceName = $addressComponent['province'] ?? '';
|
||||
$districtName = $addressComponent['district'] ?? '';
|
||||
$adcode = $addressComponent['adcode'] ?? '';
|
||||
|
||||
// 如果城市名称为空,尝试使用区县名称
|
||||
if (empty($cityName) && !empty($districtName)) {
|
||||
$cityName = $districtName;
|
||||
}
|
||||
|
||||
// 查询数据库中的城市ID
|
||||
$cityId = null;
|
||||
$cityInfo = null;
|
||||
|
||||
if (!empty($cityName)) {
|
||||
// 先尝试精确匹配
|
||||
$cityInfo = Db::name('dev_region')
|
||||
->where('name', $cityName)
|
||||
->where('level', 2) // 市级
|
||||
->find();
|
||||
|
||||
// 如果精确匹配失败,尝试模糊匹配
|
||||
if (empty($cityInfo)) {
|
||||
$cityInfo = Db::name('dev_region')
|
||||
->where('name', 'like', '%' . $cityName . '%')
|
||||
->where('level', 2)
|
||||
->find();
|
||||
}
|
||||
|
||||
if ($cityInfo) {
|
||||
$cityId = $cityInfo['id'];
|
||||
}
|
||||
}
|
||||
|
||||
$data = [
|
||||
'city_name' => $cityName,
|
||||
'province_name' => $provinceName,
|
||||
'district_name' => $districtName,
|
||||
'adcode' => $adcode,
|
||||
'city_id' => $cityId,
|
||||
'city_info' => $cityInfo,
|
||||
'formatted_address' => $result['result']['formatted_addresses']['recommend'] ?? '',
|
||||
'location' => [
|
||||
'lat' => $lat,
|
||||
'lng' => $lng
|
||||
]
|
||||
];
|
||||
|
||||
// 缓存结果(缓存1小时)
|
||||
Cache::store('redis')->set($cacheKey, $data, 3600);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'msg' => '获取成功',
|
||||
'data' => $data
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user