按周工交付的 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),不做归一化以免整树摘要失效。
286 lines
14 KiB
HTML
286 lines
14 KiB
HTML
<!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>
|