// ==UserScript==
// @name 高德地图POI采集(毛玻璃·排版优化版)
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @author empyrealtear
// @description 修复搜索,增加调试日志
// @match *://ditu.amap.com/*
// @require https://scriptcat.org/lib/513/2.1.0/ElementGetter.js
// @require https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js
// @require https://cdn.jsdelivr.net/npm/@turf/turf@6.3.0/turf.min.js
// @require https://cdn.jsdelivr.net/npm/dexie@4.4.2/dist/dexie.min.js
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_openInTab
// @grant GM_addStyle
// @grant GM_info
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_cookie
// @grant GM_notification
// @run-at document-end
// @noframes
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ================================================================
// 1. 依赖 & 全局变量
// ================================================================
const elmGetter = typeof ElementGetter !== 'undefined' ? ElementGetter : window.ElementGetter;
const Dexie = window.Dexie;
const turf = window.turf;
const XLSX = window.XLSX;
let map = null;
// ================================================================
// 2. 工具函数
// ================================================================
const Utils = {
genId: () => Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8),
sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
getPolygonCenter: (coords) => {
const rad = Math.PI / 180;
const getDistance = (lng1, lat1, lng2, lat2) => {
const latRad1 = lat1 * rad;
const latRad2 = lat2 * rad;
const a = latRad1 - latRad2;
const b = (lng1 - lng2) * rad;
let dis = 2 * Math.asin(Math.sqrt(
Math.sin(a / 2) ** 2 + Math.cos(latRad1) * Math.cos(latRad2) * Math.sin(b / 2) ** 2
));
return Math.round(dis * 6378137);
};
if (!coords || coords.length === 0) return { center: [116.397428, 39.90923], zoom: 12, radius: 0 };
let lngSum = 0, latSum = 0;
coords.forEach(p => { lngSum += p[0]; latSum += p[1]; });
const center = [lngSum / coords.length, latSum / coords.length];
let maxDist = 0;
coords.forEach(p => {
const d = getDistance(center[0], center[1], p[0], p[1]);
if (d > maxDist) maxDist = d;
});
let zoom = 12;
if (maxDist <= 50) zoom = 19;
else if (maxDist <= 200) zoom = 18;
else if (maxDist <= 500) zoom = 17;
else if (maxDist <= 1000) zoom = 16;
else if (maxDist <= 2000) zoom = 15;
else if (maxDist <= 5000) zoom = 14;
else if (maxDist <= 10000) zoom = 13;
return { center, zoom, radius: maxDist };
},
getIntersections: (coords) => {
const ring = [...coords, coords[0]];
const polygon = turf.polygon([ring]);
const [minLng, minLat, maxLng, maxLat] = turf.bbox(polygon);
const cLng = Math.round(((minLng + maxLng) / 2) * 1e6) / 1e6;
const cLat = Math.round(((minLat + maxLat) / 2) * 1e6) / 1e6;
const vertical = turf.lineString([[cLng, minLat], [cLng, maxLat]]);
const horizontal = turf.lineString([[minLng, cLat], [maxLng, cLat]]);
const getIntersectPoints = (splitLine) => {
const segments = [];
for (let i = 0; i < ring.length - 1; i++) {
const p1 = ring[i], p2 = ring[i + 1];
const seg = turf.lineString([p1, p2]);
const result = turf.lineIntersect(seg, splitLine);
if (result.features.length) {
segments.push({
index: i,
start: p1,
end: p2,
point: result.features[0].geometry.coordinates.map(v => Math.round(v * 1e6) / 1e6)
});
}
}
return segments;
};
const verticalIntersects = getIntersectPoints(vertical);
const horizontalIntersects = getIntersectPoints(horizontal);
const isVertical = (maxLng - minLng) > (maxLat - minLat);
return {
center: [cLng, cLat],
bbox: { minLng, minLat, maxLng, maxLat },
verticalLine: vertical.geometry.coordinates,
horizontalLine: horizontal.geometry.coordinates,
verticalIntersects,
horizontalIntersects,
isVertical,
line: isVertical ? vertical.geometry.coordinates : horizontal.geometry.coordinates,
intersects: isVertical ? verticalIntersects : horizontalIntersects
};
},
splitPolygon: (coords, intersects) => {
const [p1, p2] = intersects;
return [
[p1.point, ...coords.slice(p1.index + 1, p2.index + 1), p2.point],
[p2.point, ...coords.slice(p2.index + 1, coords.length), ...coords.slice(0, p1.index + 1), p1.point]
];
},
getPoiTypes: () => [
{ code: "010000", name: "汽车服务" },
{ code: "020000", name: "汽车销售" },
{ code: "030000", name: "汽车维修" },
{ code: "040000", name: "摩托车服务" },
{ code: "050000", name: "餐饮服务" },
{ code: "060000", name: "购物服务" },
{ code: "070000", name: "生活服务" },
{ code: "080000", name: "体育休闲服务" },
{ code: "090000", name: "医疗保健服务" },
{ code: "100000", name: "住宿服务" },
{ code: "110000", name: "风景名胜" },
{ code: "120000", name: "商务住宅" },
{ code: "130000", name: "政府机构及社会团体" },
{ code: "140000", name: "科教文化服务" },
{ code: "150000", name: "交通设施服务" },
{ code: "160000", name: "金融保险服务" },
{ code: "170000", name: "公司企业" },
]
};
// ================================================================
// 3. 数据库
// ================================================================
const db = new Dexie('PolygonAreaDB');
db.version(1).stores({
areas: '++id, name',
pois: '++id, area, type, name'
});
// ================================================================
// 4. 状态管理
// ================================================================
const State = {
selectedArea: { id: null, name: '' },
selectedTypes: new Set(),
poiList: [],
filteredPoiList: [],
pagination: { current: 1, size: 0, total: 0 },
fieldConfig: (() => {
const defaultConfig = [
{ key: "ID", label: "ID", visible: true },
{ key: "区域", label: "区域", visible: true },
{ key: "大类", label: "大类", visible: true },
{ key: "类型", label: "类型", visible: false },
{ key: "名称", label: "名称", visible: true },
{ key: "电话", label: "电话", visible: true },
{ key: "地址", label: "地址", visible: true },
{ key: "经度", label: "经度", visible: false },
{ key: "纬度", label: "纬度", visible: false },
];
try {
const saved = localStorage.getItem('poiFieldConfig');
return saved ? JSON.parse(saved) : defaultConfig;
} catch { return defaultConfig; }
})(),
currentPolygon: null,
polygonSubtree: [],
editor: null,
mouseTool: null,
massMarker: null,
marker: null,
overlays: [],
};
function saveFieldConfigToStorage() {
try { localStorage.setItem('poiFieldConfig', JSON.stringify(State.fieldConfig)); } catch (_) {}
}
// ================================================================
// 5. 覆盖物管理
// ================================================================
const OverlayManager = {
add(item) { if (item) State.overlays.push(item); },
removeAll() { State.overlays.forEach(item => map.remove(item)); State.overlays = []; },
clearMassMarkers() {
if (State.massMarker) { State.massMarker.clear(); State.massMarker = null; }
if (State.marker) { map.remove(State.marker); State.marker = null; }
},
drawLine(path, color = 'blue', weight = 2) {
const line = new AMap.Polyline({ path, strokeColor: color, strokeWeight: weight });
line.setMap(map);
this.add(line);
return line;
},
drawPolygon(path, color, weight = 2, opacity = 0.2) {
const polygon = new AMap.Polygon({ map, path, strokeColor: color, strokeWeight: weight, fillColor: color, fillOpacity: opacity });
this.add(polygon);
return polygon;
}
};
// ================================================================
// 6. 当前多边形管理
// ================================================================
const CurrentPolygon = {
set(target, subtree = []) { this.clear(); State.currentPolygon = target; State.polygonSubtree = subtree; },
clear() {
if (State.currentPolygon) { map.remove(State.currentPolygon); State.currentPolygon = null; }
State.polygonSubtree.forEach(p => map.remove(p));
State.polygonSubtree = [];
},
render(area) {
this.clear();
const { coords, overLimit } = area;
const target = new AMap.Polygon({ map, path: coords, strokeColor: "#2d7aff", strokeWeight: 3, fillOpacity: 0.15, fillColor: "#2d7aff" });
const subtree = (overLimit || []).map(v => new AMap.Polygon({ map, path: v.coords, strokeColor: "#f46672", strokeWeight: 3, fillOpacity: 0.15, fillColor: "#f46672" }));
this.set(target, subtree);
},
save() {
if (!State.currentPolygon) { alert("请先绘制多边形"); return; }
const name = document.getElementById("areaName").value.trim();
if (!name) { alert("请输入区域名称"); return; }
const coords = State.currentPolygon.getPath().map(p => [p.lng, p.lat]);
db.areas.filter(area => area.name == name).first().then(area => {
if (area) db.areas.update(area.id, { name, coords }).then(renderAreaList);
else db.areas.add({ name, coords }).then(renderAreaList);
}).catch(err => console.error("保存区域失败", err));
}
};
// ================================================================
// 7. 核心业务函数
// ================================================================
async function renderAreaList() {
const areas = await db.areas.toArray();
const tbody = document.getElementById("areaList");
let html = '';
for (const area of areas) {
const isActive = State.selectedArea.id === area.id;
html += `
|
${area.name} |
${(area.overLimit || []).length} |
|
`;
}
tbody.innerHTML = html;
tbody.querySelectorAll('.area-checkbox').forEach(cb => {
cb.addEventListener('change', function() {
const id = parseInt(this.dataset.id);
const name = this.dataset.name;
if (this.checked) { selectArea(id, name); } else { deselectArea(); }
renderAreaList();
});
});
tbody.querySelectorAll('.rename-btn').forEach(btn => {
btn.addEventListener('click', function(e) { e.stopPropagation(); const id = parseInt(this.dataset.id); renameArea(id); });
});
tbody.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', function(e) { e.stopPropagation(); const id = parseInt(this.dataset.id); deleteArea(id); });
});
}
function selectArea(id, name) {
State.selectedArea.id = id;
State.selectedArea.name = name;
loadPolygon(id);
document.querySelectorAll('.area-checkbox').forEach(cb => {
if (parseInt(cb.dataset.id) !== id) cb.checked = false;
});
}
function deselectArea() {
State.selectedArea.id = null;
State.selectedArea.name = '';
clearAll();
}
async function loadPolygon(id) {
const area = await db.areas.get(id);
if (!area) return;
clearAll();
CurrentPolygon.render(area);
const { center, zoom } = Utils.getPolygonCenter(area.coords);
map.setCenter(center);
map.setZoom(zoom);
loadPOIFromDB(v => v.area === area.name);
}
async function deleteArea(id) {
if (!confirm("确定删除此区域?")) return;
await db.areas.delete(id);
if (State.selectedArea.id === id) {
State.selectedArea.id = null;
State.selectedArea.name = '';
clearAll();
}
renderAreaList();
}
async function renameArea(id) {
const newName = prompt("请输入新名称:");
if (!newName || !newName.trim()) return;
const area = await db.areas.get(id);
if (!area) return;
area.name = newName.trim();
await db.areas.put(area);
renderAreaList();
}
async function savePOIToDB(data) {
try { await db.pois.bulkPut(data); } catch (err) { console.error("保存POI失败", err); }
}
async function loadPOIFromDB(filter) {
State.poiList = await db.pois.toArray();
State.filteredPoiList = filter ? State.poiList.filter(filter) : [...State.poiList];
handleAllPOIData(State.filteredPoiList);
}
function handleAllPOIData(pois) {
OverlayManager.clearMassMarkers();
const tableData = pois.map(poi => ({
ID: poi.id,
名称: poi.name || "-",
地址: poi.address || "-",
电话: poi.tel || "-",
经度: poi.location?.lng?.toFixed(6) || "-",
纬度: poi.location?.lat?.toFixed(6) || "-",
大类: poi.mainType || poi?.type?.split(';')?.[0] || '-',
类型: poi.type || "-",
区域: poi.area,
}));
State.filteredPoiList = tableData;
State.poiList = tableData;
const points = tableData.filter(p => p.经度 !== "-" && p.纬度 !== "-")
.map(p => ({ lnglat: [parseFloat(p.经度), parseFloat(p.纬度)], name: p.名称 || "" }));
if (points.length) {
const style = { url: "https://webapi.amap.com/theme/v1.3/markers/b/mark_bs.png", size: new AMap.Size(9, 15), anchor: new AMap.Pixel(5, 5) };
State.massMarker = new AMap.MassMarks(points, { zIndex: 111, zooms: [1, 100], cursor: "pointer", style });
State.massMarker.setMap(map);
State.marker = new AMap.Marker({ map, offset: new AMap.Pixel(-25, -50), icon: 'https://ssr-next.amap.com/static/amap-pc-ssr/production/3613e81e/_next/static/media/ic-point-focus.8f1dcac6.svg' });
State.massMarker.on("mouseover", (e) => { State.marker.setPosition(e.data.lnglat); State.marker.setLabel({ content: `${e.data.name}
` }); });
State.massMarker.on("mouseout", () => { State.marker.setLabel({ content: null }); });
OverlayManager.add(State.marker);
}
renderPOITable();
updateSummary();
}
function renderPoiTypes() {
const types = Utils.getPoiTypes();
const selector = document.getElementById("typeSelector");
let html = '';
types.forEach(item => {
const selected = State.selectedTypes.has(item.code);
html += `${item.name}`;
});
selector.innerHTML = html;
selector.querySelectorAll('.type-item').forEach(el => {
el.addEventListener('click', function() {
const code = this.dataset.code;
if (State.selectedTypes.has(code)) State.selectedTypes.delete(code);
else State.selectedTypes.add(code);
renderPoiTypes();
applyFilters();
});
});
}
function applyFilters() {
const typesObj = Object.fromEntries(Utils.getPoiTypes().map(v => [v.name, v.code]));
const filterFn = (v) => {
const areaMatch = !State.selectedArea.name || v.area === State.selectedArea.name;
const typeMatch = State.selectedTypes.size === 0 || State.selectedTypes.has(v.mainType) || State.selectedTypes.has(typesObj[v.mainType]);
return areaMatch && typeMatch;
};
loadPOIFromDB(filterFn);
}
function updateSummary() {
document.getElementById("poiTableSummary").innerText = `总计 ${State.filteredPoiList.length} 条`;
}
function renderPOITable() {
const data = State.filteredPoiList;
const tb = document.getElementById("poiTable");
let html = '';
data.forEach((item, idx) => {
html += ``;
State.fieldConfig.filter(f => f.visible).forEach(f => {
const val = item[f.key] ?? '-';
html += `| ${val} | `;
});
html += "
";
});
tb.innerHTML = html;
tb.querySelectorAll('.poi-item').forEach(tr => {
tr.addEventListener('click', function() { const idx = parseInt(this.dataset.index); focusPOI(idx); });
});
updateSummary();
}
function updateMassMarkerData() {
if (!State.massMarker) return;
const points = State.poiList.filter(p => p.经度 !== "-" && p.纬度 !== "-")
.map(p => ({ lnglat: [parseFloat(p.经度), parseFloat(p.纬度)], name: p.名称 || "" }));
State.massMarker.setData(points);
}
function focusPOI(idx) {
if (idx >= State.filteredPoiList.length) return;
const p = State.filteredPoiList[idx];
if (p.经度 !== "-" && p.纬度 !== "-") {
const pos = [parseFloat(p.经度), parseFloat(p.纬度)];
map.panTo(pos);
map.setZoom(19);
if (State.marker) { State.marker.setPosition(pos); State.marker.setLabel({ content: `${p.名称}
` }); }
}
document.querySelectorAll('#poiTable .poi-item').forEach((tr, i) => {
const idx = parseInt(tr.dataset.index);
tr.classList.toggle('poi-active', idx === idx);
});
}
function clearAll() {
CurrentPolygon.clear();
OverlayManager.clearMassMarkers();
OverlayManager.removeAll();
if (State.editor) { State.editor.close(); State.editor = null; }
State.poiList = [];
State.filteredPoiList = [];
renderPOITable();
updateSummary();
document.querySelectorAll(".poi-active").forEach(el => el.classList.remove("poi-active"));
map.clearMap();
}
// ================================================================
// 8. 搜索POI(增强调试版)
// ================================================================
async function searchBySelectedPolygon() {
if (!State.selectedArea.id) { alert("请先选择一个区域"); return; }
const keywords = document.getElementById("searchKey").value.trim();
const typeArr = Array.from(State.selectedTypes);
if (typeArr.length === 0) { alert("请至少选择一个POI类型"); return; }
console.log("[搜索] 关键词:", keywords, "类型:", typeArr);
try {
await new Promise((resolve, reject) => {
AMap.plugin('AMap.PlaceSearch', () => {
if (typeof AMap.PlaceSearch === 'function') resolve();
else reject(new Error('PlaceSearch加载失败'));
});
});
} catch (e) { alert('加载高德地图搜索插件失败: ' + e.message); return; }
const area = await db.areas.get(State.selectedArea.id);
if (!area) return;
const pageSize = 50;
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
function splitPolygonByDepth(coords, depthLimit) {
let queue = [{ coords, depth: 0 }];
let result = [];
while (queue.length) {
const { coords, depth } = queue.pop();
const { intersects } = Utils.getIntersections(coords);
if (intersects.length < 2) { result.push(coords); continue; }
const parts = Utils.splitPolygon(coords, intersects);
if (depth < depthLimit) parts.forEach(p => queue.push({ coords: p, depth: depth + 1 }));
else result.push(...parts);
}
return result;
}
async function searchInBounds(types, keywords, coords) {
let queue = area.overLimit && area.overLimit.length > 0
? area.overLimit.map(item => ({ coords: item.coords, types: [types.join('|')] }))
: [{ coords, types: [types.join('|')] }];
let allPois = [];
let overLimit = false;
let count = 0;
const ps = new AMap.PlaceSearch({ city: "全国", pageSize: pageSize, extensions: "all" });
while (queue.length) {
const cur = queue.shift();
// 生成符合高德要求的字符串:每个点保留6位小数,用分号分隔
const polygonStr = cur.coords.map(p => {
return p.map(c => c.toFixed(6)).join(',');
}).join(';');
console.log("[搜索] 多边形字符串:", polygonStr);
for (let type of cur.types) {
if (count % 3 === 1) await delay(1000);
console.log("[搜索] 当前类型:", type, "关键词:", keywords);
const result = await new Promise((resolve) => {
ps.setPageIndex(1);
ps.setType(type);
ps.searchInBounds(keywords, polygonStr, (status, resData) => {
console.log("[搜索] 状态:", status, "返回数据:", resData);
if (status !== "complete") {
if (resData === "USER_DAILY_QUERY_OVER_LIMIT") {
overLimit = true;
resolve({ pois: [], total: 0 });
return;
}
// 其他错误,打印并返回空
console.error("[搜索] 请求失败:", status, resData);
resolve({ pois: [], total: 0 });
return;
}
const pois = (resData?.poiList?.pois || []).map(v => ({ ...v, area: area.name, mainType: v?.type?.split(';')?.[0] }));
resolve({
pois: pois,
total: resData.poiList.count || 0
});
});
});
console.log("[搜索] 本次获得POI数量:", result.pois.length);
allPois.push(...result.pois);
count++;
if (result.total > pageSize) {
const depthLimit = Math.floor(Math.log2(result.total / pageSize));
const splitParts = splitPolygonByDepth(cur.coords, depthLimit);
queue.push(...splitParts.map(p => ({ coords: p, types: [type] })));
break;
}
const color = overLimit ? '#F4606C' : '#FFA502';
const poly = new AMap.Polygon({ path: cur.coords, fillColor: color, fillOpacity: 0.2, strokeColor: color, strokeWeight: 2 });
poly.setMap(map);
OverlayManager.add(poly);
if (overLimit) {
await db.areas.update(area.id, { overLimit: queue });
queue = [];
break;
}
}
}
if (!overLimit) await db.areas.update(area.id, { overLimit: null });
const uniqueMap = new Map();
allPois.forEach(poi => poi.id && uniqueMap.set(poi.id, poi));
return Array.from(uniqueMap.values());
}
try {
const pois = await searchInBounds(typeArr, keywords, area.coords);
console.log("[搜索] 最终去重后POI数量:", pois.length);
if (pois.length) {
await savePOIToDB(pois);
handleAllPOIData(pois.map(p => ({
ID: p.id,
名称: p.name || "-",
地址: p.address || "-",
电话: p.tel || "-",
经度: p.location?.lng?.toFixed(6) || "-",
纬度: p.location?.lat?.toFixed(6) || "-",
大类: p.mainType || p?.type?.split(';')?.[0] || '-',
类型: p.type || "-",
区域: p.area,
})));
alert(`搜索完成,共获取 ${pois.length} 个POI`);
} else {
alert("该区域未找到匹配的POI,请检查关键词和类型是否正确,或查看控制台日志");
}
} catch (err) {
console.error("[搜索] 异常:", err);
alert("搜索过程中出现错误,请查看控制台");
}
}
// ================================================================
// 9. 导出、分区、预览
// ================================================================
function exportExcel() {
if (State.filteredPoiList.length === 0) { alert("暂无数据"); return; }
const data = State.filteredPoiList.map(item => {
const row = {};
State.fieldConfig.filter(f => f.visible).forEach(f => row[f.label] = item[f.key] ?? '-');
return row;
});
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(data);
XLSX.utils.book_append_sheet(wb, ws, "POI数据");
XLSX.writeFile(wb, "多边形区域POI.xlsx");
}
async function repartPOIArea() {
const areas = await db.areas.toArray();
const pois = await db.pois.toArray();
const updates = [];
for (const poi of pois) {
const { lat, lng } = poi.location || {};
if (lat == null || lng == null) continue;
let areaName = '';
for (const area of areas) {
const polygon = turf.polygon([[...area.coords, area.coords[0]]]);
if (turf.booleanPointInPolygon(turf.point([lng, lat]), polygon)) { areaName = area.name; break; }
}
if (areaName && poi.area !== areaName) updates.push({ key: poi.id, changes: { area: areaName } });
}
if (updates.length) {
await db.pois.bulkUpdate(updates);
if (State.selectedArea.name) loadPOIFromDB(v => v.area === State.selectedArea.name);
else loadPOIFromDB();
}
alert(`重新分区完成,更新了 ${updates.length} 个POI`);
}
async function previewAreas() {
clearAll();
const areas = await db.areas.toArray();
const rainbowColors = ['#ff0000', '#ff6a00', '#ffa500', '#ffff00', '#33cc33', '#66ffff', '#6666ff'];
areas.forEach((area, idx) => {
const coords = [...area.coords, area.coords[0]];
const color = rainbowColors[idx % rainbowColors.length];
OverlayManager.drawPolygon(coords, color);
});
}
// ================================================================
// 10. 绘制功能
// ================================================================
function startDraw() {
clearAll();
if (State.selectedArea.id) {
db.areas.get(State.selectedArea.id).then(area => {
CurrentPolygon.render(area);
State.editor = new AMap.PolyEditor(map, State.currentPolygon);
State.editor.open();
});
return;
}
if (!State.mouseTool) State.mouseTool = new AMap.MouseTool(map);
State.mouseTool.polygon({ strokeColor: "#2d7aff", strokeWeight: 3, fillColor: "#2d7aff", fillOpacity: 0.2 });
State.mouseTool.on("draw", (e) => {
const polygon = e.obj;
CurrentPolygon.set(polygon);
State.editor = new AMap.PolyEditor(map, polygon);
State.editor.open();
State.mouseTool.close();
});
}
// ================================================================
// 11. 字段配置弹窗
// ================================================================
function openFieldModal() {
const list = document.getElementById("fieldList");
let html = '';
State.fieldConfig.forEach(f => {
html += `
`;
});
list.innerHTML = html;
list.querySelectorAll('.field-visibility').forEach(cb => {
cb.addEventListener('change', function() {
const key = this.dataset.key;
const item = State.fieldConfig.find(f => f.key === key);
if (item) item.visible = this.checked;
});
});
let dragItem = null;
list.addEventListener('dragstart', e => {
if (e.target.classList.contains('field-item')) {
dragItem = e.target;
e.target.style.opacity = '0.5';
}
});
list.addEventListener('dragover', e => e.preventDefault());
list.addEventListener('drop', e => {
e.preventDefault();
if (!dragItem || !e.target.classList.contains('field-item')) return;
const items = Array.from(list.querySelectorAll('.field-item'));
const oldIdx = items.indexOf(dragItem);
const newIdx = items.indexOf(e.target);
if (oldIdx < newIdx) list.insertBefore(dragItem, e.target.nextSibling);
else list.insertBefore(dragItem, e.target);
const newConfig = [];
list.querySelectorAll('.field-item').forEach(el => {
const key = el.dataset.key;
const found = State.fieldConfig.find(f => f.key === key);
if (found) newConfig.push(found);
});
State.fieldConfig = newConfig;
dragItem.style.opacity = '1';
dragItem = null;
});
list.addEventListener('dragend', e => {
if (e.target.classList.contains('field-item')) {
e.target.style.opacity = '1';
dragItem = null;
}
});
document.getElementById("fieldModalMask").classList.add("show");
document.getElementById("fieldModal").style.display = "block";
}
function closeFieldModal() {
document.getElementById("fieldModalMask").classList.remove("show");
document.getElementById("fieldModal").style.display = "none";
}
function saveFieldConfig() {
saveFieldConfigToStorage();
renderPOITableHeader();
renderPOITable();
closeFieldModal();
alert("字段配置保存成功!");
}
function renderPOITableHeader() {
const hd = document.getElementById("poiTableHeader");
let html = '';
State.fieldConfig.filter(f => f.visible).forEach(f => { html += `| ${f.label} | `; });
html += '
';
hd.innerHTML = html;
}
// ================================================================
// 12. UI构建(无分页)
// ================================================================
function buildUI() {
const style = `
* { box-sizing: border-box; }
.panel-container {
position: fixed; top: 0; right: 0; height: 100%; padding: 10px; z-index: 999;
background: rgba(255,255,255,0.4); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
border-left: 1px solid rgba(255,255,255,0.3);
}
.panel {
background: rgba(255,255,255,0.3); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
padding: 10px 12px; overflow-y: auto; height: 100%; border-radius: 20px 0 0 20px;
transition: width 0.3s ease, padding 0.3s ease; box-shadow: -4px 0 20px rgba(0,0,0,0.04);
display: flex; flex-direction: column;
}
.panel-container.collapsed .panel { width: 0; padding: 0; overflow: hidden; }
#resizeHandler {
width: 6px; height: 100%; background: rgba(255,255,255,0.2); position: absolute; left: -6px; top: 0;
cursor: col-resize; z-index: 999; border-radius: 10px; transition: background 0.2s;
}
#resizeHandler:hover { background: #2d7aff; }
#collapseBtn {
position: absolute; left: -32px; top: 90px; width: 32px; height: 60px;
background: rgba(45,122,255,0.8); backdrop-filter: blur(8px); color: white; border: none;
border-radius: 12px 0 0 12px; cursor: pointer; z-index: 998; display: flex;
align-items: center; justify-content: center; font-size: 18px; transition: all 0.2s;
}
#collapseBtn:hover { background: #2d7aff; transform: scale(1.05); }
.btn {
margin: 2px; padding: 4px 12px; border: none; border-radius: 30px;
background: rgba(255,255,255,0.5); backdrop-filter: blur(4px); color: #1e293b;
cursor: pointer; font-size: 11px; font-weight: 500; transition: all 0.15s;
box-shadow: 0 1px 4px rgba(0,0,0,0.02); border: 1px solid rgba(255,255,255,0.2);
white-space: nowrap;
}
.btn:hover { background: rgba(255,255,255,0.85); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.06); }
.btn-primary { background: linear-gradient(135deg, #2d7aff, #4a6cf7); color: #fff; border: none; }
.btn-primary:hover { background: linear-gradient(135deg, #1a5fd9, #3a5cdb); box-shadow: 0 4px 16px rgba(45,122,255,0.3); }
.btn-danger { background: linear-gradient(135deg, #e74c3c, #c0392b); color: #fff; border: none; }
.btn-danger:hover { background: linear-gradient(135deg, #c0392b, #a93226); }
.btn-light { background: rgba(255,255,255,0.3); color: #475569; border: 1px solid rgba(255,255,255,0.1); }
.btn-light:hover { background: rgba(255,255,255,0.7); }
input[type="text"], select {
width: 100%; padding: 4px 10px; margin: 2px 0; border: 1px solid rgba(0,0,0,0.06);
border-radius: 30px; font-size: 11px; background: rgba(255,255,255,0.5);
backdrop-filter: blur(4px); outline: none; transition: all 0.2s; color: #1e293b;
height: 28px; box-sizing: border-box;
}
input[type="text"]:focus, select:focus {
border-color: #2d7aff; background: rgba(255,255,255,0.8);
box-shadow: 0 0 0 4px rgba(45,122,255,0.08);
}
h4 { margin: 4px 0; font-size: 12px; color: #0f172a; font-weight: 600; letter-spacing: 0.3px; }
.section-divider {
border-top: 1px solid rgba(0,0,0,0.04); margin: 6px 0; padding-top: 4px;
}
.table-container {
overflow-y: auto; overflow-x: hidden; display: block;
background: rgba(255,255,255,0.15); border-radius: 12px; padding: 2px;
margin-top: 4px; backdrop-filter: blur(4px);
flex: 1;
}
table {
width: 100%; border-collapse: collapse; font-size: 10px; background: transparent;
}
th {
background: rgba(45,122,255,0.06); font-weight: 600; color: #1e293b;
padding: 4px 2px; text-align: center; border-bottom: 1px solid rgba(0,0,0,0.04);
font-size: 10px;
position: sticky; top: 0; background: rgba(255,255,255,0.8);
backdrop-filter: blur(4px);
}
td {
padding: 2px 4px; text-align: center; border-bottom: 1px solid rgba(0,0,0,0.02);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100px;
font-size: 10px;
}
tr:hover { background: rgba(45,122,255,0.04); }
tr.active { background: rgba(45,122,255,0.08) !important; }
tr.poi-active { background: rgba(45,122,255,0.12) !important; }
.type-selector {
margin: 3px 0; padding: 4px; background: rgba(255,255,255,0.15);
border-radius: 12px; border: 1px solid rgba(255,255,255,0.1);
backdrop-filter: blur(4px); max-height: 80px; overflow-y: auto;
}
.type-item {
display: inline-block; margin: 1px 3px; padding: 1px 10px;
cursor: pointer; font-size: 10px; border-radius: 30px;
background: rgba(255,255,255,0.2); transition: all 0.15s; color: #475569;
}
.type-item.selected { background: #2d7aff; color: #fff; box-shadow: 0 2px 8px rgba(45,122,255,0.2); }
.type-item:hover { background: rgba(45,122,255,0.1); }
.field-modal {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: rgba(255,255,255,0.85); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
padding: 20px 24px; border-radius: 28px; box-shadow: 0 40px 120px rgba(0,0,0,0.08);
border: 1px solid rgba(255,255,255,0.2); z-index: 1000; min-width: 260px;
}
.field-modal h5 { margin-bottom: 10px; font-size: 14px; color: #0f172a; }
.field-item {
display: flex; align-items: center; margin: 4px 0; font-size: 11px;
padding: 2px 6px; cursor: move; border: 1px solid transparent; border-radius: 10px;
background: rgba(255,255,255,0.2);
}
.field-item:hover { background: rgba(255,255,255,0.6); border-color: rgba(0,0,0,0.04); }
.field-item input { margin-right: 6px; }
.modal-mask {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.2); backdrop-filter: blur(8px); z-index: 999; display: none;
}
.modal-mask.show { display: block; }
.modal-footer { margin-top: 12px; display: flex; justify-content: flex-end; gap: 8px; }
#poiTableSummary {
font-size: 10px; text-align: center; color: #64748b; margin-top: 2px;
flex-shrink: 0;
}
.flex-row {
display: flex; flex-wrap: wrap; gap: 4px; align-items: center;
}
.flex-row .btn { flex: 0 0 auto; }
.flex-row input[type="text"] { flex: 1; min-width: 60px; }
.scrollbar-thin::-webkit-scrollbar { width: 3px; }
.scrollbar-thin::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.1); border-radius: 10px; }
.scrollbar-thin::-webkit-scrollbar-track { background: transparent; }
`;
GM_addStyle(style);
const html = `
`;
const div = document.createElement('div');
div.innerHTML = html;
document.body.appendChild(div);
// ---------- 面板折叠/缩放 ----------
const panelContainer = document.getElementById("panelContainer");
const panel = document.getElementById("panel");
const collapseBtn = document.getElementById("collapseBtn");
const resizeHandler = document.getElementById("resizeHandler");
const savedWidth = localStorage.getItem("panelWidth") || "380px";
panel.style.width = savedWidth;
if (localStorage.getItem("panelCollapsed") === "true") {
panelContainer.classList.add("collapsed");
panel.style.width = "0";
resizeHandler.style.display = "none";
collapseBtn.textContent = "«";
} else {
resizeHandler.style.display = "block";
collapseBtn.textContent = "»";
}
collapseBtn.addEventListener('click', () => {
const isCollapsed = panelContainer.classList.toggle("collapsed");
localStorage.setItem("panelCollapsed", isCollapsed);
if (isCollapsed) {
localStorage.setItem("preCollapseWidth", panel.style.width);
localStorage.setItem("panelWidth", panel.style.width);
collapseBtn.textContent = "«";
panel.style.width = "0";
resizeHandler.style.display = "none";
} else {
const pre = localStorage.getItem("preCollapseWidth") || "380px";
localStorage.setItem("panelWidth", pre);
collapseBtn.textContent = "»";
panel.style.width = pre;
resizeHandler.style.display = "block";
}
map?.resize();
});
let isResizing = false;
resizeHandler.addEventListener("mousedown", (e) => {
isResizing = true;
e.preventDefault();
document.body.style.userSelect = "none";
});
document.addEventListener("mousemove", (e) => {
if (!isResizing || panelContainer.classList.contains("collapsed")) return;
const newWidth = Math.max(320, Math.min(window.innerWidth - e.clientX, 1200));
panel.style.width = newWidth + "px";
});
document.addEventListener("mouseup", () => {
if (!isResizing) return;
isResizing = false;
document.body.style.userSelect = "";
const w = panel.style.width;
localStorage.setItem("panelWidth", w);
localStorage.setItem("preCollapseWidth", w);
map?.resize();
});
// ---------- 按钮事件绑定 ----------
document.getElementById("drawBtn").addEventListener('click', startDraw);
document.getElementById("savePolygonBtn").addEventListener('click', () => CurrentPolygon.save());
document.getElementById("deleteCurrentAreaBtn").addEventListener('click', function() {
if (!State.selectedArea.id) {
alert("请先选择一个区域");
return;
}
deleteArea(State.selectedArea.id);
});
document.getElementById("previewBtn").addEventListener('click', previewAreas);
document.getElementById("searchPoiBtn").addEventListener('click', searchBySelectedPolygon);
document.getElementById("exportBtn").addEventListener('click', exportExcel);
document.getElementById("repartBtn").addEventListener('click', repartPOIArea);
document.getElementById("clearMarkersBtn").addEventListener('click', () => OverlayManager.clearMassMarkers());
document.getElementById("clearAllBtn").addEventListener('click', clearAll);
document.getElementById("selectAllTypes").addEventListener('click', () => {
Utils.getPoiTypes().forEach(t => State.selectedTypes.add(t.code));
renderPoiTypes();
applyFilters();
});
document.getElementById("reverseTypes").addEventListener('click', () => {
const allCodes = new Set(Utils.getPoiTypes().map(t => t.code));
const newSet = new Set();
allCodes.forEach(c => { if (!State.selectedTypes.has(c)) newSet.add(c); });
State.selectedTypes = newSet;
renderPoiTypes();
applyFilters();
});
document.getElementById("clearTypes").addEventListener('click', () => {
State.selectedTypes.clear();
renderPoiTypes();
applyFilters();
});
document.getElementById("fieldConfigBtn").addEventListener('click', openFieldModal);
document.getElementById("fieldCancelBtn").addEventListener('click', closeFieldModal);
document.getElementById("fieldSaveBtn").addEventListener('click', saveFieldConfig);
document.getElementById("fieldModalMask").addEventListener('click', closeFieldModal);
document.getElementById("poiSearchInput").addEventListener('input', function() {
const kw = this.value.trim().toLowerCase();
if (!kw) { State.filteredPoiList = [...State.poiList]; }
else {
State.filteredPoiList = State.poiList.filter(item => {
return Object.values(item).some(v => v && v.toString().toLowerCase().includes(kw));
});
}
renderPOITable();
updateSummary();
});
// 初始化
renderPOITableHeader();
renderAreaList();
renderPoiTypes();
loadPOIFromDB();
}
// ================================================================
// 13. 初始化
// ================================================================
function init() {
const target = document.querySelector('#amap-global-container');
if (!target) return setTimeout(init, 500);
map = target.amap;
if (!map) return setTimeout(init, 500);
AMap.plugin(["AMap.MouseTool", "AMap.PolyEditor", "AMap.PlaceSearch"], () => {
State.mouseTool = new AMap.MouseTool(map);
console.log("高德插件加载完成");
});
buildUI();
console.log("POI多边形采集工具(v3.2)已启动,请查看控制台日志排查搜索问题");
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();