连接小班接口
This commit is contained in:
0
public/data/sps.json
Normal file
0
public/data/sps.json
Normal file
70
src/assets/geoserver.js
Normal file
70
src/assets/geoserver.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const GEOSERVER_CONFIG = {
|
||||
baseUrl: "http://8.130.135.159:8000/geoserver/lydc/wms",
|
||||
typeName: "lydc:NYTB",
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取图斑数据 (WFS请求)
|
||||
*/
|
||||
export const fetchPlotGeoJSON = async () => {
|
||||
// 1. 构建 WFS 请求 URL
|
||||
const params = new URLSearchParams({
|
||||
service: "",
|
||||
version: "1.0.0",
|
||||
request: "GetFeature",
|
||||
typeName: GEOSERVER_CONFIG.typeName,
|
||||
outputFormat: "application/json",
|
||||
// 强制返回 WGS84 经纬度,防止坐标跑偏
|
||||
srsName: "EPSG:3857",
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`${GEOSERVER_CONFIG.baseUrl}?${params.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GeoServer 请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const geoJsonData = await response.json();
|
||||
|
||||
// 2. 数据清洗与映射 (Mapping)
|
||||
// GeoServer 返回的属性名通常是数据库字段(如 "xb_type", "img_url")
|
||||
// 我们需要把它们映射回你前端代码里用的 "type", "images" 等
|
||||
const mappedFeatures = geoJsonData.features.map((feature) => {
|
||||
const p = feature.properties;
|
||||
|
||||
return {
|
||||
...feature,
|
||||
properties: {
|
||||
// 保留原始属性
|
||||
...p,
|
||||
|
||||
// --- 关键映射区域 (根据你的数据库字段修改左边的值) ---
|
||||
name: p.xb_name || p.name || "未命名图斑",
|
||||
|
||||
// 这里的映射决定了地图颜色!确保数据库里有 "乔木林"/"灌木林" 等值
|
||||
// 如果数据库存的是代码(1,2,3),这里需要写转换逻辑
|
||||
type: p.forest_type || p.type || "其他",
|
||||
|
||||
area: p.area_mu || 0,
|
||||
collector: p.manager || "未知人员",
|
||||
|
||||
// --- 图片/视频处理 ---
|
||||
// 假设数据库里存的是 "url1,url2" 这样的字符串
|
||||
images: p.image_urls ? p.image_urls.split(',') : [],
|
||||
videos: p.video_urls ? p.video_urls.split(',') : [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
type: "FeatureCollection",
|
||||
features: mappedFeatures,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error("图斑数据加载失败:", error);
|
||||
// 出错时返回空集合,保证地图不崩
|
||||
return { type: "FeatureCollection", features: [] };
|
||||
}
|
||||
};
|
||||
@@ -1,48 +1,72 @@
|
||||
<template>
|
||||
<div class="map-wrapper">
|
||||
<div id="header">
|
||||
<span class="title">天津市林草资源专项调查——数据展示系统</span>
|
||||
</div>
|
||||
<div id="map" ref="mapContainer" class="map-container"></div>
|
||||
<div id="dateSelectItem" class="date-panel">
|
||||
<span class="date-label">数据日期:</span>
|
||||
<el-date-picker
|
||||
v-model="selectedDate"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
:disabled-date="disabledDate"
|
||||
@change="handleDateChange"
|
||||
size="default"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="legend-panel">
|
||||
<Legend :legendData="legendItems" />
|
||||
<div class="control-item group-item">
|
||||
<label class="group-header">
|
||||
<input type="checkbox" v-model="isStatLayerVisible" />
|
||||
<span>统计数据 ({{ currentLevel === 'district' ? '区级' : '街道' }})</span>
|
||||
</label>
|
||||
|
||||
<IndicatorSelector
|
||||
:indicators="Object.keys(indicatorData)"
|
||||
v-model="selectedIndicator"
|
||||
/>
|
||||
<div v-if="isStatLayerVisible" class="group-body">
|
||||
<IndicatorSelector
|
||||
:indicators="Object.keys(indicatorData)"
|
||||
v-model="selectedIndicator"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-item">
|
||||
<label>
|
||||
<input type="checkbox" v-model="isPlotLayerVisible" />
|
||||
所有图斑
|
||||
</label>
|
||||
</div>
|
||||
<div class="control-item">
|
||||
<label>
|
||||
<input type="checkbox" />
|
||||
样地图斑
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<button
|
||||
v-if="currentLevel === 'street'"
|
||||
class="back-btn"
|
||||
@click="backToDistrict"
|
||||
>
|
||||
<button v-if="currentLevel === 'street'" class="back-btn" @click="backToDistrict">
|
||||
返回区级
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<PlotPopup
|
||||
:visible="popupVisible"
|
||||
:data="popupData"
|
||||
:position="popupPos"
|
||||
@enter-sample="showSamples"
|
||||
@enter-side="showSideTrees"
|
||||
/>
|
||||
<SamplePopup
|
||||
:visible="samplePopupVisible"
|
||||
:data="samplePopupData"
|
||||
:position="samplePopupPos"
|
||||
/>
|
||||
<PlotPopup :visible="popupVisible" :data="popupData" :position="popupPos" @enter-sample="showSamples"
|
||||
@enter-side="showSideTrees" @close="popupVisible = false" />
|
||||
<SamplePopup :visible="samplePopupVisible" :data="samplePopupData" :position="samplePopupPos" />
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { Close } from "@element-plus/icons-vue";
|
||||
import maplibregl from "maplibre-gl";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
import { TDT_KEY } from "../assets/tdt_key.js";
|
||||
import IndicatorSelector from "./IndicatorSelector.vue";
|
||||
import Legend from "./Legend.vue";
|
||||
import { plotGeoJSON, samplePlots, sideTrees } from "../assets/mockPlotData.js";
|
||||
import { samplePlots, sideTrees } from "../assets/mockPlotData.js";
|
||||
import PlotPopup from "./PlotPopup.vue";
|
||||
import SamplePopup from "./SamplePopup.vue";
|
||||
import bbox from "@turf/bbox";
|
||||
@@ -50,17 +74,80 @@ import bbox from "@turf/bbox";
|
||||
const popupVisible = ref(false);
|
||||
const popupData = ref({});
|
||||
const popupPos = ref({ x: 0, y: 0 });
|
||||
const isPlotLayerVisible = ref(false);
|
||||
const isStatLayerVisible = ref(true);
|
||||
|
||||
// 天津市行政区划数据(区级)
|
||||
const districtUrl =
|
||||
"https://geo.datav.aliyun.com/areas_v3/bound/120000_full.json";
|
||||
const districtUrl ="https://geo.datav.aliyun.com/areas_v3/bound/120000_full.json";
|
||||
|
||||
|
||||
// 区级指标数据
|
||||
const indicatorData =ref({
|
||||
完成状态:{},
|
||||
图斑面积:{},
|
||||
图斑数量:{},
|
||||
})
|
||||
// 状态管理
|
||||
const selectedIndicator = ref("完成状态");
|
||||
const currentLevel = ref("district"); // 当前层级
|
||||
const currentDistrict = ref(""); // 当前区名
|
||||
|
||||
const mapContainer = ref(null);
|
||||
let map = null;
|
||||
const legendItems = ref([]);
|
||||
let allStreetsData = null;
|
||||
watch(isStatLayerVisible, () => {
|
||||
updateStatLayerState();
|
||||
});
|
||||
|
||||
watch(isPlotLayerVisible, (newVal) => {
|
||||
if (newVal) {
|
||||
showPlotLayer();
|
||||
} else {
|
||||
hidePlotLayer();
|
||||
}
|
||||
});
|
||||
const selectedDate = ref(new Date().toISOString().split('T')[0]); // 默认今天
|
||||
|
||||
// ✅ 新增:日期变更处理函数
|
||||
function handleDateChange(val) {
|
||||
console.log("📅 日期已切换为:", val);
|
||||
|
||||
// 在这里添加你的逻辑,例如:
|
||||
// 1. 重新请求统计接口
|
||||
// fetchDistrictStatus(val);
|
||||
// 2. 刷新 WMS 图层 (如果 WMS 支持 TIME 参数)
|
||||
// refreshWmsLayer(val);
|
||||
}
|
||||
|
||||
// ✅ 可选:禁用未来日期
|
||||
function disabledDate(time) {
|
||||
return time.getTime() > Date.now();
|
||||
}
|
||||
function updateStatLayerState() {
|
||||
const isVisible = isStatLayerVisible.value ? 'visible' : 'none';
|
||||
|
||||
if (currentLevel.value === 'district') {
|
||||
// 如果在区级:显示区,隐藏街道
|
||||
if (map.getLayer("tianjin-fill")) map.setLayoutProperty("tianjin-fill", "visibility", isVisible);
|
||||
if (map.getLayer("tianjin-outline")) map.setLayoutProperty("tianjin-outline", "visibility", isVisible);
|
||||
if (map.getLayer("tianjin-label")) map.setLayoutProperty("tianjin-label", "visibility", isVisible);
|
||||
|
||||
// 确保街道层隐藏
|
||||
if (map.getLayer("street-fill")) map.setLayoutProperty("street-fill", "visibility", "none");
|
||||
if (map.getLayer("street-outline")) map.setLayoutProperty("street-outline", "visibility", "none");
|
||||
|
||||
} else if (currentLevel.value === 'street') {
|
||||
// 如果在街道级:隐藏区,显示街道
|
||||
if (map.getLayer("tianjin-fill")) map.setLayoutProperty("tianjin-fill", "visibility", "none");
|
||||
if (map.getLayer("tianjin-outline")) map.setLayoutProperty("tianjin-outline", "visibility", "none");
|
||||
if (map.getLayer("tianjin-label")) map.setLayoutProperty("tianjin-label", "visibility", "none");
|
||||
|
||||
// 控制街道层
|
||||
if (map.getLayer("street-fill")) map.setLayoutProperty("street-fill", "visibility", isVisible);
|
||||
if (map.getLayer("street-outline")) map.setLayoutProperty("street-outline", "visibility", isVisible);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDistrictStatus(){
|
||||
console.log("🔥 正在尝试发起请求...");
|
||||
@@ -86,34 +173,40 @@ async function fetchDistrictStatus(){
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟街道级指标(真实项目应从接口获取)
|
||||
const streetIndicatorData = {
|
||||
完成状态: {
|
||||
光复道街道: "已完成",
|
||||
建昌道街道: "未完成",
|
||||
王串场街道: "未开始",
|
||||
},
|
||||
图斑面积: {
|
||||
光复道街道: 200,
|
||||
建昌道街道: 350,
|
||||
王串场街道: 120,
|
||||
},
|
||||
图斑数量: {
|
||||
光复道街道: 40,
|
||||
建昌道街道: 55,
|
||||
王串场街道: 30,
|
||||
},
|
||||
};
|
||||
// 街道指标
|
||||
const streetIndicatorData = ref({
|
||||
完成状态:{},
|
||||
图斑面积:{},
|
||||
图斑数量:{},
|
||||
});
|
||||
|
||||
// 状态管理
|
||||
const selectedIndicator = ref("完成状态");
|
||||
const currentLevel = ref("district"); // 当前层级
|
||||
const currentDistrict = ref(""); // 当前区名
|
||||
async function fetchStreetStats(districtName) {
|
||||
try {
|
||||
console.log(`🔥 正在请求【${districtName}】的街道数据...`);
|
||||
|
||||
const mapContainer = ref(null);
|
||||
let map = null;
|
||||
const legendItems = ref([]);
|
||||
let allStreetsData = null;
|
||||
const url = `/api/stats/street?district=${encodeURIComponent(districtName)}`;
|
||||
const response = await fetch(url);
|
||||
const res = await response.json();
|
||||
|
||||
if (res.code === 0 && res.data) {
|
||||
streetIndicatorData.value = {
|
||||
完成状态: res.data.completionStatus || {},
|
||||
图斑面积: res.data.plotArea || {},
|
||||
图斑数量: res.data.plotCount || {},
|
||||
};
|
||||
console.log("✅ 街道数据获取成功:", streetIndicatorData.value);
|
||||
if (currentLevel.value === 'street') {
|
||||
// 为了确保地图源(Source)已经创建,可以加一个小延时,或者直接调用
|
||||
// 这里的 'street' 对应 updateMapColors 里的 sourceId判断
|
||||
updateMapColors('street');
|
||||
}
|
||||
} else {
|
||||
console.warn("街道数据返回异常:", res);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取街道指标失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载街道
|
||||
async function loadAllStreets() {
|
||||
@@ -122,7 +215,84 @@ async function loadAllStreets() {
|
||||
allStreetsData = await res.json();
|
||||
}
|
||||
}
|
||||
const MEDIA_BASE_URL = '/photos';
|
||||
async function fetchPlotData(nyxbh, xiang) {
|
||||
try {
|
||||
// 1. 构建带参数的 URL
|
||||
const url = `/api/stats/plot/attr?nyxbh=${encodeURIComponent(nyxbh)}&xiang=${encodeURIComponent(xiang)}`;
|
||||
|
||||
console.log("🚀 发起后端详情请求:", url);
|
||||
|
||||
// 2. 发起请求
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
// 如果有 token,记得在这里加 'Authorization': 'Bearer ...'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const resJson = await response.json();
|
||||
console.log("📦 后端返回的数据:", resJson);
|
||||
|
||||
// 3. 校验后端返回的业务状态码
|
||||
if (resJson.code === 0) {
|
||||
// 处理数据(特别是媒体路径)
|
||||
return processPlotData(resJson.data);
|
||||
} else {
|
||||
console.error('业务报错:', resJson.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function processPlotData(data) {
|
||||
if (!data) return null;
|
||||
|
||||
// 1. 处理 Shape 数据 (它是字符串格式的 JSON,可能需要转对象)
|
||||
let geoJson = null;
|
||||
try {
|
||||
if (data.shape) {
|
||||
geoJson = JSON.parse(data.shape);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Shape 解析失败', e);
|
||||
}
|
||||
|
||||
// 2. 处理媒体列表
|
||||
const images = [];
|
||||
const videos = [];
|
||||
|
||||
if (data.mediaPathList && Array.isArray(data.mediaPathList)) {
|
||||
data.mediaPathList.forEach(path => {
|
||||
// 拼接完整 URL
|
||||
const fullUrl = `${MEDIA_BASE_URL}${path}`;
|
||||
|
||||
// 简单通过后缀区分类型 (转小写比较)
|
||||
const lowerPath = path.toLowerCase();
|
||||
if (lowerPath.endsWith('.jpg') || lowerPath.endsWith('.png') || lowerPath.endsWith('.jpeg')) {
|
||||
images.push(fullUrl);
|
||||
} else if (lowerPath.endsWith('.avi') || lowerPath.endsWith('.mp4')) {
|
||||
videos.push(fullUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 返回处理后的对象,方便前端直接使用
|
||||
return {
|
||||
...data, // 保留原始属性 (id, cun, linZhong 等)
|
||||
geoJson, // 解析后的地理坐标对象
|
||||
images, // 只有图片链接的数组
|
||||
videos // 只有视频链接的数组
|
||||
};
|
||||
}
|
||||
// 定义点击区级多边形的处理函数
|
||||
async function handleDistrictClick(e) {
|
||||
const districtName = e.features[0].properties.name;
|
||||
@@ -144,13 +314,13 @@ function handleMapClick(e) {
|
||||
|
||||
// 初始化地图
|
||||
onMounted(async () => {
|
||||
console.log("Vue组件已挂载");
|
||||
fetchDistrictStatus();
|
||||
console.log("初始化地图");
|
||||
map = new maplibregl.Map({
|
||||
container: mapContainer.value,
|
||||
style: {
|
||||
version: 8,
|
||||
glyphs: "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
|
||||
sources: {
|
||||
tdt_vec: {
|
||||
type: "raster",
|
||||
@@ -178,6 +348,18 @@ onMounted(async () => {
|
||||
},
|
||||
center: [117.2, 39.13],
|
||||
zoom: 8,
|
||||
transformRequest: (url, resourceType) => {
|
||||
if (url.includes('/geoserver')) {
|
||||
return {
|
||||
url: url,
|
||||
headers: {
|
||||
// 将 admin:geoserver 转为 Base64 认证头
|
||||
'Authorization': 'Basic ' + btoa('admin:geoserver')
|
||||
}
|
||||
}
|
||||
}
|
||||
return { url };
|
||||
},
|
||||
});
|
||||
|
||||
const res = await fetch(districtUrl);
|
||||
@@ -190,14 +372,34 @@ onMounted(async () => {
|
||||
id: "tianjin-fill",
|
||||
type: "fill",
|
||||
source: "tianjin",
|
||||
paint: { "fill-color": ["get", "color"], "fill-opacity": 0.6 },
|
||||
layout: { visibility: isStatLayerVisible.value ? 'visible' : 'none' },
|
||||
paint: { "fill-color": ["coalesce", ["get", "color"], "#b6e3d8"], "fill-opacity": 0.9 },
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: "tianjin-outline",
|
||||
type: "line",
|
||||
source: "tianjin",
|
||||
paint: { "line-color": "#333", "line-width": 1 },
|
||||
layout: { visibility: isStatLayerVisible.value ? 'visible' : 'none' },
|
||||
paint: { "line-color": "#ffffff", "line-width": 1 },
|
||||
});
|
||||
map.addLayer({
|
||||
id: "tianjin-label",
|
||||
type: "symbol",
|
||||
source: "tianjin",
|
||||
layout: { visibility: isStatLayerVisible.value ? 'visible' : 'none' },
|
||||
layout: {
|
||||
"text-field": ["get", "name"], // 获取 properties 中的 name
|
||||
"text-size": 14, // 文字大小
|
||||
"text-font": ["Open Sans Regular"], // 指定字体(必须与 glyphs 对应)
|
||||
"text-anchor": "center", // 文字居中
|
||||
"text-allow-overlap": false // 避免文字重叠
|
||||
},
|
||||
paint: {
|
||||
"text-color": "#000000", // 文字颜色:黑色
|
||||
"text-halo-color": "#ffffff", // 文字描边:白色(为了看清)
|
||||
"text-halo-width": 2 // 描边宽度
|
||||
}
|
||||
});
|
||||
|
||||
updateMapColors();
|
||||
@@ -205,19 +407,7 @@ onMounted(async () => {
|
||||
// 点击区多边形,下钻街道级
|
||||
map.off("click", "tianjin-fill", handleDistrictClick);
|
||||
map.on("click", "tianjin-fill", handleDistrictClick);
|
||||
// 点击空白返回
|
||||
map.off("click", handleMapClick);
|
||||
map.on("click", handleMapClick);
|
||||
});
|
||||
|
||||
// 缩放监听:14级以上显示图斑
|
||||
map.on("zoom", () => {
|
||||
const zoom = map.getZoom();
|
||||
if (zoom >= 14) {
|
||||
showPlotLayer();
|
||||
} else {
|
||||
hidePlotLayer();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -227,18 +417,26 @@ function updateMapColors(level = currentLevel.value) {
|
||||
const data = map.getSource(sourceId)?._data;
|
||||
if (!data) return;
|
||||
|
||||
const indicator = level === "district" ? indicatorData.value : streetIndicatorData;
|
||||
const indicator = level === "district" ? indicatorData.value : streetIndicatorData.value;
|
||||
const selected = selectedIndicator.value;
|
||||
|
||||
// 防止接口还没返回数据时报错
|
||||
if (!indicator[selected]) return;
|
||||
|
||||
if (selected === "完成状态") {
|
||||
const colors = { 已完成: "#4CAF50", 未完成: "#FFC107", 未开始: "#BDBDBD" };
|
||||
const colors = { 已采集: "#4CAF50", 已采集未开始: "#FFC107", 未发布: "#BDBDBD" };
|
||||
data.features.forEach((f) => {
|
||||
const name = f.properties.name;
|
||||
let name;
|
||||
if (level === 'district') {
|
||||
name = f.properties.name; // 区级 GeoJSON 还是叫 name
|
||||
} else {
|
||||
name = f.properties.乡; // 街道级 GeoJSON 叫 "乡"
|
||||
}
|
||||
|
||||
// 增加一个去空格处理
|
||||
if (name) name = name.trim();
|
||||
const val = indicator[selected][name];
|
||||
f.properties.color = colors[val] || "#ccc";
|
||||
f.properties.color = colors[val] || "#BDBDBD";
|
||||
});
|
||||
map.getSource(sourceId).setData(data);
|
||||
legendItems.value = Object.entries(colors).map(([k, v]) => ({
|
||||
@@ -253,9 +451,17 @@ function updateMapColors(level = currentLevel.value) {
|
||||
const getColor = (v) =>
|
||||
colors[Math.floor(((v - min) / (max - min)) * (colors.length - 1))];
|
||||
data.features.forEach((f) => {
|
||||
const name = f.properties.name;
|
||||
let name;
|
||||
if (level === 'district') {
|
||||
name = f.properties.name;
|
||||
} else {
|
||||
name = f.properties.乡;
|
||||
}
|
||||
|
||||
if (name) name = name.trim();
|
||||
|
||||
const v = indicator[selected][name];
|
||||
f.properties.color = v ? getColor(v) : "#ccc";
|
||||
f.properties.color = (v !== undefined && v !== null) ? getColor(v) : "#ccc";
|
||||
});
|
||||
map.getSource(sourceId).setData(data);
|
||||
legendItems.value = colors.map((c, i) => {
|
||||
@@ -268,8 +474,10 @@ function updateMapColors(level = currentLevel.value) {
|
||||
|
||||
// 下钻显示街道级
|
||||
async function showStreetLevel(districtName) {
|
||||
currentLevel.value = "street";
|
||||
currentDistrict.value = districtName;
|
||||
await loadAllStreets();
|
||||
|
||||
fetchStreetStats(districtName);
|
||||
// ✅ 筛选出该区所有街道
|
||||
const filteredFeatures = allStreetsData.features.filter(
|
||||
(f) => f.properties.县 === districtName
|
||||
@@ -303,9 +511,13 @@ async function showStreetLevel(districtName) {
|
||||
id: "street-fill",
|
||||
type: "fill",
|
||||
source: "street",
|
||||
layout: {
|
||||
// ✅ 新增:初始化时就根据复选框决定可见性
|
||||
visibility: isStatLayerVisible.value ? 'visible' : 'none'
|
||||
},
|
||||
paint: {
|
||||
"fill-color": ["get", "color"], // 根据指标设色
|
||||
"fill-opacity": 0.65,
|
||||
"fill-color": "#b3e0e6", // 根据指标设色
|
||||
"fill-opacity": 0.8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -313,20 +525,24 @@ async function showStreetLevel(districtName) {
|
||||
id: "street-outline",
|
||||
type: "line",
|
||||
source: "street",
|
||||
paint: { "line-width": 1, "line-color": "#333" },
|
||||
layout: {
|
||||
// ✅ 新增:初始化时就根据复选框决定可见性
|
||||
visibility: isStatLayerVisible.value ? 'visible' : 'none'
|
||||
},
|
||||
paint: { "line-width": 1, "line-color": "#ffffff" },
|
||||
});
|
||||
|
||||
currentLevel.value = "street";
|
||||
currentDistrict.value = districtName;
|
||||
|
||||
updateMapColors("street");
|
||||
updateStatLayerState();
|
||||
|
||||
// ✅ 隐藏区级图层
|
||||
if (map.getLayer("tianjin-fill"))
|
||||
map.setLayoutProperty("tianjin-fill", "visibility", "none");
|
||||
|
||||
if (map.getLayer("tianjin-outline"))
|
||||
map.setLayoutProperty("tianjin-outline", "visibility", "none");
|
||||
if (map.getLayer("tianjin-label"))
|
||||
map.setLayoutProperty("tianjin-label", "visibility", "none");
|
||||
|
||||
// ✅ 自动缩放到该区所有街道范围
|
||||
const extent = bbox(streetGeo); // [minX, minY, maxX, maxY]
|
||||
@@ -350,47 +566,103 @@ function backToDistrict() {
|
||||
map.removeLayer("street-outline");
|
||||
map.removeSource("street");
|
||||
}
|
||||
map.setLayoutProperty("tianjin-fill", "visibility", "visible");
|
||||
map.setLayoutProperty("tianjin-outline", "visibility", "visible");
|
||||
currentLevel.value = "district";
|
||||
updateStatLayerState();
|
||||
map.flyTo({ center: [117.2, 39.13], zoom: 8 });
|
||||
updateMapColors("district");
|
||||
}
|
||||
|
||||
// 点击图斑显示弹窗
|
||||
function handlePlotClick(e) {
|
||||
const feature = e.features[0];
|
||||
let prop = feature.properties;
|
||||
async function handlePlotClick(e) {
|
||||
console.log("🖱️ 点击坐标:", e.lngLat);
|
||||
|
||||
// --- 解析属性 ---
|
||||
let images = prop.images;
|
||||
let videos = prop.videos;
|
||||
// 1. 基础检查
|
||||
if (!map.getLayer("plot-raster-layer") || map.getLayoutProperty("plot-raster-layer", "visibility") === 'none') return;
|
||||
|
||||
if (typeof images === "string") {
|
||||
try {
|
||||
images = JSON.parse(images);
|
||||
} catch {
|
||||
images = [];
|
||||
}
|
||||
}
|
||||
if (typeof videos === "string") {
|
||||
try {
|
||||
videos = JSON.parse(videos);
|
||||
} catch {
|
||||
videos = [];
|
||||
}
|
||||
// 2. 避让矢量图层 (样地/树)
|
||||
const targetLayers = ["samples-fill", "sideTrees"];
|
||||
const existingLayers = targetLayers.filter(id => map.getLayer(id) && map.getLayoutProperty(id, "visibility") !== 'none');
|
||||
if (existingLayers.length > 0) {
|
||||
const vectorFeatures = map.queryRenderedFeatures(e.point, { layers: existingLayers });
|
||||
if (vectorFeatures.length > 0) return;
|
||||
}
|
||||
|
||||
// 计算屏幕像素位置
|
||||
const canvas = map.getCanvas().getBoundingClientRect();
|
||||
const pos = map.project(e.lngLat);
|
||||
popupPos.value = {
|
||||
x: pos.x + canvas.left,
|
||||
y: pos.y + canvas.top,
|
||||
};
|
||||
const buffer = 0.0001;
|
||||
const lng = e.lngLat.lng;
|
||||
const lat = e.lngLat.lat;
|
||||
|
||||
popupData.value = { ...prop, images, videos };
|
||||
popupVisible.value = true;
|
||||
// 构造一个小小的查询框,单位是度
|
||||
const bbox = [
|
||||
lng - buffer,
|
||||
lat - buffer,
|
||||
lng + buffer,
|
||||
lat + buffer
|
||||
].join(",");
|
||||
|
||||
const baseUrl = "/geoserver/lydc/wms";
|
||||
const layerName = "lydc:NYTB";
|
||||
|
||||
const params = new URLSearchParams({
|
||||
SERVICE: "WMS",
|
||||
VERSION: "1.1.1",
|
||||
REQUEST: "GetFeatureInfo",
|
||||
FORMAT: "image/png",
|
||||
TRANSPARENT: "true",
|
||||
QUERY_LAYERS: layerName,
|
||||
LAYERS: layerName,
|
||||
|
||||
// ✅ 关键 1: 告诉服务器我用经纬度查
|
||||
SRS: "EPSG:4326",
|
||||
// ✅ 关键 2: 传过去的是经纬度
|
||||
BBOX: bbox,
|
||||
|
||||
// ✅ 关键 3: 欺骗服务器,说这一小块区域是一张 101x101 的图,我点了正中间
|
||||
WIDTH: 101,
|
||||
HEIGHT: 101,
|
||||
X: 50,
|
||||
Y: 50,
|
||||
|
||||
INFO_FORMAT: "application/json",
|
||||
FEATURE_COUNT: 1,
|
||||
EXCEPTIONS: "application/vnd.ogc.se_xml"
|
||||
});
|
||||
|
||||
try {
|
||||
const url = `${baseUrl}?${params.toString()}`;
|
||||
|
||||
const wmsRes = await fetch(url);
|
||||
if (!wmsRes.ok) throw new Error(`HTTP ${wmsRes.status}`);
|
||||
|
||||
const wmsData = await wmsRes.json();
|
||||
|
||||
if (wmsData.features && wmsData.features.length > 0) {
|
||||
const wmsProp = wmsData.features[0].properties;
|
||||
|
||||
const nyxbh = wmsProp.nyxbh || wmsProp.NYXBH;
|
||||
let xiang = wmsProp.xiang || wmsProp.XIANG;
|
||||
|
||||
if (!xiang && (wmsProp.cun || wmsProp.CUN)) {
|
||||
const cunCode = String(wmsProp.cun || wmsProp.CUN);
|
||||
xiang = cunCode.substring(0, 9);
|
||||
}
|
||||
|
||||
if (nyxbh && xiang) {
|
||||
const detailData = await fetchPlotData(nyxbh, xiang);
|
||||
if (detailData) {
|
||||
const canvasRect = map.getCanvas().getBoundingClientRect();
|
||||
const pos = map.project(e.lngLat);
|
||||
popupPos.value = { x: pos.x + canvasRect.left, y: pos.y + canvasRect.top };
|
||||
popupData.value = detailData;
|
||||
popupVisible.value = true;
|
||||
} else {
|
||||
alert("未查询到详细数据");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log("❌ 未命中图斑 (请确认点击位置是否有数据)");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("查询出错:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 点击空白关闭弹窗
|
||||
@@ -402,73 +674,58 @@ function handleMapClickClosePopup(e) {
|
||||
}
|
||||
|
||||
// === 图斑层控制 ===
|
||||
// === 图斑层控制 ===
|
||||
let cachedPlotData = null;
|
||||
|
||||
function showPlotLayer() {
|
||||
// 已存在则直接显示
|
||||
if (map.getLayer("plot-fill")) {
|
||||
map.setLayoutProperty("plot-fill", "visibility", "visible");
|
||||
map.setLayoutProperty("plot-outline", "visibility", "visible");
|
||||
} else {
|
||||
map.addSource("plot", { type: "geojson", data: plotGeoJSON });
|
||||
|
||||
const typeColors = {
|
||||
乔木林: "#4CAF50",
|
||||
灌木林: "#8BC34A",
|
||||
草地: "#CDDC39",
|
||||
};
|
||||
|
||||
map.addLayer({
|
||||
id: "plot-fill",
|
||||
type: "fill",
|
||||
source: "plot",
|
||||
paint: {
|
||||
"fill-color": [
|
||||
"match",
|
||||
["get", "type"],
|
||||
"乔木林",
|
||||
typeColors["乔木林"],
|
||||
"灌木林",
|
||||
typeColors["灌木林"],
|
||||
"草地",
|
||||
typeColors["草地"],
|
||||
"#ccc",
|
||||
],
|
||||
"fill-opacity": 0.7,
|
||||
},
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: "plot-outline",
|
||||
type: "line",
|
||||
source: "plot",
|
||||
paint: { "line-color": "#333", "line-width": 1 },
|
||||
// 3. 如果 Source 不存在,添加 WMS 源
|
||||
if (!map.getSource("plot-wms-source")) {
|
||||
const LAYER_NAME = "lydc:NYTB";
|
||||
map.addSource("plot-wms-source", {
|
||||
type: "raster",
|
||||
tiles: [
|
||||
`/geoserver/lydc/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=${LAYER_NAME}&FORMAT=image/png&TRANSPARENT=true&WIDTH=256&HEIGHT=256&SRS=EPSG:3857&BBOX={bbox-epsg-3857}`
|
||||
],
|
||||
tileSize: 256
|
||||
});
|
||||
}
|
||||
|
||||
// 隐藏行政区划与街道层
|
||||
if (map.getLayer("tianjin-fill"))
|
||||
map.setLayoutProperty("tianjin-fill", "visibility", "none");
|
||||
if (map.getLayer("tianjin-outline"))
|
||||
map.setLayoutProperty("tianjin-outline", "visibility", "none");
|
||||
if (map.getLayer("street-fill"))
|
||||
map.setLayoutProperty("street-fill", "visibility", "none");
|
||||
if (map.getLayer("street-outline"))
|
||||
map.setLayoutProperty("street-outline", "visibility", "none");
|
||||
// 4. 添加 Raster 图层(如果不存在)
|
||||
if (!map.getLayer("plot-raster-layer")) {
|
||||
map.addLayer({
|
||||
id: "plot-raster-layer",
|
||||
type: "raster",
|
||||
source: "plot-wms-source",
|
||||
paint: {
|
||||
"raster-opacity": 1.0
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 5. 如果图层已存在,确保它是可见的
|
||||
map.setLayoutProperty("plot-raster-layer", "visibility", "visible");
|
||||
}
|
||||
|
||||
// 更新图例为图斑类型
|
||||
legendItems.value = [
|
||||
{ color: "#4CAF50", range: "乔木林" },
|
||||
{ color: "#8BC34A", range: "灌木林" },
|
||||
{ color: "#CDDC39", range: "草地" },
|
||||
];
|
||||
map.off("click", handlePlotClick);
|
||||
|
||||
// === 小班点击事件 ===
|
||||
map.off("click", "plot-fill", handlePlotClick);
|
||||
map.off("click", handleMapClickClosePopup);
|
||||
// 重新绑定点击事件
|
||||
map.on("click", handlePlotClick);
|
||||
|
||||
map.on("click", "plot-fill", handlePlotClick);
|
||||
map.on("click", handleMapClickClosePopup);
|
||||
// 改变鼠标样式为小手,提示用户可以点击
|
||||
map.getCanvas().style.cursor = 'pointer';
|
||||
|
||||
console.log("✅ 图斑层已显示");
|
||||
}
|
||||
function hideBaseLayers() {
|
||||
const layersToHide = [
|
||||
"tianjin-fill", "tianjin-outline", "tianjin-label",
|
||||
"street-fill", "street-outline"
|
||||
];
|
||||
layersToHide.forEach(layerId => {
|
||||
if (map.getLayer(layerId)) {
|
||||
map.setLayoutProperty(layerId, "visibility", "none");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// === 显示样地 ===
|
||||
// === 显示样地(Polygon) ===
|
||||
function showSamples() {
|
||||
@@ -527,26 +784,18 @@ function showSideTrees() {
|
||||
}
|
||||
|
||||
function hidePlotLayer() {
|
||||
// 隐藏图斑层
|
||||
if (map.getLayer("plot-fill")) {
|
||||
map.setLayoutProperty("plot-fill", "visibility", "none");
|
||||
map.setLayoutProperty("plot-outline", "visibility", "none");
|
||||
map.setLayoutProperty("samples-fill", "visibility", "none");
|
||||
map.setLayoutProperty("samples-outline", "visibility", "none");
|
||||
map.setLayoutProperty("sideTrees", "visibility", "none");
|
||||
// ✅ 隐藏 WMS 栅格图层
|
||||
if (map.getLayer("plot-raster-layer")) {
|
||||
map.setLayoutProperty("plot-raster-layer", "visibility", "none");
|
||||
}
|
||||
map.off("click", handlePlotClick);
|
||||
// 恢复鼠标样式
|
||||
map.getCanvas().style.cursor = '';
|
||||
// (保留) 隐藏之前的样地、四旁树等逻辑...
|
||||
if (map.getLayer("samples-fill")) map.setLayoutProperty("samples-fill", "visibility", "none");
|
||||
if (map.getLayer("samples-outline")) map.setLayoutProperty("samples-outline", "visibility", "none");
|
||||
if (map.getLayer("sideTrees")) map.setLayoutProperty("sideTrees", "visibility", "none");
|
||||
|
||||
// 恢复行政区划或街道层显示
|
||||
if (currentLevel.value === "district") {
|
||||
map.setLayoutProperty("tianjin-fill", "visibility", "visible");
|
||||
map.setLayoutProperty("tianjin-outline", "visibility", "visible");
|
||||
} else if (currentLevel.value === "street") {
|
||||
map.setLayoutProperty("street-fill", "visibility", "visible");
|
||||
map.setLayoutProperty("street-outline", "visibility", "visible");
|
||||
}
|
||||
|
||||
// 恢复行政区划/街道的图例
|
||||
updateMapColors(currentLevel.value);
|
||||
}
|
||||
|
||||
// === 样地弹窗状态 ===
|
||||
@@ -600,10 +849,50 @@ onBeforeUnmount(() => {
|
||||
.html,
|
||||
body,
|
||||
#app,
|
||||
.map-wrapper,
|
||||
#header {
|
||||
/* 固定高度,比百分比更稳定 */
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
|
||||
/* ✅ 核心颜色:深森林绿 */
|
||||
background-color: #5db862;
|
||||
color: white;
|
||||
|
||||
/* 布局:左对齐,垂直居中 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24px; /*哪怕文字很少,左右留白也好看 */
|
||||
box-sizing: border-box; /* 防止padding撑破宽度 */
|
||||
|
||||
/* 装饰:底部微弱阴影,增加层次感 */
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
z-index: 10; /* 保证压在地图上面 */
|
||||
}
|
||||
|
||||
/* 标题文字样式 */
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: 600; /* 半粗体 */
|
||||
letter-spacing: 1px; /* 字间距稍微拉开一点,更大气 */
|
||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* 3. 地图区域:自动填满剩余空间 */
|
||||
.map-wrapper {
|
||||
flex: 1; /* 自动占据 Header 剩下的高度 */
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #eef2f3; /* 地图加载前的背景色 */
|
||||
}
|
||||
|
||||
.map-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.map-wrapper,
|
||||
.map-container {
|
||||
width: 100%;
|
||||
height: 95%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -644,4 +933,45 @@ body,
|
||||
.back-btn:hover {
|
||||
background: #1565c0;
|
||||
}
|
||||
.control-item {
|
||||
background: white;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 4px rgba(0,0,0,0.2);
|
||||
margin-bottom: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.control-item label {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.date-panel {
|
||||
position: absolute;
|
||||
top: 80px; /* 距离 Header 下方 20px */
|
||||
right: 20px; /* 放在左上角 */
|
||||
z-index: 100; /* 保证在地图之上 */
|
||||
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
padding: 10px 15px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.date-label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 调整 Element Plus 输入框的宽度 */
|
||||
:deep(.el-date-editor.el-input) {
|
||||
width: 160px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,44 +4,85 @@
|
||||
class="plot-popup"
|
||||
:style="{ left: position.x + 'px', top: position.y + 'px' }"
|
||||
>
|
||||
<el-card shadow="always" class="popup-card">
|
||||
<h4>{{ data.name }}</h4>
|
||||
<p>类型:{{ data.type }}</p>
|
||||
<p>面积:{{ data.area }} ha</p>
|
||||
<p>采集人员:{{ data.collector }}</p>
|
||||
<p>优势树种:{{ data.species }}</p>
|
||||
<el-card shadow="always" class="popup-card" :body-style="{ padding: '10px', position: 'relative' }">
|
||||
|
||||
<!-- 图片与视频轮播 -->
|
||||
<el-carousel
|
||||
height="220px"
|
||||
indicator-position="outside"
|
||||
:interval="4000"
|
||||
arrow="always"
|
||||
v-if="mediaItems.length"
|
||||
>
|
||||
<el-carousel-item
|
||||
v-for="(item, index) in mediaItems"
|
||||
:key="index"
|
||||
class="carousel-item"
|
||||
<span class="close-btn" @click="$emit('close')">×</span>
|
||||
|
||||
<div class="popup-header">
|
||||
<span class="title">小班号:{{ data.nyxbh || '无编号' }}</span>
|
||||
<el-tag size="small" :type="data.status === '1' ? 'success' : 'info'">
|
||||
{{ data.status === '1' ? '已采集' : '未完成' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="label">位置:</span>
|
||||
<span class="value">{{ data.zlwz || data.cun || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">面积:</span>
|
||||
<span class="value">{{ Number(data.xbmj).toFixed(2) }} 亩</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">优势树种:</span>
|
||||
<span class="value">{{ data.szzc || '-' }}</span> </div>
|
||||
<div class="info-item">
|
||||
<span class="label">林种/地类:</span>
|
||||
<span class="value">{{ data.linZhong || data.diLei || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">采集人员:</span>
|
||||
<span class="value">{{ data.collectorName || data.collector || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">更新时间:</span>
|
||||
<span class="value">{{ formatDate(data.updateTime || data.createTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="media-container" v-if="mediaItems.length > 0">
|
||||
<el-carousel
|
||||
height="180px"
|
||||
indicator-position="none"
|
||||
:interval="5000"
|
||||
arrow="hover"
|
||||
>
|
||||
<template v-if="item.type === 'image'">
|
||||
<img :src="item.src" class="media" />
|
||||
</template>
|
||||
<template v-else-if="item.type === 'video'">
|
||||
<video controls class="media">
|
||||
<source :src="item.src" type="video/mp4" />
|
||||
</video>
|
||||
</template>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<el-carousel-item
|
||||
v-for="(item, index) in mediaItems"
|
||||
:key="index"
|
||||
>
|
||||
<template v-if="item.type === 'image'">
|
||||
<el-image
|
||||
:src="item.src"
|
||||
class="media-content"
|
||||
fit="cover"
|
||||
:preview-src-list="allImages"
|
||||
:initial-index="item.imgIndex"
|
||||
preview-teleported
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'video'">
|
||||
<video controls class="media-content">
|
||||
<source :src="item.src" type="video/mp4" />
|
||||
您的浏览器不支持视频播放。
|
||||
</video>
|
||||
</template>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<div class="media-count">{{ mediaItems.length }} 个附件</div>
|
||||
</div>
|
||||
<div v-else class="no-media">
|
||||
暂无现场照片/视频
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="popup-actions">
|
||||
<el-button type="primary" size="small" @click="$emit('enter-sample')">
|
||||
进入样地
|
||||
<el-button type="primary" size="small" plain @click="$emit('enter-sample')">
|
||||
查看样地详情
|
||||
</el-button>
|
||||
<el-button type="success" size="small" @click="$emit('enter-side')">
|
||||
进入四旁树
|
||||
<el-button type="success" size="small" plain @click="$emit('enter-side')">
|
||||
四旁树
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -50,11 +91,8 @@
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { ElCard, ElButton, ElCarousel, ElCarouselItem } from "element-plus";
|
||||
import "element-plus/es/components/card/style/css";
|
||||
import "element-plus/es/components/button/style/css";
|
||||
import "element-plus/es/components/carousel/style/css";
|
||||
import "element-plus/es/components/carousel-item/style/css";
|
||||
// 不需要引入 ElIcon 或 Close,直接用原生 CSS
|
||||
import { ElCard, ElButton, ElCarousel, ElCarouselItem, ElTag, ElImage } from "element-plus";
|
||||
|
||||
const props = defineProps({
|
||||
visible: Boolean,
|
||||
@@ -62,38 +100,172 @@ const props = defineProps({
|
||||
position: { type: Object, default: () => ({ x: 0, y: 0 }) },
|
||||
});
|
||||
|
||||
defineEmits(['enter-sample', 'enter-side', 'close']);
|
||||
|
||||
// 格式化日期,只显示 YYYY-MM-DD
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-';
|
||||
return dateStr.split(' ')[0];
|
||||
};
|
||||
|
||||
// 整合图片和视频,用于轮播展示
|
||||
const mediaItems = computed(() => {
|
||||
const imgs = Array.isArray(props.data.images) ? props.data.images : [];
|
||||
const vids = Array.isArray(props.data.videos) ? props.data.videos : [];
|
||||
|
||||
let imgCounter = 0;
|
||||
|
||||
return [
|
||||
...imgs.map((src) => ({ type: "image", src })),
|
||||
...imgs.map((src) => ({ type: "image", src, imgIndex: imgCounter++ })),
|
||||
...vids.map((src) => ({ type: "video", src })),
|
||||
];
|
||||
});
|
||||
|
||||
// 单独提取所有图片用于大图预览
|
||||
const allImages = computed(() => {
|
||||
return Array.isArray(props.data.images) ? props.data.images : [];
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.plot-popup {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
z-index: 2000;
|
||||
/* 调整弹窗位置锚点,使其位于点击点的上方居中 */
|
||||
transform: translate(-50%, -100%);
|
||||
margin-top: -15px; /* 留一点间距,别挡住点击点 */
|
||||
pointer-events: auto; /* 确保弹窗可交互 */
|
||||
}
|
||||
|
||||
/* 加上小三角箭头 */
|
||||
.plot-popup::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 6px 6px 0;
|
||||
border-style: solid;
|
||||
border-color: #fff transparent transparent transparent;
|
||||
filter: drop-shadow(0 2px 2px rgba(0,0,0,0.1));
|
||||
}
|
||||
|
||||
.popup-card {
|
||||
width: 280px;
|
||||
font-size: 13px;
|
||||
width: 300px; /* 稍微加宽一点 */
|
||||
border-radius: 8px;
|
||||
overflow: visible; /* 允许箭头显示 */
|
||||
}
|
||||
|
||||
.media {
|
||||
/* ✅ 2. 新增:关闭按钮样式 */
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 6px; /* 距离顶部 */
|
||||
right: 12px; /* 距离右边 */
|
||||
font-size: 24px; /* 字体大一点,方便点击 */
|
||||
font-weight: 300; /* 细一点更好看 */
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
z-index: 100;
|
||||
line-height: 1;
|
||||
transition: color 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #f56c6c; /* 悬停变红 */
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 8px;
|
||||
/* ✅ 给标题栏右侧留出空间,防止标题文字太长挡住关闭按钮 */
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 属性网格布局:两列显示 */
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px 10px;
|
||||
font-size: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 媒体区域 */
|
||||
.media-container {
|
||||
position: relative;
|
||||
background: #000;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.media-content {
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
height: 100%;
|
||||
object-fit: contain; /* 保持比例 */
|
||||
background-color: #000;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.media-count {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
right: 5px;
|
||||
background: rgba(0,0,0,0.6);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.no-media {
|
||||
height: 60px;
|
||||
background: #f5f7fa;
|
||||
color: #c0c4cc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
margin-top: 12px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #eee;
|
||||
}
|
||||
|
||||
.popup-actions .el-button {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -8,11 +8,18 @@ export default defineConfig({
|
||||
proxy: {
|
||||
// 只要你请求以 /api 开头,就会被转发到目标服务器
|
||||
'/api': {
|
||||
target: 'http://192.168.1.254:9001',
|
||||
target: 'http://192.168.1.196:9001',
|
||||
changeOrigin: true,
|
||||
// 如果后端路径本身就有 /api,就不需要 rewrite
|
||||
// 如果你希望把 /api 前缀去掉再转发,就打开下面这行:
|
||||
// rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
},
|
||||
'/photos':{
|
||||
target: 'http://8.130.135.159:8084',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/geoserver': {
|
||||
// 注意:这里必须写 /geoserver 结尾,且没有拼写错误
|
||||
target: 'http://8.130.135.159:8000/geoserver',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/geoserver/, '')
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user