guonan
2025-05-13 75134e86655dedcb2ad574146dadaad1a24f6f01
src/views/left/CitySim.vue
@@ -1,13 +1,30 @@
<template>
  <div style="width: 100%; height: 100%">
    <div class="left-top" v-if="disForm == '行政区划仿真'" style="margin-top: 0px;">
    <div
      class="left-top"
      v-if="simStore.selectTab == '行政区划仿真'"
      style="margin-top: 0px"
    >
      行政区划仿真(30m精度)
    </div>
    <div class="left-top" v-if="disForm == '重点区域仿真'">
    <div class="left-top" v-if="simStore.selectTab == '重点区域仿真'">
      重点区域仿真(10m精度)
    </div>
    <div class="forms">
      <el-form :model="forms" label-width="auto" style="max-width: 600px">
      <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="Please input"
          >
          </el-input>
        </el-form-item>
        <el-form-item label="上传参数">
          <el-upload
            v-model:file-list="forms.fileList"
@@ -24,31 +41,53 @@
            <template #append>mm/h</template>
          </el-upload>
        </el-form-item>
        <el-form-item label="行政区域:" v-if="disForm == '行政区划仿真'">
        <el-form-item label="雨强单位" v-if="forms.fileList.length !== 0">
          <el-select
            v-model="forms.eare"
            v-model="forms.intensityUnit"
            placeholder="Select"
            style="max-width: 600px"
          >
            <el-option
              v-for="item in cityOptions"
              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="disForm == '重点区域仿真'">
        <el-form-item
          label="行政区域:"
          v-if="simStore.selectTab == '行政区划仿真'"
        >
          <el-select
            v-model="forms.eares"
            @change="changeGeom"
            v-model="forms.geom"
            placeholder="Select"
            style="max-width: 600px"
          >
            <el-option
              v-for="item in earesOptions"
              v-for="item in options"
              :key="item.value"
              :label="item.label"
              :value="item.value"
              :value="item"
            />
          </el-select>
        </el-form-item>
        <el-form-item
          label="重点区域:"
          v-if="simStore.selectTab == '重点区域仿真'"
        >
          <el-select
            @change="changeGeom"
            v-model="forms.geom"
            placeholder="Select"
            style="max-width: 600px"
          >
            <el-option
              v-for="item in options"
              :key="item.value"
              :label="item.label"
              :value="item"
            />
          </el-select>
        </el-form-item>
@@ -62,9 +101,28 @@
            <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="Select date and time"
          />
          <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="Please input"
@@ -83,146 +141,132 @@
          </el-input>
        </el-form-item>
        <el-form-item label="仿真参数:"></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="openSaveDialog">保存方案</el-button>
        <el-button type="primary" @click="addSimCheme">保存方案</el-button>
        <el-button type="success" @click="startPlay">开始模拟</el-button>
      </div>
      <!-- 保存方案对话框 -->
      <el-dialog
        v-model="saveDialogVisible"
        :title="dialogTitle"
        width="50%"
        :before-close="handleClose"
        custom-class="custom-dialog"
      >
        <div class="dialog-content">
          <p><strong>模拟类型:</strong>{{ dialogTitle }}</p>
          <p v-if="disForm === '行政区划仿真'">
            <strong>行政区域:</strong>{{ forms.eare }}
          </p>
          <p v-if="disForm === '重点区域仿真'">
            <strong>重点区域:</strong>{{ forms.eares }}
          </p>
          <p><strong>降雨量:</strong>{{ forms.rainfall }} mm</p>
          <p><strong>降雨时长:</strong>{{ forms.duration }} h</p>
          <p><strong>降雨强度:</strong>{{ forms.intensity }} mm/h</p>
          <p><strong>上传文件:</strong>{{ uploadedFilesText }}</p>
        </div>
        <template #footer>
          <span class="dialog-footer">
            <el-button @click="saveDialogVisible = false">取消</el-button>
            <el-button type="primary" @click="confirmSave">确定保存</el-button>
          </span>
        </template>
      </el-dialog>
    </div>
  </div>
</template>
<script setup>
import { reactive, ref, watch, inject, computed } from "vue";
import { reactive, ref, watch, inject, computed, onMounted } from "vue";
import * as XLSX from "xlsx";
import Papa from "papaparse";
import { ElMessage } from "element-plus";
import { ElMessage, ElMessageBox } from "element-plus";
import { initeWaterPrimitiveView } from "@/utils/water";
import { SimAPIStore } from "@/store/simAPI";
import { getRegionData } 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 intensityOptions = ref([
  { value: "mm/h", label: "mm/h" },
  { value: "mm/5min", label: "mm/5min" },
]);
// 定义一个方法,用于根据 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);
});
// 监听 selectTab 的变化
watch(selectTab, (newVal) => {
  let type;
  switch (newVal) {
    case "行政区划仿真":
      type = 1;
      break;
    case "重点区域仿真":
      type = 2;
      break;
    default:
      type = 1; // 默认值
  }
  fetchRegionData(type);
});
// 注入父组件提供的方法
const { startSimulate, endSimulate } = inject("simulateActions");
const saveDialogVisible = ref(false); // 控制保存方案对话框的显示状态
// 表单数据
const forms = reactive({
  eare: "北京市",
  eares: "孙胡沟",
  rainfall: "",
  duration: "",
  intensity: "",
  name: "",
  geom: "",
  rainfall: null,
  duration: null,
  intensity: null,
  fileList: [],
  type: 3,
  rainFallList: [],
  hours: null,
  intensityUnit: "",
});
// 城市和重点区域选项
const cityOptions = [
  { value: "北京市", label: "北京市" },
  { value: "东城区", label: "东城区" },
  { value: "西城区", label: "西城区" },
  { value: "朝阳区", label: "朝阳区" },
  { value: "海淀区", label: "海淀区" },
  { value: "丰台区", label: "丰台区" },
  { value: "石景山区", label: "石景山区" },
  { value: "门头沟区", label: "门头沟区" },
  { value: "房山区", label: "房山区" },
  { value: "通州区", label: "通州区" },
  { value: "顺义区", label: "顺义区" },
  { value: "昌平区", label: "昌平区" },
  { value: "大兴区", label: "大兴区" },
  { value: "怀柔区", label: "怀柔区" },
  { value: "平谷区", label: "平谷区" },
  { value: "密云区", label: "密云区" },
  { value: "延庆区", label: "延庆区" },
];
const earesOptions = [
  { value: "孙胡沟", label: "孙胡沟" },
  { value: "鱼水洞后沟", label: "鱼水洞后沟" },
  { value: "于家西沟", label: "于家西沟" },
  { value: "北河沟", label: "北河沟" },
  { value: "龙泉峪村", label: "龙泉峪村" },
];
const flyHeight = ref(100000);
// 计算属性:获取对话框标题
const dialogTitle = computed(() => {
  return disForm.value === "行政区划仿真" ? "行政区划仿真" : "重点区域仿真";
});
// 将选中区域传递给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 {
    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 openSaveDialog = () => {
  if (
    !forms.rainfall ||
    !forms.duration ||
    !forms.intensity ||
    (disForm.value === "行政区划仿真" && !forms.eare) ||
    (disForm.value === "重点区域仿真" && !forms.eares)
  ) {
    ElMessage.warning("请先填写所有必填项");
    return;
  }
  saveDialogVisible.value = true;
};
// 关闭保存方案对话框
const handleClose = () => {
  saveDialogVisible.value = false;
};
// 确认保存
const confirmSave = () => {
  console.log("保存方案成功", {
    模拟类型: dialogTitle.value,
    行政区域: disForm.value === "行政区划仿真" ? forms.eare : null,
    重点区域: disForm.value === "重点区域仿真" ? forms.eares : null,
    降雨量: `${forms.rainfall} mm`,
    降雨时长: `${forms.duration} h`,
    降雨强度: `${forms.intensity} mm/h`,
    上传文件: forms.fileList.map(file => file.name),
  });
  ElMessage.success("方案已保存");
  saveDialogVisible.value = false;
  resetForm();
};
// 重置表单
const resetForm = () => {
  forms.eare = "北京市";
  forms.eares = "孙胡沟";
  forms.rainfall = "";
  forms.duration = "";
  forms.intensity = "";
  forms.fileList = [];
};
// 文件变化时触发解析
const handleFileChange = (file) => {
@@ -254,74 +298,160 @@
// 解析Excel文件
const parseExcel = (data) => {
  const workbook = XLSX.read(data, { type: "array" });
  console.log(workbook, "wokr");
  const firstSheetName = workbook.SheetNames[0];
  const worksheet = workbook.Sheets[firstSheetName];
  const jsonData = XLSX.utils.sheet_to_json(worksheet);
  const jsonData = XLSX.utils.sheet_to_json(worksheet, {
    raw: false, // 使用格式化字符串而不是原始值
    dateNF: "yyyy-mm-dd hh:mm:ss", // 指定日期格式
  });
  processData(jsonData);
};
const transformKeys = (data) => {
  return data.map((item) => ({
    time: item["时间"], // "时间" → "time"
    intensity: parseFloat(item["小时雨强mm/h"]), // 转为浮点数
    total: parseFloat(item["累计雨量"]), // 转为浮点数
  }));
};
/**
 * 检查时间列是否按升序排列
 * @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|m\/s|mm|℃|%|hPa|km\/h)/; // 根据需要扩展
  const match = header.match(unitRegex);
  return match ? match[0] : "";
};
// 处理数据
const processData = (data) => {
  if (data.length === 0) return;
  // 1. 检查数据是否为空
  if (data.length === 0) {
    ElMessage.warning("文件内容为空!");
    return;
  }
  // 2. 获取表头(第一列是时间列)
  const tableColumns = Object.keys(data[0]);
  const timeColumn = tableColumns[0]; // 假设第一列是时间
  // 第一列:时间
  const firstTime = parseDateTime(data[0][tableColumns[0]]);
  const lastTime = parseDateTime(data[data.length - 1][tableColumns[0]]);
  // 3. 校验时间列是否按升序排列
  if (!isTimeColumnSorted(data, timeColumn)) {
    ElMessage.error("时间列必须按升序排列!");
    forms.fileList = [];
    return; // 终止处理
  }
  const intensityColumn = tableColumns[1]; // 雨强列(如 "小时雨强(mm/h)")
  console.log(intensityColumn, "intensityColumnintensityColumnintensityColumn");
  // 3. 提取第二列的单位(如 "(mm/h)" → "mm/h")
  const intensityUnit = extractUnitFromHeader(intensityColumn);
  console.log(
    intensityUnit,
    "intensityUnitintensityUnitintensityUnitintensityUnit"
  );
  forms.intensityUnit = intensityUnit; // 存储单位(可选)
  // 4. 如果校验通过,继续处理数据
  forms.rainFallList = transformKeys(data);
  console.log(forms.rainFallList, "data");
  // 5. 计算降雨时长、最大雨强、累计雨量(原逻辑)
  const firstTime = parseDateTime(data[0][timeColumn]);
  const lastTime = parseDateTime(data[data.length - 1][timeColumn]);
  const timeDuration = Math.floor((lastTime - firstTime) / 1000);
  forms.duration = (timeDuration / 3600).toFixed(2); // 更新降雨时长
  forms.duration = (timeDuration / 3600).toFixed(2);
  // 第二列:降雨强度
  const maxValue = Math.max(
    ...data.map((row) => {
      const value = parseFloat(row[tableColumns[1]]);
      return isNaN(value) ? -Infinity : value;
    })
  ).toFixed(2);
  forms.intensity = maxValue; // 更新降雨强度
  forms.intensity = maxValue;
  // 第三列:累计降雨量
  const lastValue = data[data.length - 1][tableColumns[2]];
  forms.rainfall = lastValue; // 更新降雨量
  forms.rainfall = lastValue;
};
// 解析日期时间
/**
 * 解析日期时间字符串或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,
        parsedDate.d,
        parsedDate.H || 0,
        parsedDate.M || 0,
        parsedDate.S || 0
      ).getTime();
        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();
    return parsedDate.getTime(); // 返回有效时间戳
  }
  const parts = dateString.split(/[/\s:]/);
  // 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;
    const day = parseInt(parts[2], 10);
    const hour = parseInt(parts[3], 10) || 0;
    const minute = parseInt(parts[4], 10) || 0;
    const second = parseInt(parts[5], 10) || 0;
    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;
};
@@ -340,24 +470,12 @@
  return true;
};
const disForm = ref("");
// 定义 Props
const props = defineProps({
  clickValue: String,
});
// 监听 Props 变化
watch(
  () => props.clickValue,
  (newValue) => {
    disForm.value = newValue || "行政区划仿真";
  },
  { immediate: true, deep: true }
);
// 开始模拟
function startPlay() {
async function startPlay() {
  // 开始模拟前需要先保存方案
  await simStore.addSimCheme(forms);
  EventBus.emit("close-selectArea");
  initeWaterPrimitiveView();
  startSimulate();
}
</script>