wangjuncheng
2025-07-14 263bf9730455a7bcc4fdc6471f8f9d0c96e47c9e
src/views/left/CitySim.vue
@@ -1,10 +1,6 @@
<template>
  <div style="width: 100%; height: 100%">
    <div
      class="left-top"
      v-if="simStore.selectTab == '行政区划仿真'"
      style="margin-top: 0px"
    >
    <div class="left-top" v-if="simStore.selectTab == '行政区划仿真'" style="margin-top: 0px">
      行政区划仿真(30m精度)
    </div>
    <div class="left-top" v-if="simStore.selectTab == '重点区域仿真'">
@@ -15,134 +11,56 @@
    </div>
    <div class="forms" :class="{ 'no-background': !showBackground }">
      <el-form
        :rules="rules"
        :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="请输入方案名称"
          >
          <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-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 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-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-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="请输入降雨量"
          >
          <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-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="请输入降雨时长"
          >
          <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="请输入降雨强度"
          >
          <el-input v-model="forms.intensity" style="max-width: 600px" placeholder="请输入降雨强度">
            <template #append>mm/h</template>
          </el-input>
        </el-form-item>
@@ -186,7 +104,7 @@
const intensityOptions = ref([
  { value: "mm/h", label: "mm/h" },
  { value: "mm/5min", label: "mm/5min" },
  { value: "mm/1min", label: "mm/1min" },
  { value: "mm/min", label: "mm/min" },
]);
// 定义一个方法,用于根据 type 获取区域数据
@@ -277,9 +195,11 @@
      forms.geom = props.selectedArea;
    }
    await simStore.addSimCheme(forms);
    // 打印拦挡坝所需要的数据
    printDamEntities();
    resetForm(); // 只有在保存成功后才重置表单
    EventBus.emit("close-selectArea");
  } catch (error) {}
  } catch (error) { }
};
// 重置表单
@@ -447,15 +367,16 @@
/**
 * 数据处理主函数
 * @param {Array} data - 解析后的原始数据数组,每个元素是一个对象
 */
const processData = (data) => {
  // 检查空数据
  // 检查是否为空数据
  if (data.length === 0) {
    ElMessage.warning("文件内容为空!");
    return;
  }
  // 匹配字段名
  // 匹配列名(例如“时间”、“小时雨强”)
  const columns = matchColumns(data[0]);
  // 校验必要字段是否存在
@@ -475,22 +396,18 @@
    return;
  }
  // 时间列校验是否升序
  // 校验时间列是否升序排列
  if (!isTimeColumnSorted(data, columns.time)) {
    ElMessage.error("时间列必须按升序排列!");
    forms.fileList = [];
    return;
  }
  // 提取单位
  forms.intensityUnit = extractUnitFromHeader(columns.intensity);
  // 提取单位(如 mm/h),若没有则设为空字符串
  forms.intensityUnit = extractUnitFromHeader(columns.intensity) || "";
  if (!forms.intensityUnit) {
    forms.intensityUnit = "";
  }
  // 转换 key 名并转换数值类型
  forms.rainFallList = data.map((row) => ({
  // 将原始数据转换为统一结构的对象数组
  const rawRainFallList = data.map((row) => ({
    time: row[columns.time],
    intensity: parseFloat(row[columns.intensity]),
    total: columns.totalRainfall
@@ -498,21 +415,44 @@
      : undefined,
  }));
  console.log(forms.rainFallList, "解析后的降雨数据");
  // 更新 forms.rainFallList,可用于图表显示等用途
  forms.rainFallList = rawRainFallList;
  // 计算统计信息
  const firstTime = parseDateTime(data[0][columns.time]);
  const lastTime = parseDateTime(data[data.length - 1][columns.time]);
  // 判断是否为整小时数据(即相邻时间间隔是否为整小时)
  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); // 小时
  forms.duration = (durationSeconds / 3600).toFixed(2); // 单位:小时
  // 找出最大小时雨强
  const maxIntensity = Math.max(
    ...data
      .map((row) => parseFloat(row[columns.intensity]))
      .filter((v) => !isNaN(v))
    ...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);
@@ -521,56 +461,80 @@
  }
};
// // 处理数据
// const processData = (data) => {
//   // 1. 检查数据是否为空
//   if (data.length === 0) {
//     ElMessage.warning("文件内容为空!");
//     return;
//   }
/**
 * 检查数据是否为整小时记录
 * @param {Array} rainList - 原始降雨数据列表,每个元素包含 time 和 intensity
 * @returns {boolean} - 是否为整小时数据
 */
function checkIfHourlyData(rainList) {
  if (rainList.length < 2) return true; // 只有一个点,默认视为整小时数据
//   // 2. 获取表头(第一列是时间列)
//   const tableColumns = Object.keys(data[0]);
//   const timeColumn = tableColumns[0]; // 假设第一列是时间
  for (let i = 1; i < rainList.length; i++) {
    // 解析两个相邻时间点
    const time1 = parseDateTime(rainList[i - 1].time);
    const time2 = parseDateTime(rainList[i].time);
//   // 3. 校验时间列是否按升序排列
//   if (!isTimeColumnSorted(data, timeColumn)) {
//     ElMessage.error("时间列必须按升序排列!");
//     forms.fileList = [];
//     return; // 终止处理
//   }
    // 计算时间差(分钟)
    const diffMinutes = Math.abs(time2 - time1) / (1000 * 60);
//   const intensityColumn = tableColumns[1]; // 雨强列(如 "小时雨强(mm/h)")
//   // console.log(intensityColumn, "intensityColumnintensityColumnintensityColumn");
//   // 3. 提取第二列的单位(如 "(mm/h)" → "mm/h")
//   const intensityUnit = extractUnitFromHeader(intensityColumn);
//   forms.intensityUnit = intensityUnit; // 存储单位(可选)
//   console.log(forms.intensityUnit,'aaaaaaaaaaaaaaaaaaaaa')
    // 如果时间差不是整小时(不能被60整除),则不是整小时数据
    if (diffMinutes % 60 !== 0) {
      return false;
    }
  }
//   // 4. 如果校验通过,继续处理数据
//   forms.rainFallList = transformKeys(data);
//   console.log(forms.rainFallList, "data");
  return true;
}
//   // 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);
//   // 降雨强度
//   const maxValue = Math.max(
//     ...data.map((row) => {
//       const value = parseFloat(row[tableColumns[1]]);
//       return isNaN(value) ? -Infinity : value;
//     })
//   ).toFixed(2);
//   forms.intensity = maxValue;
/**
 * 将任意时间粒度的雨强数据,按小时聚合为“小时雨强”
 * @param {Array} rainList - 原始数据列表,每个元素包含 time 和 intensity
 * @returns {Array} - 按小时分组的聚合结果
 */
function aggregateToHourlyRainfall(rainList) {
  const grouped = {}; // 用于临时存储每个小时的数据
//   const lastValue = data[data.length - 1][tableColumns[2]];
//   forms.rainfall = lastValue;
  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数字日期
@@ -681,6 +645,70 @@
    // ElMessage.error("启动模拟失败,请稍后再试");
  }
}
// ========================================拦挡坝===============================================================
// 获取拦挡坝数据
function printDamEntities() {
  const entities = viewer.entities.values;
  const damDataList = [];
  for (let i = 0; i < entities.length; i++) {
    const entity = entities[i];
    if (entity.name && (entity.name === '栏档坝1' || entity.name === '栏档坝2')) {
      let position = entity.position?._value;
      // 如果有位置信息,就将其从笛卡尔坐标转为经纬度和高度
      let cartographic = undefined;
      if (position) {
        cartographic = Cesium.Cartographic.fromCartesian(position);
      }
      damDataList.push({
        name: entity.name,
        position: position ? {
          x: position.x,
          y: position.y,
          z: position.z
        } : null,
        cartographic: cartographic ? {
          longitude: Cesium.Math.toDegrees(cartographic.longitude), // 经度(度)
          latitude:  Cesium.Math.toDegrees(cartographic.latitude),  // 纬度(度)
          height:    cartographic.height                           // 高度(米)
        } : null,
        heading: entity.heading?._value ?? entity.heading,
        pitch: entity.pitch?._value ?? entity.pitch,
        roll: entity.roll?._value ?? entity.roll,
        modelScale: entity.model?.scale?._value ?? entity.model?.scale
      });
    }
  }
  if (damDataList.length > 0) {
    console.log("【栏档坝实体数据列表】:", damDataList);
    deleteDamEntitiesAfterDelay();
  } else {
    console.log("未找到任何名为 '栏档坝1' 或 '栏档坝2' 的实体");
  }
}
// 保存方案后定时清除新建的拦挡坝数据
function deleteDamEntitiesAfterDelay() {
  setTimeout(() => {
    const entities = Array.from(viewer.entities.values);
    const damsToDelete = entities.filter(
      entity => entity.name === '栏档坝1' || entity.name === '栏档坝2'
    );
    damsToDelete.forEach(entity => {
      viewer.entities.remove(entity);
    });
    if (damsToDelete.length > 0) {
      console.log(`【已删除】共 ${damsToDelete.length} 个栏档坝实体`);
    } else {
      console.log("未找到任何可删除的栏档坝实体");
    }
  }, 5000);
}
</script>
<style lang="less" scoped>
@@ -698,14 +726,17 @@
  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;
}