diff --git a/app/Http/Controllers/Api/DialogController.php b/app/Http/Controllers/Api/DialogController.php index 5688268cb..c81103203 100755 --- a/app/Http/Controllers/Api/DialogController.php +++ b/app/Http/Controllers/Api/DialogController.php @@ -1362,6 +1362,63 @@ class DialogController extends AbstractController ], $botUser->userid, true); } + /** + * @api {post} api/dialog/msg/sendlocation 24. 发送位置消息 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName msg__sendlocation + * + * @apiParam {Number} dialog_id 对话ID + * @apiParam {String} type 位置类型 + * - bd: 百度地图 + * @apiParam {Number} lng 经度 + * @apiParam {Number} lat 纬度 + * @apiParam {String} title 位置名称 + * @apiParam {String} [address] 位置地址 + * @apiParam {String} [preview] 预览图片(url) + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function msg__sendlocation() + { + $user = User::auth(); + // + $dialog_id = intval(Request::input('dialog_id')); + $type = strtolower(trim(Request::input('type'))); + $lng = floatval(Request::input('lng')); + $lat = floatval(Request::input('lat')); + $title = trim(Request::input('title')); + $address = trim(Request::input('address')); + $preview = trim(Request::input('preview')); + // + if (empty($lng) || $lng < -180 || $lng > 180 + || empty($lat) || $lat < -90 || $lat > 90) { + return Base::retError('经纬度错误'); + } + if (empty($title)) { + return Base::retError('位置名称不能为空'); + } + // + WebSocketDialog::checkDialog($dialog_id); + // + if ($type == 'bd') { + $msgData = [ + 'type' => $type, + 'lng' => $lng, + 'lat' => $lat, + 'title' => $title, + 'address' => $address, + 'preview' => $preview, + ]; + return WebSocketDialogMsg::sendMsg(null, $dialog_id, 'location', $msgData, $user->userid); + } + return Base::retError('位置类型错误'); + } + /** * @api {get} api/dialog/msg/readlist 27. 获取消息阅读情况 * diff --git a/app/Http/Controllers/Api/SystemController.php b/app/Http/Controllers/Api/SystemController.php index 578550933..196be7fab 100755 --- a/app/Http/Controllers/Api/SystemController.php +++ b/app/Http/Controllers/Api/SystemController.php @@ -407,6 +407,9 @@ class SystemController extends AbstractController 'face_upload', 'face_remark', 'face_retip', + 'locat_remark', + 'locat_bd_lbs_key', + 'locat_bd_lbs_point', // 格式:{"lng":116.404, "lat":39.915, "radius":500} 'manual_remark', 'modes', 'key', @@ -422,9 +425,21 @@ class SystemController extends AbstractController if (!$botUser) { return Base::retError('创建签到机器人失败'); } + if (in_array('locat', $all['modes'])) { + if (empty($all['locat_bd_lbs_key'])) { + return Base::retError('请填写百度地图LBS Key'); + } + if (!is_array($all['locat_bd_lbs_point'])) { + return Base::retError('请选择允许签到位置'); + } + $all['locat_bd_lbs_point']['radius'] = intval($all['locat_bd_lbs_point']['radius']); + if (empty($all['locat_bd_lbs_point']['lng']) || empty($all['locat_bd_lbs_point']['lat']) || empty($all['locat_bd_lbs_point']['radius'])) { + return Base::retError('请选择有效的签到位置'); + } + } } if ($all['modes']) { - $all['modes'] = array_intersect($all['modes'], ['auto', 'manual', 'location', 'face']); + $all['modes'] = array_intersect($all['modes'], ['auto', 'manual', 'locat', 'face']); } $setting = Base::setting('checkinSetting', Base::newTrim($all)); } else { @@ -444,6 +459,8 @@ class SystemController extends AbstractController $setting['face_upload'] = $setting['face_upload'] ?: 'close'; $setting['face_remark'] = $setting['face_remark'] ?: Doo::translate('考勤机'); $setting['face_retip'] = $setting['face_retip'] ?: 'open'; + $setting['locat_remark'] = $setting['locat_remark'] ?: Doo::translate('定位签到'); + $setting['locat_bd_lbs_point'] = is_array($setting['locat_bd_lbs_point']) ? $setting['locat_bd_lbs_point'] : ['radius' => 500]; $setting['manual_remark'] = $setting['manual_remark'] ?: Doo::translate('手动签到'); $setting['time'] = $setting['time'] ? Base::json2array($setting['time']) : ['09:00', '18:00']; $setting['advance'] = intval($setting['advance']) ?: 120; diff --git a/app/Http/Controllers/IndexController.php b/app/Http/Controllers/IndexController.php index 2ca0a3eea..1c72b50f9 100755 --- a/app/Http/Controllers/IndexController.php +++ b/app/Http/Controllers/IndexController.php @@ -408,7 +408,7 @@ class IndexController extends InvokeController ], 'inline'); } // EEUI App 直接在线预览查看 - if (str_contains($userAgent, 'eeui') && Base::judgeClientVersion("0.34.47")) { + if (Base::isEEUIApp() && Base::judgeClientVersion("0.34.47")) { if ($browser === 'safari-mobile') { $redirectUrl = Base::fillUrl($path); return << [ - [ - 'key' => 'checkin', - 'label' => Doo::translate('我要打卡') - ], /*[ - 'key' => 'it', - 'label' => Doo::translate('IT资讯') - ], [ - 'key' => '36ke', - 'label' => Doo::translate('36氪') - ], [ - 'key' => '60s', - 'label' => Doo::translate('60s读世界') - ], [ - 'key' => 'joke', - 'label' => Doo::translate('开心笑话') - ], [ - 'key' => 'soup', - 'label' => Doo::translate('心灵鸡汤') - ]*/ - ], - 'anon-msg@bot.system' => [ - [ - 'key' => 'help', - 'label' => Doo::translate('使用说明') - ], [ - 'key' => 'privacy', - 'label' => Doo::translate('隐私说明') - ], - ], - 'bot-manager@bot.system' => [ - [ - 'key' => '/help', - 'label' => Doo::translate('帮助指令') - ], [ - 'key' => '/api', - 'label' => Doo::translate('API接口文档') - ], [ - 'key' => '/list', - 'label' => Doo::translate('我的机器人') - ], - ], - 'ai-openai@bot.system', - 'ai-claude@bot.system', - 'ai-wenxin@bot.system', - 'ai-gemini@bot.system', - 'ai-zhipu@bot.system', - 'ai-qianwen@bot.system' => [ - [ - 'key' => '%3A.clear', - 'label' => Doo::translate('清空上下文') - ] - ], - default => [], - }; + switch ($email) { + case 'check-in@bot.system': + $menu = [ + /*[ + 'key' => 'it', + 'label' => Doo::translate('IT资讯') + ], [ + 'key' => '36ke', + 'label' => Doo::translate('36氪') + ], [ + 'key' => '60s', + 'label' => Doo::translate('60s读世界') + ], [ + 'key' => 'joke', + 'label' => Doo::translate('开心笑话') + ], [ + 'key' => 'soup', + 'label' => Doo::translate('心灵鸡汤') + ]*/ + ]; + $setting = Base::setting('checkinSetting'); + if ($setting['open'] !== 'open') { + return $menu; + } + if (in_array('locat', $setting['modes']) && Base::isEEUIApp()) { + $menu[] = [ + 'key' => 'locat-checkin', + 'label' => $setting['locat_remark'] ?: Doo::translate('定位签到'), + 'config' => [ + 'key' => $setting['locat_bd_lbs_key'], + 'lng' => $setting['locat_bd_lbs_point']['lng'], + 'lat' => $setting['locat_bd_lbs_point']['lat'], + 'radius' => $setting['locat_bd_lbs_point']['radius'], + ] + ]; + } + if (in_array('manual', $setting['modes'])) { + $menu[] = [ + 'key' => 'manual-checkin', + 'label' => $setting['manual_remark'] ?: Doo::translate('手动打卡') + ]; + } + return $menu; + case 'anon-msg@bot.system': + return [ + [ + 'key' => 'help', + 'label' => Doo::translate('使用说明') + ], [ + 'key' => 'privacy', + 'label' => Doo::translate('隐私说明') + ], + ]; + + case 'bot-manager@bot.system': + return [ + [ + 'key' => '/help', + 'label' => Doo::translate('帮助指令') + ], [ + 'key' => '/api', + 'label' => Doo::translate('API接口文档') + ], [ + 'key' => '/list', + 'label' => Doo::translate('我的机器人') + ], + ]; + + default: + if (preg_match('/^ai-(.*?)@bot.system$/', $email)) { + return [ + [ + 'key' => '%3A.clear', + 'label' => Doo::translate('清空上下文') + ] + ]; + } + return []; + } } /** @@ -163,7 +186,7 @@ class UserBot extends AbstractModel } Cache::put("UserBot::checkinBotQuickMsg:{$userid}", "yes", Carbon::now()->addSecond()); // - if ($command === 'checkin') { + if ($command === 'manual-checkin') { $setting = Base::setting('checkinSetting'); if ($setting['open'] !== 'open') { return '暂未开启签到功能。'; @@ -182,7 +205,9 @@ class UserBot extends AbstractModel /** * 签到机器人签到 - * @param $mac + * @param mixed $mac + * - 多个使用,分隔 + * - 支持:mac地址、userid、checkin-userid * @param $time * @param bool $alreadyTip 签到过是否提示 * @return string|null 返回string表示错误信息,返回null表示签到成功 diff --git a/app/Models/WebSocketDialogMsg.php b/app/Models/WebSocketDialogMsg.php index 5b4f52435..504a0612e 100644 --- a/app/Models/WebSocketDialogMsg.php +++ b/app/Models/WebSocketDialogMsg.php @@ -957,6 +957,7 @@ class WebSocketDialogMsg extends AbstractModel /** * 发送消息、修改消息 * @param string $action 动作 + * - null:发送消息 * - reply-98:回复消息ID=98 * - update-99:更新消息ID=99(标记修改) * - change-99:更新消息ID=99(不标记修改) @@ -994,6 +995,19 @@ class WebSocketDialogMsg extends AbstractModel if (in_array($msg['ext'], ['jpg', 'jpeg', 'webp', 'png', 'gif'])) { $mtype = 'image'; } + } elseif ($type === 'location') { + if (preg_match('/^https*:\/\//', $msg['preview'])) { + $preview = file_get_contents($msg['preview']); + if (empty($preview)) { + throw new ApiException('获取地图快照失败'); + } + $filePath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/" . md5s($msg['preview']) . ".jpg"; + Base::makeDir(dirname(public_path($filePath))); + if (!Base::saveContentImage(public_path($filePath), $preview, 90)) { + throw new ApiException('保存地图快照失败'); + } + $msg['preview'] = $filePath; + } } if ($push_silence === null) { $push_silence = !in_array($type, ["text", "file", "record", "meeting"]); diff --git a/app/Module/Base.php b/app/Module/Base.php index b26a9026f..4229fccb3 100755 --- a/app/Module/Base.php +++ b/app/Module/Base.php @@ -2039,6 +2039,16 @@ class Base return $platform; } + /** + * 是否是App移动端 + * @return bool + */ + public static function isEEUIApp() + { + $userAgent = strtolower(Request::server('HTTP_USER_AGENT')); + return str_contains($userAgent, 'kuaifan_eeui'); + } + /** * 返回根据距离sql排序语句 * @param $lat diff --git a/language/original-api.txt b/language/original-api.txt index 906bb6b0a..60c025762 100644 --- a/language/original-api.txt +++ b/language/original-api.txt @@ -384,7 +384,7 @@ LDAP 用户禁止修改邮箱 匿名消息 系统管理员 我要签到 -我要打卡 +手动打卡 关键词不能为空 LICENSE 格式错误 LICENSE 保存失败 @@ -714,3 +714,5 @@ webhook地址最长仅支持255个字符。 动画表情 每日开心:(*) 心灵鸡汤:(*) + +定位签到 diff --git a/language/translate.json b/language/translate.json index 20e1d651f..ef417d79f 100644 --- a/language/translate.json +++ b/language/translate.json @@ -22967,18 +22967,6 @@ "id": "Saya ingin check-in", "ru": "Я хочу зарегистрироваться" }, - { - "key": "我要打卡", - "zh": "", - "zh-CHT": "我要打卡", - "en": "I want to clock in", - "ko": "출근하고 싶습니다", - "ja": "出勤したい", - "de": "Ich möchte einstempeln", - "fr": "Je veux pointer", - "id": "Saya ingin absen", - "ru": "Я хочу отметиться" - }, { "key": "关键词不能为空", "zh": "", @@ -26027,4 +26015,4 @@ "id": "Lokasi Gagal", "ru": "Не удалось определить местоположение" } -] \ No newline at end of file +] diff --git a/public/tools/map/index.html b/public/tools/map/index.html index 8c1a0c575..fccb294ef 100644 --- a/public/tools/map/index.html +++ b/public/tools/map/index.html @@ -17,7 +17,14 @@ -
+
+
+
+ + + +
+

diff --git a/public/tools/map/main.js b/public/tools/map/main.js index 5757475b7..0df1de222 100644 --- a/public/tools/map/main.js +++ b/public/tools/map/main.js @@ -1,356 +1,42 @@ -class LocationPicker { +class App { + static #eeui = null; + static #geolocation = null; + constructor() { - this.map = null; - this.marker = null; - this.geolocation = null; - this.localSearch = null; - this.currentPoint = null; - this.loadNum = 0; - this.config = { - theme: 'light', // 主题风格,light|dark - key: null, // 百度地图 API Key - title: null, // 页面标题,如:选择打卡地点 - label: null, // 搜索列表标签,如:附近的地点 - placeholder: null, // 搜索框占位符,如:搜索附近的地点 - point: null, // 初始坐标,如:116.404,39.915 - noresult: null, // 无搜索结果提示,如:附近没有找到地点 - radius: 300, // 搜索半径,单位:300 - zoom: 16, // 地图缩放级别 - errtip: null, // 定位失败提示 - errclose: false, // 定位失败是否关闭页面 - channel: null, // 回传数据通道 - selectclose: false, // 选择地点是否关闭页面 - }; - this.init(); + this.constructor.init(); } - async init() { - // 先初始化参数 - this.initParams(); - - // 如果没有 key,直接返回 - if (!this.config.key) { - console.error('未提供百度地图 API Key'); - return; - } - - try { - // 等待地图 JS 加载完成 - await this.loadBaiduMapScript(); - // 初始化地图 - this.initMap(); - } catch (error) { - console.error('加载百度地图失败:', error); + static async init() { + while (typeof requireModuleJs !== "function") { + await new Promise(resolve => setTimeout(resolve, 500)); } + this.#eeui = requireModuleJs("eeui"); + this.#geolocation = requireModuleJs("eeui/geolocation"); } - initParams() { - // 获取当前URL的查询参数 - const urlParams = new URLSearchParams(window.location.search); - - // 遍历 config 对象的所有属性 - Object.keys(this.config).forEach(key => { - // 从 URL 参数中获取值 - const value = urlParams.get(key); - if (value !== null) { - // 根据参数类型进行转换 - switch (key) { - case 'radius': - // 转换为数字 - this.config[key] = parseInt(value) || 300; - break; - case 'point': - // 转换为坐标数组 - const [lng, lat] = value.replace(/[|-]/, ',').split(',').map(parseFloat); - if (lng && lat) { - this.config[key] = {lng, lat}; - } - break; - default: - // 字符串类型直接赋值 - this.config[key] = value; - } - } - }); - - // 设置主题风格 - document.documentElement.classList.add(`theme-${this.config.theme}`); - document.body.style.backgroundColor = "#ffffff"; - - - // 设置标题 - if (this.config.title) { - document.title = this.config.title; - } - - // 设置搜索框占位符 - if (this.config.placeholder) { - document.getElementById('search-input').placeholder = this.config.placeholder; - } - - // 设置label - if (this.config.label) { - document.getElementById('address-label').innerText = this.config.label; + static async setVariate(key, value) { + while (!this.#eeui) { + await new Promise(resolve => setTimeout(resolve, 500)); } + await this.#eeui.setVariate(key, value); } - initMap() { - // 初始化地图 - this.map = new BMap.Map('map-container'); - - // 创建定位控件 - const locationControl = new BMap.GeolocationControl({ - anchor: BMAP_ANCHOR_BOTTOM_RIGHT, - showAddressBar: false, - enableAutoLocation: false, - locationIcon: new BMap.Icon("empty.svg", new BMap.Size(0, 0)) - }); - - // 监听定位事件 - locationControl.addEventListener("locationSuccess", (e) => { - // 定位成功事件 - this.updateCurrentPoint(e.point); - }); - - locationControl.addEventListener("locationError", (e) => { - // 定位失败事件 - console.error('定位失败:', e.message); - this.locationError(); - }); - - // 添加定位控件到地图 - this.map.addControl(locationControl); - - // 初始化本地搜索,移除地图渲染 - this.localSearch = new BMap.LocalSearch(this.map, { - renderOptions: { - autoViewport: false // 关闭自动视野调整 - } - }); - - // 设置地图中心点 - if (this.config.point) { - const {lng, lat} = this.config.point; - this.config.point = new BMap.Point(lng, lat); - // 设置地图中心点和缩放级别 - this.map.centerAndZoom(this.config.point, this.config.zoom); - // 创建圆形区域 - const circle = new BMap.Circle(this.config.point, this.config.radius, { - fillColor: "#333333", - fillOpacity: 0.1, - strokeColor: "#333333", - strokeWeight: 1, - strokeOpacity: 0.3 + static async getLocation() { + while (!this.#geolocation) { + await new Promise(resolve => setTimeout(resolve, 500)); + } + return new Promise(resolve => { + this.#geolocation.get((res) => { + resolve(res); }); - this.map.addOverlay(circle); - } - - // 绑定事件 - this.bindEvents(); - - // 初始化时自动定位 - this.getCurrentLocation(); - } - - bindEvents() { - const searchInput = document.getElementById('search-input'); - - // 监听回车键 - searchInput.addEventListener('keyup', (e) => { - if (e.key === 'Enter') { - searchInput.blur(); - } - }); - - // 监听失去焦点 - searchInput.addEventListener('blur', () => { - this.searchAddress(); }); } - getCurrentLocation() { - this.loaderShow(); - this.geolocation = new BMap.Geolocation(); - this.geolocation.getCurrentPosition((result) => { - this.loaderHide(); - if (result && result.point) { - this.updateCurrentPoint(result.point) - } else { - console.error('定位失败'); - this.locationError(); - } - }, {enableHighAccuracy: true}); - } - - updateCurrentPoint(point) { - this.currentPoint = point; - this.map.centerAndZoom(this.currentPoint, this.config.zoom); - this.updateMarker(this.currentPoint); - this.searchNearby(); - } - - updateMarker(point) { - if (this.marker) { - this.marker.setPosition(point); - } else { - this.marker = new BMap.Marker(point); - this.map.addOverlay(this.marker); - } - } - - searchAddress() { - const keyword = document.getElementById('search-input').value; - this.searchNearby(keyword ? [keyword] : []); - } - - searchNearby(keywords = [], retryCount = 0) { - // 当前位置未获取 - if (this.currentPoint === null) { - return; - } - - // 清除之前的搜索结果 - this.localSearch.clearResults(); - - // 搜索附近的关键词 - if (keywords.length === 0) { - keywords = ["写字楼", "公司", "银行", "餐馆", "商场", "超市", "学校", "医院", "公交站", "地铁站"] - } - - // 定义一个随机数,用于判断定时器是否过期 - this.searchRandom = Math.random(); - - // 设置搜索完成回调 - this.loaderShow(); - this.localSearch.setSearchCompleteCallback((results) => { - this.loaderHide(); - if (this.localSearch.getStatus() !== BMAP_STATUS_SUCCESS) { - // 搜索失败 - if (retryCount < 60) { - const tmpRand = this.searchRandom; - this.loaderShow(); - setTimeout(() => { - this.loaderHide(); - tmpRand === this.searchRandom && this.searchNearby(keywords, ++retryCount); - }, 1000) - return; - } - } - // 搜索结果 - document.getElementById('address-list').style.display = 'block'; - const array = []; - if (results instanceof Array) { - results.some(result => { - if (!result) { - return false; - } - for (let i = 0; i < result.getCurrentNumPois(); i++) { - const poi = result.getPoi(i); - poi._distance = this.config.point ? this.map.getDistance(this.config.point, poi.point) : null; - array.push(poi); - } - }); - } - this.updatePoiList(array); - }); - - // 执行搜索 - this.localSearch.searchNearby(keywords, this.currentPoint, this.config.radius); - } - - updatePoiList(results) { - const poiList = document.getElementById('poi-list'); - poiList.innerHTML = ''; - - // 如果没有搜索结果 - if (results.length === 0 && this.config.noresult) { - poiList.innerHTML = '
  • ' + this.config.noresult + '
  • '; - return; - } - - // 按距离排序(如果有距离信息) - results.sort((a, b) => { - if (a._distance && b._distance) { - return a._distance - b._distance; - } - return 0; - }); - results = results.slice(0, 20); - - // 创建列表项 - results.forEach(poi => { - const li = document.createElement('li'); - const distance = poi._distance ? `
    ${this.convertDistance(Math.round(poi._distance))}
    ` : ''; - li.innerHTML = ` -
    ${poi.title}
    -
    ${poi.address || ""}${distance}
    - `; - li.addEventListener('click', () => { - const point = poi.point; - this.updateMarker(point); - this.map.setCenter(point); - // - if (typeof requireModuleJs === "function") { - const eeui = requireModuleJs("eeui"); - eeui.setVariate("location::" + this.config.channel, JSON.stringify(poi)) - } - if (this.config.selectclose) { - this.closePage(); - } - }); - poiList.appendChild(li); - }); - - // 列表更新后,重新将当前标记点居中显示 - setTimeout(() => { - if (this.marker) { - this.map.setCenter(this.marker.getPosition()); - } - }, 100); // 添加小延时确保DOM已更新 - } - - convertDistance(d) { - if (d > 1000) { - return (d / 1000).toFixed(1) + 'km'; - } - return d.toFixed(0) + 'm'; - } - - locationError() { - if (this.config.errtip) { - alert(this.config.errtip); - } - if (this.config.errclose) { - this.closePage(); - } - } - - loaderShow() { - this.loadNum++; - this.loaderJudge(); - } - - loaderHide() { - setTimeout(() => { - this.loadNum--; - this.loaderJudge(); - }, 100) - } - - loaderJudge() { - if (this.loadNum > 0) { - document.querySelector('.loading').classList.add('show'); - } else if (this.loadNum <= 0) { - document.querySelector('.loading').classList.remove('show'); - } - } - - closePage() { + static closePage() { try { // 方法1: 如果是在 eeui 环境中 - if (typeof requireModuleJs === "function") { - const eeui = requireModuleJs("eeui"); - eeui.closePage(); + if (this.#eeui) { + this.#eeui.closePage(); } // 方法2: 如果是从其他页面打开的,可以关闭当前窗口 @@ -373,7 +59,495 @@ class LocationPicker { } } - loadBaiduMapScript() { + static isArray(obj) { + return typeof (obj) == "object" && Object.prototype.toString.call(obj).toLowerCase() == '[object array]' && typeof obj.length == "number"; + } + + static isJson(obj) { + return typeof (obj) == "object" && Object.prototype.toString.call(obj).toLowerCase() == "[object object]" && typeof obj.length == "undefined"; + } +} + +class CoordTransform { + // 私有静态常量 + static #x_PI = 3.14159265358979324 * 3000.0 / 180.0; + static #PI = 3.1415926535897932384626; + static #a = 6378245.0; + static #ee = 0.00669342162296594323; + + /** + * WGS84 转 BD09 + * @param {number} lng WGS84 经度 + * @param {number} lat WGS84 纬度 + * @returns {[number, number]} BD09 坐标 [经度, 纬度] + */ + static wgs84toBd09(lng, lat) { + const gcj = CoordTransform.wgs84ToGcj02(lng, lat); + return CoordTransform.gcj02ToBd09(gcj[0], gcj[1]); + } + + /** + * WGS84 转 GCJ02 + * @private + */ + static wgs84ToGcj02(lng, lat) { + if (CoordTransform.outOfChina(lng, lat)) { + return [lng, lat]; + } + + let dlat = CoordTransform.transformLat(lng - 105.0, lat - 35.0); + let dlng = CoordTransform.transformLng(lng - 105.0, lat - 35.0); + const radLat = lat / 180.0 * CoordTransform.#PI; + let magic = Math.sin(radLat); + magic = 1 - CoordTransform.#ee * magic * magic; + const sqrtMagic = Math.sqrt(magic); + dlat = (dlat * 180.0) / ((CoordTransform.#a * (1 - CoordTransform.#ee)) / (magic * sqrtMagic) * CoordTransform.#PI); + dlng = (dlng * 180.0) / (CoordTransform.#a / sqrtMagic * Math.cos(radLat) * CoordTransform.#PI); + const mgLat = lat + dlat; + const mgLng = lng + dlng; + return [mgLng, mgLat]; + } + + /** + * GCJ02 转 BD09 + * @private + */ + static gcj02ToBd09(lng, lat) { + const z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * CoordTransform.#x_PI); + const theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * CoordTransform.#x_PI); + const bdLng = z * Math.cos(theta) + 0.0065; + const bdLat = z * Math.sin(theta) + 0.006; + return [bdLng, bdLat]; + } + + /** + * 判断坐标是否在中国境内 + * @private + */ + static outOfChina(lng, lat) { + return (lng < 72.004 || lng > 137.8347) || (lat < 0.8293 || lat > 55.8271); + } + + /** + * 转换纬度 + * @private + */ + static transformLat(lng, lat) { + let ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng)); + ret += (20.0 * Math.sin(6.0 * lng * CoordTransform.#PI) + 20.0 * + Math.sin(2.0 * lng * CoordTransform.#PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(lat * CoordTransform.#PI) + 40.0 * + Math.sin(lat / 3.0 * CoordTransform.#PI)) * 2.0 / 3.0; + ret += (160.0 * Math.sin(lat / 12.0 * CoordTransform.#PI) + 320 * + Math.sin(lat * CoordTransform.#PI / 30.0)) * 2.0 / 3.0; + return ret; + } + + /** + * 转换经度 + * @private + */ + static transformLng(lng, lat) { + let ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng)); + ret += (20.0 * Math.sin(6.0 * lng * CoordTransform.#PI) + 20.0 * + Math.sin(2.0 * lng * CoordTransform.#PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(lng * CoordTransform.#PI) + 40.0 * + Math.sin(lng / 3.0 * CoordTransform.#PI)) * 2.0 / 3.0; + ret += (150.0 * Math.sin(lng / 12.0 * CoordTransform.#PI) + 300.0 * + Math.sin(lng / 30.0 * CoordTransform.#PI)) * 2.0 / 3.0; + return ret; + } +} + +class Loader { + static #num = 0; + + static show() { + this.#num++; + this.judge(); + } + + static hide() { + setTimeout(() => { + this.#num--; + this.judge(); + }, 100) + } + + static judge() { + if (this.#num > 0) { + document.querySelector('.loading').classList.add('show'); + } else if (this.#num <= 0) { + document.querySelector('.loading').classList.remove('show'); + } + } +} + +class BaiduMapPicker { + constructor() { + this.map = null; + this.marker = null; + this.localSearch = null; + this.currentPoint = null; + this.params = { + theme: 'light', // 主题风格,light|dark + key: null, // 百度地图 API Key + title: null, // 页面标题,如:选择打卡地点 + label: null, // 搜索列表标签,如:附近的地点 + placeholder: null, // 搜索框占位符,如:搜索附近的地点 + point: null, // 初始坐标,如:116.404,39.915 + noresult: null, // 无搜索结果提示,如:附近没有找到地点 + radius: 300, // 搜索半径,单位:300 + zoom: 16, // 地图缩放级别 + errtip: null, // 定位失败提示 + errclose: false, // 定位失败是否关闭页面 + channel: null, // 回传数据通道 + selectclose: false, // 选择地点是否关闭页面 + }; + this.init(); + } + + async init() { + // 先初始化参数 + this.initParams(); + + // 如果没有 key,直接返回 + if (!this.params.key) { + console.error('未提供百度地图 API Key'); + return; + } + + try { + // 等待地图 JS 加载完成 + await this.loadMapScript(); + // 初始化地图 + this.initMap(); + } catch (error) { + console.error('加载百度地图失败:', error); + } + } + + /** + * 初始化参数 + */ + initParams() { + // 获取当前URL的查询参数 + const urlParams = new URLSearchParams(window.location.search); + + // 遍历 params 对象的所有属性 + Object.keys(this.params).forEach(key => { + // 从 URL 参数中获取值 + const value = urlParams.get(key); + if (value !== null) { + // 根据参数类型进行转换 + switch (key) { + case 'radius': + // 转换为数字 + this.params[key] = parseInt(value) || 300; + break; + case 'point': + // 转换为坐标数组 + const [lng, lat] = value.replace(/[|-]/, ',').split(',').map(parseFloat); + if (lng && lat) { + this.params[key] = {lng, lat}; + } + break; + default: + // 字符串类型直接赋值 + this.params[key] = value; + } + } + }); + + // 设置主题风格 + if (!['dark', 'light'].includes(this.params.theme)) { + this.params.theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + document.documentElement.classList.add(`theme-${this.params.theme}`); + document.body.style.backgroundColor = "#ffffff"; + + + // 设置标题 + if (this.params.title) { + document.title = this.params.title; + } + + // 设置搜索框占位符 + if (this.params.placeholder) { + document.getElementById('search-input').placeholder = this.params.placeholder; + } + + // 设置label + if (this.params.label) { + document.getElementById('address-label').innerText = this.params.label; + } + } + + /** + * 初始化地图 + */ + initMap() { + // 初始化地图 + this.map = new BMap.Map('map-container'); + + // 初始化本地搜索,移除地图渲染 + this.localSearch = new BMap.LocalSearch(this.map, { + renderOptions: { + autoViewport: false // 关闭自动视野调整 + } + }); + + // 设置地图中心点 + if (this.params.point) { + const {lng, lat} = this.params.point; + this.params.point = new BMap.Point(lng, lat); + // 设置地图中心点和缩放级别 + this.map.centerAndZoom(this.params.point, this.params.zoom); + // 创建圆形区域 + const circle = new BMap.Circle(this.params.point, this.params.radius, { + fillColor: "#333333", + fillOpacity: 0.1, + strokeColor: "#333333", + strokeWeight: 1, + strokeOpacity: 0.3 + }); + this.map.addOverlay(circle); + } + + // 绑定事件 + this.bindEvents(); + + // 初始化时自动定位 + Loader.show(); + this.getCurrentLocation().then((point) => { + Loader.hide(); + if (point === null) { + this.locationError(); + } + document.getElementById('map-location').style.display = 'block'; + }); + } + + /** + * 绑定事件 + */ + bindEvents() { + // 输入框事件 + const searchInput = document.getElementById('search-input'); + searchInput.addEventListener('keyup', (e) => { + if (e.key === 'Enter') { + searchInput.blur(); + } + }); + searchInput.addEventListener('blur', () => { + this.searchAddress(); + }); + + // 地图定位点击事件 + const mapLocation = document.getElementById('map-location'); + mapLocation.addEventListener('click', () => { + Loader.show() + this.getCurrentLocation().then(() => { + Loader.hide() + }); + }); + } + + /** + * 获取当前位置 + * @returns {Promise} + */ + getCurrentLocation() { + return new Promise(resolve => { + App.getLocation().then(res => { + if (App.isJson(res) && res.longitude && res.latitude) { + const bd09_coord = CoordTransform.wgs84toBd09(res.longitude, res.latitude); + const point = new BMap.Point(bd09_coord[0], bd09_coord[1]); + this.updateCurrentPoint(point) + resolve(point); + } else { + console.error('定位失败'); + resolve(null); + } + }) + }) + } + + /** + * 更新当前位置 + * @param point + */ + updateCurrentPoint(point) { + this.currentPoint = point; + this.map.centerAndZoom(this.currentPoint, this.params.zoom); + this.updateMarker(this.currentPoint); + this.searchNearby(); + } + + /** + * 更新标记点 + * @param point + */ + updateMarker(point) { + if (this.marker) { + this.marker.setPosition(point); + } else { + this.marker = new BMap.Marker(point); + this.map.addOverlay(this.marker); + } + } + + /** + * 搜索地址 + */ + searchAddress() { + const keyword = document.getElementById('search-input').value; + this.searchNearby(keyword ? [keyword] : []); + } + + /** + * 搜索附近的地点 + * @param keywords + * @param retryCount + */ + searchNearby(keywords = [], retryCount = 0) { + // 当前位置未获取 + if (this.currentPoint === null) { + return; + } + + // 清除之前的搜索结果 + this.localSearch.clearResults(); + + // 搜索附近的关键词 + if (keywords.length === 0) { + keywords = ["写字楼", "公司", "银行", "餐馆", "商场", "超市", "学校", "医院", "公交站", "地铁站"] + } + + // 定义一个随机数,用于判断定时器是否过期 + this.searchRandom = Math.random(); + + // 设置搜索完成回调 + Loader.show(); + this.localSearch.setSearchCompleteCallback((results) => { + Loader.hide(); + if (this.localSearch.getStatus() !== BMAP_STATUS_SUCCESS) { + // 搜索失败 + if (retryCount < 60) { + const tmpRand = this.searchRandom; + Loader.show(); + setTimeout(() => { + Loader.hide(); + tmpRand === this.searchRandom && this.searchNearby(keywords, ++retryCount); + }, 1000) + return; + } + } + // 搜索结果 + document.getElementById('address-list').style.display = 'block'; + const array = []; + if (results instanceof Array) { + results.some(result => { + if (!result) { + return false; + } + for (let i = 0; i < result.getCurrentNumPois(); i++) { + const poi = result.getPoi(i); + poi.distance = this.params.point ? this.map.getDistance(this.params.point, poi.point) : null; + array.push(poi); + } + }); + } + this.updatePoiList(array); + }); + + // 执行搜索 + this.localSearch.searchNearby(keywords, this.currentPoint, this.params.radius); + } + + /** + * 更新搜索结果列表 + * @param results + */ + updatePoiList(results) { + const poiList = document.getElementById('poi-list'); + poiList.innerHTML = ''; + + // 如果没有搜索结果 + if (results.length === 0 && this.params.noresult) { + poiList.innerHTML = '
  • ' + this.params.noresult + '
  • '; + return; + } + + // 按距离排序(如果有距离信息) + results.sort((a, b) => { + if (a.distance && b.distance) { + return a.distance - b.distance; + } + return 0; + }); + results = results.slice(0, 20); + + // 创建列表项 + results.forEach(poi => { + const li = document.createElement('li'); + const distanceFormat = poi.distance ? `
    ${this.convertDistance(Math.round(poi.distance))}
    ` : ''; + li.innerHTML = ` +
    ${poi.title}
    +
    ${poi.address || ""}${distanceFormat}
    + `; + li.addEventListener('click', () => { + const point = poi.point; + this.updateMarker(point); + this.map.setCenter(point); + // + App.setVariate("location::" + this.params.channel, JSON.stringify(poi)); + if (this.params.selectclose) { + App.closePage(); + } + }); + poiList.appendChild(li); + }); + + // 列表更新后,重新将当前标记点居中显示 + setTimeout(() => { + if (this.marker) { + this.map.setCenter(this.marker.getPosition()); + } + }, 100); // 添加小延时确保DOM已更新 + } + + /** + * 转换距离显示 + * @param d + * @returns {string} + */ + convertDistance(d) { + if (d > 1000) { + return (d / 1000).toFixed(1) + 'km'; + } + return d.toFixed(0) + 'm'; + } + + /** + * 定位失败提示 + */ + locationError() { + if (this.params.errtip) { + alert(this.params.errtip); + } + if (this.params.errclose) { + App.closePage(); + } + } + + /** + * 加载百度地图脚本 + * @returns {Promise} + */ + loadMapScript() { return new Promise((resolve, reject) => { // 如果已经加载过,直接返回 if (window.BMap) { @@ -384,7 +558,7 @@ class LocationPicker { // 创建script标签 const script = document.createElement('script'); script.type = 'text/javascript'; - script.src = `https://api.map.baidu.com/api?v=3.0&ak=${this.config.key}&callback=initBaiduMap`; + script.src = `https://api.map.baidu.com/api?v=3.0&ak=${this.params.key}&callback=initBaiduMap`; // 添加回调函数 window.initBaiduMap = () => { @@ -405,5 +579,6 @@ class LocationPicker { // 初始化 document.addEventListener('DOMContentLoaded', () => { - new LocationPicker(); + new App(); + new BaiduMapPicker(); }); diff --git a/public/tools/map/select.html b/public/tools/map/select.html new file mode 100644 index 000000000..13310a11b --- /dev/null +++ b/public/tools/map/select.html @@ -0,0 +1,272 @@ + + + + + + + + + +
    + + diff --git a/public/tools/map/style.css b/public/tools/map/style.css index e9dee2029..bd4c69660 100644 --- a/public/tools/map/style.css +++ b/public/tools/map/style.css @@ -2,6 +2,7 @@ margin: 0; padding: 0; box-sizing: border-box; + user-select: none; } html.theme-dark { @@ -67,11 +68,40 @@ body { -webkit-user-select: text; } +.map-box { + position: relative; + display: flex; + flex: 1; +} + #map-container { flex: 1; min-height: 240px; } +#map-location { + display: none; + position: absolute; + bottom: 16px; + right: 16px; + z-index: 100; + background: #fff; + border-radius: 4px; + cursor: pointer; + width: 36px; + height: 36px; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); +} + +#map-location .icon { + width: 24px; + height: 24px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + .address-list { display: none; min-height: 300px; diff --git a/resources/assets/js/pages/manage/components/DialogWrapper.vue b/resources/assets/js/pages/manage/components/DialogWrapper.vue index 24761d89b..10b27a340 100644 --- a/resources/assets/js/pages/manage/components/DialogWrapper.vue +++ b/resources/assets/js/pages/manage/components/DialogWrapper.vue @@ -1722,11 +1722,70 @@ export default { } }, + /** + * 发送位置消息 + * @param data + */ + sendLocationMsg(data) { + this.$store.dispatch("call", { + url: 'dialog/msg/sendlocation', + data: Object.assign(data, { + dialog_id: this.dialogId, + }), + spinner: true, + method: 'post', + }).then(({data}) => { + this.sendSuccess(data) + }).catch(({msg}) => { + $A.modalConfirm({ + icon: 'error', + title: '发送失败', + content: msg, + cancelText: '取消发送', + okText: '重新发送', + onOk: _ => { + this.sendLocationMsg(data) + }, + }) + }); + }, + /** * 发送快捷消息 * @param item */ sendQuick(item) { + if (item.key === "locat-checkin") { + this.$store.dispatch('openAppMapPage', { + key: item.config.key, + point: `${item.config.lng},${item.config.lat}`, + }).then(data => { + if (!$A.isJson(data)) { + return + } + if (data.distance > item.config.radius) { + $A.modalError(`你选择的位置「${data.title}」不在签到范围内`) + return + } + const preview = $A.urlAddParams('https://api.map.baidu.com/staticimage/v2', { + ak: item.config.key, + center: `${item.config.lng},${item.config.lat}`, + width: 800, + height: 480, + zoom: 17, + copyright: 1 + }) + this.sendLocationMsg({ + type: 'bd', + lng: data.point.lng, + lat: data.point.lat, + title: data.title, + address: data.address || '', + preview + }) + }) + return; + } this.sendMsg(`

    ${item.label}

    `) }, diff --git a/resources/assets/js/pages/manage/setting/components/SystemCheckin.vue b/resources/assets/js/pages/manage/setting/components/SystemCheckin.vue index 24f1e7999..1f50cced6 100644 --- a/resources/assets/js/pages/manage/setting/components/SystemCheckin.vue +++ b/resources/assets/js/pages/manage/setting/components/SystemCheckin.vue @@ -71,13 +71,13 @@ {{$L('人脸签到')}} {{$L('WiFi签到')}} + {{$L('定位签到')}} {{$L('手动签到')}} - {{$L('定位签到')}}
    {{$L('人脸签到')}}: {{$L('通过人脸识别机签到')}}
    {{$L('WiFi签到')}}: {{$L('详情看下文安装说明')}}
    +
    {{$L('定位签到')}}: {{$L('通过在签到打卡机器人发送位置签到')}}
    {{$L('手动签到')}}: {{$L('通过在签到打卡机器人发送指令签到')}}
    -
    {{$L('定位签到')}}: {{$L('通过在签到打卡机器人发送位置签到')}}
    @@ -117,6 +117,29 @@ +