소스 검색

1、标题修改,张江高新区青浦园产业地图。
2、左侧青浦区应是青浦园。
3、地块入住企业有歧义(已替换最新数据)。
4、是否可显示道路,底图白色,参照市科委查询范围的图(接入了矢量注记图层)。
5、左侧框缩小,比例尺自己调整(这个暂时在主题配置弹窗里面控制,现在禁止用户操作)。
6、三种底图支持动态换肤和主题颜色、背景、文字等配置。

DESKTOP-6LTVLN7\Liumouren 1 주 전
부모
커밋
6c6d7935ce
12개의 변경된 파일1179개의 추가작업 그리고 114개의 파일을 삭제
  1. 3 0
      .vscode/settings.json
  2. 1 1
      docs/ARCHITECTURE.md
  3. 3 3
      docs/THEME.md
  4. BIN
      docs/青浦区产业地块信息表.xlsx
  5. 105 1
      package-lock.json
  6. 4 2
      package.json
  7. 56 0
      scripts/build-park-excel-attrs.js
  8. 74 3
      src/assets/global.css
  9. 425 31
      src/components/map/appMap.vue
  10. 96 20
      src/utils/uiTheme.js
  11. 163 0
      src/views/parkExcelAttrs.js
  12. 249 53
      src/views/qpjyj.vue

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "postman.settings.dotenv-detection-notification-visibility": false
+}

+ 1 - 1
docs/ARCHITECTURE.md

@@ -274,7 +274,7 @@ Industry_map/
 
 ## 12. 昼夜主题与本地配色
 
-昼夜由 **左下角底图** 决定(暗蓝色 → 黑夜,标准版 / 卫星影像 → 白天),**主题配置** 仅编辑 Token;持久化见 **[THEME.md](./THEME.md)**。
+界面三套换肤由 **左下角底图** 决定(标准版政务 → 白天;暗蓝色政务 → 黑夜;卫星影像 → 卫星专用暗色 UI `cyan`,仅叠层、不改影像),**主题配置** 按当前模式编辑 Token;持久化见 **[THEME.md](./THEME.md)**。
 
 ## 13. 关键文件索引
 

+ 3 - 3
docs/THEME.md

@@ -6,9 +6,9 @@
 
 - **默认「黑夜」**:与历史大屏一致的深色半透明面板、浅色文字,适合暗色政务底图。
 - **「白天」**:高对比浅底深字,适合「标准版政务底图」等浅色底图,减轻文字与弹窗看不清的问题。
-- **昼夜与底图联动**:**不单独提供**顶栏昼夜开关。左下角切换底图时自动切换主题——**「暗蓝色政务底图」**(`shmap_blue_web`)对应 **黑夜**,**「标准版政务底图」**、**「卫星影像」** 对应 **白天**。常量见 `src/utils/uiTheme.js` 的 `BASE_MAP_KEY_NIGHT`、`getUiModeForBaseMap`
+- **三套换肤与底图联动**:**不单独提供**顶栏模式开关。左下角切换底图时自动切换主题——**「标准版政务底图」**(`shmap_normal_web`)→ **白天**,**「暗蓝色政务底图」**(`shmap_blue_web`)→ **黑夜**,**「卫星影像」**(`arcgisImagery`)→ **`cyan`(卫星专用暗色 UI)**:仅侧栏、搜索、弹窗、图表等叠层的 CSS Token,**不处理、不染色卫星瓦片**。见 `getUiModeForBaseMap`、`normalizeOverridesByMode`(`day` / `night` / `cyan` 各有一份 `overrides`)
 - **右上角**:仅保留 **主题配置** 按钮(颜色 Token),不再保留独立的昼夜开关。
-- **持久化**:当前模式(`night` / `day`)与 **按昼夜分别存储** 的覆盖项 `overridesByMode` 写入 **`localStorage`**,键名常量:`industry_map_ui_theme_v1`(定义于 `src/utils/uiTheme.js` 的 `UI_THEME_STORAGE_KEY`)。在白天下调主题只写入 `overridesByMode.day`,黑夜写入 `overridesByMode.night`,互不覆盖。
+- **持久化**:当前模式(`day` / `night` / `cyan`)与 **按模式分别存储** 的覆盖项 `overridesByMode` 写入 **`localStorage`**,键名常量:`industry_map_ui_theme_v1`(定义于 `src/utils/uiTheme.js` 的 `UI_THEME_STORAGE_KEY`)。各模式只写入对应键(如 `overridesByMode.cyan`),互不覆盖。
 
 ## 配置流程(面向使用者)
 
@@ -84,7 +84,7 @@
 
 ## 与底图的关系
 
-主题只负责 **UI 叠层**(头、搜索、侧栏、图例、弹窗、ECharts),**不自动切换** Leaflet 底图。浅色底图对应白天模式;微调颜色请用 **主题配置** 或编辑 `overrides`。
+主题只负责 **UI 叠层**(头、搜索、侧栏、图例、弹窗、ECharts),**不自动切换** Leaflet 底图,也 **不改变底图瓦片颜色**。标准版政务 → 白天,暗蓝政务 → 黑夜,卫星 → 卫星暗色 UI(`cyan`);微调颜色请用 **主题配置** 或编辑 `overrides`。
 
 ## 白天模式专项视觉优化(实现说明)
 

BIN
docs/青浦区产业地块信息表.xlsx


+ 105 - 1
package-lock.json

@@ -37,7 +37,8 @@
         "@vue/cli-service": "~5.0.0",
         "less": "^4.1.3",
         "less-loader": "^11.1.0",
-        "vue-template-compiler": "^2.6.14"
+        "vue-template-compiler": "^2.6.14",
+        "xlsx": "^0.18.5"
       }
     },
     "node_modules/@achrinza/node-ipc": {
@@ -4569,6 +4570,15 @@
         "node": ">= 10.0.0"
       }
     },
+    "node_modules/adler-32": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz",
+      "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/ajv": {
       "version": "6.12.6",
       "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
@@ -5131,6 +5141,19 @@
         "node": ">=4"
       }
     },
+    "node_modules/cfb": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz",
+      "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+      "dev": true,
+      "dependencies": {
+        "adler-32": "~1.3.0",
+        "crc-32": "~1.2.0"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/chalk": {
       "version": "2.4.2",
       "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz",
@@ -5358,6 +5381,15 @@
         "node": ">=6"
       }
     },
+    "node_modules/codepage": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
+      "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/color-convert": {
       "version": "1.9.3",
       "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz",
@@ -5622,6 +5654,18 @@
         "node": ">=10"
       }
     },
+    "node_modules/crc-32": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz",
+      "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+      "dev": true,
+      "bin": {
+        "crc32": "bin/crc32.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/cross-spawn": {
       "version": "6.0.5",
       "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-6.0.5.tgz",
@@ -6759,6 +6803,15 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/frac": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz",
+      "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/fraction.js": {
       "version": "4.2.0",
       "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.2.0.tgz",
@@ -10405,6 +10458,18 @@
       "resolved": "https://registry.npmmirror.com/splaytree/-/splaytree-3.1.2.tgz",
       "integrity": "sha512-4OM2BJgC5UzrhVnnJA4BkHKGtjXNzzUfpQjCO8I05xYPsfS/VuQDwjCGGMi8rYQilHEV4j8NBqTFbls/PZEE7A=="
     },
+    "node_modules/ssf": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz",
+      "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+      "dev": true,
+      "dependencies": {
+        "frac": "~1.1.2"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/ssri": {
       "version": "8.0.1",
       "resolved": "https://registry.npmmirror.com/ssri/-/ssri-8.0.1.tgz",
@@ -11599,6 +11664,24 @@
       "resolved": "https://registry.npmmirror.com/wkt-parser/-/wkt-parser-1.3.2.tgz",
       "integrity": "sha512-A26BOOo7sHAagyxG7iuRhnKMO7Q3mEOiOT4oGUmohtN/Li5wameeU4S6f8vWw6NADTVKljBs8bzA8JPQgSEMVQ=="
     },
+    "node_modules/wmf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz",
+      "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/word": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz",
+      "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/wrap-ansi": {
       "version": "7.0.0",
       "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -11678,6 +11761,27 @@
         "@xmldom/xmldom": "^0.8.3"
       }
     },
+    "node_modules/xlsx": {
+      "version": "0.18.5",
+      "resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz",
+      "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+      "dev": true,
+      "dependencies": {
+        "adler-32": "~1.3.0",
+        "cfb": "~1.2.1",
+        "codepage": "~1.15.0",
+        "crc-32": "~1.2.1",
+        "ssf": "~0.11.2",
+        "wmf": "~1.0.1",
+        "word": "~0.3.0"
+      },
+      "bin": {
+        "xlsx": "bin/xlsx.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/y18n": {
       "version": "5.0.8",
       "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",

+ 4 - 2
package.json

@@ -4,7 +4,8 @@
   "private": true,
   "scripts": {
     "serve": "vue-cli-service serve",
-    "build": "vue-cli-service build"
+    "build": "vue-cli-service build",
+    "build:park-excel": "node scripts/build-park-excel-attrs.js"
   },
   "dependencies": {
     "@turf/turf": "^7.1.0",
@@ -36,7 +37,8 @@
     "@vue/cli-service": "~5.0.0",
     "less": "^4.1.3",
     "less-loader": "^11.1.0",
-    "vue-template-compiler": "^2.6.14"
+    "vue-template-compiler": "^2.6.14",
+    "xlsx": "^0.18.5"
   },
   "browserslist": [
     "> 1%",

+ 56 - 0
scripts/build-park-excel-attrs.js

@@ -0,0 +1,56 @@
+/**
+ * 从 docs/青浦区产业地块信息表.xlsx 生成 src/views/parkExcelAttrs.js
+ * 运行: node scripts/build-park-excel-attrs.js
+ */
+const fs = require("fs");
+const path = require("path");
+const XLSX = require("xlsx");
+
+const root = path.join(__dirname, "..");
+const xlsxPath = path.join(root, "docs", "青浦区产业地块信息表.xlsx");
+const outPath = path.join(root, "src", "views", "parkExcelAttrs.js");
+
+const wb = XLSX.readFile(xlsxPath);
+const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], {
+  header: 1,
+  defval: "",
+});
+const dataRows = rows.slice(2).filter((r) => r[0]);
+
+const obj = {};
+for (const r of dataRows) {
+  const name = String(r[0]).trim();
+  const gsgy = Number(r[1]) || 0;
+  const gsqy = Number(r[2]) || 0;
+  const areaRaw = r[7];
+  const area =
+    typeof areaRaw === "number"
+      ? areaRaw
+      : parseFloat(String(areaRaw).replace(/,/g, "")) || 0;
+  obj[name] = {
+    规上工业服务业企业数量: gsgy,
+    高企数量: gsqy,
+    入驻企业数量: gsgy + gsqy,
+    四至边界: {
+      东: String(r[3] || "").trim(),
+      南: String(r[4] || "").trim(),
+      西: String(r[5] || "").trim(),
+      北: String(r[6] || "").trim(),
+    },
+    用地面积公顷: area,
+    备注: String(r[8] || "").trim(),
+  };
+}
+
+const banner = `/**
+ * 甲方《青浦区产业地块信息表.xlsx》解析结果。
+ * 键与 parkDatas 中 properties.name2 一致。
+ * 更新数据请修改 Excel 后执行: node scripts/build-park-excel-attrs.js
+ */
+`;
+
+const body = `export const PARK_EXCEL_ATTRS_BY_NAME = ${JSON.stringify(obj, null, 2)};
+`;
+
+fs.writeFileSync(outPath, banner + body, "utf8");
+console.log("Wrote", outPath, "parks:", Object.keys(obj).length);

+ 74 - 3
src/assets/global.css

@@ -39,10 +39,55 @@
   border: 1px solid var(--ui-dropdown-border);
   box-shadow: 0 8px 32px rgba(0, 0, 0, 0.22);
   border-radius: 4px;
-  margin-top: 10vh !important;
+  margin-top: 4vh !important;
+  max-width: min(1180px, 98vw);
+  width: min(1180px, 98vw) !important;
   backdrop-filter: blur(18px) saturate(1.12);
   -webkit-backdrop-filter: blur(18px) saturate(1.12);
 }
+/* 文字 / 边界取色等:一行最多 4 列,窄屏降为 3 列、2 列 */
+.theme-config-dialog .theme-config-form.theme-config-grid {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 4px 14px;
+  align-items: start;
+}
+@media (max-width: 1100px) {
+  .theme-config-dialog .theme-config-form.theme-config-grid {
+    grid-template-columns: repeat(3, minmax(0, 1fr));
+  }
+}
+@media (max-width: 720px) {
+  .el-dialog.theme-config-dialog {
+    margin-top: 2vh !important;
+    max-width: 96vw !important;
+    width: 96vw !important;
+  }
+  .theme-config-dialog .theme-config-form.theme-config-grid {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+.theme-config-dialog .theme-config-grid-full {
+  grid-column: 1 / -1;
+}
+.theme-config-dialog .theme-config-form.theme-config-grid > .el-form-item {
+  margin-bottom: 8px;
+}
+.theme-config-dialog .theme-config-form.theme-config-grid .el-form-item__content {
+  margin-left: 108px !important;
+}
+.theme-config-dialog .theme-config-form.theme-config-grid .el-form-item__label {
+  width: 108px !important;
+}
+.theme-config-dialog .theme-config-boundary-hint {
+  margin: -2px 0 4px;
+  font-size: 12px;
+  line-height: 1.35;
+  color: var(--ui-text-3);
+}
+.theme-config-dialog .theme-config-form.theme-config-grid .el-divider {
+  margin: 8px 0 4px;
+}
 .theme-config-dialog .el-dialog__header {
   padding: 16px 20px;
   background: transparent;
@@ -61,7 +106,7 @@
   color: var(--ui-main);
 }
 .theme-config-dialog .el-dialog__body {
-  padding: 16px 20px;
+  padding: 12px 18px 14px;
   background: transparent;
   color: var(--ui-text-1);
 }
@@ -77,7 +122,7 @@
   font-size: 13px;
   font-weight: 600;
   color: var(--ui-text-title);
-  margin: 0 0 10px;
+  margin: 0 0 6px;
   letter-spacing: 0.02em;
 }
 .theme-config-dialog .el-divider {
@@ -472,6 +517,32 @@
   font-size: 12px;
 }
 
+/* 园区弹窗:四至边界(东/南/西/北) */
+.mapParkName_box .map-popup-boundary {
+  margin-top: 0.35rem;
+}
+.mapParkName_box .map-popup-boundary-row {
+  display: flex;
+  gap: 0.35rem 0.5rem;
+  align-items: flex-start;
+  padding: 0.2rem 0.35rem 0.1rem;
+  font-size: 12px;
+  line-height: 1.55;
+}
+.mapParkName_box .map-popup-boundary-k {
+  flex: 0 0 1.25rem;
+  color: var(--ui-text-2, var(--ui-popup-accent, #22e9ff));
+  font-weight: 600;
+}
+.mapParkName_box .map-popup-boundary-v {
+  flex: 1;
+  min-width: 0;
+  color: var(--ui-text-1, var(--ui-popup-text, #fff));
+}
+.mapParkName_box .map-popup-excel-remark {
+  margin-top: 0.35rem;
+}
+
 .mapParkName_box .title3 {
   border-bottom: 1px solid var(--ui-main, var(--ui-popup-accent, #22e9ff));
   padding-top: 0.5rem;

+ 425 - 31
src/components/map/appMap.vue

@@ -1,13 +1,171 @@
 <template>
-  <div class="appMapViewer" ref="appMap">
-    <filter id="glow"
-      ><feGaussianBlur stdDeviation="4" result="coloredBlur" /><feMerge
-        ><feMergeNode in="coloredBlur" /><feMergeNode in="SourceGraphic" /></feMerge
-    ></filter>
+  <div class="appMapViewer">
+    <svg
+      class="appMapBoundaryFilters"
+      xmlns="http://www.w3.org/2000/svg"
+      width="0"
+      height="0"
+      aria-hidden="true"
+    >
+      <defs>
+        <filter
+          id="industry-map-boundary-glow-district"
+          x="-60%"
+          y="-60%"
+          width="220%"
+          height="220%"
+          color-interpolation-filters="sRGB"
+        >
+          <feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur" />
+          <feFlood
+            id="industry-map-boundary-flood-district"
+            flood-color="#78FF37"
+            flood-opacity="0.82"
+            result="flood"
+          />
+          <feComposite in="flood" in2="blur" operator="in" result="glow" />
+          <feMerge>
+            <feMergeNode in="glow" />
+            <feMergeNode in="SourceGraphic" />
+          </feMerge>
+        </filter>
+        <filter
+          id="industry-map-boundary-glow-town"
+          x="-60%"
+          y="-60%"
+          width="220%"
+          height="220%"
+          color-interpolation-filters="sRGB"
+        >
+          <feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur" />
+          <feFlood
+            id="industry-map-boundary-flood-town"
+            flood-color="#FFF200"
+            flood-opacity="0.82"
+            result="flood"
+          />
+          <feComposite in="flood" in2="blur" operator="in" result="glow" />
+          <feMerge>
+            <feMergeNode in="glow" />
+            <feMergeNode in="SourceGraphic" />
+          </feMerge>
+        </filter>
+        <filter
+          id="industry-map-boundary-glow-park"
+          x="-60%"
+          y="-60%"
+          width="220%"
+          height="220%"
+          color-interpolation-filters="sRGB"
+        >
+          <feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur" />
+          <feFlood
+            id="industry-map-boundary-flood-park"
+            flood-color="#D860FF"
+            flood-opacity="0.82"
+            result="flood"
+          />
+          <feComposite in="flood" in2="blur" operator="in" result="glow" />
+          <feMerge>
+            <feMergeNode in="glow" />
+            <feMergeNode in="SourceGraphic" />
+          </feMerge>
+        </filter>
+      </defs>
+    </svg>
+    <div class="appMapLeaflet" ref="appMap"></div>
   </div>
 </template>
 <script>
 import publicFun from "@/utils/publicFunction.js";
+
+/**
+ * 地图缩放上限:高于各底图 maxNativeZoom 时由 Leaflet 拉伸最后一级瓦片。
+ * 取 22 以便在原生 19 级外仍可多级放大视图(略糊但不断图)。
+ */
+const MAP_MIN_ZOOM = 9;
+const MAP_MAX_ZOOM = 22;
+
+/**
+ * 暗蓝色政务底图 shmap_blue_web:原生最高 z(青浦园心附近瓦片 z=19 为 PNG 200,z=20 为 404)。
+ */
+const SHMAP_BLUE_WEB_MAX_NATIVE_ZOOM = 19;
+
+/**
+ * 标准版政务底图 shmap_normal_web:与 blue 同测,最高至 19。
+ */
+const SHMAP_NORMAL_WEB_MAX_NATIVE_ZOOM = 19;
+
+/**
+ * Esri World Imagery:REST 元数据 lods 可到 23,本项目业务实测有效影像至 19;再高多为占位/拉伸,按实测设为 19。
+ */
+const ARCGIS_WORLD_IMAGERY_MAX_NATIVE_ZOOM = 19;
+
+/** 天地图路网 WMTS(与政务底图相同方式拼接 proxyToken) */
+const ROAD_WMTS_PROXY_BASE =
+  "http://121.43.55.7:10011/proxy/?servertype=tdt_yxzj&proxyToken=";
+
+/** 路网瓦片原生级别上限(可按服务调整) */
+const ROAD_WMTS_MAX_NATIVE_ZOOM = 18;
+
+/** 若为 true,则 TILEROW 按 TMS 与 XYZ 互转(部分天地图 WMTS 需要) */
+const ROAD_WMTS_TMS_ROW = false;
+
+/** 与 qpjyj 图层控制中名称一致,无颜色配置 */
+const ROAD_ANNOTATION_LAYER_NAME = "影像注记图层";
+
+/**
+ * Leaflet  XYZ 瓦片 → OGC WMTS GetTile(与 Cesium WebMapTileServiceImageryProvider 同类参数)
+ */
+function createRoadWmtsLayer(L, baseUrlWithToken, options) {
+  const Layer = L.TileLayer.extend({
+    initialize: function (baseUrl, opts) {
+      this._roadBaseUrl = baseUrl;
+      L.TileLayer.prototype.initialize.call(this, "", opts);
+    },
+    getTileUrl: function (coords) {
+      const z = coords.z;
+      const x = coords.x;
+      const y = ROAD_WMTS_TMS_ROW ? Math.pow(2, z) - 1 - coords.y : coords.y;
+      return (
+        this._roadBaseUrl +
+        "&SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&STYLE=default&FORMAT=tiles" +
+        "&TILEMATRIX=" +
+        z +
+        "&TILEROW=" +
+        y +
+        "&TILECOL=" +
+        x
+      );
+    },
+  });
+  return new Layer(baseUrlWithToken, options);
+}
+
+const BOUNDARY_LAYER_LINE_CSS = {
+  区县行政边界: "--ui-layer-boundary-district",
+  乡镇行政边界: "--ui-layer-boundary-town",
+  园区范围边界: "--ui-layer-boundary-park",
+};
+
+const BOUNDARY_GLOW_FILTER = {
+  区县行政边界: "url(#industry-map-boundary-glow-district)",
+  乡镇行政边界: "url(#industry-map-boundary-glow-town)",
+  园区范围边界: "url(#industry-map-boundary-glow-park)",
+};
+
+const BOUNDARY_FLOOD_ID = {
+  区县行政边界: "industry-map-boundary-flood-district",
+  乡镇行政边界: "industry-map-boundary-flood-town",
+  园区范围边界: "industry-map-boundary-flood-park",
+};
+
+const DEFAULT_BOUNDARY_LINE = {
+  区县行政边界: "#78FF37",
+  乡镇行政边界: "#FFF200",
+  园区范围边界: "#D860FF",
+};
+
 export default {
   name: "appMap",
   data() {
@@ -57,7 +215,11 @@ export default {
           url:
             "https://szlszxdt.qpservice.org.cn/internal_map//tile/{z}/{y}/{x}?servertype=shmap_blue_web&proxyToken=" +
             localStorage.getItem("TOKEN"),
-          options: {},
+          options: {
+            minZoom: MAP_MIN_ZOOM,
+            maxZoom: MAP_MAX_ZOOM,
+            maxNativeZoom: SHMAP_BLUE_WEB_MAX_NATIVE_ZOOM,
+          },
         },
         // shmap_grey_web: {
         //   name: "浅灰色底图",
@@ -73,7 +235,11 @@ export default {
           url:
             "https://szlszxdt.qpservice.org.cn/internal_map//tile/{z}/{y}/{x}?servertype=shmap_normal_web&proxyToken=" +
             localStorage.getItem("TOKEN"),
-          options: {},
+          options: {
+            minZoom: MAP_MIN_ZOOM,
+            maxZoom: MAP_MAX_ZOOM,
+            maxNativeZoom: SHMAP_NORMAL_WEB_MAX_NATIVE_ZOOM,
+          },
         },
         // shmap_base_web: {
         //   name: "常规色底图",
@@ -89,13 +255,19 @@ export default {
           type: "tile",
           url:
             "https://server.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
-          options: {},
+          options: {
+            minZoom: MAP_MIN_ZOOM,
+            maxZoom: MAP_MAX_ZOOM,
+            maxNativeZoom: ARCGIS_WORLD_IMAGERY_MAX_NATIVE_ZOOM,
+          },
         },
       },
       // 当前激活的底图
       activeBaseMap: "shmap_blue_web",
       // 底图图层实例
       baseMapLayers: {},
+      /** 路网 WMTS,固定叠在底图之上、图层控制矢量之下(见 pane z-index) */
+      roadOverlayLayer: null,
       map: "",
       layers: "",
       marker: "",
@@ -112,7 +284,7 @@ export default {
         // 边界
         区县行政边界: {},
         乡镇行政边界: {},
-        所有园区范围边界: {},
+        园区范围边界: {},
         // 地理元素
         水系: null,
         绿地: null,
@@ -120,11 +292,6 @@ export default {
         城市路网: null,
         交通枢纽: null,
       },
-      layerColor: {
-        区县行政边界: "#67C23A",
-        乡镇行政边界: "#E6A23C",
-        所有园区范围边界: "#409EFF",
-      },
       // 图例类型配置
       legendsTypes: {
         医药器械制造: {
@@ -167,18 +334,109 @@ export default {
   props: [],
   destroy() {},
   methods: {
+    getBoundaryLineColor(title) {
+      const key = BOUNDARY_LAYER_LINE_CSS[title];
+      if (!key) return DEFAULT_BOUNDARY_LINE[title] || "#409EFF";
+      const raw = getComputedStyle(document.documentElement).getPropertyValue(key).trim();
+      if (raw) return raw;
+      return DEFAULT_BOUNDARY_LINE[title] || "#409EFF";
+    },
+    parseCssColorToRgb(css) {
+      if (css == null) return null;
+      const s = String(css).trim();
+      if (!s) return null;
+      if (s.startsWith("#")) {
+        let h = s.slice(1);
+        if (h.length === 3) {
+          h = h
+            .split("")
+            .map((c) => c + c)
+            .join("");
+        }
+        if (h.length >= 6) {
+          const n = parseInt(h.slice(0, 6), 16);
+          if (!Number.isNaN(n)) {
+            return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
+          }
+        }
+      }
+      const m = s.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
+      if (m) {
+        return { r: +m[1], g: +m[2], b: +m[3] };
+      }
+      return null;
+    },
+    /** feFlood 使用 rgb();与线条同色,内发光随线条色联动 */
+    boundaryLineToFloodColor(css) {
+      const rgb = this.parseCssColorToRgb(css);
+      if (!rgb) return "#409EFF";
+      return `rgb(${rgb.r},${rgb.g},${rgb.b})`;
+    },
+    boundaryFillColor(css) {
+      const rgb = this.parseCssColorToRgb(css);
+      if (!rgb) return "rgba(64, 158, 255, 0.07)";
+      return `rgba(${rgb.r},${rgb.g},${rgb.b},0.07)`;
+    },
+    updateBoundaryGlowFloods() {
+      const titles = Object.keys(BOUNDARY_FLOOD_ID);
+      titles.forEach((title) => {
+        const fid = BOUNDARY_FLOOD_ID[title];
+        const el = typeof document !== "undefined" ? document.getElementById(fid) : null;
+        if (!el) return;
+        const line = this.getBoundaryLineColor(title);
+        el.setAttribute("flood-color", this.boundaryLineToFloodColor(line));
+      });
+    },
+    /** 主题或底图模式切换后刷新边界样式与标签颜色 */
+    refreshBoundaryPolygonStyles() {
+      this.updateBoundaryGlowFloods();
+      const boundaryTitles = Object.keys(BOUNDARY_LAYER_LINE_CSS);
+      boundaryTitles.forEach((title) => {
+        const color = this.getBoundaryLineColor(title);
+        const fill = this.boundaryFillColor(color);
+        const flt = BOUNDARY_GLOW_FILTER[title];
+        const polys = this.layerControlPolygon[title];
+        if (!polys || typeof polys !== "object") return;
+        Object.keys(polys).forEach((key) => {
+          const polygon = polys[key];
+          if (polygon && polygon.setStyle) {
+            polygon.setStyle({
+              color,
+              weight: 2,
+              fillColor: fill,
+              opacity: 1,
+              fillOpacity: 0.7,
+              filter: flt,
+            });
+          }
+          const marker = this.markers["polygon"] && this.markers["polygon"][key];
+          if (marker && marker.options && marker.options.icon && key !== "青浦区") {
+            marker.setIcon(
+              L.divIcon({
+                html: `<div>
+                <div class="title" style="color:${color};">${key}</div>
+                </div>`,
+                className: "mapParkName2",
+                iconSize: 25,
+              })
+            );
+          }
+        });
+      });
+    },
     mapInit() {
       let center_ = [31.146179026117824, 121.11121627562943];
       this.map = L.map(this.$refs.appMap, {
         center: center_,
         zoom: 15,
-        minZoom: 9,
-        maxZoom: 18,
+        minZoom: MAP_MIN_ZOOM,
+        maxZoom: MAP_MAX_ZOOM,
         zoomControl: true,
         attributionControl: false,
         doubleClickZoom: false,
       });
-      // 加载初始底图
+      this.initLeafletPanes();
+      // 加载初始底图(末尾会挂上路网叠加层)
       this.loadBaseMap(this.activeBaseMap);
       // 移除缩放控件
       this.map.removeControl(this.map.zoomControl);
@@ -213,6 +471,45 @@ export default {
       });
       // emit 事件
       this.$emit("mapInit", true);
+      this.$nextTick(() => {
+        this.updateBoundaryGlowFloods();
+      });
+    },
+
+    /** 图层层级:tilePane 默认 200;底图 200;路网 350;矢量/GeoJSON 默认 overlayPane 400 */
+    initLeafletPanes() {
+      if (!this.map || this._leafletPanesInited) return;
+      this._leafletPanesInited = true;
+      this.map.createPane("basemap");
+      this.map.getPane("basemap").style.zIndex = 200;
+      this.map.createPane("roadOverlay");
+      this.map.getPane("roadOverlay").style.zIndex = 350;
+    },
+
+    roadWmtsBaseUrl() {
+      return ROAD_WMTS_PROXY_BASE + (localStorage.getItem("TOKEN") || "");
+    },
+
+    /** 创建/更新路网 WMTS;显隐仅由 changeLayerControl(影像注记图层) 控制,此处不强制 addTo(避免关闭后被切换底图再次打开) */
+    ensureRoadOverlayLayer() {
+      if (!this.map) return;
+      const base = this.roadWmtsBaseUrl();
+      if (this.roadOverlayLayer) {
+        this.roadOverlayLayer._roadBaseUrl = base;
+        return;
+      }
+      this.roadOverlayLayer = createRoadWmtsLayer(L, base, {
+        pane: "roadOverlay",
+        minZoom: MAP_MIN_ZOOM,
+        maxZoom: MAP_MAX_ZOOM,
+        maxNativeZoom: ROAD_WMTS_MAX_NATIVE_ZOOM,
+        opacity: 1,
+      });
+      this.roadOverlayLayer.on("error", (e) => {
+        console.warn("路网 WMTS 瓦片加载异常:", e && e.tile ? e.tile.src : e);
+      });
+      /* 默认与侧栏「影像注记图层」开关一致为开;显隐仍以 changeLayerControl 为准 */
+      this.roadOverlayLayer.addTo(this.map);
     },
 
     // 加载底图
@@ -238,11 +535,21 @@ export default {
             });
             break;
           case "tile":
-            layer = L.tileLayer(service.url, service.options);
+            layer = L.tileLayer(service.url, {
+              pane: "basemap",
+              minZoom: MAP_MIN_ZOOM,
+              maxZoom: MAP_MAX_ZOOM,
+              ...(service.options || {}),
+            });
             break;
           // 支持更多底图类型...
           case "wms":
-            layer = L.tileLayer.wms(service.url, service.options);
+            layer = L.tileLayer.wms(service.url, {
+              pane: "basemap",
+              minZoom: MAP_MIN_ZOOM,
+              maxZoom: MAP_MAX_ZOOM,
+              ...(service.options || {}),
+            });
             break;
           default:
             console.error("不支持的底图类型:", service.type);
@@ -264,6 +571,8 @@ export default {
           console.error(`${service.name} 加载失败:`, err);
           // 可以添加用户提示
         });
+
+        this.ensureRoadOverlayLayer();
       } catch (error) {
         console.error("加载底图失败:", error);
       }
@@ -369,6 +678,39 @@ export default {
         }
       }
     },
+    escapeHtml(str) {
+      if (str == null || str === undefined) return "";
+      return String(str)
+        .replace(/&/g, "&amp;")
+        .replace(/</g, "&lt;")
+        .replace(/>/g, "&gt;")
+        .replace(/"/g, "&quot;");
+    },
+    buildParkBoundaryPopupHtml(data) {
+      const b = data["四至边界"];
+      if (!b || typeof b !== "object") return "";
+      const keys = ["东", "南", "西", "北"];
+      const rows = keys
+        .map((k) => {
+          const v = (b[k] != null && String(b[k]).trim()) || "";
+          if (!v) return "";
+          return `<div class="map-popup-boundary-row"><span class="map-popup-boundary-k">${k}</span><span class="map-popup-boundary-v">${this.escapeHtml(
+            v
+          )}</span></div>`;
+        })
+        .filter(Boolean)
+        .join("");
+      if (!rows) return "";
+      return `<div class="map-popup-boundary map-popup-intro"><div class="title2">四至边界</div>${rows}</div>`;
+    },
+    buildParkExcelRemarkPopupHtml(data) {
+      const raw = data.properties && data.properties.excel备注;
+      const r = raw != null && String(raw).trim();
+      if (!r) return "";
+      return `<div class="map-popup-intro map-popup-excel-remark"><div class="title2">备注</div><div class="map-popup-intro-text">${this.escapeHtml(
+        raw
+      )}</div></div>`;
+    },
 
     // 定位到当前位置
     panToLocation(item, zoom) {
@@ -392,20 +734,21 @@ export default {
           setTimeout(() => {
             // 经纬度位置清洗
             let polygon = L.geoJSON(data).addTo(this.map);
+            const lineColor = this.getBoundaryLineColor(title);
             polygon.setStyle({
-              color: this.layerColor[title],
+              color: lineColor,
               weight: 2,
-              fillColor: this.layerColor[title] + "12",
+              fillColor: this.boundaryFillColor(lineColor),
               opacity: 1,
               fillOpacity: 0.7,
-              filter: "url(#glow)", // 应用滤镜
+              filter: BOUNDARY_GLOW_FILTER[title] || BOUNDARY_GLOW_FILTER["园区范围边界"],
             });
             //  同时显示园区文字提示
             if (data.title != "青浦区") {
               let marker = L.marker(polygon.getBounds().getCenter(), {
                 icon: L.divIcon({
                   html: `<div>
-                <div class="title" style="color:${this.layerColor[title]};">${data.title}</div>
+                <div class="title" style="color:${this.getBoundaryLineColor(title)};">${data.title}</div>
                 </div>`,
                   className: "mapParkName2",
                   iconSize: 25,
@@ -415,7 +758,7 @@ export default {
                 this.markers["polygon"] = {};
               }
 
-              if (title == "所有园区范围边界") {
+              if (title == "园区范围边界") {
                 marker.on("click", (e) => {
                   this.$emit("changePark", data);
                 });
@@ -423,27 +766,50 @@ export default {
               this.markers["polygon"][data.title] = marker;
             }
 
-            if (title == "所有园区范围边界") {
+            if (title == "园区范围边界") {
+              const boundaryHtml = this.buildParkBoundaryPopupHtml(data);
+              const remarkHtml = this.buildParkExcelRemarkPopupHtml(data);
+              const introText = this.parkInfo[data.title] || "";
               // 中间区域添加一个marker点并绑定title文字
               let popupHtml = `<div class="mapParkName_box">
-                <div class="title">${data.title}<span>${data.properties.area}</span></div>
+                <div class="title">${this.escapeHtml(data.title)}<span>${this.escapeHtml(
+                data.properties.area
+              )}</span></div>
                 <div class="card">
                   <img class="map-popup-stat-icon" src="/static/images/pup1.png" style="width:36px;height:36px"/>
-                  <div><div>主导产业</div>
-                  <div>${data.主导产业 ? data.主导产业 : "--"}</div>
+                  <div>
+                    <div>主导产业</div>
+                    <div>${this.escapeHtml(data.主导产业 ? data.主导产业 : "--")}</div>
+                  </div>
                 </div>
+                <div class="card">
+                  <img class="map-popup-stat-icon" src="/static/images/pup2.png" style="width:36px;height:36px"/>
+                  <div>
+                    <div>规上工业服务业企业数量</div>
+                    <div>${
+                      data["规上工业服务业企业数量"] != null
+                        ? this.escapeHtml(String(data["规上工业服务业企业数量"]))
+                        : "--"
+                    }</div>
+                  </div>
                 </div>
                 <div class="card">
                   <img class="map-popup-stat-icon" src="/static/images/pup2.png" style="width:36px;height:36px"/>
                   <div>
-                    <div>入驻企业数量(家)</div>
-                    <div>${data.企业数量 ? data.企业数量 : "--"}</div>
+                    <div>高企数量</div>
+                    <div>${
+                      data["高企数量"] != null
+                        ? this.escapeHtml(String(data["高企数量"]))
+                        : "--"
+                    }</div>
                   </div>
                 </div>
+                ${boundaryHtml}
                 <div class="map-popup-intro">
                   <div class="title2">园区简介</div>
-                  <div class="map-popup-intro-text">${this.parkInfo[data.title]}</div>
+                  <div class="map-popup-intro-text">${this.escapeHtml(introText)}</div>
                 </div>
+                ${remarkHtml}
                 </div>`;
               polygon.bindPopup(popupHtml);
               polygon.on("click", (e) => {
@@ -473,6 +839,18 @@ export default {
     },
     // 切换图层控制显示
     changeLayerControl(item) {
+      if (item.name === ROAD_ANNOTATION_LAYER_NAME) {
+        this.ensureRoadOverlayLayer();
+        if (!this.roadOverlayLayer || !this.map) return;
+        if (item.state) {
+          if (!this.map.hasLayer(this.roadOverlayLayer)) {
+            this.roadOverlayLayer.addTo(this.map);
+          }
+        } else if (this.map.hasLayer(this.roadOverlayLayer)) {
+          this.map.removeLayer(this.roadOverlayLayer);
+        }
+        return;
+      }
       if (this.layerControlPolygon[item.name]) {
         for (const key in this.layerControlPolygon[item.name]) {
           if (item.state) {
@@ -503,8 +881,24 @@ export default {
 </script>
 <style lang="less" scoped>
 .appMapViewer {
+  position: relative;
   width: 100%;
   height: 100%;
   background: var(--ui-map-bg, #000);
 }
+/* Leaflet 默认 .leaflet-container 为 #ddd,缩放时空隙会露白;与主题「地图底色」一致 */
+.appMapViewer /deep/ .leaflet-container {
+  background: var(--ui-map-bg, #000) !important;
+}
+.appMapBoundaryFilters {
+  position: absolute;
+  width: 0;
+  height: 0;
+  overflow: hidden;
+  pointer-events: none;
+}
+.appMapLeaflet {
+  width: 100%;
+  height: 100%;
+}
 </style>

+ 96 - 20
src/utils/uiTheme.js

@@ -1,6 +1,6 @@
 /**
- * 大屏昼夜主题:CSS 变量写入 document.documentElement,配置持久化 localStorage。
- * 详见 docs/THEME.md
+ * 大屏主题:白天、黑夜(暗蓝政务)、卫星暗色 UI(cyan)三套叠层换肤;CSS 变量写入 :root,持久化 localStorage。
+ * 卫星模式仅换界面配色,不修改底图瓦片。详见 docs/THEME.md
  */
 export const UI_THEME_STORAGE_KEY = "industry_map_ui_theme_v1";
 
@@ -27,17 +27,31 @@ export function applySidebarWidthPx(px) {
 
 /**
  * 与 `qpjyj.vue` / `appMap.vue` 中底图 `value` 一致。
- * 仅当当前底图为该项时使用「黑夜」主题,其它底图使用「白天」主题。
+ * - 标准版政务底图 → 白天
+ * - 暗蓝色政务底图 → 黑夜(暗色 UI)
+ * - 卫星影像 → cyan(第三套暗色 UI 配置,仅叠层/Token;**不修改卫星瓦片颜色**)
  */
 export const BASE_MAP_KEY_NIGHT = "shmap_blue_web";
+export const BASE_MAP_KEY_CYAN = "arcgisImagery";
 
+/** @returns {"day"|"night"|"cyan"} */
 export function getUiModeForBaseMap(mapKey) {
-  return mapKey === BASE_MAP_KEY_NIGHT ? "night" : "day";
+  if (mapKey === BASE_MAP_KEY_CYAN) return "cyan";
+  if (mapKey === BASE_MAP_KEY_NIGHT) return "night";
+  return "day";
+}
+
+/** 持久化 / 表单可用的模式枚举 */
+export const UI_THEME_MODES = ["day", "night", "cyan"];
+
+export function normalizeUiThemeMode(mode) {
+  return UI_THEME_MODES.includes(mode) ? mode : "night";
 }
 
 /** 主题配置对话框可编辑的 Token(保存时只比对这组,避免写入已废弃键) */
 export const THEME_CONFIG_PICKER_KEYS = [
   "--ui-surface-bg",
+  "--ui-map-bg",
   "--ui-text-title",
   "--ui-text-1",
   "--ui-text-2",
@@ -45,6 +59,9 @@ export const THEME_CONFIG_PICKER_KEYS = [
   "--ui-text-desc",
   "--ui-text-placeholder",
   "--ui-main",
+  "--ui-layer-boundary-district",
+  "--ui-layer-boundary-town",
+  "--ui-layer-boundary-park",
 ];
 
 /** 与文档约定的变量名(含派生/兼容项) */
@@ -97,10 +114,15 @@ export const UI_THEME_VAR_KEYS = [
   "--ui-popup-card-bg",
   "--ui-divider-main",
   "--ui-control-highlight",
+  "--ui-layer-boundary-district",
+  "--ui-layer-boundary-town",
+  "--ui-layer-boundary-park",
 ];
 
 const SURFACE_NIGHT = "rgba(0, 204, 255, 0.15)";
 const SURFACE_DAY = "rgba(255, 255, 255, 0.26)";
+/** 卫星影像(cyan)统一表面背景 */
+const SURFACE_CYAN = "rgba(68, 132, 88, 0.15)";
 
 const NIGHT = {
   "--ui-surface-bg": SURFACE_NIGHT,
@@ -150,6 +172,45 @@ const NIGHT = {
   "--ui-divider-main": "#1dc8dc",
   "--ui-control-highlight": "rgba(0, 204, 255, 0.6)",
   "--ui-panel-text": "#ffffff",
+  "--ui-layer-boundary-district": "#78FF37",
+  "--ui-layer-boundary-town": "#FFF200",
+  "--ui-layer-boundary-park": "#D860FF",
+};
+
+/** 卫星专用暗色 UI(`data-ui-theme="cyan"`):与黑夜相互独立的一套 Token,便于在真彩影像上配侧栏/弹窗;底图保持原样 */
+const CYAN = {
+  ...NIGHT,
+  "--ui-surface-bg": SURFACE_CYAN,
+  "--ui-panel-bg": SURFACE_CYAN,
+  "--ui-search-bg": SURFACE_CYAN,
+  "--ui-popup-inner-bg": SURFACE_CYAN,
+  "--ui-popup-card-bg": SURFACE_CYAN,
+  "--ui-text-2": "#9af0ff",
+  "--ui-text-3": "#7ad8eb",
+  "--ui-main": "#00ddee",
+  "--ui-main-hover": "#33e8f5",
+  "--ui-border-accent": "#00ddee",
+  "--ui-search-border": "rgba(68, 132, 88, 0.45)",
+  "--ui-search-result-active": "#00ddee",
+  "--ui-card-back-highlight": "rgba(68, 132, 88, 0.35)",
+  "--ui-title-section": "#9af0ff",
+  "--ui-title2": "#9af0ff",
+  "--ui-link": "#5dfff6",
+  "--ui-fold-border": "#5dfff6",
+  "--ui-sync-label": "#33e8f5",
+  "--ui-header-en": "#6eeaff",
+  /* 瓦片未加载时的地图底色(卫星/深绿系) */
+  "--ui-map-bg": "#97836a",
+  "--ui-dropdown-bg": SURFACE_CYAN,
+  "--ui-dropdown-border": "rgba(68, 132, 88, 0.34)",
+  "--ui-dropdown-hover": "rgba(68, 132, 88, 0.22)",
+  "--ui-dropdown-selected-bg": "rgba(68, 132, 88, 0.32)",
+  "--ui-modal-surface": "rgba(6, 28, 40, 0.9)",
+  "--ui-popup-tip-bg": "#00ddee",
+  "--ui-popup-accent": "#5dfff6",
+  "--ui-divider-main": "#00ddee",
+  "--ui-control-highlight": "rgba(68, 132, 88, 0.5)",
+  "--ui-layer-boundary-district": "#FB8D8D",
 };
 
 const DAY = {
@@ -183,7 +244,8 @@ const DAY = {
   "--ui-sync-label": "#0d7ea0",
   "--ui-header-en": "#0d7ea0",
   "--ui-header-title-solid": "#1a3a4a",
-  "--ui-map-bg": "#e8ecef",
+  /* 标准版政务:白底;暗蓝政务:黑底;卫星:深绿底 */
+  "--ui-map-bg": "#ebf0f2",
   "--ui-marker-label-bg": "rgba(255, 255, 255, 0.92)",
   "--ui-marker-label-text": "#1a1a1a",
   "--ui-dropdown-bg": "#ffffff",
@@ -199,6 +261,9 @@ const DAY = {
   "--ui-divider-main": "#0d7ea0",
   "--ui-control-highlight": "rgba(13, 126, 160, 0.35)",
   "--ui-panel-text": "#0e6969",
+  "--ui-layer-boundary-district": "#2e7d32",
+  "--ui-layer-boundary-town": "#c17900",
+  "--ui-layer-boundary-park": "#1565c0",
 };
 
 /**
@@ -247,13 +312,18 @@ export function resolveThemeTokens(merged) {
 }
 
 export function getBaseTokens(mode) {
-  const m = mode === "day" ? DAY : NIGHT;
-  return resolveThemeTokens({ ...m });
+  return resolveThemeTokens({ ...pickThemeBase(normalizeUiThemeMode(mode)) });
+}
+
+function pickThemeBase(mode) {
+  if (mode === "day") return { ...DAY };
+  if (mode === "cyan") return { ...CYAN };
+  return { ...NIGHT };
 }
 
-/** 深拷贝为 { night, day },缺省补空对象 */
+/** 深拷贝为 { night, day, cyan },缺省补空对象 */
 export function normalizeOverridesByMode(raw) {
-  const empty = { night: {}, day: {} };
+  const empty = { night: {}, day: {}, cyan: {} };
   if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { ...empty };
   const night =
     raw.night && typeof raw.night === "object" && !Array.isArray(raw.night)
@@ -263,7 +333,11 @@ export function normalizeOverridesByMode(raw) {
     raw.day && typeof raw.day === "object" && !Array.isArray(raw.day)
       ? { ...raw.day }
       : {};
-  return { night, day };
+  const cyan =
+    raw.cyan && typeof raw.cyan === "object" && !Array.isArray(raw.cyan)
+      ? { ...raw.cyan }
+      : {};
+  return { night, day, cyan };
 }
 
 export function loadStoredTheme() {
@@ -272,13 +346,13 @@ export function loadStoredTheme() {
     if (!raw) {
       return {
         mode: "night",
-        overridesByMode: { night: {}, day: {} },
+        overridesByMode: { night: {}, day: {}, cyan: {} },
         sidebarWidthPx: DEFAULT_SIDEBAR_WIDTH_PX,
       };
     }
     const o = JSON.parse(raw);
-    const mode = o.mode === "day" ? "day" : "night";
-    let overridesByMode = { night: {}, day: {} };
+    const mode = normalizeUiThemeMode(o.mode);
+    let overridesByMode = { night: {}, day: {}, cyan: {} };
 
     if (
       o.overridesByMode &&
@@ -299,17 +373,18 @@ export function loadStoredTheme() {
   } catch {
     return {
       mode: "night",
-      overridesByMode: { night: {}, day: {} },
+      overridesByMode: { night: {}, day: {}, cyan: {} },
       sidebarWidthPx: DEFAULT_SIDEBAR_WIDTH_PX,
     };
   }
 }
 
 export function applyUiTheme(mode, overrides = {}) {
-  const base = mode === "day" ? { ...DAY } : { ...NIGHT };
+  const norm = normalizeUiThemeMode(mode);
+  const base = pickThemeBase(norm);
   const merged = resolveThemeTokens({ ...base, ...overrides });
   const root = document.documentElement;
-  root.setAttribute("data-ui-theme", mode);
+  root.setAttribute("data-ui-theme", norm);
   Object.keys(merged).forEach((k) => {
     if (merged[k] != null && merged[k] !== "") {
       root.style.setProperty(k, merged[k]);
@@ -318,8 +393,8 @@ export function applyUiTheme(mode, overrides = {}) {
 }
 
 /**
- * @param {"night"|"day"} mode 当前界面模式(与底图一致)
- * @param {{ night?: Record<string,string>, day?: Record<string,string> }} overridesByMode 昼夜各自覆盖,互不影响
+ * @param {"night"|"day"|"cyan"} mode 当前界面模式(与底图一致)
+ * @param {{ night?: object, day?: object, cyan?: object }} overridesByMode 三套各自覆盖,互不影响
  */
 export function persistTheme(mode, overridesByMode, sidebarWidthPx) {
   let prev = {};
@@ -329,7 +404,7 @@ export function persistTheme(mode, overridesByMode, sidebarWidthPx) {
     prev = {};
   }
   const obm = normalizeOverridesByMode(overridesByMode);
-  const m = mode === "day" ? "day" : "night";
+  const m = normalizeUiThemeMode(mode);
   const sw =
     sidebarWidthPx !== undefined && sidebarWidthPx !== null
       ? clampSidebarWidthPx(sidebarWidthPx)
@@ -343,6 +418,7 @@ export function persistTheme(mode, overridesByMode, sidebarWidthPx) {
 }
 
 export function getMergedTokens(mode, overrides = {}) {
-  const base = mode === "day" ? { ...DAY } : { ...NIGHT };
+  const norm = normalizeUiThemeMode(mode);
+  const base = pickThemeBase(norm);
   return resolveThemeTokens({ ...base, ...overrides });
 }

+ 163 - 0
src/views/parkExcelAttrs.js

@@ -0,0 +1,163 @@
+/**
+ * 甲方《青浦区产业地块信息表.xlsx》解析结果。
+ * 键与 parkDatas 中 properties.name2 一致。
+ * 更新数据请修改 Excel 后执行: node scripts/build-park-excel-attrs.js
+ */
+export const PARK_EXCEL_ATTRS_BY_NAME = {
+  "青浦园地块": {
+    "规上工业服务业企业数量": 243,
+    "高企数量": 187,
+    "入驻企业数量": 430,
+    "四至边界": {
+      "东": "外青松公路-香大东路-华青路-天瑞路-东大盈港-北青公路-华青路-新高路-久旺路-四号河-汇金路-崧泽大道-汇滨路",
+      "南": "上达河-汇金路-汇邦路-新业路-向阳河-崧泽大道-朝阳河-四号河-外青松公路-崧泽大道",
+      "西": "青赵公路-六洞浜-西大盈港-天盈路-漕盈路",
+      "北": "新金路-久远路-天一路-久业路-金泾河-东大盈港-徐家港-外青松公路"
+    },
+    "用地面积公顷": 1416.57,
+    "备注": "含原科技园地块、先进装备区地块、高新技术成果转化基地"
+  },
+  "中纺城园": {
+    "规上工业服务业企业数量": 12,
+    "高企数量": 25,
+    "入驻企业数量": 37,
+    "四至边界": {
+      "东": "向阳河",
+      "南": "盈港东路",
+      "西": "外青松公路",
+      "北": "上达河"
+    },
+    "用地面积公顷": 83.2,
+    "备注": ""
+  },
+  "西虹桥商务区": {
+    "规上工业服务业企业数量": 19,
+    "高企数量": 19,
+    "入驻企业数量": 38,
+    "四至边界": {
+      "东": "涞清路-崧泽大道-诸光路-国家会展中心北边界-小涞港-国家会展中心南边界-盈港东路",
+      "南": "会卓路-诸光路-盈港东路-泾北河-徐民东路南约160米-北蟠龙港东约120米-盈港东路",
+      "西": "G15沈海高速",
+      "北": "蟠中路-蟠龙港-老洋泾港-蟠中路-蟠龙路-会恒路-蟠臻路-蟠中路-蟠文路-崧泽高架-蟠臻路-龙联路-蟠秀路-崧泽高架-诸光路-龙联路"
+    },
+    "用地面积公顷": 283.94,
+    "备注": ""
+  },
+  "北斗高泾路园区": {
+    "规上工业服务业企业数量": 17,
+    "高企数量": 24,
+    "入驻企业数量": 41,
+    "四至边界": {
+      "东": "高泾路",
+      "南": "居礼别墅",
+      "西": "居礼别墅",
+      "北": "河泾港"
+    },
+    "用地面积公顷": 3.7,
+    "备注": ""
+  },
+  "北斗高光路园区": {
+    "规上工业服务业企业数量": 12,
+    "高企数量": 18,
+    "入驻企业数量": 30,
+    "四至边界": {
+      "东": "祝桥头路(高泾支路)",
+      "南": "徐南路",
+      "西": "徐南佳苑",
+      "北": "高光路"
+    },
+    "用地面积公顷": 2.26,
+    "备注": ""
+  },
+  "市西软件信息园": {
+    "规上工业服务业企业数量": 36,
+    "高企数量": 52,
+    "入驻企业数量": 88,
+    "四至边界": {
+      "东": "嘉松公路",
+      "南": "G50北辅道-佳驰路-朝睿路-佳驰路东约100米-宁睿路-佳驰路西约160米-朝睿路-佳驰路-G50北辅道",
+      "西": "盆泾-东密泾-沪青平公路-许泾-张泾-佳驰路-姚家浜-许泾-佳采路-佳驰路-盈港东路-佳杰路",
+      "北": "佳杰路"
+    },
+    "用地面积公顷": 124.83,
+    "备注": "其中F3-01及F4-02(北至源硕路,东至F4-01地块,南至源逸路,西至F3-02)除外"
+  },
+  "移动智地园区": {
+    "规上工业服务业企业数量": 19,
+    "高企数量": 48,
+    "入驻企业数量": 67,
+    "四至边界": {
+      "东": "永恩公司",
+      "南": "崧锦路",
+      "西": "崧华路",
+      "北": "崧秀路"
+    },
+    "用地面积公顷": 20.58,
+    "备注": ""
+  },
+  "尚之坊园区": {
+    "规上工业服务业企业数量": 3,
+    "高企数量": 8,
+    "入驻企业数量": 11,
+    "四至边界": {
+      "东": "赵重公路西约220米",
+      "南": "崧泽大道",
+      "西": "崧盈路东约160米",
+      "北": "崧泽大道北约220米"
+    },
+    "用地面积公顷": 7.4,
+    "备注": ""
+  },
+  "崧秋路地块": {
+    "规上工业服务业企业数量": 11,
+    "高企数量": 13,
+    "入驻企业数量": 24,
+    "四至边界": {
+      "东": "崧盈路",
+      "南": "崧辉路",
+      "西": "崧达路",
+      "北": "盈港东路"
+    },
+    "用地面积公顷": 43.2,
+    "备注": ""
+  },
+  "网易上海国际文创科技园": {
+    "规上工业服务业企业数量": 1,
+    "高企数量": 1,
+    "入驻企业数量": 2,
+    "四至边界": {
+      "东": "佳凯路",
+      "南": "佳康路",
+      "西": "佳悦路",
+      "北": "盈港东路-佳凯路西约130米-盈港东路南约160米"
+    },
+    "用地面积公顷": 17.04,
+    "备注": ""
+  },
+  "库克医疗器械园": {
+    "规上工业服务业企业数量": 60,
+    "高企数量": 130,
+    "入驻企业数量": 190,
+    "四至边界": {
+      "东": "青虬江(闵行区界)",
+      "南": "双联路(现改名天山西路延伸段)",
+      "西": "汇龙路",
+      "北": "印氏达路"
+    },
+    "用地面积公顷": 5.06,
+    "备注": ""
+  },
+  "徐泾104产业社区": {
+    "规上工业服务业企业数量": 12,
+    "高企数量": 8,
+    "入驻企业数量": 20,
+    "四至边界": {
+      "东": "华徐公路绿化",
+      "南": "双浜路",
+      "西": "G15沈海高速西约300米-双联路-明珠路",
+      "北": "徐德路"
+    },
+    "用地面积公顷": 24.23,
+    "备注": ""
+  }
+};

+ 249 - 53
src/views/qpjyj.vue

@@ -1,10 +1,6 @@
 <!-- dingding首页 -->
 <template>
-  <el-container
-    class="container"
-    :class="isDayTheme ? 'theme-day' : 'theme-night'"
-    style="height: 100%"
-  >
+  <el-container class="container" :class="themeSkinClass" style="height: 100%">
     <el-header class="container_header">
       <!-- header背景 -->
       <el-image
@@ -43,7 +39,7 @@
       </div>
       <!-- 数据同步信息区域 -->
       <div class="data-sync-info">
-        <div class="theme-controls">
+        <div class="theme-controls" v-if="false">
           <el-button type="text" class="theme-config-btn" @click="openThemeConfig"
             >主题配置</el-button
           >
@@ -134,11 +130,7 @@
         >
         <div style="margin: 15px 0"></div> -->
         <el-checkbox-group v-model="checkedLegends" @change="handleCheckedCitiesChange">
-          <el-checkbox
-            v-for="item in Legends"
-            :label="item.name"
-            :key="item.name"
-          >
+          <el-checkbox v-for="item in Legends" :label="item.name" :key="item.name">
             <el-image
               class="legend-icon"
               style="width: 12px; height: 1rem"
@@ -232,24 +224,32 @@
               style="justify-content: space-between"
             >
               <div class="title2">{{ key }}</div>
-              <div class="divFlex">
+              <div v-if="layerControlBulkActionsVisible(key)" class="divFlex">
                 <div class="linkTitle" @click="checkAllLayer(key, true)">全选</div>
                 <div class="linkTitle" @click="checkAllLayer(key, false)">清空</div>
               </div>
             </div>
-            <div
-              class="divFlex layer-control-row"
-              style="justify-content: space-between"
-              v-for="item in value"
-              :key="item.name"
-            >
-              <div class="title2 side-panel-body-text">{{ item.name }}</div>
-              <div style="margin-right: 1rem">
+            <div class="layer-control-row" v-for="item in value" :key="item.name">
+              <div class="title2 side-panel-body-text layer-control-row__label">
+                {{ item.name }}
+              </div>
+              <div
+                v-if="boundaryLayerLineCssVar(item.name)"
+                class="layer-control-row__sample"
+                aria-hidden="true"
+              >
+                <div
+                  class="layer-control-line-sample"
+                  :style="layerControlLineSampleStyle(item.name)"
+                />
+              </div>
+              <div class="layer-control-row__switch">
                 <el-switch
+                  :key="'layer-sw-' + uiThemeMode + '-' + item.name"
                   v-model="item.state"
                   @change="changeLayerControl(item)"
-                  :active-color="isDayTheme ? '#0d7ea0' : '#22E9FFD6'"
-                  :inactive-color="isDayTheme ? '#c0c4cc' : '#5A6F76'"
+                  :active-color="layerControlSwitchActiveColor"
+                  :inactive-color="layerControlSwitchInactiveColor"
                 >
                 </el-switch>
               </div>
@@ -292,14 +292,18 @@
     <el-dialog
       title="主题配置"
       :visible.sync="themeConfigVisible"
-      width="560px"
+      width="1180px"
       append-to-body
       custom-class="theme-config-dialog"
       @close="onThemeConfigClose"
     >
-      <el-form label-width="152px" size="small" class="theme-config-form">
-        <div class="theme-config-section-title">背景</div>
-        <el-form-item label="统一表面背景">
+      <el-form
+        label-width="108px"
+        size="small"
+        class="theme-config-form theme-config-grid"
+      >
+        <div class="theme-config-section-title theme-config-grid-full">背景</div>
+        <el-form-item label="统一表面背景" class="theme-config-grid-full">
           <el-color-picker
             v-model="themeForm['--ui-surface-bg']"
             popper-class="theme-config-color-panel"
@@ -310,7 +314,19 @@
             @change="(c) => onThemePickerLiveChange('--ui-surface-bg', c)"
           />
         </el-form-item>
-        <el-divider content-position="left">文字</el-divider>
+        <el-form-item label="地图底色" class="theme-config-grid-full">
+          <el-color-picker
+            v-model="themeForm['--ui-map-bg']"
+            popper-class="theme-config-color-panel"
+            color-format="hex"
+            size="small"
+            @active-change="(c) => onThemePickerLiveChange('--ui-map-bg', c)"
+            @change="(c) => onThemePickerLiveChange('--ui-map-bg', c)"
+          />
+        </el-form-item>
+        <el-divider class="theme-config-grid-full" content-position="left"
+          >文字</el-divider
+        >
         <el-form-item label="标题">
           <el-color-picker
             v-model="themeForm['--ui-text-title']"
@@ -372,8 +388,10 @@
             @change="(c) => onThemePickerLiveChange('--ui-text-placeholder', c)"
           />
         </el-form-item>
-        <el-divider content-position="left">强调</el-divider>
-        <el-form-item label="强调色/描边">
+        <el-divider class="theme-config-grid-full" content-position="left"
+          >强调</el-divider
+        >
+        <el-form-item label="强调色/描边" class="theme-config-grid-full">
           <el-color-picker
             v-model="themeForm['--ui-main']"
             popper-class="theme-config-color-panel"
@@ -383,8 +401,53 @@
             @change="(c) => onThemePickerLiveChange('--ui-main', c)"
           />
         </el-form-item>
-        <el-divider content-position="left">布局</el-divider>
-        <el-form-item label="侧栏宽度" class="theme-config-slider-item">
+        <el-divider class="theme-config-grid-full" content-position="left"
+          >地图边界图层</el-divider
+        >
+        <p class="theme-config-grid-full theme-config-boundary-hint">
+          以下为图层控制中的三类行政/园区边界线条色;内发光与描边同色联动。标准版政务 /
+          暗蓝政务 / 卫星影像各对应一套主题(白天、黑夜、卫星暗色
+          UI),随左下角底图切换;卫星模式下仅换界面叠层配色,不改动底图影像。
+        </p>
+        <el-form-item label="区县行政边界">
+          <el-color-picker
+            v-model="themeForm['--ui-layer-boundary-district']"
+            popper-class="theme-config-color-panel"
+            color-format="hex"
+            size="small"
+            @active-change="
+              (c) => onThemePickerLiveChange('--ui-layer-boundary-district', c)
+            "
+            @change="(c) => onThemePickerLiveChange('--ui-layer-boundary-district', c)"
+          />
+        </el-form-item>
+        <el-form-item label="乡镇行政边界">
+          <el-color-picker
+            v-model="themeForm['--ui-layer-boundary-town']"
+            popper-class="theme-config-color-panel"
+            color-format="hex"
+            size="small"
+            @active-change="(c) => onThemePickerLiveChange('--ui-layer-boundary-town', c)"
+            @change="(c) => onThemePickerLiveChange('--ui-layer-boundary-town', c)"
+          />
+        </el-form-item>
+        <el-form-item label="园区范围边界">
+          <el-color-picker
+            v-model="themeForm['--ui-layer-boundary-park']"
+            popper-class="theme-config-color-panel"
+            color-format="hex"
+            size="small"
+            @active-change="(c) => onThemePickerLiveChange('--ui-layer-boundary-park', c)"
+            @change="(c) => onThemePickerLiveChange('--ui-layer-boundary-park', c)"
+          />
+        </el-form-item>
+        <el-divider class="theme-config-grid-full" content-position="left"
+          >布局</el-divider
+        >
+        <el-form-item
+          label="侧栏宽度"
+          class="theme-config-slider-item theme-config-grid-full"
+        >
           <div class="theme-config-slider-stack">
             <div class="theme-config-slider-row">
               <el-slider
@@ -418,6 +481,7 @@ import PieView from "@/components/card/pieView.vue";
 import BarView from "@/components/card/barView.vue";
 // 园区列表
 import { parkDatas } from "./parkDatas.js";
+import { PARK_EXCEL_ATTRS_BY_NAME } from "./parkExcelAttrs.js";
 // 企业列表
 import { industryDatas } from "./industryDatas.js";
 // 青浦区行政区划
@@ -512,14 +576,20 @@ export default {
         边界: [
           {
             name: "区县行政边界",
-            state: true,
+            state: false,
           },
           {
             name: "乡镇行政边界",
             state: true,
           },
           {
-            name: "所有园区范围边界",
+            name: "园区范围边界",
+            state: true,
+          },
+        ],
+        影像注记: [
+          {
+            name: "影像注记图层",
             state: true,
           },
         ],
@@ -577,15 +647,16 @@ export default {
       activePark: "青浦园地块",
       parkList: [],
       activeTitle: "",
-      /** 按昼夜分别保存的覆盖项(当前底图对应模式只读写其中一份) */
-      themeOverridesByMode: { night: {}, day: {} },
-      /** 左右侧悬浮栏宽度(px),昼夜共用布局 */
+      /** 按白天 / 黑夜 / 卫星暗色 UI(cyan)分别保存覆盖项(当前底图对应模式只读写其中一份) */
+      themeOverridesByMode: { night: {}, day: {}, cyan: {} },
+      /** 左右侧悬浮栏宽度(px),三套模式共用布局 */
       sidebarWidthPx: DEFAULT_SIDEBAR_WIDTH_PX,
       sidebarSliderMin: SIDEBAR_WIDTH_MIN_PX,
       sidebarSliderMax: SIDEBAR_WIDTH_MAX_PX,
       themeConfigVisible: false,
       themeForm: {
         "--ui-surface-bg": null,
+        "--ui-map-bg": null,
         "--ui-text-title": null,
         "--ui-text-1": null,
         "--ui-text-2": null,
@@ -593,6 +664,9 @@ export default {
         "--ui-text-desc": null,
         "--ui-text-placeholder": null,
         "--ui-main": null,
+        "--ui-layer-boundary-district": null,
+        "--ui-layer-boundary-town": null,
+        "--ui-layer-boundary-park": null,
       },
       /** 主题写入 localStorage 防抖定时器 */
       _themePersistTimer: null,
@@ -618,14 +692,31 @@ export default {
       const time = this.$dayjs(this.lastSyncTime).format("HH:mm:ss");
       return `${weekDays[weekdayIndex]} ${time}`;
     },
-    /** 由当前底图推导:暗蓝色政务底图 = 黑夜,其余 = 白天 */
+    /** 仅标准版政务底图为白天;黑夜与卫星暗色 UI 的图表与开关沿用深色样式 */
     isDayTheme() {
       return getUiModeForBaseMap(this.activeMap) === "day";
     },
-    /** 与主题 / CSS data-ui-theme 一致 */
+    /** 与主题 / CSS data-ui-theme 一致:day | night | cyan */
     uiThemeMode() {
       return getUiModeForBaseMap(this.activeMap);
     },
+    /** 侧栏等容器类名:卫星为 theme-cyan(卫星专用暗色 UI),与 html data-ui-theme 对齐 */
+    themeSkinClass() {
+      const m = this.uiThemeMode;
+      if (m === "day") return "theme-day";
+      if (m === "cyan") return "theme-cyan";
+      return "theme-night";
+    },
+    /** 与 uiTheme.js 中各模式 --ui-main 一致;换肤时需配合 el-switch 的 key 避免 Element 内联色不刷新 */
+    layerControlSwitchActiveColor() {
+      const m = this.uiThemeMode;
+      if (m === "day") return "#0d7ea0";
+      if (m === "cyan") return "#00ddee";
+      return "#1dc8dc";
+    },
+    layerControlSwitchInactiveColor() {
+      return this.uiThemeMode === "day" ? "#c0c4cc" : "#5a6f76";
+    },
     sideBarPlusRem() {
       return `calc(${this.sidebarWidthPx}px + 1rem)`;
     },
@@ -738,6 +829,14 @@ export default {
       const next = this.buildThemeOverridesFromForm();
       this.$set(this.themeOverridesByMode, mode, next);
       applyUiTheme(mode, next);
+      this.$nextTick(() => {
+        if (
+          this.$refs.appMap &&
+          typeof this.$refs.appMap.refreshBoundaryPolygonStyles === "function"
+        ) {
+          this.$refs.appMap.refreshBoundaryPolygonStyles();
+        }
+      });
       if (this._themePersistTimer) clearTimeout(this._themePersistTimer);
       this._themePersistTimer = setTimeout(() => {
         this._themePersistTimer = null;
@@ -858,7 +957,15 @@ export default {
         form[k] = this.normalizeColorPickerValue(m[k], allowAlpha(k));
       });
       this.themeForm = form;
-      this.$nextTick(() => window.dispatchEvent(new Event("resize")));
+      this.$nextTick(() => {
+        window.dispatchEvent(new Event("resize"));
+        if (
+          this.$refs.appMap &&
+          typeof this.$refs.appMap.refreshBoundaryPolygonStyles === "function"
+        ) {
+          this.$refs.appMap.refreshBoundaryPolygonStyles();
+        }
+      });
       this.$message.success("已恢复当前模式默认主题、颜色与侧栏宽度");
     },
     // 搜索企业
@@ -925,6 +1032,16 @@ export default {
           this.changePark(this.parkList[0]);
         }, 300);
       });
+      // 多边形在 appMap 内异步创建,需延后按开关同步显隐(区县默认关闭)
+      this.$nextTick(() => {
+        setTimeout(() => {
+          if (!this.$refs.appMap) return;
+          const boundary = this.LayerControls["边界"];
+          if (boundary) boundary.forEach((item) => this.changeLayerControl(item));
+          const anno = this.LayerControls["影像注记"];
+          if (anno) anno.forEach((item) => this.changeLayerControl(item));
+        }, 200);
+      });
     },
     // 渲染园区列表
     initParkDatas() {
@@ -938,8 +1055,17 @@ export default {
           item.title = item.properties.name2;
           parkCogs[item.properties.name2] = "";
           item.c_geojson = item.geometry;
-          item.properties.area =
-            this.$CryptoJS.calculateMultiPolygonAreaInHectare(item.geometry) + "公顷";
+          const excelAttrs = PARK_EXCEL_ATTRS_BY_NAME[item.properties.name2];
+          if (excelAttrs) {
+            const ha = Number(excelAttrs.用地面积公顷);
+            item.properties.area =
+              (Number.isFinite(ha)
+                ? (Math.round(ha * 100) / 100).toString().replace(/\.?0+$/, "")
+                : String(excelAttrs.用地面积公顷)) + "公顷";
+          } else {
+            item.properties.area =
+              this.$CryptoJS.calculateMultiPolygonAreaInHectare(item.geometry) + "公顷";
+          }
           let echartsDatas = this.echartsDatas.map((item) => ({
             ...item,
             value: 0,
@@ -980,17 +1106,28 @@ export default {
           if (maxItem.value > 0) {
             item["主导产业"] = maxItem.name;
           }
-          item["企业数量"] = 0;
-          echartsDatas.forEach((dataItem) => {
-            item["企业数量"] += dataItem.value;
-          });
+          if (excelAttrs) {
+            item["规上工业服务业企业数量"] = excelAttrs.规上工业服务业企业数量;
+            item["高企数量"] = excelAttrs.高企数量;
+            item["四至边界"] = { ...excelAttrs.四至边界 };
+            if (excelAttrs.备注) {
+              this.$set(item.properties, "excel备注", excelAttrs.备注);
+            } else {
+              this.$delete(item.properties, "excel备注");
+            }
+          } else {
+            this.$delete(item, "四至边界");
+            this.$delete(item, "规上工业服务业企业数量");
+            this.$delete(item, "高企数量");
+            this.$delete(item.properties, "excel备注");
+          }
           item.echartsDatas = echartsDatas;
           item.barEchartsDatas = barEchartsDatas;
           parkList_.push(item);
         });
         if (parkList_ && parkList_.length > 0) {
           parkList_.forEach((item) => {
-            this.$refs.appMap.pieaddParkPolygon("所有园区范围边界", item, false);
+            this.$refs.appMap.pieaddParkPolygon("园区范围边界", item, false);
           });
           resolve(parkList_);
         }
@@ -1001,7 +1138,7 @@ export default {
       // 切换选中的园区
       this.activePark = item.properties.name2;
       // 定位园区范围
-      this.$refs.appMap.toFitBounds("所有园区范围边界", item);
+      this.$refs.appMap.toFitBounds("园区范围边界", item);
       // 初始化企业数据,并统计每个企业被哪个园区所包含,并绑定到parkList中
       this.echartsDatas = item.echartsDatas;
       // let sumData_ = {};
@@ -1027,24 +1164,53 @@ export default {
     },
     // 切换所有图层状态
     checkAllLayer(key, value) {
-      this.LayerControls[key].forEach((item) => {
+      const list = this.LayerControls[key];
+      if (!list) return;
+      list.forEach((item) => {
         if (value != item.state) {
           item.state = value;
           this.changeLayerControl(item);
         }
       });
-      for (let key in this.LayerControls) {
-        this.changeLayerControl(this.LayerControls[key]);
-      }
+    },
+    /** 仅多选项分组显示全选/清空(影像注记等单项不需要) */
+    layerControlBulkActionsVisible(groupKey) {
+      const list = this.LayerControls[groupKey];
+      return list && list.length > 1;
     },
     changeLayerControl(item) {
       this.$refs.appMap.changeLayerControl(item);
     },
+    /** 图层控制条形色标:与主题配置中的边界线条 CSS 变量对应 */
+    boundaryLayerLineCssVar(layerName) {
+      const m = {
+        区县行政边界: "--ui-layer-boundary-district",
+        乡镇行政边界: "--ui-layer-boundary-town",
+        园区范围边界: "--ui-layer-boundary-park",
+      };
+      return m[layerName] || null;
+    },
+    layerControlLineSampleStyle(layerName) {
+      const v = this.boundaryLayerLineCssVar(layerName);
+      if (!v) return {};
+      return {
+        backgroundColor: `var(${v})`,
+        boxShadow: `0 0 2px var(${v}), 0 0 6px var(${v}), 0 0 12px var(${v})`,
+      };
+    },
     // 切换地图服务
     changeMapService(item) {
       this.activeMap = item.value;
       this.$refs.appMap.switchBaseMap(item.value);
       this.syncThemeWithBaseMap();
+      this.$nextTick(() => {
+        if (
+          this.$refs.appMap &&
+          typeof this.$refs.appMap.refreshBoundaryPolygonStyles === "function"
+        ) {
+          this.$refs.appMap.refreshBoundaryPolygonStyles();
+        }
+      });
     },
     // 处理全选/取消全选
     handleCheckAllChange() {
@@ -1585,11 +1751,41 @@ export default {
     margin-top: 1rem !important;
     text-align: center;
   }
-  .layer-control-head,
-  .layer-control-row {
+  .layer-control-head {
     flex-wrap: wrap;
     gap: 8px 12px;
   }
+  .layer-control-row {
+    display: flex;
+    align-items: center;
+    width: 100%;
+    gap: 10px 12px;
+    flex-wrap: nowrap;
+    min-height: 32px;
+  }
+  .layer-control-row__label {
+    flex: 0 1 auto;
+    min-width: 0;
+  }
+  .layer-control-row__sample {
+    flex: 1 1 0;
+    min-width: 40px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 0 4px;
+  }
+  .layer-control-line-sample {
+    width: 100%;
+    max-width: 132px;
+    height: 3px;
+    border-radius: 999px;
+  }
+  .layer-control-row__switch {
+    flex: 0 0 auto;
+    margin-right: 1rem;
+    margin-left: auto;
+  }
   .industry-filter-section {
     padding-bottom: 1rem;
   }