<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>
|