Files
LinyeZhanshi/src/components/TreePopup.vue

276 lines
6.9 KiB
Vue
Raw Permalink Normal View History

2025-11-28 18:54:22 +08:00
<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">&lt;</el-button>
<span class="page-info">{{ currentIndex + 1 }} / {{ total }}</span>
<el-button size="small" circle :disabled="currentIndex === total - 1" @click="nextItem" class="nav-btn">&gt;</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>