<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">
|
<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>
|
<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="time-markers">
|
<div v-for="(time, index) in timeMarkers" :key="index" class="time-marker">
|
{{ time }}
|
</div>
|
</div>
|
</div>
|
</div>
|
<el-button @click="handleBack" style="margin-top: 26px;margin-left: 30px;margin-right: 10px;">结束模拟</el-button>
|
</div>
|
</template>
|
|
<script setup>
|
import {
|
ref,
|
computed,
|
onUnmounted,
|
onMounted,
|
watch,
|
defineProps,
|
onBeforeUnmount,
|
inject,
|
} from "vue";
|
import dayjs from "dayjs";
|
import { createWaterPrimitive, destoryWaterPrimitive } from "@/utils/water";
|
import { getRainfall } from "@/api/index";
|
import { EventBus } from "@/eventBus"; // 引入事件总线
|
import { ElMessage } from 'element-plus'
|
import { fetchWaterSimulationData } from "@/api/trApi.js"
|
const emit = defineEmits(["timeUpdate", "isPlaying", "playbackFinished"]);
|
|
|
const props = defineProps({
|
waterSimulateParams: {
|
type: Object,
|
default: () => ({
|
date: ["2025-02-14T16:00:00.000Z", "2025-02-15T16:00:00.000Z"],
|
}),
|
},
|
});
|
// 响应式状态
|
const currentPlayingTime = 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 waterTimestamps = ref([]); // 存储时间轴数据
|
|
const timeMarkers = ref([
|
]);
|
const timelineTrack = ref(null);
|
|
const startDate = computed(() => {
|
return dayjs(props.waterSimulateParams.date[0]);
|
});
|
const endDate = computed(() => {
|
return dayjs(props.waterSimulateParams.date[1]);
|
});
|
let playInterval = null;
|
|
// 计算属性
|
const progressPercentage = computed(() => {
|
return (currentTime.value / duration.value) * 100;
|
});
|
|
const visibleDates = computed(() => {
|
if (waterTimestamps.value.length === 0) return [];
|
|
// 提取唯一日期
|
const uniqueDates = new Set(
|
waterTimestamps.value.map((timestamp) => dayjs(timestamp).format("YYYY-MM-DD"))
|
);
|
|
// 转换为数组
|
return Array.from(uniqueDates).map((date) => dayjs(date).toDate());
|
});
|
|
const currentTimeFormatted = computed(() => {
|
return formatTime(currentTime.value);
|
});
|
|
const togglePlay = () => {
|
// 如果当前是停止状态且已经播放完毕,点击时重置时间
|
if (!isPlaying.value && currentTime.value >= duration.value) {
|
currentTime.value = 0;
|
emit("timeUpdate", progressPercentage.value);
|
// earthCtrl.environment.disableEffect("rain");
|
}
|
|
isPlaying.value = !isPlaying.value;
|
emit("isPlaying", isPlaying.value);
|
|
if (isPlaying.value) {
|
startPlayback();
|
// 如果是从头开始播放
|
if (currentTime.value === 0) {
|
emit("playbackFinished", false);
|
}
|
} else {
|
stopPlayback();
|
}
|
};
|
|
const startPlayback = () => {
|
clearInterval(playInterval);
|
playInterval = setInterval(() => {
|
currentTime.value += 0.5 * playbackRate.value; // 每次增加0.5秒
|
if (currentTime.value >= duration.value) {
|
currentTime.value = duration.value; // 停在最后一帧
|
stopPlayback();
|
isPlaying.value = false;
|
emit("isPlaying", isPlaying.value);
|
emit("playbackFinished", true);
|
}
|
emit("timeUpdate", progressPercentage.value);
|
}, 1000); // 每秒更新一次
|
};
|
|
const stopPlayback = () => {
|
// earthCtrl.environment.disableEffect("rain");
|
clearInterval(playInterval);
|
};
|
|
const skipForward = () => {
|
currentTime.value = Math.min(currentTime.value + 1, duration.value); // 向前跳转1秒
|
emit("timeUpdate", progressPercentage.value);
|
};
|
|
const skipBackward = () => {
|
currentTime.value = Math.max(currentTime.value - 1, 0); // 向后跳转1秒
|
emit("timeUpdate", progressPercentage.value);
|
};
|
|
const toggleSpeedMenu = () => {
|
showSpeedMenu.value = !showSpeedMenu.value;
|
};
|
|
const setPlaybackRate = (rate) => {
|
playbackRate.value = rate;
|
showSpeedMenu.value = false;
|
|
if (isPlaying.value) {
|
stopPlayback();
|
startPlayback();
|
}
|
};
|
|
const formatTime = (seconds) => {
|
const hours = Math.floor(seconds / 3600);
|
const minutes = Math.floor((seconds % 3600) / 60);
|
const secs = Math.floor(seconds % 60);
|
|
return `${hours.toString().padStart(2, "0")}:${minutes
|
.toString()
|
.padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
};
|
|
const formatDate = (date) => {
|
return dayjs(date).format("YYYY-MM-DD");
|
};
|
|
const seekToPosition = (event) => {
|
const rect = timelineTrack.value.getBoundingClientRect();
|
const clickPosition = event.clientX - rect.left;
|
const percentage = clickPosition / rect.width;
|
|
// 对齐到最近的整秒
|
const clickedSeconds = Math.round(percentage * duration.value);
|
currentTime.value = clickedSeconds;
|
|
// 更新进度条和游标
|
emit("timeUpdate", progressPercentage.value);
|
|
// 计算点击位置对应的时间戳
|
const firstTimestamp = waterTimestamps.value[0];
|
if (firstTimestamp) {
|
const clickedTimestamp = dayjs(firstTimestamp).add(clickedSeconds, "second");
|
|
// 打印 Unix 时间戳(毫秒)和格式化时间
|
const unixTimestamp = clickedTimestamp.valueOf();
|
const formattedTime = clickedTimestamp.format("YYYY-MM-DD HH:mm:ss");
|
|
console.log("Clicked timestamp (Unix format):", unixTimestamp);
|
console.log("Clicked timestamp (Formatted):", formattedTime);
|
}
|
};
|
|
const rainFallData = ref([]);
|
function getRainfallData() {
|
getRainfall().then((res) => {
|
// rainFallData.value = res.data.map(item => {
|
// return dayjs(item.time).format("HH:mm")
|
// })
|
|
// const rainfallData = res.data.map(item => {
|
// return item.rainfall
|
// })
|
rainFallData.value = res.data;
|
});
|
}
|
|
let mockTimer = null;
|
let currentRainfall = ref(0.0001);
|
function randomMockWater() {
|
let delay = (0.5 / playbackRate.value) * 1000; // 根据播放速率调整延迟
|
if (delay < 200) {
|
delay = 200; // 设置最小延迟为200ms
|
}
|
if (mockTimer) {
|
clearTimeout(mockTimer);
|
mockTimer = null;
|
}
|
mockTimer = setTimeout(() => {
|
const rainfall = rainFallData.value.find(
|
(item) =>
|
dayjs(item.time).format("HH:mm:ss") == currentTimeFormatted.value
|
);
|
if (rainfall && rainfall.total) {
|
createWaterPrimitive(rainfall.total / 50000);
|
}
|
}, 100);
|
setTimeout(() => {
|
destoryWaterPrimitive();
|
}, 120000);
|
}
|
watch(
|
() => {
|
randomMockWater();
|
},
|
() => currentTime.value,
|
() => {
|
if (waterTimestamps.value.length > 0) {
|
const firstTimestamp = waterTimestamps.value[0];
|
const currentTimestamp = dayjs(firstTimestamp).add(currentTime.value, "second");
|
currentPlayingTime.value = currentTimestamp.format("YYYY-MM-DD HH:mm:ss");
|
}
|
}
|
);
|
// 动态调整时间标记格式
|
function generateTimeMarkers(timestamps) {
|
if (!timestamps || timestamps.length === 0) return [];
|
// 确保时间戳按升序排列
|
const sortedTimestamps = [...timestamps].sort((a, b) => dayjs(a).diff(dayjs(b)));
|
const startDate = dayjs(sortedTimestamps[0]);
|
const endDate = dayjs(sortedTimestamps[sortedTimestamps.length - 1]);
|
// 计算总时长(秒)
|
const totalDurationInSeconds = endDate.diff(startDate, "second");
|
// 生成 5 个均匀分布的时间点
|
const intervalInSeconds = Math.floor(totalDurationInSeconds / 4); // 每段间隔的秒数
|
const timeMarkers = [];
|
for (let i = 0; i < 5; i++) {
|
const timestamp = startDate.add(i * intervalInSeconds, "second");
|
timeMarkers.push(timestamp.format("YYYY-MM-DD HH:mm:ss")); // 格式化为 YYYY-MM-DD HH:mm:ss
|
}
|
// 强制添加结束时间
|
if (!timeMarkers.includes(endDate.format("YYYY-MM-DD HH:mm:ss"))) {
|
timeMarkers.push(endDate.format("YYYY-MM-DD HH:mm:ss"));
|
}
|
console.log("Generated time markers with date:", timeMarkers);
|
return timeMarkers;
|
}
|
|
|
// 监听时间戳变化并生成时间标记
|
watch(
|
() => waterTimestamps.value,
|
(newTimestamps) => {
|
if (newTimestamps.length > 0) {
|
timeMarkers.value = generateTimeMarkers(newTimestamps);
|
}
|
},
|
{ immediate: true }
|
);
|
// 动态更新时间轴范围
|
// 组件挂载时加载数据
|
onMounted(async () => {
|
try {
|
const simulationData = await fetchWaterSimulationData();
|
if (simulationData && simulationData.waterTimestamps) {
|
waterTimestamps.value = simulationData.waterTimestamps; // 初始化 waterTimestamps
|
updateTimelineRange(); // 更新时间轴范围
|
timeMarkers.value = generateTimeMarkers(waterTimestamps.value); // 生成时间标记
|
const firstTimestamp = waterTimestamps.value[0];
|
if (firstTimestamp) {
|
currentPlayingTime.value = dayjs(firstTimestamp).format("YYYY-MM-DD HH:mm:ss");
|
}
|
}
|
} catch (error) {
|
console.error("Error loading water simulation data:", error);
|
}
|
});
|
|
// 更新时间轴范围
|
function updateTimelineRange() {
|
if (waterTimestamps.value.length > 0) {
|
const firstTimestamp = waterTimestamps.value[0]; // 第一个时间戳
|
const lastTimestamp = waterTimestamps.value[waterTimestamps.value.length - 1]; // 最后一个时间戳
|
|
// 更新开始时间和结束时间
|
props.waterSimulateParams.date = [
|
dayjs(firstTimestamp).toISOString(), // 转换为 ISO 格式
|
dayjs(lastTimestamp).toISOString(),
|
];
|
|
// 计算总时长(秒)
|
duration.value = dayjs(lastTimestamp).diff(dayjs(firstTimestamp), "second");
|
|
console.log("Updated timeline range:", {
|
startDate: props.waterSimulateParams.date[0],
|
endDate: props.waterSimulateParams.date[1],
|
duration: duration.value,
|
});
|
}
|
}
|
onBeforeUnmount(() => {
|
stopPlayback();
|
let delay = (3 / playbackRate.value) * 1000;
|
|
setTimeout(() => {
|
destoryWaterPrimitive();
|
}, delay);
|
});
|
const { startSimulate, endSimulate } = inject("simulateActions");
|
// 返回按钮点击事件
|
function handleBack() {
|
ElMessage({
|
message: '模拟进程正在关闭中...',
|
type: 'success',
|
})
|
endSimulate();
|
EventBus.emit("hide-schemeInfo");
|
}
|
|
</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: 878px;
|
height: 108px;
|
/* 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 10px;
|
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: 20px;
|
position: relative;
|
flex: 1;
|
}
|
|
.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 {
|
display: flex;
|
justify-content: space-between;
|
position: absolute;
|
width: 110%;
|
top: 15px;
|
color: #fff;
|
}
|
|
.time-marker {
|
margin-left: 35px;
|
font-size: 12px;
|
color: #fff;
|
transform: translateX(-50%);
|
}
|
</style>
|