You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

673 lines
18 KiB

<template>
<div>
<!-- <div class="tool-actions">
<el-tooltip content="删除" placement="top">
<el-button
style="position: absolute; top: 22%; right: 79%; z-index: 999"
type="danger"
circle
@click="clearAll"
:disabled="regions.length === 0"
>
<i class="el-icon-delete"></i>
</el-button>
</el-tooltip>
</div>
<div class="tool-actions">
<el-tooltip content="保存" placement="top">
<el-button
style="position: absolute; top: 22%; right: 83%"
type="danger"
icon="el-icon-document-checked"
circle
@click="saveRegions"
:disabled="regions.length === 0"
/>
</el-tooltip>
</div> -->
<div class="image-wrapper" ref="imageWrapper">
<el-image
ref="temperatureImage"
:fit="fit"
:preview-src-list="imageList"
:src="imgUrl"
@load="handleImageLoad"
@error="handleImageLoadError"
@dblclick="onClicked"
@mousemove="handleMouseMove"
@mouseleave="stopDrawingIfNeeded"
class="temperature-image"
>
<div slot="placeholder" class="image-slot">
加载中<span class="dot">...</span>
</div>
<div slot="error" class="image-slot">
{{ errorTips }}
</div>
</el-image>
<!-- 温度悬浮显示 -->
<div
v-if="hoverData"
class="temperature-display"
:style="{
left: hoverData.x + 'px',
top: hoverData.y + 'px',
}"
>
{{ hoverData.temperature }}°C
</div>
</div>
</div>
</template>
<script>
// import { ftpImageViewUrl } from "@/utils/index";
import { mapGetters } from "vuex";
import { getConfigByKeys } from "@/api/system/config";
import Papa from "papaparse";
// 接口
import {
selectBoxData,
clearBoxData,
addBoxData,
downloadCsv,
} from "@/api/basedata/patrolpointmnt/patrolpointpreset.js";
// "@/api/analysis/index";
// 临时接口
import axios from "axios";
export default {
name: "AsyncImage",
components: {},
props: {
fit: {
type: String,
default: "scale-down",
},
srcList: {
type: Array,
default: () => [],
},
src: {
type: String,
default: "",
},
errorTips: {
type: String,
default: "加载失败",
},
supportPreview: {
type: Boolean,
default: false,
},
},
async created() {},
data() {
return {
imgUrl: "",
imgName: "",
loading: false,
imageList: [],
config: null,
uploadDialogVisible: false,
originalImage: null,
// 温度数据相关
temperatureData: [], // 存储从CSV读取的二维温度数据
tempRows: 0, // CSV行数
tempCols: 0, // CSV列数
// 图片尺寸相关
imageNaturalWidth: 0, // 图片原始宽度
imageNaturalHeight: 0, // 图片原始高度
displayWidth: 0, // 图片显示宽度
displayHeight: 0, // 图片显示高度
imageScale: 1, // 图片缩放比例
// 鼠标悬浮相关
hoverData: null, // 当前悬停位置的数据
hoverTemperatureData: [], //存续从CSV读取的温度数据用于鼠标悬浮
// 鼠标位置温度
currentTemp: null,
currentPos: { x: 0, y: 0 },
showTooltip: false,
tooltipPosition: { x: 0, y: 0 },
// 矩形绘制 - 已注释(但保留变量)
isDrawing: false,
startPos: { x: 0, y: 0 },
currentRect: { x: 0, y: 0, width: 0, height: 0 },
// 温度区域 - 已注释(但保留变量)
regions: [],
selectedRegion: null,
// 温度范围
maxGlobalTemp: 0,
minGlobalTemp: 0,
// 框的终点
secondX: 0,
secondY: 0,
showFrame: false, //返回不同坐标计算
maxTempPoints: [], // 存储每个区域的最高温点坐标
};
},
computed: {
...mapGetters(["systemParams"]),
tooltipStyle() {
return {
left: `${this.tooltipPosition.x}px`,
top: `${this.tooltipPosition.y}px`,
position: "absolute",
backgroundColor: "rgba(0, 0, 0, 0.8)",
color: "white",
padding: "5px 10px",
borderRadius: "4px",
fontSize: "14px",
pointerEvents: "none",
zIndex: 100,
whiteSpace: "nowrap",
};
},
legendGradient() {
return {
background: `linear-gradient(to top,
${this.getColorForTemp(this.minGlobalTemp)} 0%,
${this.getColorForTemp(
(this.minGlobalTemp + this.maxGlobalTemp) / 2
)} 50%,
${this.getColorForTemp(this.maxGlobalTemp)} 100%)`,
height: "200px",
borderRadius: "4px",
};
},
},
// 悬浮
mounted() {
setTimeout(() => {
this.updateImageSize();
}, 500);
window.addEventListener("resize", this.updateImageSize);
this.showUploadDialog();
},
beforeDestroy() {
window.removeEventListener("resize", this.updateImageSize);
},
methods: {
// ==================== 图片事件处理 ====================
handleImageLoad(event) {
const img = event.target;
this.imageNaturalWidth = img.naturalWidth;
this.imageNaturalHeight = img.naturalHeight;
this.$nextTick(() => {
this.updateImageSize();
});
this.$emit('imgload', {
imgOriginalHeight: this.imageNaturalHeight,
imgOriginalWidth: this.imageNaturalWidth
});
// 去除拖动鼠标图片被拖走问题
img.draggable = false;
img.ondragstart = () => false;
},
handleImageLoadError() {
this.$emit('imgloaderror');
},
onClicked() {
this.$emit('click');
},
showUploadDialog() {
this.uploadDialogVisible = true;
},
// ==================== 鼠标交互 ====================
handleMouseMove(event) {
// 处理悬浮温度显示(使用最近邻插值)
this.handleHoverShow(event);
},
stopDrawingIfNeeded() {
this.showTooltip = false;
},
// ==================== 图片尺寸更新 ====================
updateImageSize() {
const imageEl = this.$refs.temperatureImage?.$el?.querySelector("img");
if (!imageEl) {
return;
}
this.originalImage = imageEl;
const naturalWidth = imageEl.naturalWidth;
const naturalHeight = imageEl.naturalHeight;
const displayWidth = imageEl.clientWidth;
const displayHeight = imageEl.clientHeight;
// 计算正确的缩放比例
this.imageScale = naturalWidth / displayWidth;
// console.log("图片尺寸更新:", {
// 实际尺寸: `${naturalWidth}×${naturalHeight}`,
// 显示尺寸: `${displayWidth}×${displayHeight}`,
// 缩放比例: this.imageScale,
// });
const container = this.$refs.imageWrapper;
this.containerRect = container.getBoundingClientRect();
if (this.$refs.temperatureImage) {
this.imageSize = {
width: naturalWidth,
height: naturalHeight,
};
}
},
// ==================== CSV加载和解析(分块处理,避免栈溢出) ====================
downloadAndParseCSV() {
const data = {
filePath: this.imgName,
};
downloadCsv(data)
.then((response) => {
const blob = response instanceof Blob ? response : new Blob([response]);
const reader = new FileReader();
reader.onload = (e) => {
try {
const csvText = e.target.result;
this.parseCSVInChunks(csvText);
} catch (error) {
// this.$message.error('读取CSV出错: ' + error.message);
}
};
reader.onerror = () => {
// this.$message.error('文件读取失败');
};
reader.readAsText(blob);
})
.catch((error) => {
// this.$message.error('下载CSV失败');
});
},
// 分块解析CSV,避免栈溢出
parseCSVInChunks(csvText) {
try {
const lines = csvText.split('\n');
const tempData = [];
let maxCols = 0;
const BATCH_SIZE = 1000;
let index = 0;
const processBatch = () => {
const endIndex = Math.min(index + BATCH_SIZE, lines.length);
for (let i = index; i < endIndex; i++) {
const line = lines[i].trim();
if (!line) continue;
const values = line.split(',').map(v => {
const num = parseFloat(v.trim());
return isNaN(num) ? null : num;
});
const validValues = values.filter(v => v !== null && isFinite(v));
if (validValues.length > 0) {
tempData.push(validValues);
if (validValues.length > maxCols) {
maxCols = validValues.length;
}
}
}
index = endIndex;
if (index < lines.length) {
setTimeout(processBatch, 0);
} else {
this.finalizeCSVData(tempData, maxCols);
}
};
processBatch();
} catch (error) {
// this.$message.error('解析CSV出错: ' + error.message);
}
},
// 最终处理CSV数据(修复栈溢出)
finalizeCSVData(tempData, maxCols) {
try {
if (tempData.length === 0) {
this.$message.warning('CSV温度数据为空');
return;
}
const normalizedData = [];
for (let i = 0; i < tempData.length; i++) {
const row = tempData[i];
if (row.length < maxCols) {
const lastVal = row[row.length - 1] || 0;
while (row.length < maxCols) {
row.push(lastVal);
}
}
normalizedData.push(row);
}
this.temperatureData = normalizedData;
this.tempRows = normalizedData.length;
this.tempCols = normalizedData[0]?.length || 0;
// 计算温度范围(使用循环避免栈溢出)
let minTemp = Infinity;
let maxTemp = -Infinity;
let count = 0;
const sampleLimit = Math.min(normalizedData.length, 500);
for (let i = 0; i < sampleLimit; i++) {
const row = normalizedData[i];
if (Array.isArray(row)) {
const rowLimit = Math.min(row.length, 500);
for (let j = 0; j < rowLimit; j++) {
const val = row[j];
if (typeof val === 'number' && !isNaN(val) && isFinite(val)) {
if (val < minTemp) minTemp = val;
if (val > maxTemp) maxTemp = val;
count++;
}
}
}
}
// console.log('温度范围:', {
// min: count > 0 ? minTemp : 0,
// max: count > 0 ? maxTemp : 0,
// sampleCount: count
// });
// 为悬停显示准备数据(使用循环避免栈溢出)
const hoverData = [];
const rowLimit = Math.min(normalizedData.length, 100);
for (let i = 0; i < rowLimit; i++) {
const row = normalizedData[i];
if (Array.isArray(row)) {
const colLimit = Math.min(row.length, 100);
for (let j = 0; j < colLimit; j++) {
const val = row[j];
if (typeof val === 'number' && !isNaN(val) && isFinite(val)) {
hoverData.push(val);
}
}
}
}
this.hoverTemperatureData = hoverData;
// this.$message.success(`成功加载 ${this.tempRows}x${this.tempCols} 温度数据`);
} catch (error) {
// this.$message.error('处理CSV数据出错: ' + error.message);
}
},
// ==================== 最近邻插值(精确匹配温度矩阵原始值) ====================
nearestNeighborInterpolation(data, row, col, rows, cols) {
if (row < 0 || row > rows - 1 || col < 0 || col > cols - 1) {
return null;
}
const nearestRow = Math.round(row);
const nearestCol = Math.round(col);
const safeRow = Math.max(0, Math.min(nearestRow, rows - 1));
const safeCol = Math.max(0, Math.min(nearestCol, cols - 1));
const val = parseFloat(data[safeRow]?.[safeCol]);
return isNaN(val) ? null : val;
},
// ==================== 处理鼠标悬浮显示温度(使用最近邻插值) ====================
handleHoverShow(event) {
const imageEl = this.$refs.temperatureImage?.$el?.querySelector('img');
if (!imageEl) return;
if (!this.temperatureData || this.temperatureData.length === 0) {
this.hoverData = null;
return;
}
const rect = imageEl.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
const normalizedX = mouseX / rect.width;
const normalizedY = mouseY / rect.height;
if (normalizedX < 0 || normalizedX > 1 || normalizedY < 0 || normalizedY > 1) {
this.hoverData = null;
return;
}
const rows = this.tempRows;
const cols = this.tempCols;
if (rows === 0 || cols === 0) {
this.hoverData = null;
return;
}
const floatRow = normalizedY * (rows - 1);
const floatCol = normalizedX * (cols - 1);
const temperature = this.nearestNeighborInterpolation(
this.temperatureData,
floatRow,
floatCol,
rows,
cols
);
if (temperature !== null && !isNaN(temperature) && isFinite(temperature)) {
let tooltipX = event.clientX - rect.left + 15;
let tooltipY = event.clientY - rect.top - 10;
if (tooltipX + 100 > rect.width) {
tooltipX = event.clientX - rect.left - 100;
}
if (tooltipY < 20) {
tooltipY = event.clientY - rect.top + 20;
}
if (tooltipY + 30 > rect.height) {
tooltipY = rect.height - 30;
}
this.hoverData = {
x: tooltipX,
y: tooltipY,
temperature: temperature.toFixed(1),
};
} else {
this.hoverData = null;
}
},
// ==================== 辅助函数 ====================
getRandomRegionColor() {
const colors = [
"#FF5722",
"#2196F3",
"#4CAF50",
"#FFC107",
"#9C27B0",
"#00BCD4",
"#8BC34A",
"#FF9800",
"#673AB7",
"#3F51B5",
"#009688",
"#CDDC39",
];
return colors[Math.floor(Math.random() * colors.length)];
},
hexToRgba(hex, alpha) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
},
// ==================== FTP和URL处理 ====================
async ftpImageViewUrl(url) {
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
if (!this.systemParams.ftpViewAddress) {
const systemParams = {};
await getConfigByKeys([
'EXTRANET_ADDRESS',
'EXTRANET_FTP_VIEW_ADDRESS',
'INTRANET_FTP_VIEW_ADDRESS',
'VIRTUAL_RTC_MEDIAURL',
'EXTRANET_MEDIA_URL',
'INTRANET_MEDIA_URL',
'VIRTUAL_FLV_MEDIAURL',
'INTRANET_FLV_MEDIA_URL',
'EXTRANET_FLV_MEDIA_URL',
]).then((res) => {
res.data.forEach((x) => {
systemParams[x.configKey] = x.configValue;
});
systemParams.PREFIX = location.hostname === systemParams.EXTRANET_ADDRESS
? 'EXTRANET'
: 'INTRANET';
systemParams.ftpViewAddress = systemParams[systemParams.PREFIX + '_FTP_VIEW_ADDRESS'];
systemParams.MEDIA_URL = systemParams[systemParams.PREFIX + '_MEDIA_URL'];
systemParams.FLV_MEDIA_URL = systemParams[systemParams.PREFIX + '_FLV_MEDIA_URL'];
});
return systemParams.ftpViewAddress + url;
} else {
return this.systemParams.ftpViewAddress + url;
}
},
async getUrls(val) {
this.imageList = [];
const urls = [];
for (let index = 0; index < val.length; index++) {
const element = val[index];
urls.push(await this.ftpImageViewUrl(element));
}
if (this.supportPreview) {
this.imageList = urls;
}
},
},
watch: {
srcList: {
handler(val) {
this.getUrls(val);
},
immediate: true,
},
src: {
async handler(val) {
if (val && val.length > 0) {
this.imgUrl = await this.ftpImageViewUrl(val);
// this.imgUrl = "http://192.168.1.116:18891/shaoxing/videoMonitor/app/htjc6/htjcImage//test/39357.jpg";
this.imgName = val;
if (this.srcList.length === 0) {
this.getUrls([val]);
}
this.$nextTick(() => {
this.downloadAndParseCSV();
});
} else {
this.imgUrl = "";
}
},
immediate: true,
},
},
};
</script>
<style scoped>
.image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
color: #ddd;
}
.image-wrapper {
position: relative;
border-radius: 4px;
overflow: hidden;
height: calc(66vh - 350px);
display: flex;
justify-content: center;
align-items: center;
}
.temperature-image {
max-width: 100%;
max-height: 100%;
display: block;
cursor: default;
}
.image-slot {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
color: #909399;
font-size: 14px;
}
.image-slot i {
font-size: 36px;
margin-bottom: 10px;
}
/* 温度悬浮显示 */
.temperature-display {
position: absolute;
padding: 6px 12px;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.85);
color: #fff;
font-weight: bold;
font-size: 14px;
pointer-events: none;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.3);
transform: translateY(-100%);
z-index: 10;
white-space: nowrap;
border: 1px solid rgba(255, 255, 255, 0.1);
}
</style>