<template>
|
<div class="timeline-container">
|
<div class="controls">
|
<div class="control-btn" @click="skipBackward">
|
<img
|
src="@/assets/img/timeline/left.png"
|
class="fas fa-step-backward"
|
/>
|
</div>
|
<div class="control-btn play-btn" @click="togglePlay">
|
<img v-show="isPlaying" src="@/assets/img/timeline/stop.png" />
|
<img v-show="!isPlaying" src="@/assets/img/timeline/start.png" />
|
</div>
|
<div class="control-btn" @click="skipForward">
|
<img
|
src="@/assets/img/timeline/right.png"
|
class="fas fa-step-forward"
|
/>
|
</div>
|
<div class="speed-control" v-show="speedShow">
|
<div @click="toggleSpeedMenu">{{ playbackRate }}X</div>
|
<div class="speed-menu" v-show="showSpeedMenu">
|
<div
|
v-for="rate in playbackRates"
|
:key="rate"
|
@click.capture="setPlaybackRate(rate)"
|
:class="{ active: playbackRate === rate }"
|
>
|
{{ rate }}X
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="timeline">
|
<div class="dates">
|
<div class="current-date">当前播放时间:{{ currentPlayingTime }}</div>
|
<div
|
v-for="(date, index) in visibleDates"
|
:key="index"
|
class="date-label"
|
>
|
<!-- {{ formatDate(date) }} -->
|
</div>
|
<div>
|
专题渲染:
|
<el-switch
|
v-model="isColorRenderEnabled"
|
@change="handleColorRenderChange"
|
style="margin-top: -3px"
|
:disabled="!isPlaying || !isWaterPrimitiveCreated"
|
/>
|
<!-- active-text="开" inactive-text="关" -->
|
</div>
|
</div>
|
<div class="timeline-track" ref="timelineTrack" @click="seekToPosition">
|
<div
|
class="timeline-progress"
|
:style="{ width: progressPercentage + '%' }"
|
></div>
|
<div
|
class="timeline-cursor"
|
:style="{ left: progressPercentage + '%' }"
|
></div>
|
<div class="scale-markers">
|
<div class="scale-marker" style="left: 0%"></div>
|
<div class="scale-marker" style="left: 25%"></div>
|
<div class="scale-marker" style="left: 50%"></div>
|
<div class="scale-marker" style="left: 75%"></div>
|
<div class="scale-marker" style="left: 100%"></div>
|
</div>
|
<div class="time-markers">
|
<div
|
v-for="(time, index) in timeMarkers"
|
:key="index"
|
class="time-marker"
|
:style="{ left: `${index * 25}%`, transform: 'translateX(-50%)' }"
|
>
|
<div class="date-part">{{ time.split(" ")[0] }}</div>
|
<div class="time-part">{{ time.split(" ")[1] }}</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
<div>
|
<div style="display: flex">
|
<ratelevel
|
ref="ratelevelRef"
|
:playing-time="sendCurrentPlayingTime"
|
@finish-calculation="handleFinishCalculation"
|
style="
|
margin-top: 12px;
|
margin-left: 28px;
|
margin-right: 10px;
|
justify-content: flex-end;
|
"
|
/>
|
<crossanalysis
|
ref="crossRef"
|
style="
|
margin-top: 12px;
|
margin-left: 16px;
|
margin-right: 20px;
|
justify-content: flex-end;
|
"
|
/>
|
</div>
|
<el-button
|
@click="handleBack"
|
style="
|
margin-top: 3px;
|
margin-left: 28px;
|
margin-right: 10px;
|
width: 75%;
|
height: 30%;
|
"
|
>结束模拟</el-button
|
>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import {
|
ref,
|
computed,
|
onMounted,
|
watch,
|
defineProps,
|
onBeforeUnmount,
|
inject,
|
reactive,
|
watchEffect,
|
} from "vue";
|
import ratelevel from "@/components/menu/flowRate_waterLevel.vue";
|
import crossanalysis from "@/components/menu/CrossSectionalAnalysis.vue";
|
|
import dayjs from "dayjs";
|
import {
|
createWaterPrimitive,
|
destoryWaterPrimitive,
|
pauseWaterSimulation,
|
resumeWaterSimulation,
|
setTimeForWaterSimulation,
|
toggleWaterColorRender,
|
updateWaterColor,
|
} from "@/utils/water";
|
import mapUtils from "@/utils/tools.js";
|
import { fetchWaterSimulationData, stopSim } from "@/api/trApi.js";
|
import { EventBus } from "@/eventBus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
// 状态管理器
|
import { useSimStore } from "@/store/simulation";
|
import { storeToRefs } from "pinia";
|
const simStore = useSimStore();
|
const { selectedScheme, frameNum, layerDate,schemWaterInfo } = storeToRefs(simStore);
|
|
const emit = defineEmits([
|
"timeUpdate",
|
"isPlaying",
|
"playbackFinished",
|
"isColorRender",
|
]);
|
// 定义props
|
const props = defineProps({
|
waterSimulateParams: {
|
type: Object,
|
default: () => ({
|
date: ["2025-02-14T16:00:00.000Z", "2025-02-15T16:00:00.000Z"],
|
}),
|
},
|
});
|
// 响应式状态
|
let serviceInfo = ref(null); // 当前方案的服务地址
|
const ratelevelRef = ref(null); // 获取子组件实例的引用
|
const crossRef = ref(null); // 获取子组件实例的引用
|
const currentPlayingTime = ref(""); // 当前播放时间
|
const sendCurrentPlayingTime = ref(""); // 当前播放时间
|
const isPlaying = ref(false);
|
const playbackFinished = ref(true);
|
const currentTime = ref(0);
|
const duration = ref(60); // 一天的秒数
|
const playbackRate = ref(1);
|
const playbackRates = ref([1, 2, 4, 8]);
|
const showSpeedMenu = ref(false);
|
const speedShow = ref(false);
|
|
const waterTimestamps = ref([]); // 存储时间轴数据
|
const timeMarkers = ref([]);
|
const timelineTrack = ref(null);
|
const isColorRenderEnabled = ref(false); // 假设这是你的颜色渲染开关状态
|
const isWaterPrimitiveCreated = ref(false);
|
let playInterval = null;
|
let timeStepInfo = null;
|
let rainTotalInfo = [];
|
const isRainEnabled = ref(false);
|
const rainParams = reactive({
|
rainSize: 0.5,
|
rainSpeed: 50,
|
rainColor: "#99B3CC",
|
rainDensity: 30, // 雨的密度
|
});
|
let minFlowRate = ref();
|
let maxFlowRate = ref();
|
// 计算属性
|
const progressPercentage = computed(
|
() => (currentTime.value / duration.value) * 100
|
);
|
const visibleDates = computed(() =>
|
Array.from(
|
new Set(waterTimestamps.value.map((ts) => dayjs(ts).format("YYYY-MM-DD")))
|
).map((date) => dayjs(date).toDate())
|
);
|
|
// 我需要加一个判断
|
const finishPlay = ref(false);
|
// 播放控制
|
const togglePlay = () => {
|
// 这里应该再设定几个限制,如果缺少什么数据,无法进行仿真
|
if (!isPlaying.value && currentTime.value >= duration.value)
|
currentTime.value = 0;
|
|
isPlaying.value = !isPlaying.value;
|
emit("isPlaying", isPlaying.value);
|
|
if (isPlaying.value) {
|
startPlayback();
|
|
if (!isWaterPrimitiveCreated.value) {
|
// console.log(serviceInfo, '这里是当前方案的服务信息!');
|
// 这里通过water.js中去发送请求获取水面模拟
|
createWaterPrimitive({
|
baseUrl: `/simu/${serviceInfo}`,
|
// baseUrl: `/simu/c2h1dc`,
|
interval: intervalMap[playbackRate.value],
|
colorRender: isColorRenderEnabled.value,
|
minFlowRate,
|
maxFlowRate,
|
});
|
isWaterPrimitiveCreated.value = true;
|
} else {
|
resumeWaterSimulation();
|
toggleWaterColorRender(isColorRenderEnabled.value); // 更新颜色渲染
|
}
|
|
if (currentTime.value === 0) emit("playbackFinished", false);
|
|
if (isRainEnabled.value) {
|
mapUtils.toggleRain(rainParams, true);
|
}
|
} else {
|
stopPlayback();
|
pauseWaterSimulation();
|
|
isRainEnabled.value = true;
|
setTimeout(() => {
|
mapUtils.delRain();
|
}, 3000);
|
}
|
};
|
|
// 颜色渲染切换事件
|
const handleColorRenderChange = (enabled) => {
|
if (!isPlaying.value) {
|
ElMessage({
|
message: "请先启动水体模拟后再进行专题效果切换。",
|
type: "warning",
|
});
|
return; // 阻止后续逻辑执行
|
}
|
if (isWaterPrimitiveCreated.value) {
|
console.log("当前是否开启专题渲染:", enabled);
|
emit("isColorRender", enabled);
|
toggleWaterColorRender(enabled);
|
}
|
};
|
const intervalMap = {
|
1: 1000, // 1倍速
|
2: 500, // 2倍速
|
4: 250, // 4倍速
|
8: 125, // 8倍速
|
};
|
// 播放逻辑
|
// const startPlayback = () => {
|
// clearInterval(playInterval);
|
|
// playInterval = setInterval(() => {
|
// // 找到当前时间对应的索引
|
// const currentIndex = findClosestTimestampIndex(currentTime.value);
|
// const nextIndex = currentIndex + 1;
|
|
// // 如果已经是最后一个时间点,停止播放
|
// if (nextIndex >= waterTimestamps.value.length) {
|
// currentTime.value = duration.value;
|
// stopPlayback();
|
// isPlaying.value = false;
|
// emit("isPlaying", false);
|
// emit("playbackFinished", true);
|
// return;
|
// }
|
|
// // 更新时间为下一个时间点的时间差(秒)
|
// const nextTimestamp = waterTimestamps.value[nextIndex];
|
// const baseTimestamp = waterTimestamps.value[0];
|
// currentTime.value = (nextTimestamp - baseTimestamp) / 1000;
|
|
// // 触发更新
|
// if (selectedScheme.value.type !== 2) {
|
// updateWaterColorByTime();
|
// updateWeatherByProgress();
|
// }
|
|
// const progress = currentTime.value / duration.value;
|
// emit("timeUpdate", progress * 100);
|
// }, 1000 / playbackRate.value); // 根据播放速率调整间隔
|
// };
|
const startPlayback = () => {
|
clearInterval(playInterval);
|
|
// 新建方案中的实时模拟不能倍速
|
if (selectedScheme.value.type === 2 && simStore.rePlayList.length == 0) {
|
console.log("新建方案实时模拟五秒一跳");
|
// 实时模拟:每 5 秒跳动一次
|
playInterval = setInterval(() => {
|
const fiveSeconds = 5;
|
const totalDuration = duration.value; // 总时长(秒)
|
|
currentTime.value += fiveSeconds;
|
|
if (currentTime.value >= totalDuration) {
|
currentTime.value = totalDuration;
|
stopPlayback();
|
isPlaying.value = false;
|
finishPlay.value = true;
|
emit("isPlaying", false);
|
emit("playbackFinished", true);
|
return;
|
}
|
|
// 触发进度更新
|
const progress = currentTime.value / totalDuration;
|
emit("timeUpdate", progress * 100);
|
|
// 如果需要触发某些更新函数,也可以保留
|
updateWaterColorByTime();
|
// updateWeatherByProgress();
|
// 修改为固定阶段,缓慢下雨的状态
|
const rainParams = {
|
rainSize: 0.5,
|
rainSpeed: 20,
|
rainDensity: 15,
|
rainColor: "#ADD8E6",
|
};
|
console.log("实时模拟开始下雨");
|
// 调用工具方法更新雨效
|
mapUtils.toggleRain(rainParams, true);
|
}, 5000); // 每 5 秒执行一次
|
} else {
|
// 这里面还是你的播放代码,上面的if中是五秒钟跳动一次的实时模拟
|
playInterval = setInterval(() => {
|
const currentIndex = findClosestTimestampIndex(currentTime.value);
|
const nextIndex = currentIndex + 1;
|
|
if (nextIndex >= waterTimestamps.value.length) {
|
currentTime.value = duration.value;
|
stopPlayback();
|
isPlaying.value = false;
|
finishPlay.value = true;
|
emit("isPlaying", false);
|
emit("playbackFinished", true);
|
return;
|
}
|
|
const nextTimestamp = waterTimestamps.value[nextIndex];
|
const baseTimestamp = waterTimestamps.value[0];
|
currentTime.value = (nextTimestamp - baseTimestamp) / 1000;
|
|
if (selectedScheme.value.type !== 2) {
|
updateWaterColorByTime();
|
updateWeatherByProgress();
|
}
|
|
const progress = currentTime.value / duration.value;
|
if (selectedScheme.value.type !== 2) {
|
emit("timeUpdate", progress * 100);
|
}
|
}, 1000 / playbackRate.value);
|
}
|
};
|
// 降雨变化部分
|
// 降雨数据相关变量
|
let rainFallValues = ref([]); // 存储原始降雨量数据
|
let minRainValue = ref(Infinity);
|
let maxRainValue = ref(-Infinity);
|
// 获取降雨数据
|
function getRainfallData() {
|
if (!selectedScheme.value || !selectedScheme.value.data) {
|
console.warn("selectedScheme 或 data 不存在");
|
return;
|
}
|
|
let data = selectedScheme.value.data;
|
|
// 如果是字符串,则尝试解析为对象
|
if (typeof data === "string") {
|
try {
|
data = JSON.parse(data);
|
console.log("解析后的降雨数据:", data);
|
} catch (e) {
|
console.error("data 不是有效的 JSON 字符串");
|
return;
|
}
|
}
|
|
console.log("降雨强度的单位是:", data.intensityUnit);
|
|
// 判断 rainfalls 是否为对象,如果是则转成数组
|
let rainfalls = data.rainfalls;
|
if (typeof rainfalls === "object" && !Array.isArray(rainfalls)) {
|
rainfalls = Object.values(rainfalls);
|
console.warn("⚠️ rainfalls 是对象,已转换为数组");
|
}
|
|
// 按小时聚合降雨数据
|
const hourlyRainfallMap = {};
|
|
rainfalls.forEach((record) => {
|
const originalTime = new Date(record.time);
|
if (isNaN(originalTime.getTime())) {
|
console.warn("无效的时间格式:", record.time);
|
return;
|
}
|
|
// 构造“小时”级别的时间键,比如:2024-08-25 20:00:00
|
const hourKey = new Date(
|
originalTime.getFullYear(),
|
originalTime.getMonth(),
|
originalTime.getDate(),
|
originalTime.getHours()
|
);
|
|
const hourStr = hourKey.toISOString().slice(0, 16).replace("T", " ");
|
|
if (!hourlyRainfallMap[hourStr]) {
|
hourlyRainfallMap[hourStr] = {
|
intensity: 0,
|
time: hourStr,
|
total: record.total, // 默认用第一个记录的 total
|
};
|
}
|
|
hourlyRainfallMap[hourStr].intensity += record.intensity;
|
// 取最大的 total(因为是累积值)
|
if (record.total > hourlyRainfallMap[hourStr].total) {
|
hourlyRainfallMap[hourStr].total = record.total;
|
}
|
});
|
|
// 转换 map 成数组并排序
|
const hourlyRainfallList = Object.values(hourlyRainfallMap).sort((a, b) =>
|
a.time.localeCompare(b.time)
|
);
|
|
console.log("✅ 按小时聚合后的降雨数据:", hourlyRainfallList);
|
|
// 设置全局变量
|
rainTotalInfo.value = hourlyRainfallList;
|
|
// 计算时间步长
|
timeStepInfo = calculateTimeStep(rainTotalInfo.value);
|
|
// 提取 intensity 值
|
rainFallValues.value = hourlyRainfallList.map((r) => r.intensity);
|
minRainValue.value = Math.min(...rainFallValues.value);
|
maxRainValue.value = Math.max(...rainFallValues.value);
|
|
console.log(
|
"当前方案下最小雨量和最大雨量:",
|
minRainValue.value,
|
maxRainValue.value
|
);
|
}
|
// 定义降雨等级及其对应的视觉参数
|
const rainLevels = [
|
{
|
name: "小雨",
|
min: 0.1,
|
max: 9.9,
|
size: 0.5, // 雨滴大小:更小
|
speed: 20, // 下落速度:更慢
|
density: 15, // 雨滴密度:更稀疏
|
color: "#ADD8E6", // 浅蓝色,象征轻柔的小雨
|
},
|
{
|
name: "中雨",
|
min: 10,
|
max: 24.9,
|
size: 0.6,
|
speed: 24,
|
density: 18,
|
color: "#ADD8E6",
|
},
|
{
|
name: "大雨",
|
min: 25,
|
max: 49.9,
|
size: 0.7,
|
speed: 28,
|
density: 21,
|
color: "#ADD8E6",
|
},
|
{
|
name: "暴雨",
|
min: 50,
|
max: 99.9,
|
size: 0.8,
|
speed: 32,
|
density: 24,
|
color: "#ADD8E6",
|
},
|
{
|
name: "大暴雨",
|
min: 100,
|
size: 0.9,
|
speed: 36,
|
density: 27,
|
color: "#ADD8E6",
|
},
|
];
|
// 根据降雨量返回对应的雨形配置
|
function getRainLevel(rainValue) {
|
for (let level of rainLevels) {
|
if (
|
level.min <= rainValue &&
|
(level.max === undefined || rainValue <= level.max)
|
) {
|
return level;
|
}
|
}
|
// 默认无雨状态
|
|
return { name: "无雨", size: 0.3, speed: 10, density: 10, color: "#F0F8FF" };
|
}
|
// 根据播放进度更新天气效果(已优化)
|
let lastUsedIndex = -1; // 缓存上一次使用的索引,防止重复更新
|
let lastRainValue = null;
|
|
function calculateTimeStep(dataArray) {
|
if (!dataArray || dataArray.length < 2) {
|
console.warn("数据不足,无法计算时间步长");
|
return null;
|
}
|
|
// 解析时间字符串为 Date 对象
|
function parseTime(timeStr) {
|
return new Date(timeStr.replace(" ", "T")); // 兼容 ISO 格式
|
}
|
|
const firstTime = parseTime(dataArray[0].time);
|
const secondTime = parseTime(dataArray[1].time);
|
|
// 计算时间差(毫秒)
|
const diffMs = Math.abs(secondTime - firstTime);
|
|
// 转换为小时数(保留小数)
|
let timeStepHours = diffMs / (1000 * 60 * 60); // 毫秒 -> 小时
|
|
// 可选:遍历所有相邻项检查是否一致
|
for (let i = 1; i < dataArray.length - 1; i++) {
|
const current = parseTime(dataArray[i].time);
|
const next = parseTime(dataArray[i + 1].time);
|
const step = Math.abs(next - current) / (1000 * 60 * 60); // 毫秒 -> 小时
|
if (Math.abs(step - timeStepHours) > 0.01) {
|
console.warn(
|
`在索引 ${i} 处发现了不同的时间步长: ${step.toFixed(2)} 小时`
|
);
|
}
|
}
|
|
return timeStepHours;
|
}
|
// ============================================================================
|
// 优化方式,可以求出整个时间轴上,第一次遇到这六个阈值得时间点,然后分时间段显示,a时间内显示状态1,然后状态交界处设置颜色渐变,其余同理,这样跳转得时候能够直接跳转到当前得颜色信息阶段,直接应用,即可
|
// ============================================================================
|
// 全局状态记录
|
const colorState = {
|
currentColor: "#F5F0E6", // 当前颜色
|
currentAlpha: -0.3, // 当前透明度
|
colorStages: null, // 预计算的颜色阶段时间点
|
maxColorTime: null, // 记录达到最深颜色时的时间点
|
};
|
|
// 预计算颜色阶段时间点
|
function precomputeColorStages() {
|
if (!rainTotalInfo.value || rainTotalInfo.value.length === 0) return;
|
// 颜色配置(亮度递减)
|
const COLOR_STOPS = [
|
{ hex: "#F5F0E6", luminance: 240.4 }, // stage 0
|
{ hex: "#E6D5B8", luminance: 214.8 }, // stage 1
|
{ hex: "#D4B483", luminance: 184.0 }, // stage 2
|
{ hex: "#B78B6A", luminance: 148.4 }, // stage 3
|
{ hex: "#8B5A3A", luminance: 101.0 }, // stage 4
|
{ hex: "#744C33", luminance: 84.5 }, // stage 5
|
{ hex: "#5D3D2C", luminance: 68.1 }, // stage 6
|
];
|
const alphaStops = [
|
-0.2, // stage 0
|
-0.3, // stage 1
|
-0.4, // stage 2
|
-0.5, // stage 3
|
-0.6, // stage 4
|
-0.7, // stage 5
|
-0.8, // stage 6
|
];
|
// 累计降雨量阈值(mm)
|
const R_THRESHOLDS = [0, 200, 240, 280, 310, 350]; // 共6个阶段对应6个阈值
|
// 时间和降雨量信息
|
const timeTotals = [];
|
const initialTimestamp = new Date(rainTotalInfo.value[0].time).getTime();
|
for (let i = 0; i < rainTotalInfo.value.length; i++) {
|
const timestamp = new Date(rainTotalInfo.value[i].time).getTime();
|
const time = (timestamp - initialTimestamp) / 1000;
|
const total = rainTotalInfo.value[i].total; // 使用 total 替代 intensity
|
timeTotals.push({
|
time,
|
total,
|
});
|
}
|
|
// 找出每个阶段首次达到的时间点
|
const stages = [];
|
for (let stage = 1; stage < R_THRESHOLDS.length + 1; stage++) {
|
const threshold = R_THRESHOLDS[stage - 1];
|
for (let i = 0; i < timeTotals.length; i++) {
|
const { time, total } = timeTotals[i];
|
if (total >= threshold) {
|
stages[stage] = {
|
startTime: time,
|
color: COLOR_STOPS[stage].hex,
|
alpha: alphaStops[stage],
|
threshold,
|
};
|
break;
|
}
|
}
|
}
|
|
// 填充阶段0
|
stages[0] = {
|
startTime: 0,
|
color: COLOR_STOPS[0].hex,
|
alpha: alphaStops[0],
|
threshold: 0,
|
};
|
|
colorState.colorStages = stages;
|
}
|
|
function updateWaterColorByTime(isForceUpdate = false) {
|
if (!rainTotalInfo.value || rainTotalInfo.value.length === 0) return;
|
|
// 首次调用时预计算颜色阶段
|
if (colorState.colorStages === null) {
|
precomputeColorStages();
|
}
|
|
// 查找当前时间点所属的阶段
|
let currentStage = 0;
|
for (let i = colorState.colorStages.length - 1; i >= 0; i--) {
|
if (
|
colorState.colorStages[i] &&
|
currentTime.value >= colorState.colorStages[i].startTime
|
) {
|
currentStage = i;
|
break;
|
}
|
}
|
|
// 记录达到最深颜色的时间点
|
if (currentStage >= colorState.colorStages.length - 1) {
|
if (
|
colorState.maxColorTime === null ||
|
currentTime.value > colorState.maxColorTime
|
) {
|
colorState.maxColorTime = currentTime.value;
|
}
|
}
|
|
// 判断是否需要强制更新颜色
|
const isTimeGoingBackward = currentTime.value < colorState.lastTime;
|
const isBeforeMaxColorTime =
|
colorState.maxColorTime !== null &&
|
currentTime.value <= colorState.maxColorTime;
|
const shouldForceUpdate =
|
isForceUpdate && (isTimeGoingBackward || isBeforeMaxColorTime);
|
|
// 更新颜色逻辑
|
if (shouldForceUpdate || isTimeGoingBackward) {
|
// 强制更新或时间回退时,直接应用当前阶段的颜色
|
colorState.currentColor = colorState.colorStages[currentStage].color;
|
colorState.currentAlpha = colorState.colorStages[currentStage].alpha;
|
} else {
|
// 正常时间前进时,保持渐进变化
|
const newColor = colorState.colorStages[currentStage].color;
|
const newAlpha = colorState.colorStages[currentStage].alpha;
|
|
// 只应用更暗的颜色和更低的透明度
|
if (
|
calculateLuminance(newColor) < calculateLuminance(colorState.currentColor)
|
) {
|
colorState.currentColor = newColor;
|
}
|
if (newAlpha < colorState.currentAlpha) {
|
colorState.currentAlpha = newAlpha;
|
}
|
}
|
|
// 更新时间记录
|
colorState.lastTime = currentTime.value;
|
// ====== 新增:在 updateWaterColor 前打印当前信息 ======
|
// // 获取当前累计降雨量
|
let currentTotal = null;
|
const baseTimestamp = new Date(rainTotalInfo.value[0].time).getTime();
|
const currentTimeMs = baseTimestamp + currentTime.value * 1000;
|
|
// 找到最接近的降雨数据点
|
for (let i = rainTotalInfo.value.length - 1; i >= 0; i--) {
|
const dataTimeMs = new Date(rainTotalInfo.value[i].time).getTime();
|
if (dataTimeMs <= currentTimeMs) {
|
currentTotal = rainTotalInfo.value[i].total;
|
break;
|
}
|
}
|
|
// 打印信息
|
// console.log("========================================");
|
// console.log(`【时间戳】: ${new Date(currentTimeMs).toLocaleString()}`);
|
console.log(
|
`【累计降雨量 R】: ${
|
currentTotal !== null ? currentTotal.toFixed(2) : "未知"
|
} mm`
|
);
|
// console.log(`【当前阶段】: 第 ${currentStage} 阶段`);
|
console.log(
|
`【颜色 HEX】: ${colorState.colorStages[currentStage]?.color || "未定义"}`
|
);
|
// console.log(`【透明度 Alpha】: ${colorState.colorStages[currentStage]?.alpha || '未定义'}`);
|
// console.log("========================================");
|
// 应用颜色
|
updateWaterColor(colorState.currentColor, colorState.currentAlpha);
|
}
|
|
// 辅助函数保持不变
|
function calculateLuminance(hex) {
|
const [r, g, b] = hexToRgb(hex);
|
return 0.299 * r + 0.587 * g + 0.114 * b;
|
}
|
|
function hexToRgb(hex) {
|
const bigint = parseInt(hex.slice(1), 16);
|
return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255];
|
}
|
|
// 时间轴跳转函数
|
const seekToPosition = (event) => {
|
if (!isWaterPrimitiveCreated.value) {
|
ElMessage.warning("请先启动水体模拟后再进行时间轴跳转。");
|
return;
|
}
|
|
const rect = timelineTrack.value.getBoundingClientRect();
|
const percentage = (event.clientX - rect.left) / rect.width;
|
const targetTime = Math.round(percentage * duration.value);
|
|
const closestIndex = findClosestTimestampIndex(targetTime);
|
const baseTimestamp = waterTimestamps.value[0];
|
const newTime = (waterTimestamps.value[closestIndex] - baseTimestamp) / 1000;
|
|
// 判断是否需要强制更新颜色
|
const isGoingBackward = newTime < currentTime.value;
|
const isBeforeMaxColor =
|
colorState.maxColorTime !== null && newTime <= colorState.maxColorTime;
|
const shouldForceUpdate = isGoingBackward || isBeforeMaxColor;
|
|
currentTime.value = newTime;
|
setTimeForWaterSimulation(closestIndex);
|
|
// 根据条件更新颜色
|
updateWaterColorByTime(shouldForceUpdate);
|
|
if (!isPlaying.value) pauseWaterSimulation();
|
};
|
// ============================================================================
|
|
function updateWeatherByProgress() {
|
if (rainFallValues.value.length === 0) return;
|
// console.log(`时间轴总时长: ${duration.value}, 当前时间: ${currentTime.value}`); // 打印时间轴信息
|
const progress = currentTime.value / duration.value;
|
const floatIndex = progress * (rainFallValues.value.length - 1);
|
const index = Math.floor(floatIndex); // 当前索引
|
const nextIndex = Math.min(index + 1, rainFallValues.value.length - 1); // 下一索引
|
const currentRain = rainFallValues.value[index];
|
const nextRain = rainFallValues.value[nextIndex];
|
// 启用插值(alpha 平滑过渡)
|
const alpha = floatIndex - index;
|
// const rainValue = currentRain + (nextRain - currentRain) * alpha;
|
const rainValue = currentRain + (nextRain - currentRain);
|
// 打印当前处理的雨量数据
|
// console.log(
|
// `正在处理的雨量数据点: 当前=${currentRain}, 下一个=${nextRain}, 插值后=${rainValue.toFixed(
|
// 2
|
// )}, 索引=${index}`
|
// );
|
// 如果当前索引未变化且插值差异不大,跳过重复更新
|
if (index === lastUsedIndex && Math.abs(rainValue - lastRainValue) < 0.1) {
|
// console.log('由于数据无显著变化,跳过本次更新');
|
return;
|
}
|
|
lastUsedIndex = index;
|
lastRainValue = rainValue;
|
|
// 获取对应的雨形配置
|
const rainLevel = getRainLevel(rainValue);
|
|
// if (rainLevel.name === '无雨') {
|
// // 无雨状态:清除雨效
|
// mapUtils.delRain();
|
// console.log('执行了无雨状态,清除了雨效');
|
// return;
|
// }
|
|
// 非无雨状态:构建雨滴参数并更新雨效
|
const rainParams = {
|
rainSize: rainLevel.size,
|
rainSpeed: rainLevel.speed,
|
rainDensity: rainLevel.density,
|
rainColor: rainLevel.color,
|
};
|
console.log("当前雨量数据:", rainValue, "当前雨形:", rainLevel);
|
// 调用工具方法更新雨效
|
mapUtils.toggleRain(rainParams, true);
|
}
|
const stopPlayback = () => {
|
clearInterval(playInterval);
|
};
|
|
const skipForward = () => {
|
if (waterTimestamps.value.length === 0) return;
|
const currentIndex = findClosestTimestampIndex(currentTime.value);
|
const nextIndex = currentIndex + 1;
|
if (nextIndex >= waterTimestamps.value.length) {
|
return;
|
}
|
const baseTimestamp = waterTimestamps.value[0];
|
currentTime.value = (waterTimestamps.value[nextIndex] - baseTimestamp) / 1000;
|
setTimeForWaterSimulation(nextIndex);
|
if (!isPlaying.value) pauseWaterSimulation();
|
};
|
|
const skipBackward = () => {
|
if (waterTimestamps.value.length === 0) return;
|
const currentIndex = findClosestTimestampIndex(currentTime.value);
|
const prevIndex = currentIndex - 1;
|
if (prevIndex < 0) {
|
return;
|
}
|
const baseTimestamp = waterTimestamps.value[0];
|
currentTime.value = (waterTimestamps.value[prevIndex] - baseTimestamp) / 1000;
|
setTimeForWaterSimulation(prevIndex);
|
if (!isPlaying.value) pauseWaterSimulation();
|
};
|
const toggleSpeedMenu = () => (showSpeedMenu.value = !showSpeedMenu.value);
|
|
// 设置播放速率
|
const setPlaybackRate = (rate) => {
|
isColorRenderEnabled.value = false;
|
playbackRate.value = rate;
|
showSpeedMenu.value = false;
|
// 停止当前播放
|
stopPlayback();
|
setTimeout(() => {
|
mapUtils.delRain();
|
}, 3000);
|
// 重置时间轴到初始状态
|
currentTime.value = 0; // 时间归零
|
emit("timeUpdate", progressPercentage.value);
|
isPlaying.value = false;
|
emit("isPlaying", false);
|
|
// 销毁现有的水体模拟层
|
if (isWaterPrimitiveCreated.value) {
|
destoryWaterPrimitive();
|
isWaterPrimitiveCreated.value = false; // 重置标志变量
|
}
|
isPlaying.value = false;
|
emit("isPlaying", false);
|
|
pauseWaterSimulation(); // 调用暂停接口
|
EventBus.emit("clear-echart");
|
EventBus.emit("reset-table");
|
};
|
|
// 辅助函数:找到最接近的时间戳索引
|
function findClosestTimestampIndex(currentTimeValue) {
|
if (waterTimestamps.value.length === 0) return 0;
|
|
// 计算当前时间对应的毫秒时间戳
|
const baseTime = waterTimestamps.value[0];
|
const currentTimestamp = baseTime + currentTimeValue * 1000;
|
|
// 找到最接近的 timestamp 索引
|
let closestIndex = 0;
|
let minDiff = Infinity;
|
|
waterTimestamps.value.forEach((timestamp, index) => {
|
const diff = Math.abs(timestamp - currentTimestamp);
|
if (diff < minDiff) {
|
minDiff = diff;
|
closestIndex = index;
|
}
|
});
|
|
return closestIndex;
|
}
|
watch(
|
() => selectedScheme.value,
|
(newVal) => {
|
if (newVal) {
|
console.log("选中方案已改变:", newVal);
|
}
|
}
|
);
|
watch(
|
() => currentTime.value,
|
() => {
|
if (waterTimestamps.value.length > 0) {
|
sendCurrentPlayingTime.value = dayjs(waterTimestamps.value[0])
|
.add(currentTime.value, "second")
|
.valueOf(); // 使用 valueOf() 获取原始时间戳
|
|
// 更新 currentPlayingTime 格式化后的时间字符串
|
currentPlayingTime.value = dayjs(sendCurrentPlayingTime.value).format(
|
"YYYY-MM-DD HH:mm:ss"
|
);
|
EventBus.emit("time-update", currentPlayingTime.value);
|
}
|
}
|
);
|
|
// 时间标记生成
|
function generateTimeMarkers(timestamps) {
|
if (!timestamps || timestamps.length === 0) return [];
|
const sorted = [...timestamps].sort((a, b) => dayjs(a).diff(dayjs(b)));
|
const interval = Math.floor(
|
dayjs(sorted.at(-1)).diff(dayjs(sorted[0]), "second") / 4
|
);
|
return Array.from({ length: 5 }, (_, i) =>
|
dayjs(sorted[0])
|
.add(i * interval, "second")
|
.format("MM-DD HH:mm:ss")
|
);
|
}
|
|
watch(
|
() => waterTimestamps.value,
|
(newTimestamps) => {
|
if (newTimestamps.length > 0)
|
timeMarkers.value = generateTimeMarkers(newTimestamps);
|
},
|
{ immediate: true }
|
);
|
|
const jsonFetch = ref(null);
|
const currentReplayIndex = ref(0); // 当前播放的rePlayList索引
|
// 提取为独立函数
|
async function initializeSimulationData(replayItem = null) {
|
try {
|
const schemeInfo = selectedScheme.value;
|
serviceInfo = schemeInfo.serviceName;
|
|
if (schemeInfo.type == 2) {
|
if (
|
replayItem ||
|
(simStore.rePlayList && simStore.rePlayList.length != 0)
|
) {
|
jsonFetch.value =
|
replayItem || simStore.rePlayList[currentReplayIndex.value];
|
speedShow.value = true;
|
} else {
|
jsonFetch.value = layerDate.value;
|
speedShow.value = false;
|
}
|
} else {
|
getRainfallData();
|
speedShow.value = true;
|
jsonFetch.value = null;
|
}
|
|
// console.log('获取到的 serviceName:', serviceInfo);
|
// 根据 layer.json 获取时间轴信息
|
const {
|
waterTimestamps: timestamps,
|
watersMaxHeight,
|
watersMinHeight,
|
} = await fetchWaterSimulationData(serviceInfo, jsonFetch.value);
|
|
console.log(
|
"当前方案下的最大水位深度和最小水位深度",
|
watersMaxHeight,
|
watersMinHeight
|
);
|
const waterInfoArr = [watersMaxHeight]
|
schemWaterInfo.value = waterInfoArr
|
// 更新时间轴相关数据
|
if (timestamps) {
|
frameNum.value = timestamps.length;
|
waterTimestamps.value = timestamps;
|
updateTimelineRange();
|
timeMarkers.value = generateTimeMarkers(timestamps);
|
sendCurrentPlayingTime.value = timestamps[0];
|
currentPlayingTime.value = dayjs(timestamps[0]).format(
|
"YYYY-MM-DD HH:mm:ss"
|
);
|
}
|
minFlowRate = watersMinHeight;
|
maxFlowRate = watersMaxHeight;
|
} catch (error) {
|
console.error("Error loading water simulation data:", error);
|
ElMessage({
|
message: "降雨数据出错,请重新新建模拟方案!",
|
type: "warning",
|
});
|
}
|
}
|
// 播放完成后的回调
|
function handlePlayFinished() {
|
if (selectedScheme.value.type !== 2) return;
|
finishPlay.value = false;
|
currentReplayIndex.value++;
|
|
if (currentReplayIndex.value < simStore.rePlayList.length) {
|
// 自动播放下一个
|
initializeSimulationData(simStore.rePlayList[currentReplayIndex.value]);
|
togglePlay();
|
shouldAutoPlay.value = false;
|
} else {
|
// 所有项目播放完成
|
currentReplayIndex.value = 0; // 重置索引
|
isPlaying.value = false; // 停止播放
|
emit("timeUpdate", 100); // 在所有项目播放完毕后触发
|
}
|
}
|
|
// 监听播放完成事件
|
watch(
|
() => finishPlay.value,
|
(newVal) => {
|
if (newVal && selectedScheme.value.type === 2) {
|
handlePlayFinished();
|
}
|
}
|
);
|
|
const shouldAutoPlay = ref(false);
|
// 监听 layerDate 变化后标记准备播放
|
watch(
|
() => layerDate.value,
|
async (newVal) => {
|
if (selectedScheme.value.type === 2 && newVal) {
|
shouldAutoPlay.value = true;
|
}
|
},
|
{ deep: true }
|
);
|
// 等待 finishPlay 成功后再播放
|
watchEffect(() => {
|
if (shouldAutoPlay.value && finishPlay.value && !isPlaying.value) {
|
initializeSimulationData();
|
togglePlay();
|
shouldAutoPlay.value = false;
|
}
|
});
|
|
// 挂载时调用
|
onMounted(async () => {
|
// 因为这个函数实时模拟监听也需要使用,所以封装了一个函数
|
await initializeSimulationData();
|
});
|
|
// 根据返回数据的个数去渲染时间轴
|
function updateTimelineRange() {
|
if (waterTimestamps.value.length > 0) {
|
const [first, last] = [
|
waterTimestamps.value[0],
|
waterTimestamps.value.at(-1),
|
];
|
duration.value = (last - first) / 1000; // 毫秒转秒
|
}
|
}
|
onBeforeUnmount(() => {
|
stopPlayback();
|
destoryWaterPrimitive();
|
});
|
|
const { endSimulate } = inject("simulateActions");
|
|
async function handleBack() {
|
// 实时模拟弹窗确认是返回方案列表还是停止模拟
|
if (selectedScheme.value.type === 2) {
|
try {
|
await ElMessageBox.confirm("方案未停止时结束模拟后,后台将停止计算", {
|
confirmButtonText: "返回列表",
|
cancelButtonText: "结束模拟",
|
type: "warning",
|
});
|
// 用户点击了确认,这里不执行任何操作,仅关闭对话框
|
} catch (error) {
|
stopSim(selectedScheme.value.id).then((res) => {
|
if (res.code == 404) {
|
ElMessage.warning("该服务已停止");
|
} else {
|
ElMessage.success("服务正在停止中");
|
}
|
});
|
// return;
|
}
|
}
|
// 不管type是不是2,最终都执行结束模拟的操作
|
endSimulation();
|
}
|
|
async function endSimulation() {
|
// 结束模拟之后清除layer列表
|
simStore.rePlayList = [];
|
console.log(simStore.rePlayList, "结束模拟清除rePlayListrePlayList列表");
|
EventBus.emit("close-time");
|
endSimulate();
|
isWaterPrimitiveCreated.value = false;
|
|
// 结束计算和停止拾取
|
if (ratelevelRef.value) {
|
ratelevelRef.value.endCalculation();
|
ratelevelRef.value.stopPicking();
|
}
|
|
// 清除点
|
if (crossRef.value) {
|
crossRef.value.clearPoints();
|
console.log("执行删除点功能");
|
}
|
|
emit("isColorRender", false);
|
|
// 延迟删除雨量图层
|
setTimeout(() => {
|
mapUtils.delRain();
|
}, 3000);
|
|
destoryWaterPrimitive();
|
|
// 发送事件隐藏相关信息
|
EventBus.emit("hide-schemeInfo");
|
EventBus.emit("clear-water-depth");
|
EventBus.emit("clear-water-velocity");
|
|
ElMessage({ message: "模拟进程正在关闭中...", type: "success" });
|
}
|
</script>
|
<style scoped>
|
.timeline-container {
|
display: flex;
|
/* align-items: center; */
|
justify-content: space-between;
|
position: absolute;
|
bottom: 10%;
|
left: 50%;
|
transform: translateX(-50%);
|
z-index: 99;
|
width: 44%;
|
height: 10%;
|
/* background-color: #1a2634; */
|
background: url("@/assets/img/menubar/bar.png");
|
background-size: 100% 100%;
|
color: white;
|
/* border-radius: 8px; */
|
font-family: Arial, sans-serif;
|
padding: 0 25px;
|
}
|
|
.controls {
|
display: flex;
|
/* align-items: center; */
|
margin: 25px 25px 10px 0;
|
}
|
|
.control-btn {
|
background: none;
|
border: none;
|
color: white;
|
font-size: 16px;
|
cursor: pointer;
|
margin-right: 10px;
|
width: 30px;
|
height: 30px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
border-radius: 50%;
|
|
img {
|
width: 100%;
|
height: 100%;
|
}
|
}
|
|
.control-btn:hover {
|
background-color: rgba(255, 255, 255, 0.1);
|
}
|
|
.play-btn {
|
background-color: #4a90e2;
|
width: 36px;
|
height: 36px;
|
}
|
|
.speed-control {
|
position: relative;
|
cursor: pointer;
|
/* padding: 5px 10px; */
|
border-radius: 4px;
|
background-color: rgba(255, 255, 255, 0.1);
|
width: 36px;
|
height: 36px;
|
line-height: 36px;
|
text-align: center;
|
}
|
|
.speed-menu {
|
position: absolute;
|
top: 100%;
|
left: 0;
|
background-color: #2a3a4a;
|
border-radius: 4px;
|
z-index: 10;
|
width: 60px;
|
}
|
|
.speed-menu div {
|
/* padding: 5px 5px; */
|
text-align: center;
|
}
|
|
.speed-menu div:hover,
|
.speed-menu div.active {
|
/* background-color: #4a90e2; */
|
background-color: rgba(127, 255, 212, 0.5);
|
}
|
|
.timeline {
|
margin-top: 10px;
|
position: relative;
|
flex: 0.9;
|
}
|
|
.dates {
|
display: flex;
|
justify-content: space-between;
|
margin-bottom: 5px;
|
}
|
|
.date-label {
|
font-size: 14px;
|
color: #fff;
|
}
|
|
.timeline-track {
|
height: 8px;
|
background-color: rgba(255, 255, 255, 0.1);
|
border-radius: 4px;
|
position: relative;
|
cursor: pointer;
|
}
|
|
.timeline-progress {
|
height: 100%;
|
background-color: #4a90e2;
|
border-radius: 4px;
|
position: absolute;
|
top: 0;
|
left: 0;
|
}
|
|
.timeline-cursor {
|
width: 12px;
|
height: 12px;
|
background-color: white;
|
border-radius: 50%;
|
position: absolute;
|
top: 50%;
|
transform: translate(-50%, -50%);
|
z-index: 2;
|
}
|
|
.time-markers {
|
position: absolute;
|
width: 100%;
|
top: 20px;
|
display: flex;
|
justify-content: space-between;
|
}
|
|
.time-marker {
|
position: absolute;
|
font-size: 12px;
|
color: #fff;
|
text-align: center;
|
width: 25%;
|
white-space: nowrap;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
}
|
|
/* .date-part {
|
margin-bottom: 2px;
|
} */
|
.time-part {
|
font-size: 11px;
|
opacity: 0.8;
|
}
|
|
.current-date {
|
margin-bottom: 5px;
|
font-size: 15px;
|
color: #fff;
|
transform: translateX(-8%);
|
}
|
|
.scale-markers {
|
position: absolute;
|
width: 100%;
|
height: 10px;
|
top: 10px;
|
}
|
|
.scale-marker {
|
position: absolute;
|
width: 3px;
|
height: 6px;
|
background-color: rgba(255, 255, 255, 1);
|
transform: translateX(-50%);
|
}
|
</style>
|