<template>
|
<div style="width: 100%; height: 100%">
|
<div
|
class="left-top"
|
v-if="simStore.selectTab == '行政区划仿真'"
|
style="margin-top: 0px"
|
>
|
行政区划仿真(30m精度)
|
</div>
|
<div class="left-top" v-if="simStore.selectTab == '重点区域仿真'">
|
重点区域仿真(10m精度)
|
</div>
|
<div class="left-top" v-if="simStore.selectTab == '重点沟仿真'">
|
历史模拟
|
</div>
|
|
<div class="forms" :class="{ 'no-background': !showBackground }">
|
<el-form
|
:rules="rules"
|
:model="forms"
|
label-width="auto"
|
style="max-width: 600px"
|
>
|
<el-form-item label="方案名称:">
|
<el-input
|
v-model="forms.name"
|
style="max-width: 600px"
|
placeholder="请输入方案名称"
|
>
|
</el-input>
|
</el-form-item>
|
<el-form-item label="上传参数">
|
<el-upload
|
:on-remove="handleRemove"
|
v-model:file-list="forms.fileList"
|
class="upload-demo"
|
:auto-upload="false"
|
:multiple="false"
|
:on-change="handleFileChange"
|
:limit="1"
|
:on-exceed="handleExceed"
|
:before-upload="beforeUpload"
|
accept=".xlsx,.xls,.csv"
|
>
|
<el-button type="primary">点击上传降雨数据</el-button>
|
<template #append>mm/h</template>
|
</el-upload>
|
</el-form-item>
|
<el-form-item label="雨强单位" v-if="forms.fileList.length !== 0">
|
<el-select
|
v-model="forms.intensityUnit"
|
placeholder="请选择雨强单位"
|
style="max-width: 600px"
|
:disabled="!!forms.intensityUnit"
|
>
|
<el-option
|
v-for="item in intensityOptions"
|
:key="item.value"
|
:label="item.label"
|
:value="item.value"
|
/>
|
</el-select>
|
</el-form-item>
|
<el-form-item
|
label="行政区域:"
|
v-if="simStore.selectTab == '行政区划仿真'"
|
>
|
<el-select
|
@change="changeGeom"
|
v-model="forms.geom"
|
placeholder="请选择模拟区域"
|
style="max-width: 600px"
|
>
|
<el-option
|
v-for="item in options"
|
:key="item.value"
|
:label="item.label"
|
:value="item"
|
/>
|
</el-select>
|
</el-form-item>
|
<el-form-item
|
label="重点区域:"
|
v-if="simStore.selectTab == '重点区域仿真'"
|
>
|
<el-select
|
@change="changeGeom"
|
v-model="forms.geom"
|
placeholder="请选择模拟区域"
|
style="max-width: 600px"
|
>
|
<el-option
|
v-for="item in options"
|
:key="item.value"
|
:label="item.label"
|
:value="item"
|
/>
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="降雨量:">
|
<el-input
|
v-model="forms.rainfall"
|
style="max-width: 600px"
|
placeholder="请输入降雨量"
|
>
|
<template #append>mm</template>
|
</el-input>
|
</el-form-item>
|
<el-form-item label="选择时间:">
|
<el-date-picker
|
v-if="forms.fileList.length !== 0"
|
v-model="forms.hours"
|
type="datetime"
|
placeholder="请选择开始时间"
|
/>
|
<el-date-picker
|
v-if="forms.fileList.length == 0"
|
v-model="forms.hours"
|
type="datetimerange"
|
start-placeholder="开始时间"
|
end-placeholder="结束时间"
|
format="YYYY-MM-DD HH:mm:ss"
|
date-format="YYYY/MM/DD ddd"
|
time-format="A hh:mm:ss"
|
@change="change"
|
/>
|
</el-form-item>
|
<el-form-item label="降雨时长:">
|
<el-input
|
disabled
|
v-model="forms.duration"
|
style="max-width: 600px"
|
placeholder="请输入降雨时长"
|
>
|
<template #append>h</template>
|
</el-input>
|
</el-form-item>
|
|
<el-form-item label="降雨强度:">
|
<el-input
|
v-model="forms.intensity"
|
style="max-width: 600px"
|
placeholder="请输入降雨强度"
|
>
|
<template #append>mm/h</template>
|
</el-input>
|
</el-form-item>
|
|
<!-- <el-form-item label="仿真参数:"></el-form-item> -->
|
</el-form>
|
<div style="display: flex; justify-content: flex-end">
|
<el-button type="primary" @click="addSimCheme">保存方案</el-button>
|
<el-button type="success" @click="startPlay">保存并开始模拟</el-button>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import { reactive, ref, watch, inject, computed, onMounted } from "vue";
|
import * as XLSX from "xlsx";
|
import Papa from "papaparse";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
import { initeWaterPrimitiveView } from "@/utils/water";
|
import { SimAPIStore } from "@/store/simAPI";
|
import { getRegionData, getSimStart, getSimDataById } from "@/api/trApi";
|
|
import { storeToRefs } from "pinia";
|
import dayjs from "dayjs";
|
import { EventBus } from "@/eventBus"; // 引入事件总线
|
|
const simStore = SimAPIStore();
|
const { selectTab } = storeToRefs(simStore);
|
|
const options = reactive([]);
|
|
// 历史模拟选中区域
|
const props = defineProps({
|
selectedArea: {
|
type: Object,
|
required: true,
|
},
|
});
|
|
const intensityOptions = ref([
|
{ value: "mm/h", label: "mm/h" },
|
{ value: "mm/5min", label: "mm/5min" },
|
{ value: "mm/1min", label: "mm/1min" },
|
]);
|
|
// 定义一个方法,用于根据 type 获取区域数据
|
const fetchRegionData = (type) => {
|
getRegionData({ type: type }).then((res) => {
|
// 使用响应式数组的方法更新内容
|
options.splice(
|
0,
|
options.length,
|
...res.data.map((item) => ({
|
value: item.geom,
|
label: item.name,
|
}))
|
);
|
});
|
};
|
|
onMounted(() => {
|
fetchRegionData(1);
|
});
|
|
const showBackground = ref(true); // 默认显示背景图
|
|
// 监听 selectTab 的变化
|
watch(selectTab, (newVal) => {
|
let type;
|
switch (newVal) {
|
case "行政区划仿真":
|
type = 1;
|
break;
|
case "重点区域仿真":
|
type = 2;
|
break;
|
case "重点沟仿真":
|
type = 3;
|
break;
|
default:
|
type = 1; // 默认值
|
}
|
// 根据 type 设置是否显示背景图(因为历史模拟中表单带了背景图)
|
if (type == 3) {
|
showBackground.value = false;
|
} else {
|
showBackground.value = true;
|
}
|
fetchRegionData(type);
|
// Tab切换的时候清空表单
|
resetForm();
|
});
|
|
// 注入父组件提供的方法
|
const { startSimulate, endSimulate } = inject("simulateActions");
|
// 表单数据
|
const forms = reactive({
|
name: "",
|
geom: "",
|
rainfall: null,
|
duration: null,
|
intensity: null,
|
fileList: [],
|
type: 3,
|
rainFallList: [],
|
hours: null,
|
intensityUnit: "",
|
});
|
|
const flyHeight = ref(100000);
|
|
// 将选中区域传递给gisView文件,做标红flyTo显示
|
const changeGeom = (val) => {
|
if (selectTab.value == "行政区划仿真") {
|
flyHeight.value = 100000;
|
} else {
|
flyHeight.value = 5000;
|
}
|
EventBus.emit("select-geom", { geom: val.value, flyHeight: flyHeight.value });
|
};
|
|
const { calculateHoursDifference } = inject("calculateHours");
|
|
const change = (val) => {
|
forms.duration = calculateHoursDifference(val);
|
};
|
|
const addSimCheme = async () => {
|
try {
|
if (selectTab.value == "重点沟仿真") {
|
forms.geom = props.selectedArea;
|
}
|
await simStore.addSimCheme(forms);
|
resetForm(); // 只有在保存成功后才重置表单
|
EventBus.emit("close-selectArea");
|
} catch (error) {}
|
};
|
|
// 重置表单
|
const resetForm = () => {
|
forms.name = "";
|
forms.geom = "";
|
forms.rainfall = null;
|
forms.duration = null;
|
forms.intensity = null;
|
forms.fileList = [];
|
forms.rainFallList = [];
|
forms.hours = null;
|
forms.intensityUnit = "";
|
};
|
|
// 计算属性:获取上传文件的名称列表
|
const uploadedFilesText = computed(() => {
|
return forms.fileList.map((file) => file.name).join(", ") || "无";
|
});
|
|
// 文件变化时触发解析
|
const handleFileChange = (file) => {
|
const reader = new FileReader();
|
reader.onload = (e) => {
|
const data = e.target.result;
|
if (file.name.endsWith(".csv")) {
|
parseCSV(data);
|
} else {
|
parseExcel(data);
|
}
|
};
|
reader.readAsArrayBuffer(file.raw);
|
};
|
|
// 解析CSV文件
|
const parseCSV = (data) => {
|
Papa.parse(new TextDecoder("utf-8").decode(data), {
|
complete: (results) => {
|
if (results.data.length > 0) {
|
processData(results.data);
|
}
|
},
|
header: true,
|
skipEmptyLines: true,
|
});
|
};
|
|
// 解析Excel文件
|
const parseExcel = (data) => {
|
const workbook = XLSX.read(data, { type: "array" });
|
const firstSheetName = workbook.SheetNames[0];
|
const worksheet = workbook.Sheets[firstSheetName];
|
const jsonData = XLSX.utils.sheet_to_json(worksheet, {
|
raw: false, // 使用格式化字符串而不是原始值
|
dateNF: "yyyy-mm-dd hh:mm:ss", // 指定日期格式
|
});
|
|
processData(jsonData);
|
};
|
|
/**
|
* 检查时间列是否按升序排列
|
* @param {Array} data - 表格数据
|
* @param {string} timeColumn - 时间列的字段名
|
* @returns {boolean} - 是否按升序排列
|
*/
|
const isTimeColumnSorted = (data, timeColumn) => {
|
for (let i = 1; i < data.length; i++) {
|
const prevTime = parseDateTime(data[i - 1][timeColumn]);
|
const currentTime = parseDateTime(data[i][timeColumn]);
|
|
// 如果前一个时间 > 当前时间,说明不是升序
|
if (prevTime > currentTime) {
|
console.error(`时间乱序:第 ${i} 行`, {
|
prevTime: new Date(prevTime),
|
currentTime: new Date(currentTime),
|
});
|
return false;
|
}
|
}
|
return true; // 所有时间都按升序排列
|
};
|
|
/**
|
* 从表头提取单位(如 "小时雨强mm/h" → "mm/h")
|
* @param {string} header - 表头字符串
|
* @returns {string} - 提取的单位(如 "mm/h"),默认返回空字符串
|
*/
|
const extractUnitFromHeader = (header) => {
|
if (!header) return "";
|
|
// 直接匹配 "mm/h"、"m/s" 等常见单位
|
const unitRegex = /(mm\/h|mm\/1min|mm\/5min|mm\/15min)/; // 根据需要扩展
|
const match = header.match(unitRegex);
|
return match ? match[0] : "";
|
};
|
|
const transformKeys = (data) => {
|
return data.map((item) => ({
|
time: item["时间"], // "时间" → "time"
|
intensity: parseFloat(item["小时雨强"]), // 转为浮点数
|
total: parseFloat(item["累计雨量"]), // 转为浮点数
|
}));
|
};
|
|
// 可配置的字段名匹配规则
|
const COLUMN_MATCH_RULES = {
|
time: ["时间", "time", "datetime", "date"],
|
intensity: [
|
"雨强",
|
"小时雨强",
|
"rain_intensity",
|
"rain_rate",
|
"hour_rain",
|
"降雨强度",
|
],
|
totalRainfall: [
|
"累计雨量",
|
"总雨量",
|
"total_rain",
|
"cumulative_rainfall",
|
"降雨总量",
|
],
|
};
|
|
/**
|
* 自动匹配字段名
|
* @param {Object} headers - 表头对象(第一行)
|
* @returns {{time: string, intensity: string, totalRainfall: string}}
|
*/
|
function matchColumns(headers) {
|
const matched = {
|
time: null,
|
intensity: null,
|
totalRainfall: null,
|
};
|
|
for (const header of Object.keys(headers)) {
|
const cleanHeader = header.trim();
|
|
if (
|
!matched.time &&
|
COLUMN_MATCH_RULES.time.some((k) => cleanHeader.includes(k))
|
) {
|
matched.time = header;
|
}
|
|
if (
|
!matched.intensity &&
|
COLUMN_MATCH_RULES.intensity.some((k) => cleanHeader.includes(k))
|
) {
|
matched.intensity = header;
|
}
|
|
if (
|
!matched.totalRainfall &&
|
COLUMN_MATCH_RULES.totalRainfall.some((k) => cleanHeader.includes(k))
|
) {
|
matched.totalRainfall = header;
|
}
|
}
|
|
return matched;
|
}
|
|
/**
|
* 数据处理主函数
|
* @param {Array} data - 解析后的原始数据数组,每个元素是一个对象
|
*/
|
const processData = (data) => {
|
// 检查是否为空数据
|
if (data.length === 0) {
|
ElMessage.warning("文件内容为空!");
|
return;
|
}
|
|
// 匹配列名(例如“时间”、“小时雨强”)
|
const columns = matchColumns(data[0]);
|
|
// 校验必要字段是否存在
|
if (!columns.time) {
|
ElMessage.error(
|
"未找到有效的时间列,请检查列名是否为“时间”或其他支持的格式"
|
);
|
forms.fileList = [];
|
return;
|
}
|
|
if (!columns.intensity) {
|
ElMessage.error(
|
"未找到有效的雨强列,请检查列名是否为“小时雨强”或其他支持的格式"
|
);
|
forms.fileList = [];
|
return;
|
}
|
|
// 校验时间列是否升序排列
|
if (!isTimeColumnSorted(data, columns.time)) {
|
ElMessage.error("时间列必须按升序排列!");
|
forms.fileList = [];
|
return;
|
}
|
|
// 提取单位(如 mm/h),若没有则设为空字符串
|
forms.intensityUnit = extractUnitFromHeader(columns.intensity) || "";
|
|
// 将原始数据转换为统一结构的对象数组
|
const rawRainFallList = data.map((row) => ({
|
time: row[columns.time],
|
intensity: parseFloat(row[columns.intensity]),
|
total: columns.totalRainfall
|
? parseFloat(row[columns.totalRainfall])
|
: undefined,
|
}));
|
|
// 更新 forms.rainFallList,可用于图表显示等用途
|
forms.rainFallList = rawRainFallList;
|
|
|
// 判断是否为整小时数据(即相邻时间间隔是否为整小时)
|
const isHourlyData = checkIfHourlyData(rawRainFallList);
|
|
let hourlyRainfallList = [];
|
|
if (!isHourlyData) {
|
// 如果不是整小时数据,按小时进行聚合处理
|
hourlyRainfallList = aggregateToHourlyRainfall(rawRainFallList);
|
console.log(hourlyRainfallList, "修正后的小时雨强");
|
} else {
|
// 如果是整小时数据,直接使用原始雨强值
|
hourlyRainfallList = rawRainFallList.map((item) => ({
|
time: item.time,
|
intensity: item.intensity,
|
}));
|
}
|
|
// 计算起始时间和结束时间(毫秒数)
|
const firstTime = parseDateTime(hourlyRainfallList[0]?.time);
|
const lastTime = parseDateTime(
|
hourlyRainfallList[hourlyRainfallList.length - 1]?.time
|
);
|
|
// 计算持续时间(单位:小时)
|
const durationSeconds = Math.floor((lastTime - firstTime) / 1000);
|
forms.duration = (durationSeconds / 3600).toFixed(2); // 单位:小时
|
|
// 找出最大小时雨强
|
const maxIntensity = Math.max(
|
...hourlyRainfallList.map((item) => item.intensity).filter((v) => !isNaN(v))
|
).toFixed(2);
|
forms.intensity = maxIntensity;
|
|
// 若有总降雨量列,取出最后一个值作为总降雨量
|
if (columns.totalRainfall) {
|
const lastTotal = parseFloat(data[data.length - 1][columns.totalRainfall]);
|
forms.rainfall = isNaN(lastTotal) ? 0 : lastTotal.toFixed(2);
|
} else {
|
forms.rainfall = 0;
|
}
|
};
|
|
/**
|
* 检查数据是否为整小时记录
|
* @param {Array} rainList - 原始降雨数据列表,每个元素包含 time 和 intensity
|
* @returns {boolean} - 是否为整小时数据
|
*/
|
function checkIfHourlyData(rainList) {
|
if (rainList.length < 2) return true; // 只有一个点,默认视为整小时数据
|
|
for (let i = 1; i < rainList.length; i++) {
|
// 解析两个相邻时间点
|
const time1 = parseDateTime(rainList[i - 1].time);
|
const time2 = parseDateTime(rainList[i].time);
|
|
// 计算时间差(分钟)
|
const diffMinutes = Math.abs(time2 - time1) / (1000 * 60);
|
|
// 如果时间差不是整小时(不能被60整除),则不是整小时数据
|
if (diffMinutes % 60 !== 0) {
|
return false;
|
}
|
}
|
|
return true;
|
}
|
|
/**
|
* 将任意时间粒度的雨强数据,按小时聚合为“小时雨强”
|
* @param {Array} rainList - 原始数据列表,每个元素包含 time 和 intensity
|
* @returns {Array} - 按小时分组的聚合结果
|
*/
|
function aggregateToHourlyRainfall(rainList) {
|
const grouped = {}; // 用于临时存储每个小时的数据
|
|
for (const item of rainList) {
|
// 解析时间字符串为时间戳
|
const timestamp = parseDateTime(item.time);
|
|
// 如果解析失败,跳过当前项
|
if (isNaN(timestamp)) {
|
console.warn("无效的时间格式,已跳过", item.time);
|
continue;
|
}
|
|
// 将时间戳转为 Date 对象以便操作日期
|
const dt = new Date(timestamp);
|
|
// 构造年月日+小时键(如:"2024-08-25 14")
|
const year = String(dt.getFullYear()).padStart(4, "0");
|
const month = String(dt.getMonth() + 1).padStart(2, "0"); // 注意月份从0开始
|
const date = String(dt.getDate()).padStart(2, "0");
|
const hour = String(dt.getHours()).padStart(2, "0");
|
|
const hourKey = `${year}-${month}-${date} ${hour}`;
|
|
// 初始化该小时的聚合对象
|
if (!grouped[hourKey]) {
|
grouped[hourKey] = {
|
time: `${hourKey}:00:00`, // 标准化为整点时间
|
intensity: 0,
|
};
|
}
|
|
// 累加该小时内所有雨强值
|
grouped[hourKey].intensity += item.intensity;
|
}
|
|
// 将聚合结果转为数组并保留两位小数
|
const result = Object.values(grouped).map((item) => ({
|
time: item.time,
|
intensity: Number(item.intensity.toFixed(2)),
|
}));
|
|
return result;
|
}
|
/**
|
* 解析日期时间字符串或Excel数字日期,返回时间戳(毫秒数)
|
* @param {string|number} dateString - 日期字符串或Excel数字日期
|
* @returns {number} 时间戳(毫秒数),解析失败返回 NaN
|
*/
|
const parseDateTime = (dateString) => {
|
// 1. 处理 Excel 数字日期(如 45136.91666666666),但是此处我在Excel解析时间的时候已经转换了,所以这个暂时无用
|
if (typeof dateString === "number") {
|
// 使用 XLSX 工具解析 Excel 日期编码
|
const parsedDate = XLSX.SSF.parse_date_code(dateString);
|
if (parsedDate) {
|
// 转换为 JavaScript Date 对象并返回时间戳
|
return new Date(
|
parsedDate.y, // 年
|
parsedDate.m - 1, // 月(Excel 中 1-12,JS 中 0-11)
|
parsedDate.d, // 日
|
parsedDate.H || 0, // 时(可能不存在,默认为 0)
|
parsedDate.M || 0, // 分(可能不存在,默认为 0)
|
parsedDate.S || 0 // 秒(可能不存在,默认为 0)
|
).getTime(); // 返回时间戳
|
}
|
}
|
|
// 2. 尝试直接解析日期字符串(如 "2023-07-30T16:00:00"),现在使用的是这个
|
const parsedDate = new Date(dateString);
|
if (!isNaN(parsedDate.getTime())) {
|
return parsedDate.getTime(); // 返回有效时间戳
|
}
|
|
// 3. 处理自定义格式的日期字符串(如 "2023/07/30 16:00:00")
|
const parts = dateString.split(/[/\s:]/); // 按 `/`、空格、`:` 分割
|
if (parts.length >= 6) {
|
const year = parseInt(parts[0], 10); // 年
|
const month = parseInt(parts[1], 10) - 1; // 月(转换为 JS 的 0-11)
|
const day = parseInt(parts[2], 10); // 日
|
const hour = parseInt(parts[3], 10) || 0; // 时(默认 0)
|
const minute = parseInt(parts[4], 10) || 0; // 分(默认 0)
|
const second = parseInt(parts[5], 10) || 0; // 秒(默认 0)
|
|
// 构造 Date 对象并返回时间戳
|
const date = new Date(year, month, day, hour, minute, second);
|
if (!isNaN(date.getTime())) {
|
return date.getTime();
|
}
|
}
|
|
// 4. 解析失败时警告并返回 NaN
|
console.warn(`无法解析日期: ${dateString}`);
|
return NaN;
|
};
|
|
const handleExceed = () => {
|
ElMessage.warning("每次只能上传一个文件");
|
};
|
|
const handleRemove = () => {
|
forms.rainfall = null;
|
forms.duration = null;
|
forms.intensity = null;
|
forms.fileList = [];
|
forms.rainFallList = [];
|
forms.hours = null;
|
forms.intensityUnit = "";
|
};
|
|
const beforeUpload = (file) => {
|
const isExcel = file.name.endsWith(".xlsx") || file.name.endsWith(".xls");
|
const isCSV = file.name.endsWith(".csv");
|
if (!isExcel && !isCSV) {
|
ElMessage.error("只能上传Excel或CSV文件");
|
return false;
|
}
|
return true;
|
};
|
|
// 开始模拟
|
async function startPlay() {
|
try {
|
// 保存方案
|
if (selectTab.value == "重点沟仿真") {
|
forms.geom = props.selectedArea;
|
}
|
const res = await simStore.addSimCheme(forms);
|
const schemeId = res.data?.data?.id;
|
|
if (!schemeId) {
|
ElMessage.error("方案保存失败,未获取到有效 ID");
|
return;
|
}
|
|
// 调用求解器
|
const simStartRes = await getSimStart(schemeId);
|
|
// 关闭选择区域窗口、初始化视图并开始模拟
|
EventBus.emit("close-selectArea");
|
simStore.shouldPoll = true;
|
|
// 暂时不在此处开始模拟,模拟都在方案列表中进行模拟
|
// initeWaterPrimitiveView();
|
// startSimulate();
|
|
ElMessage.warning({
|
message: "请返回方案列表等待模拟结果!",
|
duration: 10000, // 提示框显示时长,单位为毫秒,默认是3000毫秒
|
});
|
} catch (error) {
|
console.error("启动模拟过程中发生错误:", error);
|
// ElMessage.error("启动模拟失败,请稍后再试");
|
}
|
}
|
</script>
|
|
<style lang="less" scoped>
|
.forms {
|
background: url("@/assets/img/screen/leftbg.png");
|
margin-top: 10px;
|
width: 100%;
|
height: 100%;
|
padding: 10px 10px 0px 0px;
|
box-sizing: border-box;
|
transition: background 0.3s ease; // 可选过渡效果
|
}
|
|
.forms.no-background {
|
margin-top: 0px;
|
background-image: none;
|
}
|
/deep/ .el-input-group__append,
|
.el-input-group__prepend {
|
background-color: #084b42;
|
color: #fff;
|
}
|
/deep/ .el-form-item__label {
|
color: #61f7d4 !important;
|
}
|
/deep/ .el-upload-list__item-file-name {
|
white-space: normal;
|
}
|
</style>
|