chore(repo): 导入 M-Z 业务底座 V1.9 基线并建立仓库规范
按周工交付的 SEC-Z-01 清理后源码建立 M-Z 主干,并按 Git 仓库惯例重排目录。 - aomin-gateway/ 后端工程 87 文件,原样保留,路径与构建命令未变 - demo/ 演示前端与设备模拟器 9 文件(原 02_前端与设备端),非正式三端 - docs/handover/ 交付与安全清理材料 6 份,内容未改 - docs/contract/ J-00 规格、OpenAPI、内部五接口、状态机、Mock 样例 - docs/reference/ 指纹与校验清单,含 84 个源码文件的 sha256 清单 目录重排只动位置不动内容:源码树 84 文件哈希与交付一致,逐文件自校验通过; 12 条 Flyway 迁移与交付包内镜像逐字节一致。 未纳入:迁移镜像目录(与工程内一套重复,改留指纹)、target/、证据 raw、 应急恢复脚本、评审用虚构台账数据。 安全:SEC-Z-01 15 处明文已清零,导入前重新扫描无新增命中,本仓库无历史污染。 行尾按交付字节原样保存(.gitattributes 设 * -text),不做归一化以免整树摘要失效。
This commit is contained in:
commit
a7605ef8b8
121 files changed
+23629
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,252 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
||||
<title>选择家的位置</title>
|
||||
<!--
|
||||
百度地图选点(在线底图 + 离线兜底)
|
||||
─────────────────────────────────────────────────────────
|
||||
· 底图用百度 JS API v2.0,ak 从下方 AK 常量读;
|
||||
· **坐标系必须转**:百度用 BD-09,我们库里存 GCJ-02,两者差 500~600 米。
|
||||
取点后 bd09ToGcj02() 转换再回传,否则存进去的家会落在隔壁街道;
|
||||
· ak 失效 / 没网 / 白名单没放行时,3 秒内自动跳 map-offline.html,不让用户看白屏。
|
||||
-->
|
||||
<style>
|
||||
:root{--brand:#146b7a;--ink:#1b2430;--sub:#6b7784;--line:#e4eaef}
|
||||
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||||
html,body{margin:0;height:100%;overflow:hidden}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;
|
||||
background:#eef2f4;color:var(--ink);font-size:14px;display:flex;flex-direction:column}
|
||||
.top{flex:0 0 auto;background:var(--brand);color:#fff;padding:11px 12px;display:flex;align-items:center;gap:9px;z-index:10}
|
||||
.top button{background:rgba(255,255,255,.16);border:0;color:#fff;border-radius:9px;padding:7px 11px;font-size:13px;cursor:pointer}
|
||||
.top h1{font-size:15px;margin:0;flex:1;font-weight:600}
|
||||
.stage{flex:1;min-height:200px;position:relative}
|
||||
#map{width:100%;height:100%}
|
||||
/* 中心准星:拖地图选点,比"点哪儿算哪儿"在手机上准得多 */
|
||||
#pin{position:absolute;left:50%;top:50%;transform:translate(-50%,-100%);pointer-events:none;z-index:5}
|
||||
#pin svg{filter:drop-shadow(0 2px 4px rgba(0,0,0,.35))}
|
||||
#pinTip{position:absolute;left:50%;top:50%;transform:translate(-50%,6px);font-size:10.5px;
|
||||
color:#fff;background:rgba(20,40,55,.62);padding:2px 8px;border-radius:10px;pointer-events:none;z-index:5}
|
||||
#loading{position:absolute;inset:0;background:#f2f6f8;display:flex;align-items:center;justify-content:center;
|
||||
flex-direction:column;gap:10px;z-index:8;font-size:13px;color:var(--sub)}
|
||||
#loading .sp{width:26px;height:26px;border:3px solid #d6e2e6;border-top-color:var(--brand);border-radius:50%;
|
||||
animation:spin .9s linear infinite}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
.bottom{flex:0 0 auto;background:#fff;border-top:1px solid var(--line);padding:10px 12px 12px;z-index:10}
|
||||
.addr{font-size:13px;color:var(--ink);margin-bottom:5px;min-height:18px;line-height:1.5;font-weight:600}
|
||||
.sub{font-size:11.5px;color:var(--sub);margin-bottom:9px;line-height:1.5}
|
||||
.coord{font-family:Consolas,Menlo,monospace;font-size:11.5px;color:var(--brand);margin-bottom:9px}
|
||||
.row{display:flex;gap:7px;align-items:center;margin-bottom:8px}
|
||||
.row input{flex:1;min-width:0;border:1px solid var(--line);border-radius:8px;padding:8px 9px;font-size:12.5px;outline:none}
|
||||
.btn{border:0;border-radius:9px;padding:11px 14px;font-size:14px;font-weight:600;cursor:pointer}
|
||||
.btn-main{background:var(--brand);color:#fff;width:100%}
|
||||
.btn-main:disabled{background:#c3d3d7}
|
||||
.btn-line{background:#fff;border:1px solid var(--line);color:var(--ink);white-space:nowrap}
|
||||
.BMap_cpyCtrl,.anchorBL{display:none!important}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="top">
|
||||
<button onclick="cancel()">← 返回</button>
|
||||
<h1>选择家的位置</h1>
|
||||
<button onclick="locate()">定位</button>
|
||||
</div>
|
||||
<div class="stage">
|
||||
<div id="map"></div>
|
||||
<div id="pin"><svg width="30" height="40" viewBox="0 0 30 40">
|
||||
<path d="M15 0C6.7 0 0 6.7 0 15c0 11 15 25 15 25s15-14 15-25C30 6.7 23.3 0 15 0z" fill="#146b7a"/>
|
||||
<circle cx="15" cy="15" r="5.5" fill="#fff"/></svg></div>
|
||||
<div id="pinTip">拖动地图,把准星对准你家</div>
|
||||
<div id="loading"><div class="sp"></div><span id="ldTxt">正在加载地图…</span></div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="addr" id="addr">拖动地图选择位置</div>
|
||||
<div class="sub" id="sub">地址会自动识别,选不准可以手动改</div>
|
||||
<div class="coord" id="coord">坐标:—(GCJ-02)</div>
|
||||
<div class="row">
|
||||
<input id="mlng" placeholder="经度 113.39" inputmode="decimal">
|
||||
<input id="mlat" placeholder="纬度 22.52" inputmode="decimal">
|
||||
<button class="btn btn-line" onclick="applyManual()">填入</button>
|
||||
</div>
|
||||
<button class="btn btn-main" id="ok" onclick="confirmPick()">确定这个位置</button>
|
||||
</div>
|
||||
<script>
|
||||
/* 百度 ak —— 不写死在源码里:部署时把 __BAIDU_AK__ 替换成自己的 AK,
|
||||
或用 window.BAIDU_AK 注入;没有 AK 时本页会自动降级到离线地图。 */
|
||||
var AK = window.BAIDU_AK || '__BAIDU_AK__';
|
||||
|
||||
/* ================= 坐标转换(必须) =================
|
||||
百度 BD-09 / 国测局 GCJ-02 相差 500~600 米。
|
||||
库里存 GCJ-02,所以「取点 → 转 GCJ」,「显示已知点 → 转 BD」。 */
|
||||
function bd09ToGcj02(bdLng, bdLat) {
|
||||
var x = bdLng - 0.0065, y = bdLat - 0.006;
|
||||
var z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * Math.PI * 3000 / 180);
|
||||
var t = Math.atan2(y, x) - 0.000003 * Math.cos(x * Math.PI * 3000 / 180);
|
||||
return { lng: z * Math.cos(t), lat: z * Math.sin(t) };
|
||||
}
|
||||
function gcj02ToBd09(lng, lat) {
|
||||
var z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * Math.PI * 3000 / 180);
|
||||
var t = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * Math.PI * 3000 / 180);
|
||||
return { lng: z * Math.cos(t) + 0.0065, lat: z * Math.sin(t) + 0.006 };
|
||||
}
|
||||
|
||||
var map = null, marker = null, geocoder = null;
|
||||
var cur = null; // 当前选的 GCJ-02 坐标
|
||||
var geoTimer = null;
|
||||
var HOME = { lng: 113.3926, lat: 22.5176 }; // 中山市政府(GCJ-02)
|
||||
|
||||
function fallback(reason) {
|
||||
var t = document.getElementById('ldTxt');
|
||||
if (t) t.textContent = reason + ',切换到离线地图…';
|
||||
setTimeout(function () { location.replace('map-offline.html'); }, 700);
|
||||
}
|
||||
|
||||
function initMap() {
|
||||
if (map) return; // callback 和轮询都可能触发,只认第一次
|
||||
try {
|
||||
map = new BMap.Map('map', { enableMapClick: false });
|
||||
var c = gcj02ToBd09(HOME.lng, HOME.lat);
|
||||
map.centerAndZoom(new BMap.Point(c.lng, c.lat), 16);
|
||||
map.enableScrollWheelZoom(true);
|
||||
map.enableDragging();
|
||||
geocoder = new BMap.Geocoder();
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
map.addEventListener('moveend', onMoveEnd);
|
||||
onMoveEnd();
|
||||
} catch (e) {
|
||||
fallback('地图初始化失败');
|
||||
}
|
||||
}
|
||||
|
||||
/* 地图停稳后取中心点,转成 GCJ-02 并反查地址 */
|
||||
function onMoveEnd() {
|
||||
if (!map) return;
|
||||
var c = map.getCenter();
|
||||
var g = bd09ToGcj02(c.lng, c.lat);
|
||||
cur = { lng: +g.lng.toFixed(6), lat: +g.lat.toFixed(6) };
|
||||
document.getElementById('coord').textContent =
|
||||
'坐标:' + cur.lng + ', ' + cur.lat + '(GCJ-02)';
|
||||
document.getElementById('mlng').value = cur.lng;
|
||||
document.getElementById('mlat').value = cur.lat;
|
||||
|
||||
// 逆地理编码要联网,慢一点,节流 400ms,别拖一下发十个请求
|
||||
clearTimeout(geoTimer);
|
||||
geoTimer = setTimeout(function () {
|
||||
if (!geocoder) return;
|
||||
geocoder.getLocation(new BMap.Point(c.lng, c.lat), function (r) {
|
||||
if (!r) { return; }
|
||||
var a = r.address || {};
|
||||
var parts = [a.province, a.city, a.district, a.street, a.streetNumber].filter(Boolean);
|
||||
var poi = (r.surroundingPois && r.surroundingPois[0]) ? r.surroundingPois[0].title : '';
|
||||
// 街道名常常拼不出完整地址(百度在小区内部只回商圈),所以依次退:街道串 → 最近POI → 商圈
|
||||
var main = parts.join('') || poi || a.business || '已选位置';
|
||||
document.getElementById('addr').textContent = main;
|
||||
document.getElementById('sub').textContent = poi && main !== poi
|
||||
? '附近:' + poi
|
||||
: '地址已识别,可手动补充门牌';
|
||||
});
|
||||
}, 400);
|
||||
}
|
||||
|
||||
function applyManual() {
|
||||
var lng = parseFloat(document.getElementById('mlng').value);
|
||||
var lat = parseFloat(document.getElementById('mlat').value);
|
||||
if (!isFinite(lng) || !isFinite(lat)) { alert('请填入有效的经纬度数字'); return; }
|
||||
if (lng < 112.5 || lng > 114.5 || lat < 21.5 || lat > 23.5) {
|
||||
alert('这个坐标不在中山附近,请填 113.x / 22.x'); return;
|
||||
}
|
||||
cur = { lng: +lng.toFixed(6), lat: +lat.toFixed(6) };
|
||||
var b = gcj02ToBd09(lng, lat);
|
||||
if (map) { map.centerAndZoom(new BMap.Point(b.lng, b.lat), 17); }
|
||||
}
|
||||
|
||||
function locate() {
|
||||
if (!navigator.geolocation) { alert('这个浏览器不支持定位'); return; }
|
||||
navigator.geolocation.getCurrentPosition(function (p) {
|
||||
// 浏览器给的是 WGS-84,百度要 BD-09:先粗转 GCJ 再转 BD(市域内误差 <10m)
|
||||
var g = wgs84ToGcj02(p.coords.longitude, p.coords.latitude);
|
||||
var b = gcj02ToBd09(g.lng, g.lat);
|
||||
if (map) map.centerAndZoom(new BMap.Point(b.lng, b.lat), 17);
|
||||
}, function () {
|
||||
alert('定位失败(可能是没授权或信号不好),直接拖地图选吧');
|
||||
}, { timeout: 8000 });
|
||||
}
|
||||
|
||||
/* WGS-84 → GCJ-02(国内加密偏移的常用近似实现) */
|
||||
function wgs84ToGcj02(lng, lat) {
|
||||
var a = 6378245.0, ee = 0.00669342162296594323;
|
||||
var dLat = tfLat(lng - 105, lat - 35), dLng = tfLng(lng - 105, lat - 35);
|
||||
var radLat = lat / 180 * Math.PI, magic = Math.sin(radLat);
|
||||
magic = 1 - ee * magic * magic;
|
||||
var sqrtMagic = Math.sqrt(magic);
|
||||
dLat = (dLat * 180) / ((a * (1 - ee)) / (magic * sqrtMagic) * Math.PI);
|
||||
dLng = (dLng * 180) / (a / sqrtMagic * Math.cos(radLat) * Math.PI);
|
||||
return { lng: lng + dLng, lat: lat + dLat };
|
||||
}
|
||||
function tfLat(x, y) {
|
||||
var r = -100 + 2 * x + 3 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
|
||||
r += (20 * Math.sin(6 * x * Math.PI) + 20 * Math.sin(2 * x * Math.PI)) * 2 / 3;
|
||||
r += (20 * Math.sin(y * Math.PI) + 40 * Math.sin(y / 3 * Math.PI)) * 2 / 3;
|
||||
r += (160 * Math.sin(y / 12 * Math.PI) + 320 * Math.sin(y * Math.PI / 30)) * 2 / 3;
|
||||
return r;
|
||||
}
|
||||
function tfLng(x, y) {
|
||||
var r = 300 + x + 2 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
|
||||
r += (20 * Math.sin(6 * x * Math.PI) + 20 * Math.sin(2 * x * Math.PI)) * 2 / 3;
|
||||
r += (20 * Math.sin(x * Math.PI) + 40 * Math.sin(x / 3 * Math.PI)) * 2 / 3;
|
||||
r += (150 * Math.sin(x / 12 * Math.PI) + 300 * Math.sin(x / 30 * Math.PI)) * 2 / 3;
|
||||
return r;
|
||||
}
|
||||
|
||||
function confirmPick() {
|
||||
if (!cur) { alert('还没选位置'); return; }
|
||||
var addr = document.getElementById('addr').textContent || '';
|
||||
var d = {
|
||||
type: 'heserv:pick',
|
||||
lng: cur.lng, lat: cur.lat,
|
||||
name: addr.length > 24 ? addr.slice(0, 24) : addr,
|
||||
addr: addr,
|
||||
coordSys: 'GCJ-02',
|
||||
source: 'baidu'
|
||||
};
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage(d, '*');
|
||||
} else {
|
||||
alert('已选:' + d.lng + ', ' + d.lat);
|
||||
}
|
||||
}
|
||||
function cancel() {
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'heserv:pick-cancel' }, '*');
|
||||
} else { history.back(); }
|
||||
}
|
||||
|
||||
/* ================= 加载百度脚本 =================
|
||||
注意:百度 v2.0 的 loader 内部用 document.write 注入主脚本。
|
||||
如果用 createElement 动态插入,write 在页面加载完后调用会被浏览器忽略,
|
||||
BMap 永远不出现 —— 表现就是"一直加载中,最后跳离线版"。
|
||||
所以这里必须用**静态 script 标签**,并靠 &callback= 拿加载完成的通知。 */
|
||||
function onBMapReady() {
|
||||
window.__bmapOk = true;
|
||||
setTimeout(initMap, 80);
|
||||
}
|
||||
|
||||
(function () {
|
||||
var timer = setInterval(function () {
|
||||
if (window.BMap && window.__bmapOk !== false) {
|
||||
clearInterval(timer);
|
||||
if (!map) initMap();
|
||||
return;
|
||||
}
|
||||
if (window.__bmapTries === undefined) window.__bmapTries = 0;
|
||||
if (++window.__bmapTries > 22) { // 约 4.4 秒还没起来
|
||||
clearInterval(timer);
|
||||
if (!map) fallback('地图加载超时');
|
||||
}
|
||||
}, 200);
|
||||
})();
|
||||
</script>
|
||||
<script src="https://api.map.baidu.com/api?v=2.0&ak=__BAIDU_AK__&callback=onBMapReady"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,422 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
||||
<title>选择家的位置(离线)</title>
|
||||
<!--
|
||||
离线示意底图 · 中山市 · GCJ-02
|
||||
· 零第三方依赖:不加载任何在线瓦片 / 不请求 SDK / 不需要 key,断网可用;
|
||||
· 边界为中山市行政区划轮廓(DataV 公开数据,Douglas-Peucker 简化至 206 点),仅作示意;
|
||||
· 地标为示意位置,实际选点以回传经纬度为准;
|
||||
· 坐标系 GCJ-02,与腾讯/高德一致,可直接入 household.longitude / latitude。
|
||||
-->
|
||||
<style>
|
||||
:root{--brand:#146b7a;--ink:#1b2430;--sub:#6b7784;--line:#e4eaef}
|
||||
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||||
html,body{margin:0;height:100%;overflow:hidden}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;
|
||||
background:#eef2f4;color:var(--ink);font-size:14px;display:flex;flex-direction:column}
|
||||
.top{flex:0 0 auto;background:var(--brand);color:#fff;padding:11px 12px;display:flex;align-items:center;gap:9px}
|
||||
.top button{background:rgba(255,255,255,.16);border:0;color:#fff;border-radius:9px;padding:7px 11px;font-size:13px;cursor:pointer}
|
||||
.top h1{font-size:15px;margin:0;flex:1;font-weight:600}
|
||||
.stage{flex:1;min-height:200px;position:relative;overflow:hidden}
|
||||
#cv{display:block;width:100%;height:100%;touch-action:none;cursor:crosshair}
|
||||
.hud{position:absolute;left:10px;top:10px;background:rgba(255,255,255,.92);border:1px solid var(--line);
|
||||
border-radius:8px;padding:6px 9px;font-size:11px;color:#5c6b76;line-height:1.55;max-width:64%;pointer-events:none}
|
||||
.hud b{color:var(--brand)}
|
||||
.zoom{position:absolute;right:10px;bottom:44px;display:flex;flex-direction:column;gap:6px}
|
||||
.zoom button{width:34px;height:34px;border:1px solid var(--line);background:rgba(255,255,255,.95);
|
||||
border-radius:8px;font-size:17px;line-height:1;cursor:pointer;color:var(--ink);padding:0}
|
||||
.zoom button:active{background:#e8f2f4}
|
||||
.scalebar{position:absolute;right:10px;bottom:12px;background:rgba(255,255,255,.9);border-radius:6px;
|
||||
padding:3px 7px;font-size:10.5px;color:#5c6b76;font-family:Consolas,monospace}
|
||||
.bottom{flex:0 0 auto;background:#fff;border-top:1px solid var(--line);padding:10px 12px 12px}
|
||||
.addr{font-size:12.5px;color:var(--sub);margin-bottom:6px;min-height:17px;line-height:1.5}
|
||||
.coord{font-family:Consolas,Menlo,monospace;font-size:11.5px;color:var(--brand);margin-bottom:9px}
|
||||
.row{display:flex;gap:7px;align-items:center;margin-bottom:8px}
|
||||
.row input{flex:1;min-width:0;border:1px solid var(--line);border-radius:8px;padding:8px 9px;font-size:12.5px;outline:none}
|
||||
.row input:focus{border-color:var(--brand)}
|
||||
.btn{border:0;border-radius:9px;padding:11px 14px;font-size:14px;font-weight:600;cursor:pointer}
|
||||
.btn-main{background:var(--brand);color:#fff;width:100%}
|
||||
.btn-main:disabled{background:#c3d3d7;cursor:not-allowed}
|
||||
.btn-line{background:#fff;border:1px solid var(--line);color:var(--ink);white-space:nowrap}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="top">
|
||||
<button onclick="cancel()">← 返回</button>
|
||||
<h1>选择家的位置</h1>
|
||||
<button onclick="recenter()">回中山</button>
|
||||
</div>
|
||||
<div class="stage">
|
||||
<canvas id="cv"></canvas>
|
||||
<div class="hud"><b>离线示意底图</b> · 中山市 · GCJ-02<br>拖动平移 · 滚轮/双指缩放 · 点一下落点</div>
|
||||
<div class="zoom">
|
||||
<button onclick="zoomBy(1.4)" title="放大">+</button>
|
||||
<button onclick="zoomBy(1/1.4)" title="缩小">-</button>
|
||||
</div>
|
||||
<div class="scalebar" id="sb">—</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="addr" id="addr">点一下地图,落在你家的位置</div>
|
||||
<div class="coord" id="coord">坐标:未选择(GCJ-02)</div>
|
||||
<div class="row">
|
||||
<input id="mlng" placeholder="经度 113.39" inputmode="decimal">
|
||||
<input id="mlat" placeholder="纬度 22.52" inputmode="decimal">
|
||||
<button class="btn btn-line" onclick="applyManual()">填入</button>
|
||||
</div>
|
||||
<button class="btn btn-main" id="ok" disabled onclick="confirmPick()">确定这个位置</button>
|
||||
</div>
|
||||
<script>
|
||||
/* ---------- 底图数据 ---------- */
|
||||
var RING = [[113.56875,22.41193],[113.55253,22.41263],[113.53410,22.41640],[113.53042,22.41545],[113.52798,22.41236],[113.52620,22.41180],[113.52365,22.41337],[113.52061,22.41303],[113.51771,22.40835],[113.51432,22.40872],[113.50892,22.41338],[113.50695,22.41329],[113.50372,22.41067],[113.49589,22.40755],[113.49500,22.40566],[113.49523,22.39541],[113.48956,22.39253],[113.48700,22.39020],[113.48588,22.38502],[113.48803,22.38003],[113.49134,22.37694],[113.49130,22.37042],[113.48923,22.36746],[113.48917,22.36582],[113.49244,22.36082],[113.49324,22.35404],[113.48967,22.34973],[113.49155,22.34591],[113.48964,22.34327],[113.49033,22.34072],[113.49478,22.33971],[113.49611,22.33815],[113.49634,22.33370],[113.50074,22.33182],[113.50074,22.32920],[113.50410,22.32431],[113.50313,22.32285],[113.49336,22.31800],[113.49029,22.31784],[113.48686,22.31932],[113.48302,22.31878],[113.48400,22.31703],[113.48202,22.31499],[113.47779,22.31363],[113.47501,22.31493],[113.47048,22.31035],[113.47579,22.30664],[113.47480,22.30521],[113.47939,22.30301],[113.48042,22.30386],[113.48232,22.30099],[113.48292,22.30150],[113.48793,22.29630],[113.48669,22.29544],[113.49139,22.29028],[113.49286,22.28822],[113.49216,22.28750],[113.49639,22.28225],[113.49507,22.27997],[113.49882,22.27468],[113.50126,22.27421],[113.49964,22.27293],[113.50473,22.26698],[113.50826,22.26971],[113.51250,22.26467],[113.51438,22.26154],[113.51303,22.24989],[113.50938,22.25137],[113.50675,22.24673],[113.48628,22.24203],[113.47888,22.23832],[113.48004,22.23364],[113.48614,22.22913],[113.48635,22.22710],[113.47778,22.22498],[113.46353,22.21938],[113.45835,22.21379],[113.44256,22.20705],[113.43260,22.20398],[113.43056,22.20129],[113.42462,22.20104],[113.41576,22.20290],[113.40045,22.22018],[113.39411,22.22897],[113.39036,22.23230],[113.38639,22.23019],[113.37949,22.23591],[113.37693,22.23445],[113.35611,22.24628],[113.34931,22.25263],[113.34287,22.26324],[113.33660,22.28013],[113.32251,22.30867],[113.32130,22.31218],[113.32038,22.32959],[113.31736,22.33573],[113.27383,22.38478],[113.26264,22.39207],[113.25634,22.41081],[113.25587,22.44063],[113.25137,22.44987],[113.24516,22.45727],[113.24015,22.47615],[113.22501,22.49708],[113.21538,22.51310],[113.20940,22.51948],[113.20181,22.52478],[113.19224,22.53551],[113.18711,22.53993],[113.18872,22.55278],[113.18510,22.57429],[113.17640,22.59001],[113.16556,22.60192],[113.16223,22.60966],[113.15701,22.61424],[113.15838,22.61810],[113.15705,22.62357],[113.15722,22.63205],[113.16008,22.64432],[113.16306,22.65136],[113.16967,22.65076],[113.17063,22.65167],[113.16999,22.65870],[113.16617,22.65808],[113.16365,22.66379],[113.16185,22.66349],[113.16084,22.66541],[113.16610,22.66796],[113.16140,22.67370],[113.16408,22.67558],[113.16684,22.67518],[113.17200,22.68067],[113.17315,22.67977],[113.17382,22.68069],[113.17651,22.67911],[113.17318,22.68450],[113.17415,22.68489],[113.17967,22.67882],[113.18182,22.67863],[113.18938,22.67388],[113.19451,22.67541],[113.20104,22.67596],[113.19983,22.67788],[113.19926,22.68254],[113.20437,22.68948],[113.20502,22.69307],[113.20423,22.69732],[113.20955,22.69874],[113.22223,22.71226],[113.23176,22.72820],[113.23632,22.74077],[113.23912,22.74371],[113.24303,22.74535],[113.24807,22.74486],[113.25407,22.74084],[113.26089,22.73033],[113.26392,22.72667],[113.26654,22.72547],[113.27056,22.72648],[113.27828,22.73514],[113.28127,22.73755],[113.28488,22.73888],[113.29124,22.73896],[113.30144,22.73627],[113.31308,22.73764],[113.32912,22.74126],[113.32826,22.74630],[113.33030,22.75051],[113.33746,22.75398],[113.34600,22.76014],[113.35874,22.76487],[113.36516,22.77260],[113.38145,22.76156],[113.41219,22.74283],[113.42614,22.73801],[113.44112,22.73722],[113.44778,22.73585],[113.46797,22.72852],[113.46481,22.72096],[113.47131,22.71499],[113.49191,22.69979],[113.52338,22.67930],[113.54072,22.66621],[113.53951,22.65794],[113.53309,22.65637],[113.53297,22.65498],[113.56163,22.60751],[113.56514,22.60348],[113.56740,22.60593],[113.57192,22.60280],[113.57472,22.60572],[113.57786,22.60391],[113.57855,22.60461],[113.58998,22.59520],[113.59962,22.59439],[113.61501,22.58529],[113.62078,22.57953],[113.63932,22.54828],[113.65161,22.51572],[113.68234,22.51436],[113.69204,22.51515],[113.66843,22.48043],[113.63148,22.47597],[113.61011,22.44571],[113.57372,22.41172],[113.56875,22.41193]];
|
||||
|
||||
// 地标:lv1=全域可见(大区域),lv2=放大后才出现(市区细点)
|
||||
// 坐标为示意位置(误差 1km 内),实际选点以回传经纬度为准
|
||||
var MARKS = [
|
||||
{n:"中山市人民政府", lng:113.3926, lat:22.5176, big:1, lv:1},
|
||||
{n:"小榄镇", lng:113.2450, lat:22.6700, lv:1},
|
||||
{n:"三乡镇", lng:113.4300, lat:22.3580, lv:1},
|
||||
{n:"坦洲镇", lng:113.4700, lat:22.2900, lv:1},
|
||||
{n:"火炬开发区", lng:113.4392, lat:22.5495, lv:1},
|
||||
{n:"孙文西路步行街", lng:113.3794, lat:22.5186, lv:2},
|
||||
{n:"中山纪念堂", lng:113.3836, lat:22.5205, lv:2},
|
||||
{n:"市人民医院", lng:113.3893, lat:22.5245, lv:2},
|
||||
{n:"中山北站", lng:113.3902, lat:22.5567, lv:2},
|
||||
{n:"中山站", lng:113.4470, lat:22.5320, lv:2}
|
||||
];
|
||||
var DIRS = [
|
||||
{t:"↑ 广州 / 佛山", lng:113.40, lat:22.80},
|
||||
{t:"← 江门", lng:113.13, lat:22.57},
|
||||
{t:"↓ 珠海 / 澳门", lng:113.45, lat:22.17},
|
||||
{t:"深圳 / 东莞 →", lng:113.72, lat:22.60}
|
||||
];
|
||||
|
||||
/* ---------- 视图 ---------- */
|
||||
var BBOX = (function(){
|
||||
var x1=1e9,y1=1e9,x2=-1e9,y2=-1e9;
|
||||
for(var i=0;i<RING.length;i++){ var p=RING[i];
|
||||
if(p[0]<x1)x1=p[0]; if(p[0]>x2)x2=p[0];
|
||||
if(p[1]<y1)y1=p[1]; if(p[1]>y2)y2=p[1]; }
|
||||
return {cx:(x1+x2)/2, cy:(y1+y2)/2, w:x2-x1, h:y2-y1};
|
||||
})();
|
||||
var HOME = {lng:BBOX.cx, lat:BBOX.cy, scale:900};
|
||||
var view = {lng:HOME.lng, lat:HOME.lat, scale:HOME.scale};
|
||||
var K = Math.cos(22.5*Math.PI/180);
|
||||
var cv = document.getElementById('cv'), ctx = cv.getContext('2d');
|
||||
var W = 0, H = 0, dpr = Math.max(1, Math.min(3, window.devicePixelRatio||1));
|
||||
var picked = null;
|
||||
|
||||
function toScreen(lng, lat){
|
||||
return [ (lng - view.lng)*view.scale*K + W/2, (view.lat - lat)*view.scale + H/2 ];
|
||||
}
|
||||
function toLL(x, y){
|
||||
return [ view.lng + (x - W/2)/(view.scale*K), view.lat - (y - H/2)/view.scale ];
|
||||
}
|
||||
function distKm(a, b){
|
||||
var dLat=(b.lat-a.lat)*111.32, dLng=(b.lng-a.lng)*111.32*Math.cos((a.lat+b.lat)/2*Math.PI/180);
|
||||
return Math.sqrt(dLat*dLat + dLng*dLng);
|
||||
}
|
||||
|
||||
/* ---------- 绘制 ---------- */
|
||||
function draw(){
|
||||
if(!W) return;
|
||||
ctx.setTransform(dpr,0,0,dpr,0,0);
|
||||
ctx.clearRect(0,0,W,H);
|
||||
ctx.fillStyle = '#e9eff1';
|
||||
ctx.fillRect(0,0,W,H);
|
||||
drawGrid();
|
||||
ctx.beginPath();
|
||||
for(var i=0;i<RING.length;i++){
|
||||
var p = toScreen(RING[i][0], RING[i][1]);
|
||||
i ? ctx.lineTo(p[0],p[1]) : ctx.moveTo(p[0],p[1]);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = '#f8f6ef';
|
||||
ctx.fill();
|
||||
ctx.lineWidth = 1.4; ctx.strokeStyle = '#9fb6b2'; ctx.stroke();
|
||||
ctx.lineWidth = 5; ctx.strokeStyle = 'rgba(20,107,122,.07)'; ctx.stroke();
|
||||
drawMarks();
|
||||
drawDirs();
|
||||
drawPick();
|
||||
drawScale();
|
||||
}
|
||||
|
||||
function drawGrid(){
|
||||
// 网格步长按像素自适应:保证每格 70~140px,避免全览时刻度挤成一团
|
||||
var steps = [0.005,0.01,0.02,0.05,0.1,0.2,0.5];
|
||||
var stepDeg = 0.5;
|
||||
for(var i=0;i<steps.length;i++){
|
||||
if(steps[i]*view.scale*K >= 72){ stepDeg = steps[i]; break; }
|
||||
}
|
||||
var tl = toLL(0,0), br = toLL(W,H);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = 'rgba(140,160,168,.32)';
|
||||
ctx.font = '9px Consolas,monospace';
|
||||
ctx.fillStyle = 'rgba(110,130,140,.6)';
|
||||
|
||||
var lastX = -999;
|
||||
for(var lng=Math.floor(tl[0]/stepDeg)*stepDeg; lng<=br[0]; lng+=stepDeg){
|
||||
var px = toScreen(lng, 0)[0];
|
||||
ctx.beginPath(); ctx.moveTo(px,0); ctx.lineTo(px,H); ctx.stroke();
|
||||
if(px - lastX > 62){ ctx.fillText(lng.toFixed(2)+'°E', px+2, H-3); lastX = px; }
|
||||
}
|
||||
var lastY = -999;
|
||||
for(var lat=Math.floor(br[1]/stepDeg)*stepDeg; lat<=tl[1]; lat+=stepDeg){
|
||||
var py = toScreen(0, lat)[1];
|
||||
ctx.beginPath(); ctx.moveTo(0,py); ctx.lineTo(W,py); ctx.stroke();
|
||||
if(py - lastY > 34){ ctx.fillText(lat.toFixed(2)+'°N', 3, py-3); lastY = py; }
|
||||
}
|
||||
}
|
||||
|
||||
// 标签避让:放进不来就不画字,只留点
|
||||
var placed = [];
|
||||
function overlaps(r){
|
||||
for(var i=0;i<placed.length;i++){
|
||||
var p=placed[i];
|
||||
if(r.x < p.x+p.w && r.x+r.w > p.x && r.y < p.y+p.h && r.y+r.h > p.y) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function putLabel(x, y, text, bold, color){
|
||||
ctx.font = (bold?'600 ':'') + '11px -apple-system,"PingFang SC","Microsoft YaHei",sans-serif';
|
||||
var w = ctx.measureText(text).width, h = 13;
|
||||
var tries = [[x+8, y-4], [x-8-w, y-4], [x-w/2, y-19], [x-w/2, y+7]];
|
||||
for(var i=0;i<tries.length;i++){
|
||||
var r = {x:tries[i][0]-2, y:tries[i][1]-2, w:w+5, h:h+4};
|
||||
if(r.x > -2 && r.x+r.w < W+2 && r.y > 0 && r.y+r.h < H-2 && !overlaps(r)){
|
||||
placed.push(r);
|
||||
ctx.fillStyle = 'rgba(255,255,255,.86)';
|
||||
ctx.fillRect(r.x, r.y, r.w, r.h);
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillText(text, tries[i][0], tries[i][1]+10);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function drawMarks(){
|
||||
placed = [];
|
||||
var lv = view.scale > 2400 ? 2 : 1; // 全览只标大区域,放大后才出细地标
|
||||
for(var i=0;i<MARKS.length;i++){
|
||||
var m = MARKS[i], p = toScreen(m.lng, m.lat);
|
||||
if((m.lv||1) > lv) continue;
|
||||
if(p[0]<-60||p[0]>W+60||p[1]<-30||p[1]>H+30) continue;
|
||||
var r = m.big ? 5 : 3.6;
|
||||
ctx.beginPath(); ctx.arc(p[0],p[1],r+2.5,0,7); ctx.fillStyle='rgba(255,255,255,.9)'; ctx.fill();
|
||||
ctx.beginPath(); ctx.arc(p[0],p[1],r,0,7);
|
||||
ctx.fillStyle = m.big ? '#c2410c' : '#146b7a'; ctx.fill();
|
||||
putLabel(p[0], p[1], m.n, !!m.big, m.big ? '#9a3412' : '#334a55');
|
||||
}
|
||||
}
|
||||
|
||||
function drawDirs(){
|
||||
ctx.font = '10px -apple-system,"PingFang SC",sans-serif';
|
||||
ctx.fillStyle = 'rgba(110,130,140,.5)';
|
||||
ctx.textAlign='center';
|
||||
for(var i=0;i<DIRS.length;i++){
|
||||
var d = DIRS[i], p = toScreen(d.lng, d.lat);
|
||||
if(p[0]>24 && p[0]<W-24 && p[1]>18 && p[1]<H-10) ctx.fillText(d.t, p[0], p[1]);
|
||||
}
|
||||
ctx.textAlign='left';
|
||||
}
|
||||
|
||||
function drawPick(){
|
||||
if(!picked) return;
|
||||
var p = toScreen(picked.lng, picked.lat);
|
||||
ctx.strokeStyle='rgba(20,107,122,.35)'; ctx.lineWidth=1;
|
||||
ctx.beginPath(); ctx.moveTo(p[0]-14,p[1]); ctx.lineTo(p[0]+14,p[1]);
|
||||
ctx.moveTo(p[0],p[1]-14); ctx.lineTo(p[0],p[1]+14); ctx.stroke();
|
||||
ctx.save(); ctx.translate(p[0],p[1]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0,0); ctx.bezierCurveTo(-11,-9,-11,-22,0,-22); ctx.bezierCurveTo(11,-22,11,-9,0,0);
|
||||
ctx.closePath(); ctx.fillStyle='#146b7a'; ctx.fill();
|
||||
ctx.beginPath(); ctx.arc(0,-15,4.2,0,7); ctx.fillStyle='#fff'; ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawScale(){
|
||||
var perPx = 111320 / view.scale; // 每像素多少米(纬度方向)
|
||||
var m = perPx * 78;
|
||||
var nice = [50,100,200,500,1000,2000,5000,10000,20000,50000];
|
||||
var pick = nice[0];
|
||||
for(var i=0;i<nice.length;i++){ if(nice[i] <= m) pick = nice[i]; }
|
||||
var px = Math.round(pick / perPx);
|
||||
var txt = pick >= 1000 ? (pick/1000)+' km' : pick+' m';
|
||||
document.getElementById('sb').innerHTML =
|
||||
'<span style="display:inline-block;width:'+px+'px;height:5px;border:1.5px solid #7d929a;border-top:0;'
|
||||
+ 'vertical-align:middle;margin-right:6px"></span><span style="vertical-align:middle">'+txt+'</span>';
|
||||
}
|
||||
|
||||
/* ---------- 交互 ---------- */
|
||||
var fitted = false;
|
||||
function resize(){
|
||||
var r = cv.getBoundingClientRect();
|
||||
W = r.width; H = r.height;
|
||||
cv.width = Math.round(W*dpr); cv.height = Math.round(H*dpr);
|
||||
if(!fitted && W > 0 && H > 0){ fitView(); fitted = true; }
|
||||
draw();
|
||||
}
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
// 按视口把整个中山市装进去
|
||||
function fitView(){
|
||||
var s = Math.min((W*0.90)/(BBOX.w*K), (H*0.90)/BBOX.h);
|
||||
HOME = {lng:BBOX.cx, lat:BBOX.cy, scale:clamp(s, 380, 26000)};
|
||||
view = {lng:HOME.lng, lat:HOME.lat, scale:HOME.scale};
|
||||
}
|
||||
|
||||
var drag = null;
|
||||
function pos(e){
|
||||
var r = cv.getBoundingClientRect();
|
||||
return [ e.clientX - r.left, e.clientY - r.top ];
|
||||
}
|
||||
cv.addEventListener('pointerdown', function(e){
|
||||
cv.setPointerCapture(e.pointerId);
|
||||
drag = {x:e.clientX, y:e.clientY, lng:view.lng, lat:view.lat, moved:0};
|
||||
});
|
||||
cv.addEventListener('pointermove', function(e){
|
||||
if(!drag) return;
|
||||
var dx = e.clientX - drag.x, dy = e.clientY - drag.y;
|
||||
drag.moved = Math.max(drag.moved, Math.abs(dx)+Math.abs(dy));
|
||||
view.lng = drag.lng - dx/(view.scale*K);
|
||||
view.lat = drag.lat + dy/view.scale;
|
||||
draw();
|
||||
});
|
||||
cv.addEventListener('pointerup', function(e){
|
||||
if(!drag) return;
|
||||
var moved = drag.moved; drag = null;
|
||||
if(moved > 6) return;
|
||||
var p = pos(e), ll = toLL(p[0], p[1]);
|
||||
setPick(ll[0], ll[1]);
|
||||
});
|
||||
cv.addEventListener('wheel', function(e){
|
||||
e.preventDefault();
|
||||
var p = pos(e);
|
||||
zoomAt(p[0], p[1], e.deltaY < 0 ? 1.18 : 1/1.18);
|
||||
}, {passive:false});
|
||||
|
||||
var pinch = null;
|
||||
cv.addEventListener('touchstart', function(e){
|
||||
if(e.touches.length === 2) pinch = {d: tdist(e), scale: view.scale};
|
||||
}, {passive:true});
|
||||
cv.addEventListener('touchmove', function(e){
|
||||
if(pinch && e.touches.length === 2){
|
||||
e.preventDefault();
|
||||
var r = pinch.d>0 ? tdist(e)/pinch.d : 1;
|
||||
view.scale = clamp(pinch.scale * r, 380, 26000);
|
||||
draw();
|
||||
}
|
||||
}, {passive:false});
|
||||
cv.addEventListener('touchend', function(){ pinch = null; }, {passive:true});
|
||||
function tdist(e){
|
||||
var a=e.touches[0], b=e.touches[1];
|
||||
return Math.hypot(a.clientX-b.clientX, a.clientY-b.clientY);
|
||||
}
|
||||
function clamp(v,a,b){ return Math.max(a, Math.min(b, v)); }
|
||||
|
||||
function zoomAt(px, py, k){
|
||||
var before = toLL(px, py);
|
||||
view.scale = clamp(view.scale*k, 380, 26000);
|
||||
var after = toLL(px, py);
|
||||
view.lng += before[0]-after[0];
|
||||
view.lat += before[1]-after[1];
|
||||
draw();
|
||||
}
|
||||
function zoomBy(k){ zoomAt(W/2, H/2, k); }
|
||||
function recenter(){
|
||||
fitView();
|
||||
draw();
|
||||
}
|
||||
|
||||
/* ---------- 选点 ---------- */
|
||||
function setPick(lng, lat){
|
||||
picked = {lng:lng, lat:lat};
|
||||
document.getElementById('mlng').value = lng.toFixed(6);
|
||||
document.getElementById('mlat').value = lat.toFixed(6);
|
||||
document.getElementById('coord').textContent = '坐标:' + lng.toFixed(6) + ', ' + lat.toFixed(6) + '(GCJ-02)';
|
||||
document.getElementById('addr').textContent = describe(lng, lat);
|
||||
document.getElementById('ok').disabled = false;
|
||||
draw();
|
||||
}
|
||||
|
||||
function nearest(lng, lat){
|
||||
var best=null, bd=1e9;
|
||||
for(var i=0;i<MARKS.length;i++){
|
||||
var d = distKm({lng:lng,lat:lat}, MARKS[i]);
|
||||
if(d<bd){ bd=d; best=MARKS[i]; }
|
||||
}
|
||||
return {m:best, d:bd};
|
||||
}
|
||||
|
||||
function describe(lng, lat){
|
||||
var r = nearest(lng, lat);
|
||||
var city = {lng:113.3926, lat:22.5176};
|
||||
var dc = distKm({lng:lng,lat:lat}, city);
|
||||
var bearing = azimuth(city, {lng:lng,lat:lat});
|
||||
var part;
|
||||
if(r.d < 0.8) part = r.m.n + '附近';
|
||||
else if(r.d < 6) part = '距' + r.m.n + '约 ' + r.d.toFixed(1) + ' 公里';
|
||||
else part = '在' + r.m.n + '方向约 ' + r.d.toFixed(1) + ' 公里';
|
||||
var tail = dc < 0.25 ? '(已选位置)' : '(' + bearing + ',离市政府 ' + dc.toFixed(1) + ' 公里)';
|
||||
return '中山市 · ' + part + tail;
|
||||
}
|
||||
|
||||
function azimuth(a, b){
|
||||
var dLng = (b.lng-a.lng)*Math.cos((a.lat+b.lat)/2*Math.PI/180), dLat = b.lat-a.lat;
|
||||
var ang = Math.atan2(dLng, dLat)*180/Math.PI;
|
||||
var dirs = ['正北','东北','正东','东南','正南','西南','正西','西北'];
|
||||
return dirs[Math.round(((ang+360)%360)/45) % 8];
|
||||
}
|
||||
|
||||
function applyManual(){
|
||||
var lng = parseFloat(document.getElementById('mlng').value);
|
||||
var lat = parseFloat(document.getElementById('mlat').value);
|
||||
if(!isFinite(lng) || !isFinite(lat)){ alert('请填入有效的经纬度数字'); return; }
|
||||
if(lng < 112.5 || lng > 114.5 || lat < 21.5 || lat > 23.5){
|
||||
alert('这个坐标不在中山附近。\n请填 113.x / 22.x(GCJ-02)。'); return;
|
||||
}
|
||||
setPick(lng, lat);
|
||||
view.lng = lng; view.lat = lat;
|
||||
if(view.scale < 3000) view.scale = 6000;
|
||||
draw();
|
||||
}
|
||||
|
||||
/* ---------- 回传 ---------- */
|
||||
function payload(){
|
||||
if(!picked) return null;
|
||||
var r = nearest(picked.lng, picked.lat);
|
||||
return {
|
||||
type: 'heserv:pick',
|
||||
lng: +picked.lng.toFixed(6),
|
||||
lat: +picked.lat.toFixed(6),
|
||||
name: r.d < 2 ? r.m.n : '自选位置',
|
||||
addr: describe(picked.lng, picked.lat),
|
||||
coordSys: 'GCJ-02',
|
||||
source: 'offline-zhongshan'
|
||||
};
|
||||
}
|
||||
function confirmPick(){
|
||||
var d = payload();
|
||||
if(!d) return;
|
||||
if(window.parent && window.parent !== window){
|
||||
window.parent.postMessage(d, '*');
|
||||
} else {
|
||||
alert('已选:' + d.lng + ', ' + d.lat + '\n(当前是单独打开,无法回传给主页面)');
|
||||
}
|
||||
}
|
||||
function cancel(){
|
||||
if(window.parent && window.parent !== window){
|
||||
window.parent.postMessage({type:'heserv:pick-cancel'}, '*');
|
||||
} else {
|
||||
history.back();
|
||||
}
|
||||
}
|
||||
|
||||
resize();
|
||||
setTimeout(resize, 60);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,285 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
|
||||
<title>选择家的位置</title>
|
||||
<!--
|
||||
地图合规说明(务必保留):
|
||||
· 只用腾讯地图 GL JS,SDK 地址不带 key,通过 _TMapSecurityConfig 走 WorkBuddy 本地代理;
|
||||
· __WB_HTTP_PORT__ / __WB_TMAP_SECRET__ 是运行时占位符,禁止改成具体值、禁止内嵌任何 key;
|
||||
· 坐标系 GCJ-02;浏览器 GPS 拿到的 WGS-84 必须换算后再用(见 wgs84ToGcj02);
|
||||
· 占位符没被运行时替换时(例如从业务服务器 :8080 直接打开),
|
||||
自动降级为「演示坐标板」——不加载任何第三方瓦片,功能仍可用。
|
||||
-->
|
||||
<script type="text/javascript">
|
||||
window._TMapSecurityConfig = {
|
||||
serviceHost: 'http://127.0.0.1:__WB_HTTP_PORT__/_TMapService/_wbt/__WB_TMAP_SECRET__',
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
:root{--brand:#146b7a;--ink:#1b2430;--sub:#6b7784;--line:#e4eaef}
|
||||
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||||
html,body{margin:0;height:100%;overflow:hidden}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;
|
||||
background:#f2f5f8;color:var(--ink);font-size:14px;display:flex;flex-direction:column}
|
||||
.top{flex:0 0 auto;background:var(--brand);color:#fff;padding:11px 12px;display:flex;align-items:center;gap:9px}
|
||||
.top button{background:rgba(255,255,255,.16);border:0;color:#fff;border-radius:9px;padding:7px 11px;
|
||||
font-size:13px;cursor:pointer}
|
||||
.top h1{font-size:15px;margin:0;flex:1;font-weight:600}
|
||||
.search{flex:0 0 auto;padding:9px 12px;background:#fff;border-bottom:1px solid var(--line);position:relative}
|
||||
.search input{width:100%;border:1px solid var(--line);border-radius:9px;padding:9px 12px;
|
||||
font-size:13.5px;outline:none}
|
||||
.search input:focus{border-color:var(--brand)}
|
||||
#suggest{position:absolute;left:12px;right:12px;top:48px;background:#fff;border:1px solid var(--line);
|
||||
border-radius:9px;box-shadow:0 8px 20px rgba(0,0,0,.1);z-index:8;display:none;max-height:190px;overflow-y:auto}
|
||||
#suggest div{padding:9px 12px;font-size:13px;border-bottom:1px solid #f2f5f8;cursor:pointer}
|
||||
#suggest div:last-child{border-bottom:0}
|
||||
#suggest div:hover{background:#f2f8fa}
|
||||
#suggest small{display:block;color:var(--sub);font-size:11px;margin-top:1px}
|
||||
#map{flex:1;min-height:260px;position:relative;background:#e8eef0}
|
||||
#board{width:100%;height:100%;display:none;cursor:crosshair;position:relative;
|
||||
background:linear-gradient(0deg,#e7eef0 1px,transparent 1px) 0 0/32px 32px,
|
||||
linear-gradient(90deg,#e7eef0 1px,transparent 1px) 0 0/32px 32px,#f5f9fa}
|
||||
#mode{position:absolute;left:10px;top:10px;background:rgba(255,255,255,.94);border-radius:8px;
|
||||
padding:6px 10px;font-size:11px;color:#60707c;z-index:5;max-width:82%;line-height:1.6}
|
||||
#pin{position:absolute;width:26px;height:34px;transform:translate(-50%,-100%);pointer-events:none;
|
||||
display:none;z-index:6}
|
||||
.bottom{flex:0 0 auto;background:#fff;border-top:1px solid var(--line);padding:11px 12px}
|
||||
.addr{font-size:12.5px;color:var(--sub);margin-bottom:8px;min-height:18px;line-height:1.5}
|
||||
.coord{font-family:Consolas,Menlo,monospace;font-size:11.5px;color:var(--brand);margin-bottom:9px}
|
||||
.row{display:flex;gap:8px;align-items:center}
|
||||
.row input{flex:1;min-width:0;border:1px solid var(--line);border-radius:8px;padding:8px 9px;
|
||||
font-size:12.5px;outline:none}
|
||||
.btn{border:0;border-radius:9px;padding:10px 14px;font-size:14px;font-weight:600;cursor:pointer}
|
||||
.btn-main{background:var(--brand);color:#fff;flex:1}
|
||||
.btn-line{background:#fff;border:1px solid var(--line);color:var(--ink)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="top">
|
||||
<button onclick="cancel()">← 返回</button>
|
||||
<h1>选择家的位置</h1>
|
||||
<button onclick="locate()">📍 定位</button>
|
||||
</div>
|
||||
<div class="search">
|
||||
<input id="q" placeholder="搜小区 / 大厦名称" autocomplete="off">
|
||||
<div id="suggest"></div>
|
||||
</div>
|
||||
<div id="map">
|
||||
<div id="board"></div>
|
||||
<div id="mode"></div>
|
||||
<img id="pin" alt="" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='34' viewBox='0 0 26 34'%3E%3Cpath d='M13 0C6 0 1 5 1 12c0 8 12 22 12 22s12-14 12-22C25 5 20 0 13 0z' fill='%23146b7a'/%3E%3Ccircle cx='13' cy='12' r='5' fill='white'/%3E%3C/svg%3E">
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="addr" id="addr">点一下地图,或按右上角「定位」用当前位置</div>
|
||||
<div class="coord" id="coord">坐标:未选择(GCJ-02)</div>
|
||||
<div class="row">
|
||||
<input id="mlng" placeholder="经度" inputmode="decimal">
|
||||
<input id="mlat" placeholder="纬度" inputmode="decimal">
|
||||
<button class="btn btn-line" onclick="applyManual()">填入</button>
|
||||
</div>
|
||||
<div class="row" style="margin-top:8px">
|
||||
<button class="btn btn-main" onclick="confirmPick()">确定这个位置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ===== 坐标换算:WGS-84 → GCJ-02(国内地图必须用火星坐标) ===== */
|
||||
const PI = 3.1415926535897932384626, A = 6378245.0, EE = 0.00669342162296594323;
|
||||
function outOfChina(lng, lat){ return !(lng > 73.66 && lng < 135.05 && lat > 3.86 && lat < 53.55); }
|
||||
function tLat(x, y){
|
||||
let r = -100 + 2*x + 3*y + .2*y*y + .1*x*y + .2*Math.sqrt(Math.abs(x));
|
||||
r += (20*Math.sin(6*x*PI) + 20*Math.sin(2*x*PI)) * 2/3;
|
||||
r += (20*Math.sin(y*PI) + 40*Math.sin(y/3*PI)) * 2/3;
|
||||
r += (160*Math.sin(y/12*PI) + 320*Math.sin(y*PI/30)) * 2/3;
|
||||
return r;
|
||||
}
|
||||
function tLng(x, y){
|
||||
let r = 300 + x + 2*y + .1*x*x + .1*x*y + .1*Math.sqrt(Math.abs(x));
|
||||
r += (20*Math.sin(6*x*PI) + 20*Math.sin(2*x*PI)) * 2/3;
|
||||
r += (20*Math.sin(x*PI) + 40*Math.sin(x/3*PI)) * 2/3;
|
||||
r += (150*Math.sin(x/12*PI) + 300*Math.sin(x/30*PI)) * 2/3;
|
||||
return r;
|
||||
}
|
||||
function wgs84ToGcj02(lng, lat){
|
||||
if (outOfChina(lng, lat)) return [lng, lat];
|
||||
let dLat = tLat(lng-105, lat-35), dLng = tLng(lng-105, lat-35);
|
||||
const rad = lat/180*PI, magic = Math.sin(rad), sq = 1 - EE*magic*magic;
|
||||
dLat = (dLat*180)/((A*(1-EE))/(sq*Math.sqrt(sq))*PI);
|
||||
dLng = (dLng*180)/(A/Math.sqrt(sq)*Math.cos(rad)*PI);
|
||||
return [+(lng+dLng).toFixed(7), +(lat+dLat).toFixed(7)];
|
||||
}
|
||||
|
||||
/* ===== 演示用的虚构小区(地图服务不可用时给几个可选点) ===== */
|
||||
const DEMO_POIS = [
|
||||
{ name:'鹤仆云示范小区', addr:'广东省中山市东区 示范路 1 号', lng:113.3821, lat:22.5213 },
|
||||
{ name:'幸福里', addr:'广东省深圳市南山区 测试路 1 号', lng:113.9301, lat:22.5321 },
|
||||
{ name:'演示花园', addr:'广东省广州市天河区 演示大道 8 号', lng:113.3308, lat:23.1288 },
|
||||
{ name:'安居苑', addr:'广东省佛山市禅城区 安居街 6 号', lng:113.1214, lat:23.0218 },
|
||||
];
|
||||
|
||||
let PICK = null; // {lng, lat, name, addr}
|
||||
let CENTER = [113.3821, 22.5213];
|
||||
let MAP = null, MARKER = null, TENCENT_READY = false;
|
||||
const proxied = !String(window._TMapSecurityConfig.serviceHost).includes('__WB_');
|
||||
|
||||
/* ===== 降级坐标板:不加载任何第三方瓦片,只是给演示选一个坐标 ===== */
|
||||
function boardToLngLat(px, py, el){
|
||||
const w = el.clientWidth, h = el.clientHeight;
|
||||
return [+(CENTER[0] + (px/w - .5)*0.08).toFixed(7),
|
||||
+(CENTER[1] - (py/h - .5)*0.06).toFixed(7)];
|
||||
}
|
||||
function lngLatToBoard(lng, lat, el){
|
||||
return [( (lng-CENTER[0])/0.08 + .5 ) * el.clientWidth,
|
||||
( .5 - (lat-CENTER[1])/0.06 ) * el.clientHeight];
|
||||
}
|
||||
|
||||
/* ===== 选点 ===== */
|
||||
function setPick(lng, lat, name, addr){
|
||||
PICK = { lng:+lng.toFixed(7), lat:+lat.toFixed(7), name:name||'', addr:addr||'' };
|
||||
document.getElementById('coord').textContent =
|
||||
'坐标:' + PICK.lng + ', ' + PICK.lat + '(GCJ-02)';
|
||||
document.getElementById('addr').textContent =
|
||||
(name ? name + ' · ' : '') + (addr || '已选择这个位置,回填到地址表单');
|
||||
document.getElementById('mlng').value = PICK.lng;
|
||||
document.getElementById('mlat').value = PICK.lat;
|
||||
const board = document.getElementById('board');
|
||||
const [x,y] = lngLatToBoard(lng, lat, board);
|
||||
const pin = document.getElementById('pin');
|
||||
pin.style.display = 'block';
|
||||
pin.style.left = Math.max(13, Math.min(board.clientWidth-13, x)) + 'px';
|
||||
pin.style.top = Math.max(34, Math.min(board.clientHeight, y)) + 'px';
|
||||
if (TENCENT_READY && MARKER) {
|
||||
MARKER.setGeometries([{ id:'pick', styleId:'pin', position:new TMap.LatLng(lat, lng) }]);
|
||||
MAP.setCenter(new TMap.LatLng(lat, lng));
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 搜索建议 ===== */
|
||||
function renderSuggest(items){
|
||||
const box = document.getElementById('suggest');
|
||||
if (!items.length){ box.style.display='none'; return; }
|
||||
box.innerHTML = items.map((p,i) =>
|
||||
'<div data-i="'+i+'">'+p.name+'<small>'+p.addr+'</small></div>').join('');
|
||||
box.style.display='block';
|
||||
box.querySelectorAll('div[data-i]').forEach(el=>el.onclick=()=>{
|
||||
const p = items[+el.dataset.i];
|
||||
box.style.display='none';
|
||||
document.getElementById('q').value = p.name;
|
||||
CENTER = [p.lng, p.lat];
|
||||
setPick(p.lng, p.lat, p.name, p.addr);
|
||||
});
|
||||
}
|
||||
document.getElementById('q').addEventListener('input', e=>{
|
||||
const k = e.target.value.trim();
|
||||
if (!k) return renderSuggest([]);
|
||||
const local = DEMO_POIS.filter(p => (p.name+p.addr).includes(k));
|
||||
if (TENCENT_READY && window.TMap && TMap.service && TMap.service.Search) {
|
||||
// 正式地图模式:走 SDK 的地点搜索(SDK 内部经代理,不手拼 URL)
|
||||
try {
|
||||
const search = new TMap.service.Search({ pageSize: 8 });
|
||||
search.searchKeyword({ keyword: k }).then(res => {
|
||||
const list = (res.data || []).map(x => ({
|
||||
name: x.title || x.address, addr: x.address || '',
|
||||
lng: x.location.lng, lat: x.location.lat,
|
||||
}));
|
||||
renderSuggest(local.concat(list).slice(0, 8));
|
||||
}).catch(()=>renderSuggest(local));
|
||||
return;
|
||||
} catch(e){ /* 退回本地 */ }
|
||||
}
|
||||
renderSuggest(local.length ? local : DEMO_POIS);
|
||||
});
|
||||
document.addEventListener('click', e=>{
|
||||
if (!e.target.closest('.search')) document.getElementById('suggest').style.display='none';
|
||||
});
|
||||
|
||||
/* ===== GPS 定位 ===== */
|
||||
function locate(){
|
||||
if (!navigator.geolocation) return setMode('这个浏览器不支持定位,请点地图选点');
|
||||
setMode('正在定位…');
|
||||
navigator.geolocation.getCurrentPosition(pos=>{
|
||||
const [lng, lat] = wgs84ToGcj02(pos.coords.longitude, pos.coords.latitude);
|
||||
CENTER = [lng, lat];
|
||||
setPick(lng, lat, '当前位置', 'GPS 定位(WGS-84 已换算为 GCJ-02)');
|
||||
setMode('已用 GPS 定位');
|
||||
}, err=>{
|
||||
setMode('定位失败或未被授权;可直接点地图,或点下面「试一个位置」');
|
||||
const p = DEMO_POIS[0];
|
||||
CENTER = [p.lng, p.lat];
|
||||
setPick(p.lng, p.lat, p.name, p.addr + '(演示位置)');
|
||||
}, { enableHighAccuracy:true, timeout:8000, maximumAge:60000 });
|
||||
}
|
||||
function setMode(t){ document.getElementById('mode').textContent = t; }
|
||||
function applyManual(){
|
||||
const lng = parseFloat(document.getElementById('mlng').value);
|
||||
const lat = parseFloat(document.getElementById('mlat').value);
|
||||
if (!isFinite(lng) || !isFinite(lat)) return setMode('请填有效经纬度');
|
||||
CENTER = [lng, lat]; setPick(lng, lat, '', '手工输入坐标');
|
||||
}
|
||||
function confirmPick(){
|
||||
if (!PICK) return setMode('先点一下地图选个位置');
|
||||
const msg = { type:'heserv:pick', ...PICK };
|
||||
if (window.parent && window.parent !== window) window.parent.postMessage(msg, '*');
|
||||
else if (window.opener) window.opener.postMessage(msg, '*');
|
||||
else { localStorage.setItem('heserv_last_pick', JSON.stringify(PICK)); setMode('已保存位置'); }
|
||||
}
|
||||
function cancel(){
|
||||
if (window.parent && window.parent !== window) window.parent.postMessage({type:'heserv:pick-cancel'}, '*');
|
||||
else if (window.opener) window.opener.postMessage({type:'heserv:pick-cancel'}, '*');
|
||||
}
|
||||
|
||||
/* ===== 启动:能用腾讯地图就用,不能就降级 ===== */
|
||||
(function init(){
|
||||
const board = document.getElementById('board');
|
||||
if (!proxied) {
|
||||
board.style.display = 'block';
|
||||
setMode('演示坐标板(腾讯地图代理未启用):点网格选点、用上面的搜索选小区,或直接按「定位」。'
|
||||
+ ' 正式接入时在 WorkBuddy 预览里打开本页即可显示真实地图。');
|
||||
document.getElementById('pin').style.zIndex = 7;
|
||||
board.addEventListener('click', e=>{
|
||||
const r = board.getBoundingClientRect();
|
||||
const [lng, lat] = boardToLngLat(e.clientX-r.left, e.clientY-r.top, board);
|
||||
setPick(lng, lat, '', '地图选点(演示坐标板)');
|
||||
});
|
||||
renderSuggest([]);
|
||||
setPick(CENTER[0], CENTER[1], '鹤仆云示范小区', DEMO_POIS[0].addr);
|
||||
document.getElementById('addr').textContent = '默认演示位置,点地图可改';
|
||||
return;
|
||||
}
|
||||
// 代理可用:加载官方 SDK(不带 key),失败就降级
|
||||
const s = document.createElement('script');
|
||||
s.src = 'https://map.qq.com/api/gljs?v=1.exp&libraries=service';
|
||||
s.onload = ()=>{
|
||||
try {
|
||||
MAP = new TMap.Map('map', { zoom:15, center:new TMap.LatLng(CENTER[1], CENTER[0]) });
|
||||
MARKER = new TMap.MultiMarker({
|
||||
map:MAP,
|
||||
styles:{ pin:new TMap.MarkerStyle({ width:26, height:34, anchor:{x:13,y:34} }) },
|
||||
geometries:[],
|
||||
});
|
||||
MAP.on('click', e=> setPick(e.latLng.lng, e.latLng.lat, '', '地图选点'));
|
||||
TENCENT_READY = true;
|
||||
setMode('腾讯地图(GCJ-02)· 点地图选点');
|
||||
setPick(CENTER[0], CENTER[1], '鹤仆云示范小区', DEMO_POIS[0].addr);
|
||||
} catch(err){ fallback('地图初始化失败:' + err.message); }
|
||||
};
|
||||
s.onerror = ()=> fallback('地图 SDK 没加载成功');
|
||||
document.head.appendChild(s);
|
||||
|
||||
function fallback(reason){
|
||||
TENCENT_READY = false;
|
||||
board.style.display = 'block';
|
||||
setMode(reason + ',已切到演示坐标板');
|
||||
board.onclick = e=>{
|
||||
const r=board.getBoundingClientRect();
|
||||
const [lng,lat]=boardToLngLat(e.clientX-r.left,e.clientY-r.top,board);
|
||||
setPick(lng,lat,'','地图选点(演示坐标板)');
|
||||
};
|
||||
setPick(CENTER[0], CENTER[1], '鹤仆云示范小区', DEMO_POIS[0].addr);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user