Files
rond-with-you/internal/web/templates/admin/fog-preview.html
T
tamakyi 30c502ce7b 备份与迷雾的差异分析、合并执行,附属数据改挂坐标键
备份(.rondbackup)与迷雾(.fwss)各加一层「先看差异再动手」:

- 迷雾:上传先出差异页,说清叠加能补多少、整体替换会丢多少,并在真实底图上
  用绿/灰/红标出会新增 / 不变 / 会丢的区域
- 备份:与当前数据集按 UUID 逐条比对(rond 每次导出都会重排 Z_PK,编号认不出人),
  新增合入、冲突逐条选、仅站内有逐条勾选删除,全在一个事务里执行;
  合入的行写回 entity_raw,保证再传同一份备份时差异归零
- 地点按坐标对齐:站点的「补全地址」会把地点改名,拿名称当键会让一整个地点的
  到访全变成假冲突
- 附属数据(备注/结论/访客票/图片)从 dataset_id+src_pk 换成坐标键,换数据集不再失联;
  访客票没有 dataset_id,回填按活跃数据集认领
- 修正 contentHash 拿 Z_PK 当指纹的隐患,以及两处「src_pk 跨快照稳定」的错误注释

导出格式(.rondbackup / .fwss)保持原样,往返测试通过。
2026-09-19 16:04:00 +08:00

175 lines
8.1 KiB
HTML
Raw 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.
{{define "content"}}
<div class="wrap">
<div class="page-head">
<div>
<h1>迷雾导入预览</h1>
<div class="sub">这份快照<strong>还没有写入</strong>。先看清两种导入方式各自的后果,再决定用哪一种。</div>
</div>
<div class="row">
<a class="btn" href="/admin">← 返回后台</a>
</div>
</div>
<div class="card">
<h2>数量对比</h2>
<div class="tw">
<table class="tb tb-stack">
<thead>
<tr><th>状态</th><th class="num">块</th><th class="num">格</th><th class="num">面积</th></tr>
</thead>
<tbody>
<tr>
<td>当前迷雾</td>
<td class="num" data-label="块">{{num .Diff.CurBlocks}}</td>
<td class="num" data-label="格">{{num .Diff.CurCells}}</td>
<td class="num" data-label="面积">{{printf "%.1f" .Diff.CurAreaKm2}} km²</td>
</tr>
<tr>
<td>这份快照</td>
<td class="num" data-label="块">{{num .Diff.IncBlocks}}</td>
<td class="num" data-label="格">{{num .Diff.IncCells}}</td>
<td class="num" data-label="面积">{{printf "%.1f" .Diff.IncAreaKm2}} km²</td>
</tr>
<tr>
<td>叠加导入后</td>
<td class="num" data-label="块">{{num .Diff.CurBlocks}}<span class="muted"> +{{num .Diff.GainBlocks}}</span></td>
<td class="num" data-label="格">{{num .Diff.CurCells}}<span class="muted"> +{{num .Diff.GainCells}}</span></td>
<td class="num" data-label="面积">{{printf "%.1f" .Diff.MergeAreaKm2}} km²</td>
</tr>
<tr>
<td>整体替换后</td>
<td class="num" data-label="块">{{num .Diff.IncBlocks}}</td>
<td class="num" data-label="格">{{num .Diff.IncCells}}</td>
<td class="num" data-label="面积">{{printf "%.1f" .Diff.CoverAreaKm2}} km²</td>
</tr>
</tbody>
</table>
</div>
<div class="card-sub" style="margin-top:10px">
两个块坐标相同就只按位合并,所以「叠加后」的块数不会等于两边相加——快照里有 {{num .Diff.OverlapBlocks}} 个块与当前迷雾落在同一块,
其中 {{num .Diff.SameBlocks}} 个一格都不会变。
</div>
</div>
{{if .HasBounds}}
<div class="card">
<h2>差异地图</h2>
<div class="card-sub">在真实底图上核对差异落在哪些区域,可以缩放平移。</div>
<div class="row" style="gap:16px;margin:2px 0 10px;font-size:12.5px;flex-wrap:wrap">
<span style="display:flex;align-items:center;gap:6px">
<i style="width:13px;height:13px;border-radius:3px;background:#24a45a;display:inline-block"></i>叠加会新增
</span>
<span style="display:flex;align-items:center;gap:6px">
<i style="width:13px;height:13px;border-radius:3px;background:#7e8692;display:inline-block"></i>两边都有
</span>
<span style="display:flex;align-items:center;gap:6px">
<i style="width:13px;height:13px;border-radius:3px;background:#d63838;display:inline-block"></i>整体替换会丢掉
</span>
</div>
<div id="fog-diff-map" style="height:400px;border-radius:10px;background:var(--line-soft)"
data-token="{{.Token}}"
data-min-lat="{{printf "%.6f" .MinLat}}" data-min-lon="{{printf "%.6f" .MinLon}}"
data-max-lat="{{printf "%.6f" .MaxLat}}" data-max-lon="{{printf "%.6f" .MaxLon}}"></div>
</div>
{{end}}
<div class="card">
<h2>叠加导入</h2>
{{if .Diff.Changed}}
<div class="alert ok">
会补上当前迷雾没有的 <strong>{{num .Diff.GainBlocks}}</strong> 块、<strong>{{num .Diff.GainCells}}</strong> 格
(约 {{printf "%.1f" .Diff.GainAreaKm2}} km²)。叠加只做按位合并,<strong>不会丢失任何已有数据</strong>。
</div>
{{else}}
<div class="alert err">
这份快照的点亮区域已经被当前迷雾<strong>完全覆盖</strong>——叠加导入不会带来任何变化,
多半是同一批数据又导出了一次。
</div>
{{end}}
<form method="post" action="/admin/fog?stash={{.Token}}"
onsubmit="this.querySelector('button').disabled=true">
<input type="hidden" name="mode" value="overlay">
<button class="btn primary" type="submit">叠加导入</button>
</form>
</div>
<div class="card">
<h2>整体替换</h2>
{{if gt .Diff.LostCells 0}}
<div class="alert err">
⚠️ 快照里<strong>没有</strong>当前迷雾的 <strong>{{num .Diff.LostBlocks}}</strong> 块、<strong>{{num .Diff.LostCells}}</strong> 格
(约 {{printf "%.1f" .Diff.LostAreaKm2}} km²)。整体替换后,这些区域会<strong>从点亮变回未探索</strong>,且无法恢复。
</div>
{{else}}
<div class="alert ok">
这份快照完整包含了当前迷雾的全部点亮区域,替换不会丢数据。
</div>
{{end}}
{{if and (eq .TildeLayers 0) (eq .HashLayers 0)}}
<div class="alert err">
⚠️ 快照里没有 <span class="mono">Model/~</span> 与 <span class="mono">Model/#</span> 两层数据,替换会把现有的这两层一并清掉。
</div>
{{else}}
<div class="muted" style="font-size:12.5px;margin-bottom:8px">
另外,站点不解析但会留存的 <span class="mono">Model/~</span>({{.TildeLayers}} 项)与
<span class="mono">Model/#</span>({{.HashLayers}} 项)也会一并换成这份快照里的{{if .OtherLayers}},另有 {{.OtherLayers}} 项其它条目{{end}}。
</div>
{{end}}
<form method="post" action="/admin/fog?stash={{.Token}}"
onsubmit="return confirm('整体替换会清空当前迷雾。{{if gt .Diff.LostCells 0}}快照里缺 {{num .Diff.LostCells}} 格,替换后这些区域会变回未探索,且不可恢复。{{end}}确认替换?')">
<input type="hidden" name="mode" value="cover">
<button class="btn danger" type="submit">整体替换</button>
</form>
</div>
</div>
<script>
(function () {
// Leaflet 由 layout 在 content 之后引入,脚本执行时 window.L 还没有,
// 必须等 load 之后再初始化,否则地图整个不出现(与迷雾遮罩页同一条约定)
function whenReady(cb) {
if (document.readyState === 'complete') { setTimeout(cb, 0); return; }
window.addEventListener('load', cb);
}
var el = document.getElementById('fog-diff-map');
if (!el) return;
// 底图走站点配置(与迷雾遮罩页同一套取法):带密钥的图层由服务端转发,
// 免密钥的是浏览器直连的 {s} 模板,不能硬写 /tiles/ 路由
var fallback = {
def: { url: 'https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', maxZoom: 18, attr: '© 高德地图', crs: 'gcj02' },
subs: ['1', '2', '3', '4']
};
function init(cfg) {
var def = cfg.def;
var map = L.map(el, { scrollWheelZoom: false });
L.tileLayer(def.url, { subdomains: cfg.subs, maxZoom: def.maxZoom || 18, minZoom: 3, attribution: def.attr || '' }).addTo(map);
// 差异瓦片必须和底图同坐标系:迷雾网格是 WGS-84,服务端按 crs 逐块纠偏
L.tileLayer('/admin/fog/preview/' + el.dataset.token + '/{z}/{x}/{y}?crs=' + (def.crs || 'gcj02'), {
minZoom: 3, maxZoom: 18, opacity: 0.95
}).addTo(map);
var b = [
[parseFloat(el.dataset.minLat), parseFloat(el.dataset.minLon)],
[parseFloat(el.dataset.maxLat), parseFloat(el.dataset.maxLon)]
];
// 只有一个块时两个角重合,不设上限会一路放大到最大级别
map.fitBounds(b, { padding: [24, 24], maxZoom: 13 });
L.control.scale({ imperial: false }).addTo(map);
}
whenReady(function () {
if (!window.L) return;
fetch('/api/map-config', { headers: { 'Accept': 'application/json' } })
.then(function (r) { return r.json(); })
.then(function (c) {
var def = (c && c.base && c.base.length) ? c.base[0] : fallback.def;
init({ def: def, subs: (c && c.subs && c.subs.length) ? c.subs : fallback.subs });
})
.catch(function () { init(fallback); });
});
})();
</script>
{{end}}