更新样地和样木弹窗逻辑
This commit is contained in:
@@ -36,21 +36,16 @@ export const fetchPlotGeoJSON = async () => {
|
|||||||
return {
|
return {
|
||||||
...feature,
|
...feature,
|
||||||
properties: {
|
properties: {
|
||||||
// 保留原始属性
|
|
||||||
...p,
|
...p,
|
||||||
|
|
||||||
// --- 关键映射区域 (根据你的数据库字段修改左边的值) ---
|
|
||||||
name: p.xb_name || p.name || "未命名图斑",
|
name: p.xb_name || p.name || "未命名图斑",
|
||||||
|
|
||||||
// 这里的映射决定了地图颜色!确保数据库里有 "乔木林"/"灌木林" 等值
|
|
||||||
// 如果数据库存的是代码(1,2,3),这里需要写转换逻辑
|
|
||||||
type: p.forest_type || p.type || "其他",
|
type: p.forest_type || p.type || "其他",
|
||||||
|
|
||||||
area: p.area_mu || 0,
|
area: p.area_mu || 0,
|
||||||
collector: p.manager || "未知人员",
|
collector: p.manager || "未知人员",
|
||||||
|
|
||||||
// --- 图片/视频处理 ---
|
|
||||||
// 假设数据库里存的是 "url1,url2" 这样的字符串
|
|
||||||
images: p.image_urls ? p.image_urls.split(',') : [],
|
images: p.image_urls ? p.image_urls.split(',') : [],
|
||||||
videos: p.video_urls ? p.video_urls.split(',') : [],
|
videos: p.video_urls ? p.video_urls.split(',') : [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="map" ref="mapContainer" class="map-container"></div>
|
<div id="map" ref="mapContainer" class="map-container"></div>
|
||||||
<div id="dateSelectItem" class="date-panel">
|
<div id="dateSelectItem" class="date-panel">
|
||||||
<span class="date-label">数据日期:</span>
|
<span class="date-label">选择日期:</span>
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="selectedDate"
|
v-model="selectedDate"
|
||||||
type="date"
|
type="date"
|
||||||
@@ -16,6 +16,23 @@
|
|||||||
@change="handleDateChange"
|
@change="handleDateChange"
|
||||||
size="default"
|
size="default"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
v-if="!hasDateMarkers && selectedDate"
|
||||||
|
class="action-btn show-btn"
|
||||||
|
@click="handleShowData"
|
||||||
|
title="加载当前日期的统计数据"
|
||||||
|
>
|
||||||
|
<el-icon><Search /></el-icon> 显示数据
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="hasDateMarkers"
|
||||||
|
class="action-btn clear-btn"
|
||||||
|
@click="clearDateMarkers"
|
||||||
|
title="清除地图上的标注"
|
||||||
|
>
|
||||||
|
<el-icon><Close /></el-icon> 清除数据
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="legend-panel">
|
<div class="legend-panel">
|
||||||
@@ -23,7 +40,7 @@
|
|||||||
<div class="control-item group-item">
|
<div class="control-item group-item">
|
||||||
<label class="group-header">
|
<label class="group-header">
|
||||||
<input type="checkbox" v-model="isStatLayerVisible" />
|
<input type="checkbox" v-model="isStatLayerVisible" />
|
||||||
<span>统计数据 ({{ currentLevel === 'district' ? '区级' : '街道' }})</span>
|
<span>统计数据 </span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div v-if="isStatLayerVisible" class="group-body">
|
<div v-if="isStatLayerVisible" class="group-body">
|
||||||
@@ -36,13 +53,13 @@
|
|||||||
<div class="control-item">
|
<div class="control-item">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" v-model="isPlotLayerVisible" />
|
<input type="checkbox" v-model="isPlotLayerVisible" />
|
||||||
所有图斑
|
小班图层
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-item">
|
<div class="control-item">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" />
|
<input type="checkbox" v-model="isSampleLayerVisible" />
|
||||||
样地图斑
|
样地图层
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -60,7 +77,6 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, onBeforeUnmount, ref, watch } from "vue";
|
import { onMounted, onBeforeUnmount, ref, watch } from "vue";
|
||||||
import { Close } from "@element-plus/icons-vue";
|
|
||||||
import maplibregl from "maplibre-gl";
|
import maplibregl from "maplibre-gl";
|
||||||
import "maplibre-gl/dist/maplibre-gl.css";
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
import { TDT_KEY } from "../assets/tdt_key.js";
|
import { TDT_KEY } from "../assets/tdt_key.js";
|
||||||
@@ -70,12 +86,15 @@ import { samplePlots, sideTrees } from "../assets/mockPlotData.js";
|
|||||||
import PlotPopup from "./PlotPopup.vue";
|
import PlotPopup from "./PlotPopup.vue";
|
||||||
import SamplePopup from "./SamplePopup.vue";
|
import SamplePopup from "./SamplePopup.vue";
|
||||||
import bbox from "@turf/bbox";
|
import bbox from "@turf/bbox";
|
||||||
|
import { Close, Search } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
|
||||||
const popupVisible = ref(false);
|
const popupVisible = ref(false);
|
||||||
const popupData = ref({});
|
const popupData = ref({});
|
||||||
const popupPos = ref({ x: 0, y: 0 });
|
const popupPos = ref({ x: 0, y: 0 });
|
||||||
const isPlotLayerVisible = ref(false);
|
const isPlotLayerVisible = ref(false);
|
||||||
const isStatLayerVisible = ref(true);
|
const isStatLayerVisible = ref(true);
|
||||||
|
const isSampleLayerVisible = ref(false);
|
||||||
|
|
||||||
// 天津市行政区划数据(区级)
|
// 天津市行政区划数据(区级)
|
||||||
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";
|
||||||
@@ -96,6 +115,14 @@ const mapContainer = ref(null);
|
|||||||
let map = null;
|
let map = null;
|
||||||
const legendItems = ref([]);
|
const legendItems = ref([]);
|
||||||
let allStreetsData = null;
|
let allStreetsData = null;
|
||||||
|
const districtCountCache=ref({});
|
||||||
|
let hoverPopup=null;
|
||||||
|
const dateMarkers = ref([]); // 存储 Marker 实例
|
||||||
|
const hasDateMarkers = ref(false); // 控制清除按钮显示
|
||||||
|
const districtCenters = {};
|
||||||
|
const SAMPLE_SOURCE_ID = "sample-wms-source";
|
||||||
|
const SAMPLE_LAYER_ID = "sample-raster-layer";
|
||||||
|
const SAMPLE_LAYER_NAME = "lydc:YDTB";
|
||||||
watch(isStatLayerVisible, () => {
|
watch(isStatLayerVisible, () => {
|
||||||
updateStatLayerState();
|
updateStatLayerState();
|
||||||
});
|
});
|
||||||
@@ -107,17 +134,112 @@ watch(isPlotLayerVisible, (newVal) => {
|
|||||||
hidePlotLayer();
|
hidePlotLayer();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
watch(isSampleLayerVisible, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
showSamplePlotLayer();
|
||||||
|
} else {
|
||||||
|
hideSamplePlotLayer();
|
||||||
|
}
|
||||||
|
});
|
||||||
const selectedDate = ref(new Date().toISOString().split('T')[0]); // 默认今天
|
const selectedDate = ref(new Date().toISOString().split('T')[0]); // 默认今天
|
||||||
|
|
||||||
// ✅ 新增:日期变更处理函数
|
function calculateDistrictCenters(geojson) {
|
||||||
function handleDateChange(val) {
|
if (!geojson || !geojson.features) return;
|
||||||
|
geojson.features.forEach(feature => {
|
||||||
|
const name = feature.properties.name;
|
||||||
|
if (name) {
|
||||||
|
// 使用 bbox 计算几何中心
|
||||||
|
const [minX, minY, maxX, maxY] = bbox(feature);
|
||||||
|
const centerX = (minX + maxX) / 2;
|
||||||
|
const centerY = (minY + maxY) / 2;
|
||||||
|
districtCenters[name] = [centerX, centerY];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log("📍 行政区中心点计算完毕:", districtCenters);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ✅ 修改:日期变更处理 ---
|
||||||
|
async function handleDateChange(val) {
|
||||||
|
if (!val) return;
|
||||||
console.log("📅 日期已切换为:", val);
|
console.log("📅 日期已切换为:", val);
|
||||||
|
|
||||||
// 在这里添加你的逻辑,例如:
|
// 1. 先清除旧的标注
|
||||||
// 1. 重新请求统计接口
|
clearDateMarkers();
|
||||||
// fetchDistrictStatus(val);
|
|
||||||
// 2. 刷新 WMS 图层 (如果 WMS 支持 TIME 参数)
|
// 2. 请求新数据
|
||||||
// refreshWmsLayer(val);
|
await fetchDateStats(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ✅ 新增:请求日期统计数据 ---
|
||||||
|
async function fetchDateStats(dateStr) {
|
||||||
|
try {
|
||||||
|
const url = `/api/stats/date?date=${dateStr}`;
|
||||||
|
const res = await fetch(url);
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.code === 0 && json.data && json.data.length > 0) {
|
||||||
|
renderDateMarkers(json.data);
|
||||||
|
} else {
|
||||||
|
console.log("该日期无数据");
|
||||||
|
// Element Plus 的消息提示 (如果项目里有引入 ElMessage)
|
||||||
|
// ElMessage.warning("该日期无统计数据");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("日期统计请求失败:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ✅ 新增:渲染标注 ---
|
||||||
|
function renderDateMarkers(dataList) {
|
||||||
|
// 遍历后端返回的数据
|
||||||
|
dataList.forEach(item => {
|
||||||
|
const name = item.district;
|
||||||
|
const center = districtCenters[name];
|
||||||
|
|
||||||
|
// 如果能找到该区的坐标
|
||||||
|
if (center) {
|
||||||
|
// 1. 创建自定义 HTML 元素
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'date-stat-marker';
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="marker-title">${name}</div>
|
||||||
|
<div class="marker-row">完成小班: <b>${item.plotcount}</b></div>
|
||||||
|
<div class="marker-row">完成样地: <b>${item.sampleplotcount}</b></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 2. 创建 MapLibre Marker
|
||||||
|
const marker = new maplibregl.Marker({
|
||||||
|
element: el,
|
||||||
|
anchor: 'bottom', // 标注的“脚”对准中心点
|
||||||
|
offset: [0, -10] // 稍微往上提一点,不遮挡文字
|
||||||
|
})
|
||||||
|
.setLngLat(center)
|
||||||
|
.addTo(map);
|
||||||
|
|
||||||
|
// 3. 存入数组方便管理
|
||||||
|
dateMarkers.value.push(marker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dateMarkers.value.length > 0) {
|
||||||
|
hasDateMarkers.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ✅ 新增:点击“显示数据”按钮的处理函数
|
||||||
|
async function handleShowData() {
|
||||||
|
if (!selectedDate.value) {
|
||||||
|
alert("请先选择日期");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 手动触发请求
|
||||||
|
await fetchDateStats(selectedDate.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ✅ 新增:清除标注 ---
|
||||||
|
function clearDateMarkers() {
|
||||||
|
dateMarkers.value.forEach(marker => marker.remove());
|
||||||
|
dateMarkers.value = [];
|
||||||
|
hasDateMarkers.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 可选:禁用未来日期
|
// ✅ 可选:禁用未来日期
|
||||||
@@ -172,7 +294,23 @@ async function fetchDistrictStatus(){
|
|||||||
console.error("获取区级指标数据失败:",error);
|
console.error("获取区级指标数据失败:",error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function fetchDistrictCount(districtName){
|
||||||
|
if (districtCountCache.value[districtName]){
|
||||||
|
return districtCountCache.value[districtName];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url=`/api/stats/district/count?district=${encodeURIComponent(districtName)}`;
|
||||||
|
const res=await fetch(url);
|
||||||
|
const json=await res.json();
|
||||||
|
if(json.code===0&&json.data){
|
||||||
|
districtCountCache.value[districtName]=json.data;
|
||||||
|
return json.data;
|
||||||
|
}
|
||||||
|
}catch (error){
|
||||||
|
console.log(`获取${districtName}图斑数量失败:`,error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// 街道指标
|
// 街道指标
|
||||||
const streetIndicatorData = ref({
|
const streetIndicatorData = ref({
|
||||||
完成状态:{},
|
完成状态:{},
|
||||||
@@ -293,6 +431,19 @@ function processPlotData(data) {
|
|||||||
videos // 只有视频链接的数组
|
videos // 只有视频链接的数组
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
async function fetchPlotStats(rootId, databaseName) {
|
||||||
|
try {
|
||||||
|
const url = `/api/stats/plot/count?rootId=${rootId}&databaseName=${encodeURIComponent(databaseName)}`;
|
||||||
|
const res = await fetch(url);
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.code === 0) {
|
||||||
|
return json.data;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取小班下样地统计失败:", error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// 定义点击区级多边形的处理函数
|
// 定义点击区级多边形的处理函数
|
||||||
async function handleDistrictClick(e) {
|
async function handleDistrictClick(e) {
|
||||||
const districtName = e.features[0].properties.name;
|
const districtName = e.features[0].properties.name;
|
||||||
@@ -364,6 +515,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
const res = await fetch(districtUrl);
|
const res = await fetch(districtUrl);
|
||||||
const geojson = await res.json();
|
const geojson = await res.json();
|
||||||
|
calculateDistrictCenters(geojson);
|
||||||
|
|
||||||
map.on("load", () => {
|
map.on("load", () => {
|
||||||
map.addSource("tianjin", { type: "geojson", data: geojson });
|
map.addSource("tianjin", { type: "geojson", data: geojson });
|
||||||
@@ -401,13 +553,101 @@ onMounted(async () => {
|
|||||||
"text-halo-width": 2 // 描边宽度
|
"text-halo-width": 2 // 描边宽度
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
updateMapColors();
|
updateMapColors();
|
||||||
|
|
||||||
// 点击区多边形,下钻街道级
|
// 点击区多边形,下钻街道级
|
||||||
map.off("click", "tianjin-fill", handleDistrictClick);
|
// map.off("click", "tianjin-fill", handleDistrictClick);
|
||||||
map.on("click", "tianjin-fill", handleDistrictClick);
|
// map.on("click", "tianjin-fill", handleDistrictClick);
|
||||||
|
|
||||||
|
|
||||||
|
hoverPopup = new maplibregl.Popup({
|
||||||
|
closeButton: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
className: 'district-hpver-popup',
|
||||||
|
maxWidth: '300px'
|
||||||
|
});
|
||||||
|
let lastHoveredDistrict = null;
|
||||||
|
|
||||||
|
// 鼠标移动事件
|
||||||
|
map.on('mousemove', 'tianjin-fill', async (e) => {
|
||||||
|
// 1. 基础校验
|
||||||
|
if (currentLevel.value !== 'district' || !isStatLayerVisible.value) return;
|
||||||
|
|
||||||
|
const features = map.queryRenderedFeatures(e.point, { layers: ['tianjin-fill'] });
|
||||||
|
if (!features.length) {
|
||||||
|
// 如果虽然在图层范围内但没取到要素(极端情况),也视为移出
|
||||||
|
hoverPopup.remove();
|
||||||
|
lastHoveredDistrict = null;
|
||||||
|
map.getCanvas().style.cursor = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const districtName = features[0].properties.name;
|
||||||
|
|
||||||
|
// 2. 始终更新:鼠标样式和弹窗位置(让弹窗跟手)
|
||||||
|
map.getCanvas().style.cursor = "pointer";
|
||||||
|
hoverPopup.setLngLat(e.lngLat);
|
||||||
|
|
||||||
|
// 🔥🔥🔥 核心修复:如果鼠标还在同一个区内,直接返回,不执行后面逻辑!
|
||||||
|
if (lastHoveredDistrict === districtName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 只有当进入新区域时,才执行下面的代码
|
||||||
|
lastHoveredDistrict = districtName; // 更新当前区名
|
||||||
|
|
||||||
|
// 先显示弹窗(加载状态),提升交互响应速度
|
||||||
|
if (!hoverPopup.isOpen()) hoverPopup.addTo(map);
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
let countData = districtCountCache.value[districtName];
|
||||||
|
|
||||||
|
if (!countData) {
|
||||||
|
// 缓存没有,显示加载中
|
||||||
|
hoverPopup.setHTML(`
|
||||||
|
<div class="hover-content">
|
||||||
|
<div class="hover-title">${districtName}</div>
|
||||||
|
<div class="hover-loading">暂无数据</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 发起请求
|
||||||
|
countData = await fetchDistrictCount(districtName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 数据获取回来后,再次确认:鼠标是否还停留在该区?
|
||||||
|
// (防止网络慢,请求回来时鼠标已经移到别的区了,导致弹窗内容错乱)
|
||||||
|
if (lastHoveredDistrict === districtName && countData) {
|
||||||
|
const htmlContent = `
|
||||||
|
<div class="hover-content">
|
||||||
|
<div class="hover-title">${districtName}</div>
|
||||||
|
<div class="hover-row">
|
||||||
|
<span class="label">内业数量:</span>
|
||||||
|
<span class="value val-ny">${countData.ny || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div class="hover-row">
|
||||||
|
<span class="label">外业完成:</span>
|
||||||
|
<span class="value val-wy">${countData.wy || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div class="hover-row">
|
||||||
|
<span class="label">样地数量:</span>
|
||||||
|
<span class="value val-yd">${countData.yd || 0}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
hoverPopup.setHTML(htmlContent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 鼠标移出事件
|
||||||
|
map.on('mouseleave', 'tianjin-fill', () => {
|
||||||
|
map.getCanvas().style.cursor = "";
|
||||||
|
// 清空记录
|
||||||
|
lastHoveredDistrict = null;
|
||||||
|
if (hoverPopup) {
|
||||||
|
hoverPopup.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -599,7 +839,7 @@ async function handlePlotClick(e) {
|
|||||||
].join(",");
|
].join(",");
|
||||||
|
|
||||||
const baseUrl = "/geoserver/lydc/wms";
|
const baseUrl = "/geoserver/lydc/wms";
|
||||||
const layerName = "lydc:NYTB";
|
const layerName = "lydc:NYTB4";
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
SERVICE: "WMS",
|
SERVICE: "WMS",
|
||||||
@@ -633,6 +873,7 @@ async function handlePlotClick(e) {
|
|||||||
if (!wmsRes.ok) throw new Error(`HTTP ${wmsRes.status}`);
|
if (!wmsRes.ok) throw new Error(`HTTP ${wmsRes.status}`);
|
||||||
|
|
||||||
const wmsData = await wmsRes.json();
|
const wmsData = await wmsRes.json();
|
||||||
|
console.log("WMS GetFeatureInfo 返回:", wmsData);
|
||||||
|
|
||||||
if (wmsData.features && wmsData.features.length > 0) {
|
if (wmsData.features && wmsData.features.length > 0) {
|
||||||
const wmsProp = wmsData.features[0].properties;
|
const wmsProp = wmsData.features[0].properties;
|
||||||
@@ -644,14 +885,19 @@ async function handlePlotClick(e) {
|
|||||||
const cunCode = String(wmsProp.cun || wmsProp.CUN);
|
const cunCode = String(wmsProp.cun || wmsProp.CUN);
|
||||||
xiang = cunCode.substring(0, 9);
|
xiang = cunCode.substring(0, 9);
|
||||||
}
|
}
|
||||||
|
const rootId = wmsProp.id || wmsProp.ID;
|
||||||
|
const databaseName = wmsProp.database_n || wmsProp.DATABASE_N;
|
||||||
|
|
||||||
if (nyxbh && xiang) {
|
if (nyxbh && xiang&&rootId&&databaseName) {
|
||||||
const detailData = await fetchPlotData(nyxbh, xiang);
|
const [plotData, sampleData] = await Promise.all([
|
||||||
if (detailData) {
|
fetchPlotData(nyxbh, xiang),
|
||||||
|
fetchPlotStats(rootId, databaseName)
|
||||||
|
]);
|
||||||
|
if (plotData&&sampleData) {
|
||||||
const canvasRect = map.getCanvas().getBoundingClientRect();
|
const canvasRect = map.getCanvas().getBoundingClientRect();
|
||||||
const pos = map.project(e.lngLat);
|
const pos = map.project(e.lngLat);
|
||||||
popupPos.value = { x: pos.x + canvasRect.left, y: pos.y + canvasRect.top };
|
popupPos.value = { x: pos.x + canvasRect.left, y: pos.y + canvasRect.top };
|
||||||
popupData.value = detailData;
|
popupData.value = {plotData, sampleData };
|
||||||
popupVisible.value = true;
|
popupVisible.value = true;
|
||||||
} else {
|
} else {
|
||||||
alert("未查询到详细数据");
|
alert("未查询到详细数据");
|
||||||
@@ -678,9 +924,9 @@ function handleMapClickClosePopup(e) {
|
|||||||
let cachedPlotData = null;
|
let cachedPlotData = null;
|
||||||
|
|
||||||
function showPlotLayer() {
|
function showPlotLayer() {
|
||||||
// 3. 如果 Source 不存在,添加 WMS 源
|
// 1. 添加 Source (保持不变)
|
||||||
if (!map.getSource("plot-wms-source")) {
|
if (!map.getSource("plot-wms-source")) {
|
||||||
const LAYER_NAME = "lydc:NYTB";
|
const LAYER_NAME = "lydc:NYTB4";
|
||||||
map.addSource("plot-wms-source", {
|
map.addSource("plot-wms-source", {
|
||||||
type: "raster",
|
type: "raster",
|
||||||
tiles: [
|
tiles: [
|
||||||
@@ -690,41 +936,74 @@ function showPlotLayer() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 添加 Raster 图层(如果不存在)
|
// 2. 添加 Layer
|
||||||
if (!map.getLayer("plot-raster-layer")) {
|
if (!map.getLayer("plot-raster-layer")) {
|
||||||
|
// ✅ 关键修改:添加时,尝试将其放在样地图层下面 (作为 beforeId)
|
||||||
|
// 如果 SAMPLE_LAYER_ID 不存在,beforeId 为 undefined,图层会加到最上面
|
||||||
|
const beforeId = map.getLayer(SAMPLE_LAYER_ID) ? SAMPLE_LAYER_ID : undefined;
|
||||||
|
|
||||||
map.addLayer({
|
map.addLayer({
|
||||||
id: "plot-raster-layer",
|
id: "plot-raster-layer",
|
||||||
type: "raster",
|
type: "raster",
|
||||||
source: "plot-wms-source",
|
source: "plot-wms-source",
|
||||||
paint: {
|
paint: { "raster-opacity": 1.0 }
|
||||||
"raster-opacity": 1.0
|
}, beforeId); // <-- 这里传第二个参数
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// 5. 如果图层已存在,确保它是可见的
|
|
||||||
map.setLayoutProperty("plot-raster-layer", "visibility", "visible");
|
map.setLayoutProperty("plot-raster-layer", "visibility", "visible");
|
||||||
|
|
||||||
|
// ✅ 关键修改:如果图层已存在,且样地图层也在,强制把小班图层挪到样地下面
|
||||||
|
if (map.getLayer(SAMPLE_LAYER_ID)) {
|
||||||
|
map.moveLayer("plot-raster-layer", SAMPLE_LAYER_ID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
map.off("click", handlePlotClick);
|
map.off("click", handlePlotClick);
|
||||||
|
|
||||||
// 重新绑定点击事件
|
|
||||||
map.on("click", handlePlotClick);
|
map.on("click", handlePlotClick);
|
||||||
|
|
||||||
// 改变鼠标样式为小手,提示用户可以点击
|
|
||||||
map.getCanvas().style.cursor = 'pointer';
|
map.getCanvas().style.cursor = 'pointer';
|
||||||
|
|
||||||
console.log("✅ 图斑层已显示");
|
console.log("✅ 图斑层已显示");
|
||||||
}
|
}
|
||||||
function hideBaseLayers() {
|
|
||||||
const layersToHide = [
|
function showSamplePlotLayer() {
|
||||||
"tianjin-fill", "tianjin-outline", "tianjin-label",
|
// 1. 添加 Source (保持不变)
|
||||||
"street-fill", "street-outline"
|
if (!map.getSource(SAMPLE_SOURCE_ID)) {
|
||||||
];
|
map.addSource(SAMPLE_SOURCE_ID, {
|
||||||
layersToHide.forEach(layerId => {
|
type: "raster",
|
||||||
if (map.getLayer(layerId)) {
|
tiles: [
|
||||||
map.setLayoutProperty(layerId, "visibility", "none");
|
`/geoserver/lydc/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=${SAMPLE_LAYER_NAME}&FORMAT=image/png&TRANSPARENT=true&WIDTH=256&HEIGHT=256&SRS=EPSG:3857&BBOX={bbox-epsg-3857}`
|
||||||
}
|
],
|
||||||
});
|
tileSize: 256
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 添加 Layer
|
||||||
|
if (!map.getLayer(SAMPLE_LAYER_ID)) {
|
||||||
|
map.addLayer({
|
||||||
|
id: SAMPLE_LAYER_ID,
|
||||||
|
type: "raster",
|
||||||
|
source: SAMPLE_SOURCE_ID,
|
||||||
|
paint: { "raster-opacity": 1.0 }
|
||||||
|
});
|
||||||
|
// 添加后,如果小班图层存在,样地默认就在它上面(因为是后加的),所以不用特别处理
|
||||||
|
} else {
|
||||||
|
map.setLayoutProperty(SAMPLE_LAYER_ID, "visibility", "visible");
|
||||||
|
|
||||||
|
// ✅ 关键修改:显式将样地图层挪到最上面 (或者挪到高亮图层下面)
|
||||||
|
// 简单做法:直接 moveLayer 到顶层
|
||||||
|
map.moveLayer(SAMPLE_LAYER_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 额外优化:如果这时候有高亮的矢量图层(如选中的样地框),要把它们重新提到最上面,否则会被WMS遮住
|
||||||
|
if (map.getLayer("samples-fill")) map.moveLayer("samples-fill");
|
||||||
|
if (map.getLayer("samples-outline")) map.moveLayer("samples-outline");
|
||||||
|
if (map.getLayer("sideTrees")) map.moveLayer("sideTrees");
|
||||||
|
|
||||||
|
console.log("✅ 样地 WMS 图层已加载");
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSamplePlotLayer() {
|
||||||
|
if (map.getLayer(SAMPLE_LAYER_ID)) {
|
||||||
|
map.setLayoutProperty(SAMPLE_LAYER_ID, "visibility", "none");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// === 显示样地 ===
|
// === 显示样地 ===
|
||||||
// === 显示样地(Polygon) ===
|
// === 显示样地(Polygon) ===
|
||||||
@@ -950,28 +1229,104 @@ body,
|
|||||||
}
|
}
|
||||||
.date-panel {
|
.date-panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 80px; /* 距离 Header 下方 20px */
|
top: 80px;
|
||||||
right: 20px; /* 放在左上角 */
|
right: 20px;
|
||||||
z-index: 100; /* 保证在地图之上 */
|
z-index: 100;
|
||||||
|
|
||||||
background-color: rgba(255, 255, 255, 0.95);
|
background-color: rgba(255, 255, 255, 0.95);
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px; /* 元素之间的间距 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-label {
|
/* 按钮通用基础样式 */
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
height: 32px; /* 和 Element Plus 的输入框高度一致 */
|
||||||
|
color: white;
|
||||||
|
transition: background 0.3s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 🔵 显示按钮:蓝色 */
|
||||||
|
.show-btn {
|
||||||
|
background-color: #409eff; /* Element Blue */
|
||||||
|
}
|
||||||
|
.show-btn:hover {
|
||||||
|
background-color: #66b1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 🔴 清除按钮:红色 */
|
||||||
|
.clear-btn {
|
||||||
|
background-color: #f56c6c; /* Element Red */
|
||||||
|
}
|
||||||
|
.clear-btn:hover {
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图标微调 */
|
||||||
|
.action-btn :deep(.el-icon) {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 调整 Element Plus 输入框的宽度 */
|
/* 调整 Element Plus 输入框的宽度 */
|
||||||
:deep(.el-date-editor.el-input) {
|
:deep(.el-date-editor.el-input) {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
}
|
}
|
||||||
|
:deep(.district-hover-popup .maplibregl-popup-content) {
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||||
|
font-family: "Microsoft YaHei", sans-serif;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.hover-content) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.hover-title) {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.hover-row) {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.hover-row .value) {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 给不同数值加点颜色区分 */
|
||||||
|
:deep(.val-ny) { color: #1976d2; }
|
||||||
|
:deep(.val-wy) { color: #388e3c; }
|
||||||
|
:deep(.val-yd) { color: #f57c00; }
|
||||||
|
|
||||||
|
:deep(.hover-loading) {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,148 +1,318 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="visible"
|
v-if="visible"
|
||||||
class="plot-popup"
|
class="plot-popup-container"
|
||||||
:style="{ left: position.x + 'px', top: position.y + 'px' }"
|
:style="{ left: position.x + 'px', top: position.y + 'px' }"
|
||||||
>
|
>
|
||||||
<el-card shadow="always" class="popup-card" :body-style="{ padding: '10px', position: 'relative' }">
|
<div class="popup-wrapper">
|
||||||
|
|
||||||
<span class="close-btn" @click="$emit('close')">×</span>
|
<el-card shadow="always" class="popup-card" :body-style="{ padding: '0', position: 'relative' }">
|
||||||
|
<span class="close-btn" @click="$emit('close')">×</span>
|
||||||
|
|
||||||
<div class="popup-header">
|
<div class="popup-header-wrapper">
|
||||||
<span class="title">小班号:{{ data.nyxbh || '无编号' }}</span>
|
<div class="popup-header">
|
||||||
<el-tag size="small" :type="data.status === '1' ? 'success' : 'info'">
|
<span class="title">小班号:{{ data.plotData?.nyxbh || '无编号' }}</span>
|
||||||
{{ data.status === '1' ? '已采集' : '未完成' }}
|
<el-tag size="small" :type="data.plotData?.status === '1' ? 'success' : 'info'">
|
||||||
</el-tag>
|
{{ data.plotData?.status === '1' ? '已采集' : '未完成' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scroll-content">
|
||||||
|
<div class="content-padding">
|
||||||
|
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item" v-for="(label, key) in plotMap" :key="key">
|
||||||
|
<span class="label">{{ label }}:</span>
|
||||||
|
<span class="value">{{ getPlotDisplay(key) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="media-container" v-if="mediaItems.length > 0">
|
||||||
|
<el-carousel height="180px" indicator-position="none" :interval="5000" arrow="hover">
|
||||||
|
<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="divider" v-if="data.sampleData"></div>
|
||||||
|
|
||||||
|
<div class="stats-group" v-if="data.sampleData">
|
||||||
|
<div class="stats-title">小班下样地统计</div>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-box" v-for="(val, key) in data.sampleData" :key="key">
|
||||||
|
<span class="stat-name">{{ getStatLabel(key) }}</span>
|
||||||
|
<span class="stat-num">{{ val }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="popup-actions">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
plain
|
||||||
|
@click="handleViewSample"
|
||||||
|
:disabled="!data.sampleData"
|
||||||
|
:loading="loadingSample"
|
||||||
|
>
|
||||||
|
查看样地详情
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<div v-if="sampleVisible" class="sample-wrapper">
|
||||||
|
<SamplePopup
|
||||||
|
:visible="true"
|
||||||
|
:data="sampleData"
|
||||||
|
@close="sampleVisible = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-grid">
|
</div>
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<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" plain @click="$emit('enter-sample')">
|
|
||||||
查看样地详情
|
|
||||||
</el-button>
|
|
||||||
<el-button type="success" size="small" plain @click="$emit('enter-side')">
|
|
||||||
四旁树
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from "vue";
|
import { computed, ref } from "vue";
|
||||||
// 不需要引入 ElIcon 或 Close,直接用原生 CSS
|
|
||||||
import { ElCard, ElButton, ElCarousel, ElCarouselItem, ElTag, ElImage } from "element-plus";
|
import { ElCard, ElButton, ElCarousel, ElCarouselItem, ElTag, ElImage } from "element-plus";
|
||||||
|
// ✅ 引入样地组件
|
||||||
|
import SamplePopup from "./SamplePopup.vue";
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
data: { type: Object, default: () => ({}) },
|
data: { type: Object, default: () => ({ plotData: {}, sampleData: {} }) },
|
||||||
position: { type: Object, default: () => ({ x: 0, y: 0 }) },
|
position: { type: Object, default: () => ({ x: 0, y: 0 }) },
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(['enter-sample', 'enter-side', 'close']);
|
// 因为逻辑内聚了,这里不需要 emit view-sample 了
|
||||||
|
const emit = defineEmits(['enter-sample', 'enter-side', 'close']);
|
||||||
|
|
||||||
// 格式化日期,只显示 YYYY-MM-DD
|
const MEDIA_BASE_URL = '/photos'; // 媒体路径前缀
|
||||||
|
|
||||||
|
// --- 样地弹窗相关状态 ---
|
||||||
|
const sampleVisible = ref(false);
|
||||||
|
const sampleData = ref({});
|
||||||
|
const loadingSample = ref(false);
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
const formatDate = (dateStr) => {
|
const formatDate = (dateStr) => {
|
||||||
if (!dateStr) return '-';
|
if (!dateStr) return '-';
|
||||||
return dateStr.split(' ')[0];
|
return dateStr.split(' ')[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 整合图片和视频,用于轮播展示
|
// 媒体数据处理
|
||||||
const mediaItems = computed(() => {
|
const mediaItems = computed(() => {
|
||||||
const imgs = Array.isArray(props.data.images) ? props.data.images : [];
|
const pd = props.data.plotData || {};
|
||||||
const vids = Array.isArray(props.data.videos) ? props.data.videos : [];
|
const imgs = Array.isArray(pd.images) ? pd.images : [];
|
||||||
|
const vids = Array.isArray(pd.videos) ? pd.videos : [];
|
||||||
let imgCounter = 0;
|
let imgCounter = 0;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...imgs.map((src) => ({ type: "image", src, imgIndex: imgCounter++ })),
|
...imgs.map((src) => ({ type: "image", src, imgIndex: imgCounter++ })),
|
||||||
...vids.map((src) => ({ type: "video", src })),
|
...vids.map((src) => ({ type: "video", src })),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
// 单独提取所有图片用于大图预览
|
|
||||||
const allImages = computed(() => {
|
const allImages = computed(() => {
|
||||||
return Array.isArray(props.data.images) ? props.data.images : [];
|
const pd = props.data.plotData || {};
|
||||||
|
return Array.isArray(pd.images) ? pd.images : [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 字段映射表
|
||||||
|
const plotMap = {
|
||||||
|
xian: "县名",
|
||||||
|
xiang: "乡名",
|
||||||
|
cun: "村名",
|
||||||
|
nyxbh: "小班号",
|
||||||
|
zlwz: "造林位置",
|
||||||
|
xbmj: "小班面积",
|
||||||
|
lmsyqs: "林木所有权",
|
||||||
|
lmqs: "林木权属",
|
||||||
|
tdsyq: "土地所有权",
|
||||||
|
diLei: "地类",
|
||||||
|
linZhong: "林种",
|
||||||
|
sllb: "森林类别",
|
||||||
|
szzc: "树种组成",
|
||||||
|
yssz: "优势树种",
|
||||||
|
qiYuan: "起源",
|
||||||
|
yuBiDu: "郁闭度",
|
||||||
|
pjSg: "平均树高",
|
||||||
|
pjXj: "平均胸径",
|
||||||
|
pjnl: "平均年龄",
|
||||||
|
xbzs: "小班株数",
|
||||||
|
meiGqXj: "每公顷蓄积",
|
||||||
|
huoLmXj: "活立木蓄积",
|
||||||
|
diMao: "地貌",
|
||||||
|
poDu: "坡度",
|
||||||
|
poXiang: "坡向",
|
||||||
|
poWei: "坡位",
|
||||||
|
tchd: "土层厚度",
|
||||||
|
gclx: "工程类型",
|
||||||
|
bcl: "保存率",
|
||||||
|
bhdj: "公益林保护等级",
|
||||||
|
bhlydj: "保护利用等级",
|
||||||
|
sljkdj: "森林健康等级",
|
||||||
|
slzhdj: "森林灾害等级",
|
||||||
|
collectorName: "采集人员",
|
||||||
|
updateTime: "更新时间"
|
||||||
|
};
|
||||||
|
|
||||||
|
function getPlotDisplay(key) {
|
||||||
|
const pd = props.data.plotData || {};
|
||||||
|
const root = props.data || {};
|
||||||
|
let val = pd[key];
|
||||||
|
|
||||||
|
if (key === 'zlwz') val = val || root.cun;
|
||||||
|
if (key === 'xbmj') return val ? Number(val).toFixed(2) + ' 亩' : '-';
|
||||||
|
if (key === 'linZhong') val = val || pd.diLei;
|
||||||
|
if (key === 'collectorName') val = val || root.collector;
|
||||||
|
if (key === 'updateTime') return formatDate(val || pd.createTime);
|
||||||
|
|
||||||
|
return (val !== null && val !== undefined && val !== '') ? val : '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sampleMap = {
|
||||||
|
yds: "样地总数",
|
||||||
|
ysyfs: "幼树样方数",
|
||||||
|
xmyfs: "下木样方数",
|
||||||
|
wclyfs: "未成林样方数",
|
||||||
|
jjlyfs: "经济林样方数",
|
||||||
|
gmyfs: "灌木样方数",
|
||||||
|
tbyfs: "藤本样方数",
|
||||||
|
cbyfs: "草本样方数",
|
||||||
|
dbyfs: "地被样方数",
|
||||||
|
mcxms: "目测项目数",
|
||||||
|
ssms: "散生单木数",
|
||||||
|
spss: "四旁树单木数"
|
||||||
|
};
|
||||||
|
|
||||||
|
function getStatLabel(key) {
|
||||||
|
return sampleMap[key] || key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 核心逻辑:直接在这里请求样地数据
|
||||||
|
async function handleViewSample() {
|
||||||
|
const pd = props.data.plotData || {};
|
||||||
|
const id = pd.id || pd.ID;
|
||||||
|
const database = pd.databaseName || pd.database_n || pd.DATABASE_N;
|
||||||
|
|
||||||
|
if (!id || !database) {
|
||||||
|
alert("缺少 ID 或 databaseName,无法查询样地");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingSample.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `/api/stats/t1sub1?database=${encodeURIComponent(database)}&id=${id}`;
|
||||||
|
const res = await fetch(url);
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.code === 0 && json.data) {
|
||||||
|
|
||||||
|
// ✅ 兼容处理:无论后端返回单个对象还是数组,都转成数组
|
||||||
|
let rawList = [];
|
||||||
|
if (Array.isArray(json.data)) {
|
||||||
|
rawList = json.data;
|
||||||
|
} else {
|
||||||
|
rawList = [json.data];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历处理每一个样地的媒体路径
|
||||||
|
const processedList = rawList.map(item => processSampleData(item));
|
||||||
|
|
||||||
|
sampleData.value = processedList;
|
||||||
|
sampleVisible.value = true;
|
||||||
|
} else {
|
||||||
|
alert("未查询到样地详情数据");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("样地请求失败", error);
|
||||||
|
alert("网络请求失败");
|
||||||
|
} finally {
|
||||||
|
loadingSample.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:处理样地媒体路径
|
||||||
|
function processSampleData(data) {
|
||||||
|
const images = [];
|
||||||
|
const videos = [];
|
||||||
|
|
||||||
|
if (data.mediaPathList && Array.isArray(data.mediaPathList)) {
|
||||||
|
data.mediaPathList.forEach(path => {
|
||||||
|
const fullUrl = `${MEDIA_BASE_URL}${path}`;
|
||||||
|
const lower = path.toLowerCase();
|
||||||
|
if (lower.endsWith('.mp4') || lower.endsWith('.avi')) {
|
||||||
|
videos.push(fullUrl);
|
||||||
|
} else {
|
||||||
|
images.push(fullUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { ...data, images, videos };
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.plot-popup {
|
/* 1. Main Container: Positioned absolutely on the map */
|
||||||
|
.plot-popup-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
/* 调整弹窗位置锚点,使其位于点击点的上方居中 */
|
/* Adjust anchor to be above the click point */
|
||||||
transform: translate(-50%, -100%);
|
transform: translate(-50%, -100%);
|
||||||
margin-top: -15px; /* 留一点间距,别挡住点击点 */
|
margin-top: -15px;
|
||||||
pointer-events: auto; /* 确保弹窗可交互 */
|
pointer-events: auto;
|
||||||
|
/* Allow flex contents to overflow this container if needed */
|
||||||
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 加上小三角箭头 */
|
/* 2. Flex Wrapper: Holds Plot Card (left) and Sample Popup (right) */
|
||||||
.plot-popup::after {
|
.popup-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start; /* Align tops */
|
||||||
|
gap: 10px; /* Space between the two popups */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. The Plot Card itself */
|
||||||
|
.popup-card {
|
||||||
|
width: 320px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: visible;
|
||||||
|
flex-shrink: 0; /* Prevent shrinking */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4. Sample Popup Wrapper */
|
||||||
|
.sample-wrapper {
|
||||||
|
/* No absolute positioning needed here, flex handles it */
|
||||||
|
z-index: 2001;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Arrow indicator for the main popup */
|
||||||
|
.plot-popup-container::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
/* Position relative to the main container */
|
||||||
bottom: -6px;
|
bottom: -6px;
|
||||||
left: 50%;
|
left: 160px; /* Center of the first card (320px / 2) */
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
border-width: 6px 6px 0;
|
border-width: 6px 6px 0;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
@@ -150,122 +320,34 @@ const allImages = computed(() => {
|
|||||||
filter: drop-shadow(0 2px 2px rgba(0,0,0,0.1));
|
filter: drop-shadow(0 2px 2px rgba(0,0,0,0.1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-card {
|
/* ... (Keep all your existing inner styles for close-btn, header, media, etc.) ... */
|
||||||
width: 300px; /* 稍微加宽一点 */
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: visible; /* 允许箭头显示 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ✅ 2. 新增:关闭按钮样式 */
|
|
||||||
.close-btn {
|
.close-btn {
|
||||||
position: absolute;
|
position: absolute; top: 8px; 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;
|
||||||
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; }
|
||||||
|
|
||||||
.close-btn:hover {
|
.popup-header-wrapper { padding: 12px 12px 0 12px; background-color: #fff; border-top-left-radius: 8px; border-top-right-radius: 8px; }
|
||||||
color: #f56c6c; /* 悬停变红 */
|
.popup-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 8px; border-bottom: 1px solid #eee; padding-right: 25px; }
|
||||||
}
|
.title { font-weight: bold; font-size: 16px; color: #333; }
|
||||||
|
.scroll-content { max-height: 400px; overflow-y: auto; overflow-x: hidden; }
|
||||||
.popup-header {
|
.content-padding { padding: 10px 12px 12px 12px; }
|
||||||
display: flex;
|
.scroll-content::-webkit-scrollbar { width: 6px; }
|
||||||
justify-content: space-between;
|
.scroll-content::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 3px; }
|
||||||
align-items: center;
|
.scroll-content::-webkit-scrollbar-track { background: #f5f7fa; }
|
||||||
margin-bottom: 10px;
|
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 10px; font-size: 12px; margin-bottom: 12px; }
|
||||||
border-bottom: 1px solid #eee;
|
.info-item { display: flex; overflow: hidden; white-space: nowrap; }
|
||||||
padding-bottom: 8px;
|
.info-item .label { color: #909399; flex-shrink: 0; }
|
||||||
/* ✅ 给标题栏右侧留出空间,防止标题文字太长挡住关闭按钮 */
|
.info-item .value { color: #303133; font-weight: 500; overflow: hidden; text-overflow: ellipsis; }
|
||||||
padding-right: 25px;
|
.media-container { position: relative; background: #000; border-radius: 4px; overflow: hidden; margin-bottom: 10px; }
|
||||||
}
|
.media-content { width: 100%; 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; }
|
||||||
.title {
|
.no-media { height: 60px; background: #f5f7fa; color: #c0c4cc; display: flex; align-items: center; justify-content: center; font-size: 12px; border-radius: 4px; margin-bottom: 10px; }
|
||||||
font-weight: bold;
|
.divider { height: 1px; background-color: #ebeef5; margin: 15px 0; width: 100%; }
|
||||||
font-size: 16px;
|
.stats-title { font-size: 13px; font-weight: bold; color: #333; margin-bottom: 10px; padding-left: 6px; border-left: 3px solid #409eff; }
|
||||||
color: #333;
|
.stats-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
|
||||||
}
|
.stat-box { background-color: #f9f9f9; border: 1px solid #eee; border-radius: 4px; padding: 8px 4px; display: flex; flex-direction: column; align-items: center; justify-content: center; transition: all 0.2s; }
|
||||||
|
.stat-box:hover { background-color: #ecf5ff; }
|
||||||
/* 属性网格布局:两列显示 */
|
.stat-name { font-size: 12px; color: #909399; margin-bottom: 4px; transform: scale(0.9); }
|
||||||
.info-grid {
|
.stat-num { font-size: 14px; font-weight: bold; color: #409eff; }
|
||||||
display: grid;
|
.popup-actions { margin-top: 15px; text-align: right; border-top: 1px dashed #eee; padding-top: 10px; }
|
||||||
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: 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: 12px;
|
|
||||||
padding-top: 8px;
|
|
||||||
border-top: 1px dashed #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-actions .el-button {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,257 +1,263 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div v-if="visible" class="sample-popup-container">
|
||||||
v-if="visible"
|
|
||||||
class="sample-popup"
|
<div class="popup-wrapper">
|
||||||
:style="{ left: position.x + 'px', top: position.y + 'px' }"
|
|
||||||
>
|
<el-card shadow="always" class="popup-card" :body-style="{ padding: '0', position: 'relative' }">
|
||||||
<el-card shadow="always" class="popup-card">
|
<span class="close-btn" @click="$emit('close')">×</span>
|
||||||
<!-- 🟢 样地信息 -->
|
|
||||||
<template v-if="!selectedTree">
|
<div class="popup-header-wrapper">
|
||||||
<h3 class="plot-title">{{ data.name }}</h3>
|
<div class="popup-header">
|
||||||
<p>面积:{{ data.area }} ha</p>
|
<span class="title">样地详情</span>
|
||||||
<p>采集人员:{{ data.collector }}</p>
|
<div class="pagination" v-if="total > 1">
|
||||||
<p>样木数量:{{ trees.length }}</p>
|
<el-button size="small" circle :disabled="currentIndex === 0" @click="prevSample" class="nav-btn"><</el-button>
|
||||||
|
<span class="page-info">{{ currentIndex + 1 }} / {{ total }}</span>
|
||||||
<!-- 图片与视频 -->
|
<el-button size="small" circle :disabled="currentIndex === total - 1" @click="nextSample" class="nav-btn">></el-button>
|
||||||
<el-carousel
|
</div>
|
||||||
height="220px"
|
<el-tag v-else size="small" type="warning">样地</el-tag>
|
||||||
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"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 🌳 样木列表 -->
|
|
||||||
<h4 class="section-title">样木列表</h4>
|
|
||||||
<ul class="tree-list">
|
|
||||||
<li
|
|
||||||
v-for="(tree, i) in trees"
|
|
||||||
:key="i"
|
|
||||||
class="tree-item"
|
|
||||||
@click="selectTree(tree)"
|
|
||||||
>
|
|
||||||
<div class="tree-info">
|
|
||||||
<span class="tree-id">#{{ tree.id }}</span>
|
|
||||||
<span class="tree-species">{{ tree.species }}</span>
|
|
||||||
</div>
|
|
||||||
<el-icon class="tree-arrow">
|
|
||||||
<i class="el-icon-arrow-right" />
|
|
||||||
</el-icon>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 🟣 样木详情 -->
|
|
||||||
<template v-else>
|
|
||||||
<div class="tree-header">
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
link
|
|
||||||
size="small"
|
|
||||||
icon="el-icon-arrow-left"
|
|
||||||
@click="selectedTree = null"
|
|
||||||
>
|
|
||||||
返回样地
|
|
||||||
</el-button>
|
|
||||||
<h3>样木 {{ selectedTree.id }}</h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<p>树种:{{ selectedTree.species }}</p>
|
|
||||||
<p>胸径:{{ selectedTree.dbh }} cm</p>
|
<div class="scroll-content">
|
||||||
<p>树高:{{ selectedTree.height }} m</p>
|
<div class="content-padding">
|
||||||
|
<div class="sub-header">ID: {{ currentData.id || '-' }}</div>
|
||||||
<el-carousel
|
|
||||||
height="220px"
|
<div class="info-grid">
|
||||||
indicator-position="outside"
|
<div class="info-item" v-for="(label, key) in fieldMap" :key="key">
|
||||||
:interval="4000"
|
<span class="label">{{ label }}:</span>
|
||||||
arrow="always"
|
<span class="value">{{ getDisplayValue(key) }}</span>
|
||||||
v-if="treeMedia.length"
|
</div>
|
||||||
>
|
</div>
|
||||||
<el-carousel-item
|
|
||||||
v-for="(item, index) in treeMedia"
|
<div class="media-container" v-if="mediaItems.length > 0">
|
||||||
:key="index"
|
<el-carousel :key="currentIndex" height="160px" indicator-position="none" :interval="4000" arrow="hover">
|
||||||
class="carousel-item"
|
<el-carousel-item v-for="(item, index) in mediaItems" :key="index">
|
||||||
>
|
<template v-if="item.type === 'image'">
|
||||||
<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 />
|
||||||
<img :src="item.src" class="media" />
|
</template>
|
||||||
</template>
|
<template v-else-if="item.type === 'video'">
|
||||||
<template v-else-if="item.type === 'video'">
|
<video controls class="media-content"><source :src="item.src" type="video/mp4" /></video>
|
||||||
<video controls class="media">
|
</template>
|
||||||
<source :src="item.src" type="video/mp4" />
|
</el-carousel-item>
|
||||||
</video>
|
</el-carousel>
|
||||||
</template>
|
<div class="media-count">{{ mediaItems.length }} 个附件</div>
|
||||||
</el-carousel-item>
|
</div>
|
||||||
</el-carousel>
|
<div v-else class="no-media">暂无样地影像</div>
|
||||||
</template>
|
|
||||||
|
<div class="popup-actions">
|
||||||
|
<el-button type="success" size="small" plain @click="handleViewTree" :loading="loadingTree">
|
||||||
|
查看样木信息
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<div v-if="treeVisible" class="tree-wrapper">
|
||||||
|
<TreePopup
|
||||||
|
:visible="true"
|
||||||
|
:data="treeData"
|
||||||
|
@close="treeVisible = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
</template>
|
||||||
<script setup>
|
|
||||||
import { ref, computed } from "vue";
|
<script setup>
|
||||||
import {
|
import { computed, ref, watch } from "vue";
|
||||||
ElCard,
|
import { ElCard, ElCarousel, ElCarouselItem, ElTag, ElImage, ElButton } from "element-plus";
|
||||||
ElButton,
|
import TreePopup from "./TreePopup.vue";
|
||||||
ElCarousel,
|
|
||||||
ElCarouselItem,
|
|
||||||
ElIcon,
|
|
||||||
} 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";
|
|
||||||
import "element-plus/es/components/icon/style/css";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
data: { type: Object, default: () => ({}) },
|
data: { type: [Array, Object], default: () => [] },
|
||||||
position: { type: Object, default: () => ({ x: 0, y: 0 }) },
|
// position 不需要了,因为完全由 CSS 布局控制
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedTree = ref(null);
|
defineEmits(['close']);
|
||||||
|
|
||||||
// 样地内样木列表
|
// 样地相关逻辑 (保持不变)
|
||||||
const trees = computed(() => props.data.trees || []);
|
const currentIndex = ref(0);
|
||||||
|
watch(() => props.visible, (val) => { if (val) currentIndex.value = 0; });
|
||||||
|
|
||||||
// 样地媒体
|
const dataList = computed(() => {
|
||||||
|
if (Array.isArray(props.data)) return props.data;
|
||||||
|
if (props.data && Object.keys(props.data).length > 0) return [props.data];
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
const total = computed(() => dataList.value.length);
|
||||||
|
const currentData = computed(() => dataList.value[currentIndex.value] || {});
|
||||||
|
const prevSample = () => { if (currentIndex.value > 0) currentIndex.value--; };
|
||||||
|
const nextSample = () => { if (currentIndex.value < total.value - 1) currentIndex.value++; };
|
||||||
|
|
||||||
|
const fieldMap = { parentId: "所属小班ID", databaseName: "数据库名", pjXj: "平均胸径(cm)", zs: "株数", ydzxj: "样地总蓄积", angle: "角度", area: "面积", length: "长度", width: "宽度", version: "版本号", bz: "备注" };
|
||||||
|
function getDisplayValue(key) {
|
||||||
|
const val = currentData.value[key];
|
||||||
|
if (val === null || val === undefined || val === '') return '-';
|
||||||
|
if (key === 'pjXj' || key === 'ydzxj') return Number(val).toFixed(2);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
const mediaItems = computed(() => {
|
const mediaItems = computed(() => {
|
||||||
const imgs = Array.isArray(props.data.images) ? props.data.images : [];
|
const cur = currentData.value;
|
||||||
const vids = Array.isArray(props.data.videos) ? props.data.videos : [];
|
const imgs = Array.isArray(cur.images) ? cur.images : [];
|
||||||
return [
|
const vids = Array.isArray(cur.videos) ? cur.videos : [];
|
||||||
...imgs.map((src) => ({ type: "image", src })),
|
let imgCounter = 0;
|
||||||
...vids.map((src) => ({ type: "video", src })),
|
return [...imgs.map((src) => ({ type: "image", src, imgIndex: imgCounter++ })), ...vids.map((src) => ({ type: "video", src }))];
|
||||||
];
|
|
||||||
});
|
});
|
||||||
|
const allImages = computed(() => { const cur = currentData.value; return Array.isArray(cur.images) ? cur.images : []; });
|
||||||
|
|
||||||
// 当前选中样木媒体
|
// --- 样木相关逻辑 ---
|
||||||
const treeMedia = computed(() => {
|
const treeVisible = ref(false);
|
||||||
if (!selectedTree.value) return [];
|
const treeData = ref([]);
|
||||||
const imgs = selectedTree.value.images || [];
|
const loadingTree = ref(false);
|
||||||
const vids = selectedTree.value.videos || [];
|
|
||||||
return [
|
|
||||||
...imgs.map((src) => ({ type: "image", src })),
|
|
||||||
...vids.map((src) => ({ type: "video", src })),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
function selectTree(tree) {
|
async function handleViewTree() {
|
||||||
selectedTree.value = tree;
|
const cur = currentData.value;
|
||||||
|
const id = cur.id;
|
||||||
|
const database = cur.databaseName;
|
||||||
|
|
||||||
|
if (!id || !database) {
|
||||||
|
alert("缺少样地ID或数据库名");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingTree.value = true;
|
||||||
|
try {
|
||||||
|
const url = `/api/stats/t1sub2?database=${encodeURIComponent(database)}&id=${id}`;
|
||||||
|
const res = await fetch(url);
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.code === 0 && json.data) {
|
||||||
|
// 兼容单条/多条
|
||||||
|
if (Array.isArray(json.data)) {
|
||||||
|
treeData.value = json.data;
|
||||||
|
} else {
|
||||||
|
treeData.value = [json.data];
|
||||||
|
}
|
||||||
|
treeVisible.value = true;
|
||||||
|
} else {
|
||||||
|
alert("未查询到样木数据");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("样木查询失败", error);
|
||||||
|
alert("请求失败");
|
||||||
|
} finally {
|
||||||
|
loadingTree.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 1. 外层容器:相对定位 (相对于父组件 PlotPopup 的 flex item) */
|
||||||
.sample-popup {
|
.sample-popup {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2001;
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 箭头 (指向左边的小班弹窗) */
|
||||||
|
.sample-popup::after {
|
||||||
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 999;
|
bottom: 20px;
|
||||||
transform: translate(-50%, -100%);
|
left: -6px; /* 指向左侧 */
|
||||||
|
border-width: 6px 6px 6px 0; /* 向右的三角形 */
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent #fff transparent transparent;
|
||||||
|
filter: drop-shadow(-2px 0 2px rgba(0,0,0,0.1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 2. Flex 布局:让样地卡片和样木弹窗并排 */
|
||||||
|
.popup-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px; /* 间距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. 样地卡片样式 */
|
||||||
.popup-card {
|
.popup-card {
|
||||||
width: 300px;
|
width: 280px;
|
||||||
font-size: 13px;
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plot-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #2e7d32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1976d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === 样木列表 === */
|
|
||||||
.tree-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 5px 0;
|
|
||||||
max-height: 130px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
background: #e3f2fd;
|
|
||||||
padding: 8px 10px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-item:hover {
|
|
||||||
background: #bbdefb;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-id {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #1565c0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-species {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-arrow {
|
|
||||||
color: #1565c0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 滚动条优化 */
|
|
||||||
.tree-list::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
.tree-list::-webkit-scrollbar-thumb {
|
|
||||||
background-color: #90caf9;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
.tree-list::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: #64b5f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 图片与视频 */
|
|
||||||
.media {
|
|
||||||
width: 100%;
|
|
||||||
height: 220px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
overflow: visible;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||||
|
flex-shrink: 0; /* 防止被挤压 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 样木详情页头部 */
|
/* 4. 样木弹窗包装器 */
|
||||||
.tree-header {
|
.tree-wrapper {
|
||||||
display: flex;
|
z-index: 2002;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
/* 关闭按钮 */
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px; /* 稍微靠右一点 */
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 100; /* 确保在最上层 */
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.close-btn:hover { color: #ffe6e6; }
|
||||||
|
|
||||||
|
.popup-header-wrapper {
|
||||||
|
background-color: #5db862; /* 绿色主题 */
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 15px;
|
||||||
|
/* ✅ 关键修复:右侧预留空间,防止被关闭按钮遮挡 */
|
||||||
|
padding-right: 35px;
|
||||||
|
color: white;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title { font-weight: bold; font-size: 14px; white-space: nowrap; }
|
||||||
|
|
||||||
|
/* 翻页器样式 */
|
||||||
|
.pagination {
|
||||||
|
display: flex; align-items: center; gap: 4px;
|
||||||
|
}
|
||||||
|
.page-info { font-size: 12px; font-weight: bold; white-space: nowrap; }
|
||||||
|
.nav-btn {
|
||||||
|
width: 18px; height: 18px; min-height: 18px; font-size: 12px; padding: 0;
|
||||||
|
background: rgba(255,255,255,0.2); border: none; color: white;
|
||||||
|
}
|
||||||
|
.nav-btn:hover:not(:disabled) { background: rgba(255,255,255,0.4); }
|
||||||
|
.nav-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
|
.scroll-content {
|
||||||
|
max-height: 350px; overflow-y: auto; overflow-x: hidden;
|
||||||
|
background: #fff; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px;
|
||||||
|
}
|
||||||
|
.content-padding { padding: 12px; }
|
||||||
|
|
||||||
|
.sub-header { font-size: 12px; color: #909399; margin-bottom: 8px; border-bottom: 1px solid #eee; padding-bottom: 4px; }
|
||||||
|
|
||||||
|
.info-grid { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
|
||||||
|
.info-item { display: flex; justify-content: space-between; border-bottom: 1px dashed #eee; padding-bottom: 4px; font-size: 12px; }
|
||||||
|
.info-item .label { color: #909399; flex-shrink: 0; }
|
||||||
|
.info-item .value { color: #303133; font-weight: bold; word-break: break-all; text-align: right; }
|
||||||
|
|
||||||
|
.media-container { position: relative; background: #000; border-radius: 4px; overflow: hidden; }
|
||||||
|
.media-content { width: 100%; 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: 50px; background: #f5f7fa; color: #c0c4cc; display: flex; align-items: center; justify-content: center; font-size: 12px; border-radius: 4px; }
|
||||||
|
|
||||||
|
.popup-actions { margin-top: 15px; text-align: right; border-top: 1px dashed #eee; padding-top: 10px; }
|
||||||
|
|
||||||
|
/* 滚动条 */
|
||||||
|
.scroll-content::-webkit-scrollbar { width: 6px; }
|
||||||
|
.scroll-content::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 3px; }
|
||||||
|
.scroll-content::-webkit-scrollbar-track { background: #f5f7fa; }
|
||||||
|
</style>
|
||||||
276
src/components/TreePopup.vue
Normal file
276
src/components/TreePopup.vue
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="visible" class="tree-popup" :style="{ left: position.x + 'px', top: position.y + 'px' }">
|
||||||
|
<el-card shadow="always" class="popup-card" :body-style="{ padding: '0', position: 'relative' }">
|
||||||
|
|
||||||
|
<span class="close-btn" @click="$emit('close')">×</span>
|
||||||
|
|
||||||
|
<div class="popup-header-wrapper">
|
||||||
|
<div class="popup-header">
|
||||||
|
<span class="title">样木详情</span>
|
||||||
|
<div class="pagination" v-if="total > 1">
|
||||||
|
<el-button size="small" circle :disabled="currentIndex === 0" @click="prevItem" class="nav-btn"><</el-button>
|
||||||
|
<span class="page-info">{{ currentIndex + 1 }} / {{ total }}</span>
|
||||||
|
<el-button size="small" circle :disabled="currentIndex === total - 1" @click="nextItem" class="nav-btn">></el-button>
|
||||||
|
</div>
|
||||||
|
<el-tag v-else size="small" type="success">样木</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scroll-content">
|
||||||
|
<div class="content-padding">
|
||||||
|
|
||||||
|
<div class="sub-header">
|
||||||
|
ID: {{ currentData.id || '-' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item" v-for="(label, key) in fieldMap" :key="key">
|
||||||
|
<span class="label">{{ label }}:</span>
|
||||||
|
<span class="value">{{ getDisplayValue(key) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="media-container" v-if="mediaItems.length > 0">
|
||||||
|
<el-carousel :key="currentIndex" height="160px" indicator-position="none" :interval="4000" arrow="hover">
|
||||||
|
<el-carousel-item v-for="(item, index) in mediaItems" :key="index">
|
||||||
|
<el-image
|
||||||
|
:src="item.src"
|
||||||
|
class="media-content"
|
||||||
|
fit="contain"
|
||||||
|
:preview-src-list="allImages"
|
||||||
|
:initial-index="item.imgIndex"
|
||||||
|
preview-teleported
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
</el-carousel-item>
|
||||||
|
</el-carousel>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-media">暂无样木照片</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { ElCard, ElCarousel, ElCarouselItem, ElTag, ElImage, ElButton } from "element-plus";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: Boolean,
|
||||||
|
data: { type: [Array, Object], default: () => [] },
|
||||||
|
position: { type: Object, default: () => ({ x: 0, y: 0 }) }
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(['close']);
|
||||||
|
|
||||||
|
const MEDIA_BASE_URL = '/photos'; // 确保和之前一致
|
||||||
|
const currentIndex = ref(0);
|
||||||
|
|
||||||
|
watch(() => props.visible, (val) => {
|
||||||
|
if (val) currentIndex.value = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数据标准化:转数组
|
||||||
|
const dataList = computed(() => {
|
||||||
|
if (Array.isArray(props.data)) return props.data;
|
||||||
|
if (props.data && Object.keys(props.data).length > 0) return [props.data];
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const total = computed(() => dataList.value.length);
|
||||||
|
const currentData = computed(() => dataList.value[currentIndex.value] || {});
|
||||||
|
|
||||||
|
const prevItem = () => { if (currentIndex.value > 0) currentIndex.value--; };
|
||||||
|
const nextItem = () => { if (currentIndex.value < total.value - 1) currentIndex.value++; };
|
||||||
|
|
||||||
|
// ✅ 样木字段映射
|
||||||
|
const fieldMap = {
|
||||||
|
sz: "树种",
|
||||||
|
xj: "胸径(cm)",
|
||||||
|
cjl: "材积率", // 或 蓄积
|
||||||
|
ymh: "样木号",
|
||||||
|
parentId: "所属样地ID",
|
||||||
|
grandparentid: "所属小班ID",
|
||||||
|
databaseName: "数据库",
|
||||||
|
version: "版本",
|
||||||
|
type: "类型"
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDisplayValue(key) {
|
||||||
|
const val = currentData.value[key];
|
||||||
|
if (val === null || val === undefined || val === '') return '-';
|
||||||
|
if (key === 'xj' || key === 'cjl') return Number(val).toFixed(2);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 媒体处理:样木接口返回的是 photo 字符串,不是 list
|
||||||
|
const mediaItems = computed(() => {
|
||||||
|
const photoPath = currentData.value.photo;
|
||||||
|
if (!photoPath) return [];
|
||||||
|
|
||||||
|
// 构造完整的图片路径
|
||||||
|
const fullSrc = `${MEDIA_BASE_URL}${photoPath}`;
|
||||||
|
return [{ type: "image", src: fullSrc, imgIndex: 0 }];
|
||||||
|
});
|
||||||
|
|
||||||
|
const allImages = computed(() => mediaItems.value.map(i => i.src));
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tree-popup {
|
||||||
|
position: relative; /* 配合 Flex 布局 */
|
||||||
|
z-index: 2002;
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 箭头指向左侧 */
|
||||||
|
.tree-popup::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: -6px;
|
||||||
|
border-width: 6px 6px 6px 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent #fff transparent transparent;
|
||||||
|
filter: drop-shadow(-2px 0 2px rgba(0,0,0,0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 关闭按钮 */
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px; /* 稍微靠右一点 */
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 10; /* 确保在 header 内容之上 */
|
||||||
|
line-height: 1;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: #ffe6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header-wrapper {
|
||||||
|
background-color: #67c23a; /* 绿色头部 */
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 15px;
|
||||||
|
/* ✅ 关键修改:右边距避让关闭按钮 */
|
||||||
|
padding-right: 35px;
|
||||||
|
color: white;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap; /* 防止标题换行 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 翻页器 */
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.page-info {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.nav-btn {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
min-height: 18px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0;
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.nav-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容区域 */
|
||||||
|
.scroll-content {
|
||||||
|
max-height: 350px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom-left-radius: 8px;
|
||||||
|
border-bottom-right-radius: 8px;
|
||||||
|
}
|
||||||
|
.content-padding {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-header {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px dashed #eee;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.info-item .label { color: #909399; flex-shrink: 0; }
|
||||||
|
.info-item .value { color: #303133; font-weight: bold; text-align: right; word-break: break-all; }
|
||||||
|
|
||||||
|
.media-container {
|
||||||
|
position: relative;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.media-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.no-media {
|
||||||
|
height: 50px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
color: #c0c4cc;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条美化 */
|
||||||
|
.scroll-content::-webkit-scrollbar { width: 6px; }
|
||||||
|
.scroll-content::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 3px; }
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user