// ==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 = `

📍 区域

名称超量操作

📋 POI

`; 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(); } })();