返回文章列表
2025年12月10日yodfz

#飞牛相册Hack搜索框

javascript飞牛油猴
115 分钟阅读
34371
yodfz

我的NAS一直都是使用飞牛系统,但是有个问题,他的相册AI搜索不好用,并且在地图模式下没有任何可以搜索的内容。但是我又需要在地图模式下搜索地址,不然整体拖拽非常的麻烦。

最终痛定思痛,自己写了一个脚本HACK了飞牛相册,在飞牛相册加载之后,自动初始化一个搜索框进去。采用公开的数据地址,进行定位搜索。

// ==UserScript==
// @name         飞牛相册地图增强
// @match        [飞牛相册地址]/map*
// @run-at       document-start
// @grant        none
// ==/UserScript==
(function () {
  if (window.__TM_FN_ENHANCED) {
    console.log("[TM] script already loaded, skip");
    return;
  }
  window.__TM_FN_ENHANCED = true;
  const tmLog = (...a) => console.log("[TM]", ...a);
  // 注入转圈动画样式(若未存在)
  (() => {
    if (document.getElementById("tm-spin-style")) return;
    const style = document.createElement("style");
    style.id = "tm-spin-style";
    style.textContent = `
      @keyframes tm-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg);} }
    `;
    document.head.appendChild(style);
  })();

  // 1) 捕获/兜底腾讯地图 key
  const prevOnLoad = window.onLoad;
  tmLog("install onLoad interceptor");
  window.TX_MAP_KEY = "OZNBZ-DDAKG-7WDQB-Q2WJH-NUBP5-NPFSX"; // 兜底
  window.onLoad = function (cfg, ...rest) {
    tmLog("onLoad called", cfg);
    if (cfg && cfg.key) {
      window.TX_MAP_KEY = cfg.key;
      tmLog("captured TX key:", cfg.key);
    }
    if (typeof prevOnLoad === "function") {
      return prevOnLoad.apply(this, [cfg, ...rest]);
    }
  };
  const tryParseKeyFromGljs = async () => {
    try {
      const script =
        document.querySelector('script[src*="gljs"]') ||
        document.querySelector('script[src*="map.qq.com"]');
      if (!script || !script.src) return;
      const res = await fetch(script.src);
      const txt = await res.text();
      const m =
        txt.match(/"key"\s*:\s*"([A-Z0-9-]+)"/i) ||
        txt.match(/key["']?\s*[:=]\s*["']([A-Z0-9-]{10,})["']/i);
      if (m && m[1]) {
        window.TX_MAP_KEY = m[1];
        tmLog("captured TX key via fetch:", m[1]);
      }
    } catch (e) {
      tmLog("parse key from gljs failed:", e);
    }
  };
  setTimeout(() => {
    if (!window.TX_MAP_KEY) tryParseKeyFromGljs();
  }, 1000);

  const wait = (fn, key = "TMap", timeout = 20000) =>
    new Promise((res, rej) => {
      const t0 = Date.now();
      (function poll() {
        if (window[key]) return res(window[key]);
        if (Date.now() - t0 > timeout) return rej(new Error("wait timeout"));
        requestAnimationFrame(poll);
      })();
    });

  // 2) 全局引用
  let firstMap = null;
  let TMapRef = null;
  let firstCluster = null;
  let scanAttempts = 0;
  let geocoderPromise = null;
  let lastSuggestToken = 0;
  let lastSuggestQ = "";
  let suggestLoadingEl = null;
  // 点高亮层移除;仅保留行政区热力
  let highlightLayer = null;
  const highlightGeos = new Map();
  const highlightQueue = [];
  let highlightScheduled = false;
  const ENABLE_HIGHLIGHT = false; // 关闭点高亮
  // 行政区域热力(简化用 bounding box)
  let adminLayer = null;
  const adminPolys = new Map(); // key=adminName, val={id,path,weight}
  const adminQueue = [];
  let adminScheduled = false;
  const ADMIN_MAX = 120; // 控制最多绘制的区域数
  let adminLastDrawLog = 0;
  const adminCellCache = new Map(); // key=cell lat,lng -> {name, paths}
  const adminRegionCache = new Map(); // key=region name -> {paths, bbox:[s,n,w,e]}
  const adminCacheTrim = (map, max = 400) => {
    if (map.size <= max) return;
    const keys = Array.from(map.keys());
    for (let i = 0; i < map.size - max; i++) map.delete(keys[i]);
  };
  const latLngInBbox = (lat, lng, bbox) => {
    if (!bbox || bbox.length < 4) return false;
    const [s, n, w, e] = bbox;
    return (
      Number.isFinite(s) &&
      Number.isFinite(n) &&
      Number.isFinite(w) &&
      Number.isFinite(e) &&
      lat >= s - 0.01 &&
      lat <= n + 0.01 &&
      lng >= w - 0.01 &&
      lng <= e + 0.01
    );
  };
  const adminVisibleFirst = (lat, lng) => {
    if (!firstMap || typeof firstMap.getBounds !== "function") return false;
    try {
      const b = firstMap.getBounds();
      if (!b || !b.southwest || !b.northeast) return false;
      const sw = b.southwest;
      const ne = b.northeast;
      return (
        lat >= sw.lat - 0.1 &&
        lat <= ne.lat + 0.1 &&
        lng >= sw.lng - 0.1 &&
        lng <= ne.lng + 0.1
      );
    } catch (_) {
      return false;
    }
  };

  const enqueueHighlight = () => {
    if (!ENABLE_HIGHLIGHT) return;
    if (highlightScheduled) return;
    highlightScheduled = true;
    requestAnimationFrame(() => {
      highlightScheduled = false;
      if (!highlightLayer) return;
      // 控制数量,避免过多导致卡顿/黑屏
      const MAX = 1500;
      if (highlightGeos.size > MAX) {
        const extra = highlightGeos.size - MAX;
        const keys = Array.from(highlightGeos.keys());
        for (let i = 0; i < extra; i++) highlightGeos.delete(keys[i]);
      }
      highlightLayer.setGeometries([...highlightGeos.values()]);
    });
  };

  const enqueueAdminDraw = () => {
    if (adminScheduled) return;
    adminScheduled = true;
    requestAnimationFrame(() => {
      adminScheduled = false;
      if (!adminLayer) return;
      // 控制数量
      if (adminPolys.size > ADMIN_MAX) {
        const keys = Array.from(adminPolys.keys());
        const extra = adminPolys.size - ADMIN_MAX;
        for (let i = 0; i < extra; i++) adminPolys.delete(keys[i]);
      }
      const geos = [...adminPolys.values()];
      adminLayer.setGeometries(geos);
      if (Date.now() - adminLastDrawLog > 2000) {
        adminLastDrawLog = Date.now();
        tmLog("admin heat draw", {
          regions: geos.length,
          sample: geos
            .slice(0, 3)
            .map((g) => `${g.id}:${g.weight}`)
            .join(","),
        });
      }
    });
  };

  // 3) 捕获 map
  const captureMap = (m) => {
    if (m) m.__isTMapMap = true;
    if (!firstMap && m) {
      firstMap = m;
      tmLog("captured map instance");
    }
    return m;
  };

  // 4) 提前安装 window.TMap setter,防止早期创建溜走
  try {
    const desc = Object.getOwnPropertyDescriptor(window, "TMap");
    if (!desc || desc.configurable) {
      let _tmap;
      Object.defineProperty(window, "TMap", {
        configurable: true,
        get() {
          return _tmap;
        },
        set(v) {
          _tmap = v;
          tmLog("window.TMap assigned (setter)");
          if (v) {
            // wrap Map
            if (v.Map && !v.Map.__TM_WRAPPED) {
              const RawMapEarly = v.Map;
              const WrappedMap = function (...args) {
                const m = new RawMapEarly(...args);
                captureMap(m);
                return m;
              };
              WrappedMap.prototype = RawMapEarly.prototype;
              Object.setPrototypeOf(WrappedMap, RawMapEarly);
              WrappedMap.__TM_WRAPPED = true;
              v.Map = WrappedMap;
              tmLog("wrapped TMap.Map (setter)");
            }
            // wrap MarkerCluster
            if (v.MarkerCluster && !v.MarkerCluster.__TM_WRAPPED) {
              const RawMCEarly = v.MarkerCluster;
              const mcAddEarly =
                RawMCEarly.prototype && RawMCEarly.prototype.add;
              const WrappedMC = function (opts = {}) {
                // 仅在 map 缺失时回退到 firstMap,避免传入非 Map 实例导致 SDK 报错
                if (!opts.map && firstMap) opts.map = firstMap;
                const inst = new RawMCEarly(opts);
                if (opts?.map) captureMap(opts.map);
                if (!firstCluster) {
                  firstCluster = inst;
                  tmLog("captured MarkerCluster (setter)");
                }
                return inst;
              };
              WrappedMC.prototype = RawMCEarly.prototype;
              Object.setPrototypeOf(WrappedMC, RawMCEarly);
              WrappedMC.__TM_WRAPPED = true;
              v.MarkerCluster = WrappedMC;
              if (mcAddEarly) {
                RawMCEarly.prototype.add = function (geos = []) {
                  const patched = geos.map((g) => {
                    const p = g.properties || {};
                    const hasPhoto =
                      p.count > 0 ||
                      p.url ||
                      p.photos?.length > 0 ||
                      p.images?.length > 0 ||
                      p.imgUrl;
                    return hasPhoto ? { ...g, styleId: "photoMarker" } : g;
                  });
                  return mcAddEarly.call(this, patched);
                };
              }
              tmLog("wrapped MarkerCluster (setter)");
            }
          }
        },
      });
    } else {
      tmLog("window.TMap not configurable, setter skipped");
    }
  } catch (e) {
    tmLog("install TMap setter failed", e);
  }

  // 5) 样式与搜索
  const photoIcon =
    "https://mapapi.qq.com/web/lbs/javascriptGL/demo/img/markerDefault.png";
  const highlightStyles = () => ({
    photoMarker: new TMapRef.MarkerStyle({
      width: 22,
      height: 30,
      src: photoIcon,
      anchor: { x: 11, y: 28 },
      color: "#ffd966",
      strokeColor: "#d48806",
      strokeWidth: 1,
    }),
  });
  const photoMeta = new Map();
  const rawFetch = window.fetch;
  window.fetch = async (...args) => {
    const resp = await rawFetch(...args);
    try {
      const url = (args[0] || "").toString();
      if (/photo|album|image/i.test(url)) {
        resp
          .clone()
          .json()
          .then((data) => {
            const list = data?.list || data?.data || [];
            list.forEach((item) => {
              if (!item.lat || !item.lng) return;
              const key = `${item.lat},${item.lng}`;
              const count =
                item.photoCount || item.count || item.photos?.length || 0;
              photoMeta.set(key, { photoCount: count });
            });
          });
      }
    } catch (_) {}
    return resp;
  };

  const addSearchBox = (map) => {
    if (!map || !TMapRef) {
      tmLog("addSearchBox skipped: map/TMap missing");
      return;
    }
    if (document.getElementById("tm-search-box")) {
      tmLog("search box already exists");
      return;
    }
    const box = document.createElement("div");
    box.id = "tm-search-box";
    box.style.cssText = `
          position:absolute;top:12px;right:100px;z-index:9999;
          background:#fff;padding:8px;border-radius:6px;
          box-shadow:0 2px 10px rgba(0,0,0,0.2);
          display:flex;gap:6px;align-items:center;font-size:13px;`;
    box.innerHTML = `
          <div style="position:relative;display:flex;flex-direction:column;gap:4px;">
            <div style="display:flex;gap:6px;align-items:center;">
              <input id="tm-addr" placeholder="输入地址回车" style="color:#333;width:400px;padding:6px 8px;border:1px solid #ccc;border-radius:4px;">
              <button id="tm-go" style="padding:6px 10px;border:0;background:#1677ff;color:#fff;border-radius:4px;cursor:pointer;">定位</button>
              <div id="tm-loading" style="display:none;margin-left:4px;width:16px;height:16px;border:2px solid #1677ff;border-top-color:transparent;border-radius:50%;animation:tm-spin 0.6s linear infinite;"></div>
            </div>
            <div id="tm-suggest" style="color:#000;display:none;position:absolute;top:38px;left:0;width:280px;max-height:200px;overflow:auto;background:#fff;border:1px solid #ddd;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,0.15);z-index:10000;"></div>
          </div>
        `;
    (document.body || document.documentElement).appendChild(box);
    tmLog("search box injected; key =", window.TX_MAP_KEY);

    const GeocoderCls = TMapRef && TMapRef.service && TMapRef.service.Geocoder;
    let geocoder = null;
    if (GeocoderCls) {
      geocoder = new GeocoderCls({
        key: window.TX_MAP_KEY || "",
      });
    } else {
      tmLog("Geocoder class missing; will load qq.maps Geocoder");
      if (!geocoderPromise) {
        geocoderPromise = new Promise((resolve, reject) => {
          const cb = `TM_QQ_GEO_CB_${Date.now()}`;
          window[cb] = () => {
            try {
              if (window.qq && window.qq.maps && window.qq.maps.Geocoder) {
                const geo = new window.qq.maps.Geocoder({
                  complete: (res) => resolve(res),
                });
                resolve(geo);
              } else {
                reject(new Error("qq.maps.Geocoder unavailable"));
              }
            } catch (e) {
              reject(e);
            }
          };
          const s = document.createElement("script");
          s.src = `https://map.qq.com/api/js?v=2.exp&libraries=convertor,geocoder&key=${
            window.TX_MAP_KEY || ""
          }&callback=${cb}`;
          s.onerror = (e) => {
            delete window[cb];
            reject(e);
          };
          document.head.appendChild(s);
        });
      }
    }
    const pinStyle = new TMapRef.MultiMarker({
      map,
      styles: {
        temp: new TMapRef.MarkerStyle({
          width: 26,
          height: 36,
          src: photoIcon,
          anchor: { x: 13, y: 34 },
        }),
      },
      geometries: [],
    });

    const suggestBox = box.querySelector("#tm-suggest");
    suggestLoadingEl = box.querySelector("#tm-loading");
    suggestLoadingEl = box.querySelector("#tm-loading");
    const renderSuggest = (items) => {
      if (!items || !items.length) {
        suggestBox.style.display = "none";
        suggestBox.innerHTML = "";
        return;
      }
      suggestBox.innerHTML = items
        .map(
          (it, idx) =>
            `<div data-idx="${idx}" style="padding:6px 10px;cursor:pointer;border-bottom:1px solid #f0f0f0;">${
              it.display_name || it.title || it.name
            }</div>`
        )
        .join("");
      suggestBox.style.display = "block";
      suggestBox.querySelectorAll("[data-idx]").forEach((el) => {
        el.onclick = () => {
          const i = Number(el.getAttribute("data-idx"));
          const it = items[i];
          if (it) {
            input.value = it.display_name || it.title || it.name || "";
            suggestBox.style.display = "none";
            suggestBox.innerHTML = "";
            const loc =
              it.location ||
              (it.lat && it.lng
                ? { lat: it.lat, lng: it.lng }
                : it.lat && it.lon
                ? { lat: parseFloat(it.lat), lng: parseFloat(it.lon) }
                : null);
            if (loc) {
              map.setCenter(new TMapRef.LatLng(loc.lat, loc.lng));
              map.zoomTo(16);
              pinStyle.setGeometries([
                {
                  id: "search",
                  styleId: "temp",
                  position: new TMapRef.LatLng(loc.lat, loc.lng),
                },
              ]);
            }
          }
        };
      });
    };

    const input = box.querySelector("#tm-addr");
    let suggestTimer = null;
    const fetchSuggest = (q) => {
      // 使用 OSM Nominatim 作为模糊搜索
      const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(
        q
      )}&format=json&limit=6&addressdetails=0`;
      return fetch(url, {
        headers: { "User-Agent": "userscript-geocoder" },
      }).then((r) => r.json());
    };

    const locate = () => {
      const addr = input.value.trim();
      if (!addr) return;
      lastSuggestQ = addr;
      const doGeocode = geocoder
        ? () =>
            geocoder
              .getLocation({ address: addr })
              .then((res) => res.result?.location)
        : () =>
            geocoderPromise
              .then((geo) => {
                return new Promise((resolve, reject) => {
                  geo.setComplete((res) => {
                    const loc = res.detail && res.detail.location;
                    resolve(loc);
                  });
                  geo.setError((e) => reject(e));
                  geo.getLocation(addr);
                });
              })
              .catch(() => {
                // QQ geocoder无权限则回退到 OSM Nominatim
                return fetch(
                  `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(
                    addr
                  )}&format=json&limit=1`,
                  {
                    headers: { "User-Agent": "userscript-geocoder" },
                  }
                )
                  .then((r) => r.json())
                  .then((j) =>
                    j && j[0]
                      ? { lat: parseFloat(j[0].lat), lng: parseFloat(j[0].lon) }
                      : null
                  );
              });
      doGeocode()
        .then((loc) => {
          if (!loc) return alert("未找到地址");
          map.setCenter(new TMapRef.LatLng(loc.lat, loc.lng));
          map.zoomTo(16);
          pinStyle.setGeometries([
            {
              id: "search",
              styleId: "temp",
              position: new TMapRef.LatLng(loc.lat, loc.lng),
            },
          ]);
        })
        .catch((e) => {
          tmLog("geocode failed", e);
          alert("搜索失败");
        });
    };
    box.querySelector("#tm-go").onclick = locate;
    input.addEventListener("keydown", (e) => {
      if (e.key === "Enter") {
        suggestBox.style.display = "none";
        suggestBox.innerHTML = "";
        locate();
      }
    });
    input.addEventListener("input", () => {
      const val = input.value.trim();
      if (!val) {
        renderSuggest([]);
        return;
      }
      if (suggestTimer) clearTimeout(suggestTimer);
      suggestTimer = setTimeout(() => {
        const token = ++lastSuggestToken;
        lastSuggestQ = val;
        if (suggestLoadingEl) suggestLoadingEl.style.display = "inline-block";
        fetchSuggest(val)
          .then((list) => {
            if (token !== lastSuggestToken || val !== lastSuggestQ) return;
            renderSuggest(list || []);
          })
          .catch(() => {
            if (token !== lastSuggestToken) return;
            renderSuggest([]);
          })
          .finally(() => {
            if (token === lastSuggestToken && suggestLoadingEl)
              suggestLoadingEl.style.display = "none";
          });
      }, 300);
    });
    input.addEventListener("focus", () => {
      if (suggestBox.innerHTML.trim()) suggestBox.style.display = "block";
    });
    document.addEventListener("click", (e) => {
      if (!box.contains(e.target)) {
        renderSuggest([]);
      }
    });
  };

  // 6) 主流程
  wait()
    .then((TMap) => {
      tmLog("TMap ready; TX_MAP_KEY =", window.TX_MAP_KEY);
      TMapRef = TMap;

      // wrap Map
      if (TMap.Map && !TMap.Map.__TM_WRAPPED) {
        const RawMap = TMap.Map;
        const WrappedMap = function (...args) {
          const map = new RawMap(...args);
          captureMap(map);
          return map;
        };
        WrappedMap.prototype = RawMap.prototype;
        Object.setPrototypeOf(WrappedMap, RawMap);
        WrappedMap.__TM_WRAPPED = true;
        TMap.Map = WrappedMap;
        tmLog("wrapped TMap.Map (wait)");
      }

      // wrap MarkerCluster
      if (TMap.MarkerCluster && !TMap.MarkerCluster.__TM_WRAPPED) {
        const RawMC = TMap.MarkerCluster;
        const mcAdd = RawMC && RawMC.prototype && RawMC.prototype.add;
        const WrappedMC = function (opts = {}) {
          // 仅在 map 缺失时回退到 firstMap
          if (!opts.map && firstMap) opts.map = firstMap;
          const inst = new RawMC(opts);
          if (opts?.map) captureMap(opts.map);
          if (!firstCluster) {
            firstCluster = inst;
            tmLog("captured MarkerCluster (wait)");
          }
          return inst;
        };
        WrappedMC.prototype = RawMC.prototype;
        Object.setPrototypeOf(WrappedMC, RawMC);
        WrappedMC.__TM_WRAPPED = true;
        TMap.MarkerCluster = WrappedMC;
        if (mcAdd) {
          RawMC.prototype.add = function (geos = []) {
            const patched = geos.map((g) => {
              const p = g.properties || {};
              const hasPhoto =
                p.count > 0 ||
                p.url ||
                p.photos?.length > 0 ||
                p.images?.length > 0 ||
                p.imgUrl ||
                photoMeta.get(`${g.position?.lat},${g.position?.lng}`)
                  ?.photoCount > 0;
              // 仅入队行政区反查,不再绘制点高亮
              if (hasPhoto && g.position) {
                if (adminQueue.length < 300) {
                  adminQueue.push({
                    lat: g.position.lat,
                    lng: g.position.lng,
                    count: p.count || 1,
                  });
                }
              }
              return hasPhoto ? { ...g, styleId: "photoMarker" } : g;
            });
            return mcAdd.call(this, patched);
          };
        }
        tmLog("wrapped MarkerCluster (wait)");
      }

      // hook Map prototype以捕获已有实例的后续调用
      if (TMap.Map && TMap.Map.prototype && !TMap.Map.prototype.__TM_HOOKED) {
        const mp = TMap.Map.prototype;
        const hook = (name) => {
          const orig = mp[name];
          if (typeof orig === "function") {
            mp[name] = function (...args) {
              captureMap(this);
              return orig.apply(this, args);
            };
          }
        };
        ["setCenter", "easeTo", "setZoom", "zoomTo", "panTo"].forEach(hook);
        mp.__TM_HOOKED = true;
        tmLog("hooked Map prototype methods");
      }

      // hook MarkerCluster prototype以捕获已有实例的 add 调用(仅入队行政区,无点高亮)
      if (
        TMap.MarkerCluster &&
        TMap.MarkerCluster.prototype &&
        !TMap.MarkerCluster.prototype.__TM_HOOKED
      ) {
        const mcp = TMap.MarkerCluster.prototype;
        const origAddProto = mcp.add;
        if (typeof origAddProto === "function") {
          mcp.add = function (geos = []) {
            if (!firstCluster) {
              firstCluster = this;
              tmLog("captured MarkerCluster via proto add");
            }
            const patched = geos.map((g) => {
              const p = g.properties || {};
              const hasPhoto =
                p.count > 0 ||
                p.url ||
                p.photos?.length > 0 ||
                p.images?.length > 0 ||
                p.imgUrl ||
                photoMeta.get(`${g.position?.lat},${g.position?.lng}`)
                  ?.photoCount > 0;
              if (hasPhoto && g.position) {
                if (adminQueue.length < 300) {
                  adminQueue.push({
                    lat: g.position.lat,
                    lng: g.position.lng,
                    count: p.count || 1,
                  });
                }
              }
              return hasPhoto ? { ...g, styleId: "photoMarker" } : g;
            });
            return origAddProto.call(this, patched);
          };
        }
        mcp.__TM_HOOKED = true;
        tmLog("hooked MarkerCluster prototype add");
      }

      // 自定义样式
      const getStyles = () => ({
        default: new TMapRef.MarkerStyle({
          width: 24,
          height: 35,
          src: photoIcon,
          anchor: { x: 12, y: 32 },
        }),
        photoMarker: new TMapRef.MarkerStyle({
          width: 26,
          height: 36,
          src: photoIcon,
          anchor: { x: 13, y: 34 },
          color: "#ffcc00",
          strokeColor: "#ff8800",
          strokeWidth: 2,
        }),
      });

      // hook MultiMarker,合并样式并高亮(已禁用点高亮,仅入队行政区)
      const RawMultiMarker = TMap.MultiMarker;
      if (RawMultiMarker && !RawMultiMarker.__TM_WRAPPED) {
        const WrappedMM = function (opts) {
          if (!firstMap && opts?.map) firstMap = opts.map;
          if (opts?.styles && opts?.geometries) {
            opts.styles = Object.assign(getStyles(), opts.styles);
            opts.geometries = opts.geometries.map((g) => {
              const p = g.properties || {};
              const key = `${g.position?.lat},${g.position?.lng}`;
              const meta = photoMeta.get(key);
              const hasPhoto =
                p.photoCount > 0 ||
                p.photos?.length > 0 ||
                p.images?.length > 0 ||
                p.imgUrl ||
                meta?.photoCount > 0;
              if (hasPhoto && g.position) {
                if (adminQueue.length < 300) {
                  const item = {
                    lat: g.position.lat,
                    lng: g.position.lng,
                    count: p.count || 1,
                  };
                  const cellKey = `${item.lat.toFixed(3)},${item.lng.toFixed(
                    3
                  )}`;
                  // 若格网已缓存结果,减少重复入队
                  if (adminCellCache.has(cellKey)) {
                    const cached = adminCellCache.get(cellKey);
                    const existing = adminPolys.get(cached.name);
                    const weight = (existing?.weight || 0) + (item.count || 1);
                    adminPolys.set(cached.name, {
                      id: cached.name,
                      styleId: "region",
                      paths: cached.paths,
                      weight,
                    });
                    enqueueAdminDraw();
                    return { ...g, styleId: "photoMarker" };
                  }
                  // 可视范围内的优先处理
                  if (adminVisibleFirst(item.lat, item.lng)) {
                    adminQueue.unshift(item);
                  } else {
                    adminQueue.push(item);
                  }
                }
              }
              return hasPhoto ? { ...g, styleId: "photoMarker" } : g;
            });
          }
          return new RawMultiMarker(opts);
        };
        WrappedMM.__TM_WRAPPED = true;
        WrappedMM.prototype = RawMultiMarker.prototype;
        Object.setPrototypeOf(WrappedMM, RawMultiMarker);
        TMap.MultiMarker = WrappedMM;
      }

      // 自动注入搜索框
      const timer = setInterval(() => {
        if (firstMap) {
          clearInterval(timer);
          addSearchBox(firstMap);
        }
      }, 500);

      // 周期扫描 window,兜底抓 map(防止业务先解构了 Map 类)
      const duckScan = () => {
        if (firstMap) return;
        scanAttempts += 1;
        try {
          const values = Object.values(window);
          for (const v of values) {
            if (
              v &&
              typeof v === "object" &&
              typeof v.getCenter === "function" &&
              typeof v.setCenter === "function" &&
              typeof v.zoomTo === "function"
            ) {
              captureMap(v);
              addSearchBox(firstMap);
              tmLog("captured map via duck-scan");
              return;
            }
          }
        } catch (_) {}
        if (scanAttempts > 30) clearInterval(scanTimer);
      };
      const scanTimer = setInterval(duckScan, 500);

      // 控制台入口
      const runEnhance = () => {
        if (!firstMap) {
          tmLog("manual run skipped: map not ready");
          return;
        }
        tmLog("manual run: addSearchBox");
        addSearchBox(firstMap);
      };
      window.TM_RUN = runEnhance;
      window.TM_LOG = () => {
        tmLog("status", {
          map: !!firstMap,
          cluster: !!firstCluster,
          TMapWrapped: !!(TMapRef && TMapRef.Map && TMapRef.Map.__TM_WRAPPED),
          MCWrapped: !!(
            TMapRef &&
            TMapRef.MarkerCluster &&
            TMapRef.MarkerCluster.__TM_WRAPPED
          ),
          TX_KEY: window.TX_MAP_KEY,
        });
      };
      window.TM_SET_MAP = (m) => {
        captureMap(m);
        addSearchBox(firstMap);
      };
      window.TM_SCAN = () => duckScan();

      // 监控容器尝试抓实例(若 SDK 挂 _tmap_instance)
      (() => {
        const tryGrab = () => {
          const el = document.getElementById("map-container");
          if (el && el._tmap_instance) {
            captureMap(el._tmap_instance);
            tmLog("captured map from container _tmap_instance");
            addSearchBox(firstMap);
            return true;
          }
          return false;
        };
        const obs = new MutationObserver(() => {
          if (tryGrab()) obs.disconnect();
        });
        obs.observe(document.documentElement, {
          childList: true,
          subtree: true,
        });
        setTimeout(() => obs.disconnect(), 15000);
        setTimeout(tryGrab, 2000);
      })();

      // 行政区反查与渲染(bbox 近似)
      const processAdmin = () => {
        return;
        if (!adminQueue.length || !firstMap || !TMapRef) return;
        // 可视区域优先:取出第一个可视的,若无则取队首
        let idx = adminQueue.findIndex((t) => adminVisibleFirst(t.lat, t.lng));
        if (idx < 0) idx = 0;
        const task = adminQueue.splice(idx, 1)[0];
        if (!task) return;
        const { lat, lng, count } = task;
        const cellKey = `${lat.toFixed(3)},${lng.toFixed(3)}`;
        // 若格网已缓存,直接复用
        const cacheHit = adminCellCache.get(cellKey);
        if (cacheHit && cacheHit.name && cacheHit.paths) {
          const existing = adminPolys.get(cacheHit.name);
          const weight = (existing?.weight || 0) + (count || 1);
          adminPolys.set(cacheHit.name, {
            id: cacheHit.name,
            styleId: "region",
            paths: cacheHit.paths,
            weight,
          });
          enqueueAdminDraw();
          tmLog("admin heat cache hit", {
            name: cacheHit.name,
            weight,
            total: adminPolys.size,
          });
          return;
        }
        // 若已有同名行政区且包含当前点,直接复用
        for (const [name, info] of adminRegionCache.entries()) {
          if (latLngInBbox(lat, lng, info.bbox)) {
            const existing = adminPolys.get(name);
            const weight = (existing?.weight || 0) + (count || 1);
            adminPolys.set(name, {
              id: name,
              styleId: "region",
              paths: info.paths,
              weight,
            });
            adminCellCache.set(cellKey, { name, paths: info.paths });
            enqueueAdminDraw();
            tmLog("admin heat region reuse", {
              name,
              weight,
              total: adminPolys.size,
            });
            return;
          }
        }
        const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&zoom=10&addressdetails=1&polygon_geojson=1`;
        const toPaths = (geojson) => {
          if (!geojson) return null;
          const coords =
            geojson.type === "MultiPolygon"
              ? geojson.coordinates?.[0]?.[0]
              : geojson.type === "Polygon"
              ? geojson.coordinates?.[0]
              : null;
          if (!coords || !coords.length) return null;
          // 下采样,减少点数
          const MAX_POINTS = 500;
          const step = Math.max(1, Math.ceil(coords.length / MAX_POINTS));
          const trimmed = coords.filter((_, i) => i % step === 0);
          return [
            trimmed.map(
              (c) =>
                new TMapRef.LatLng(parseFloat(c[1]) || 0, parseFloat(c[0]) || 0)
            ),
          ];
        };
        fetch(url, { headers: { "User-Agent": "userscript-admin" } })
          .then((r) => r.json())
          .then((j) => {
            // 只保留到市级,县/区若存在城市则归并到城市
            const cityLike =
              j.address?.city || j.address?.town || j.address?.village;
            const provinceLike = j.address?.state || j.address?.region;
            const name = cityLike || provinceLike;
            const bbox = j.boundingbox;
            if (!name || !bbox || bbox.length < 4) return;
            const south = parseFloat(bbox[0]);
            const north = parseFloat(bbox[1]);
            const west = parseFloat(bbox[2]);
            const east = parseFloat(bbox[3]);
            if (
              !Number.isFinite(south) ||
              !Number.isFinite(north) ||
              !Number.isFinite(west) ||
              !Number.isFinite(east)
            )
              return;
            if (north - south > 10 || east - west > 10) return;
            if (!adminLayer) {
              adminLayer = new TMapRef.MultiPolygon({
                map: firstMap,
                styles: {
                  region: new TMapRef.PolygonStyle({
                    color: "rgba(255,153,0,0.12)",
                    showBorder: true,
                    borderColor: "rgba(255,102,0,0.5)",
                    borderWidth: 2,
                  }),
                },
                geometries: [],
              });
              tmLog("admin layer created");
            }
            const path = [
              new TMapRef.LatLng(south, west),
              new TMapRef.LatLng(south, east),
              new TMapRef.LatLng(north, east),
              new TMapRef.LatLng(north, west),
            ];
            // 优先使用真实多边形,缺失时退回 bbox
            const geoPaths = toPaths(j.geojson) || [path];
            const id = name;
            const existing = adminPolys.get(id);
            const weight = (existing?.weight || 0) + (count || 1);
            adminPolys.set(id, {
              id,
              styleId: "region",
              paths: geoPaths,
              weight,
            });
            const bboxArr = [south, north, west, east];
            adminCellCache.set(cellKey, { name: id, paths: geoPaths });
            adminRegionCache.set(id, { paths: geoPaths, bbox: bboxArr });
            adminCacheTrim(adminCellCache, 600);
            adminCacheTrim(adminRegionCache, 300);
            tmLog("admin heat queue consumed", {
              name: id,
              weight,
              total: adminPolys.size,
            });
            enqueueAdminDraw();
          })
          .catch(() => {});
      };
      setInterval(processAdmin, 1000);
    })
    .catch((err) => {
      tmLog("TMap wait failed:", err);
    });
})();

将这个脚本放到油猴里面就可以了。

javascript飞牛油猴
分享:

// End of article

/* Thanks for reading */