<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,
|
onMounted,
|
watch,
|
defineProps,
|
onBeforeUnmount,
|
inject,
|
} from "vue";
|
import dayjs from "dayjs";
|
import { createWaterPrimitive, destoryWaterPrimitive } from "@/utils/water";
|
import { fetchWaterSimulationData } from "@/api/trApi.js";
|
import { EventBus } from "@/eventBus";
|
import { ElMessage } from 'element-plus';
|
|
const emit = defineEmits(["timeUpdate", "isPlaying", "playbackFinished"]);
|
|
// 定义props
|
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(8);
|
const playbackRates = ref([1, 2, 4, 8]);
|
const showSpeedMenu = ref(false);
|
const waterTimestamps = ref([]); // 存储时间轴数据
|
const timeMarkers = ref([]);
|
const timelineTrack = ref(null);
|
|
let playInterval = null;
|
|
// 计算属性
|
const startDate = computed(() => dayjs(props.waterSimulateParams.date[0]));
|
const endDate = computed(() => dayjs(props.waterSimulateParams.date[1]));
|
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 currentTimeFormatted = computed(() => formatTime(currentTime.value));
|
|
// 播放控制
|
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 (currentTime.value === 0) emit("playbackFinished", false);
|
} else stopPlayback();
|
};
|
const intervalMap = {
|
1: 1000, // 1倍速
|
2: 500, // 2倍速
|
4: 250, // 4倍速
|
8: 125, // 8倍速
|
};
|
const startPlayback = () => {
|
// 根据当前倍速获取对应的 interval
|
const interval = intervalMap[playbackRate.value] || 1000; // 默认为1000
|
// 调用 createWaterPrimitive 并传递 interval
|
createWaterPrimitive({ interval });
|
clearInterval(playInterval);
|
playInterval = setInterval(() => {
|
// 计算每次增加的时间量
|
const timeIncrement = playbackRate.value; // 倍速直接作为增量
|
currentTime.value += timeIncrement;
|
// 如果超过总时长,则停止播放
|
if (currentTime.value >= duration.value) {
|
currentTime.value = duration.value; // 停在最后一帧
|
stopPlayback();
|
isPlaying.value = false;
|
emit("isPlaying", false);
|
emit("playbackFinished", true);
|
}
|
// 触发时间更新事件
|
emit("timeUpdate", progressPercentage.value);
|
}, 1000); // 每秒更新一次
|
};
|
|
const stopPlayback = () => clearInterval(playInterval);
|
const skipForward = () => (currentTime.value = Math.min(currentTime.value + 1, duration.value)); // 向前跳转1秒
|
const skipBackward = () => (currentTime.value = Math.max(currentTime.value - 1, 0)); // 向后跳转1秒
|
const toggleSpeedMenu = () => (showSpeedMenu.value = !showSpeedMenu.value);
|
const setPlaybackRate = (rate) => {
|
playbackRate.value = rate;
|
showSpeedMenu.value = false;
|
if (isPlaying.value) stopPlayback(), startPlayback(); // 如果正在播放,则重新启动以应用新的速率
|
};
|
const seekToPosition = (event) => {
|
const rect = timelineTrack.value.getBoundingClientRect();
|
const percentage = (event.clientX - rect.left) / rect.width;
|
currentTime.value = Math.round(percentage * duration.value);
|
emit("timeUpdate", progressPercentage.value);
|
if (waterTimestamps.value.length > 0) {
|
const clickedTimestamp = dayjs(waterTimestamps.value[0]).add(currentTime.value, "second");
|
console.log("Clicked timestamp:", clickedTimestamp.valueOf(), clickedTimestamp.format("YYYY-MM-DD HH:mm:ss"));
|
}
|
};
|
watch(() => currentTime.value, () => {
|
if (waterTimestamps.value.length > 0) {
|
currentPlayingTime.value = dayjs(waterTimestamps.value[0])
|
.add(currentTime.value, "second")
|
.format("YYYY-MM-DD mm:ss");
|
}
|
});
|
// 时间标记生成
|
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") / 7);
|
return Array.from({ length: 8 }, (_, i) => dayjs(sorted[0]).add(i * interval, "second").format("mm:ss"));
|
}
|
watch(() => waterTimestamps.value, (newTimestamps) => {
|
if (newTimestamps.length > 0) timeMarkers.value = generateTimeMarkers(newTimestamps);
|
}, { immediate: true });
|
|
onMounted(async () => {
|
try {
|
const { waterTimestamps: timestamps } = await fetchWaterSimulationData();
|
if (timestamps) {
|
waterTimestamps.value = timestamps;
|
updateTimelineRange();
|
timeMarkers.value = generateTimeMarkers(timestamps);
|
currentPlayingTime.value = dayjs(timestamps[0]).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 [first, last] = [waterTimestamps.value[0], waterTimestamps.value.at(-1)];
|
props.waterSimulateParams.date = [dayjs(first).toISOString(), dayjs(last).toISOString()];
|
duration.value = dayjs(last).diff(dayjs(first), "second");
|
console.log("Updated timeline range:", { ...props.waterSimulateParams, duration: duration.value });
|
}
|
}
|
onBeforeUnmount(() => {
|
stopPlayback();
|
destoryWaterPrimitive();
|
});
|
const { endSimulate } = inject("simulateActions");
|
function handleBack() {
|
ElMessage({ message: '模拟进程正在关闭中...', type: 'success' }); // 显示消息通知用户模拟进程正在关闭
|
endSimulate();
|
destoryWaterPrimitive();
|
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 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: 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 {
|
margin-top: 5px;
|
font-size: 12px;
|
color: #fff;
|
transform: translateX(-20%);
|
}
|
|
.current-date {
|
margin-bottom: 5px;
|
font-size: 15px;
|
color: #fff;
|
transform: translateX(-3%);
|
}
|
</style>
|