<template>
|
<div class="timeline-container">
|
<div class="controls">
|
<!-- <div @click="endSimulate">结束模拟</div> -->
|
<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
|
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>
|
</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";
|
|
const props = defineProps({
|
waterSimulateParams: {
|
type: Object,
|
default: () => ({
|
date: ["2025-02-14T16:00:00.000Z", "2025-02-15T16:00:00.000Z"],
|
}),
|
},
|
});
|
// 响应式状态
|
const isPlaying = ref(false);
|
const playbackFinished = ref(true);
|
const currentTime = ref(0);
|
const duration = ref(86400); // 一天的秒数
|
const playbackRate = ref(1);
|
const playbackRates = ref([1, 2, 4, 8]);
|
const showSpeedMenu = ref(false);
|
|
const timeMarkers = ref([
|
"00:00",
|
"03:19",
|
"06:39",
|
"09:59",
|
"13:19",
|
"16:39",
|
"19:59",
|
"23:19",
|
]);
|
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(() => {
|
// 生成时间轴上显示的日期
|
const dates = [];
|
const currentDateValue = dayjs(startDate.value);
|
const endDateValue = dayjs(endDate.value);
|
|
// let tempDate = currentDateValue
|
// while (tempDate.isSame(endDateValue) || tempDate.isBefore(endDateValue)) {
|
// dates.push(tempDate.toDate())
|
// tempDate = tempDate.add(1, 'day')
|
// }
|
|
return [currentDateValue, endDateValue];
|
});
|
|
const currentTimeFormatted = computed(() => {
|
return formatTime(currentTime.value);
|
});
|
|
const togglePlay = () => {
|
// 如果当前是停止状态且已经播放完毕,点击时重置时间
|
if (!isPlaying.value && currentTime.value >= duration.value) {
|
currentTime.value = 0;
|
emit("timeUpdate", progressPercentage.value);
|
}
|
|
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 += 600 * playbackRate.value;
|
// emit("playbackFinished", true);
|
|
if (currentTime.value >= duration.value) {
|
emit("playbackFinished", false);
|
|
currentTime.value = duration.value; // 停在最后一帧
|
stopPlayback(); // 停止播放
|
isPlaying.value = false; // 更新播放状态
|
emit("isPlaying", isPlaying.value); // 通知播放状态变化
|
emit("playbackFinished", true); // 通知播放完成
|
emit("timeUpdate", progressPercentage.value); // 更新进度条位置
|
}
|
|
emit("timeUpdate", progressPercentage.value);
|
}, 1000);
|
};
|
|
const stopPlayback = () => {
|
clearInterval(playInterval);
|
};
|
|
const skipForward = () => {
|
// 向前跳转10分钟
|
currentTime.value = Math.min(currentTime.value + 600, duration.value);
|
emit("timeUpdate", progressPercentage.value);
|
};
|
|
const skipBackward = () => {
|
// 向后跳转10分钟
|
currentTime.value = Math.max(currentTime.value - 600, 0);
|
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;
|
|
currentTime.value = percentage * duration.value;
|
emit("timeUpdate", progressPercentage.value);
|
};
|
|
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 = (3 / playbackRate.value) * 1000;
|
if (delay < 1000) {
|
delay = 1000;
|
}
|
if (mockTimer) {
|
clearImmediate(mockTimer);
|
mockTimer = null;
|
}
|
mockTimer = setTimeout(() => {
|
const rainfall = rainFallData.value.find(
|
(item) =>
|
dayjs(item.time).format("HH:mm:ss") == currentTimeFormatted.value
|
);
|
if (rainfall && rainfall.total) {
|
// console.log(rainfall.total);
|
|
createWaterPrimitive(rainfall.total / 50000);
|
}
|
}, delay);
|
}
|
watch(
|
() => currentTime.value,
|
() => {
|
randomMockWater();
|
}
|
);
|
|
// 定义组件事件
|
const emit = defineEmits(["timeUpdate", "isPlaying", "playbackFinished"]);
|
|
// 初始化时触发一次时间更新,确保父组件能获取初始时间
|
onMounted(() => {
|
getRainfallData();
|
emit("timeUpdate", progressPercentage.value);
|
});
|
onBeforeUnmount(() => {
|
stopPlayback();
|
let delay = (3 / playbackRate.value) * 1000;
|
|
setTimeout(() => {
|
destoryWaterPrimitive();
|
}, delay);
|
});
|
const { startSimulate, endSimulate } = inject("simulateActions");
|
|
</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: 838px;
|
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: 100%;
|
top: 15px;
|
color: #fff;
|
}
|
|
.time-marker {
|
font-size: 12px;
|
color: #fff;
|
transform: translateX(-50%);
|
}
|
</style>
|