Files
aomin a7605ef8b8 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),不做归一化以免整树摘要失效。
2026-11-13 17:32:37 +08:00

286 lines
14 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>