效果:

根据当前时间:2025-03-01 20:00:00来播放控制时间轴上的时刻展示,默认时刻19:30(当前时间的前半个小时作为第一个时刻点),展示24个小时的时刻数据
js
<template>
<div class="timeline-wrapper">
<!-- 预报时效控制行 -->
<div class="forecast-controls">
<div class="forecast-title">预报时效:</div>
<el-button @click="prevForecast" icon="ArrowLeftBold" circle />
<el-button
@click="toggleForecastPlay"
:icon="isForecastPlaying ? 'VideoPause' : 'VideoPlay'"
circle
/>
<el-button @click="nextForecast" icon="ArrowRightBold" circle />
<div class="forecast-scale">
<span
class="forecast-hour"
:class="{ 'current-forecast': currentForecast === 'now' }"
@click="currentForecast = 'now'"
>
当前
</span>
<span
v-for="hour in forecastHours"
:key="hour"
class="forecast-hour"
:class="{ 'current-forecast': hour === currentForecast }"
@click="currentForecast = hour"
>
{{ hour }}时
</span>
</div>
</div>
<!-- 时间轴控制行 -->
<div class="time-controls">
<div class="current-time-display">
{{ currentTime.format("YYYY-MM-DD HH:mm:ss") }}
</div>
<el-button @click="prevHour" icon="ArrowLeftBold" circle />
<el-button
@click="togglePlay"
:icon="isPlaying ? 'VideoPause' : 'VideoPlay'"
circle
/>
<el-button @click="nextHour" icon="ArrowRightBold" circle />
</div>
<!-- 刻度尺部分 -->
<div class="ruler" ref="rulerRef">
<!-- 刻度线 -->
<div
v-for="(tick, idx) in tickList"
:key="'tick_' + idx"
class="tick"
:class="[
tick.isHour ? 'full' : 'short',
{ 'current-tick': tick.isCurrentHour },
]"
:style="{ left: tick.left + 'px' }"
></div>
<!-- 时间文本 -->
<div
v-for="(tick, idx) in hourTickList"
:key="'label' + idx"
class="time-label"
:style="{ left: tick.left + 'px' }"
>
{{ tick.label }}
</div>
<!-- 当前时间标记 -->
<div
v-if="currentTick"
class="current-time-marker"
:style="{ left: currentTick.left + 'px' }"
>
{{ currentHour }}:00
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch, onBeforeUnmount } from "vue";
import dayjs from "dayjs";
// 初始时间(基准时间)
const baseTime = ref(dayjs("2025-03-01 20:00"));
const currentHour = ref(baseTime.value.hour()); // 当前选中小时
const isPlaying = ref(false);
let interval = null;
// 预报时效相关
const forecastHours = [6, 12, 24, 36, 48, 72, 96, 120];
// 当前预报时效可以是'now'或小时数
// const currentForecast = ref(6);
const currentForecast = ref("now"); // 默认选中"当前"
const isForecastPlaying = ref(false);
let forecastInterval = null;
// 固定刻度数据(只在初始化时计算一次)
const fixedTicks = ref([]);
const fixedHourTicks = ref([]);
const hoursToShow = 24; // 显示24小时
const ticksPerHour = 6; // 每小时6个刻度(每10分钟一个)
const preTicks = 3; // 第一个时刻前显示3个短刻度(30分钟)
const totalTicks = hoursToShow * ticksPerHour + preTicks;
// 初始化固定刻度
onMounted(() => {
const ticks = [];
const hourTicks = [];
const startHour = baseTime.value.hour();
const startMinute = baseTime.value.minute();
// 添加第一个时刻前的短刻度(3个,30分钟)
for (let i = 0; i < preTicks; i++) {
const minute = startMinute - (i + 1) * 10; // 依次减10分钟
let hour = startHour;
let adjustedMinute = minute;
// 处理分钟小于0的情况
if (minute < 0) {
adjustedMinute = 60 + minute;
hour = startHour - 1;
if (hour < 0) hour = 23;
}
ticks.push({
hour,
minute: adjustedMinute,
isHour: false,
left:
i * (tickSpacing / ticksPerHour) -
(tickSpacing / ticksPerHour) * preTicks,
});
}
// 添加主要刻度(24小时)
for (let i = 0; i < hoursToShow * ticksPerHour; i++) {
const totalMinutes = startMinute + i * 10;
const hourOffset = Math.floor(totalMinutes / 60);
const displayHour = (startHour + hourOffset) % 24;
const minute = totalMinutes % 60;
const isHourTick = minute === 0;
const tick = {
hour: displayHour,
minute,
isHour: isHourTick,
left: i * (tickSpacing / ticksPerHour),
};
ticks.push(tick);
if (isHourTick) {
hourTicks.push({
...tick,
label: `${displayHour.toString().padStart(2, "0")}:00`,
});
}
}
fixedTicks.value = ticks;
fixedHourTicks.value = hourTicks;
});
// 预报时效播放控制
function toggleForecastPlay() {
isForecastPlaying.value = !isForecastPlaying.value;
if (isForecastPlaying.value) {
forecastInterval = setInterval(nextForecast, 1000);
} else {
clearInterval(forecastInterval);
}
}
// 修改预报时效播放控制 ----加了"当前"状态
function nextForecast() {
if (currentForecast.value === "now") {
currentForecast.value = forecastHours[0];
} else {
const currentIndex = forecastHours.indexOf(currentForecast.value);
if (currentIndex < forecastHours.length - 1) {
currentForecast.value = forecastHours[currentIndex + 1];
} else {
currentForecast.value = "now"; // 循环回到"当前"
}
}
}
// ----加了"当前"状态
function prevForecast() {
if (currentForecast.value === "now") {
currentForecast.value = forecastHours[forecastHours.length - 1];
} else {
const currentIndex = forecastHours.indexOf(currentForecast.value);
if (currentIndex > 0) {
currentForecast.value = forecastHours[currentIndex - 1];
} else {
currentForecast.value = "now"; // 循环回到"当前"
}
}
}
// 时间轴播放控制
function togglePlay() {
isPlaying.value = !isPlaying.value;
if (isPlaying.value) {
interval = setInterval(nextHour, 1000);
} else {
clearInterval(interval);
}
}
// 播放控制(只改变currentHour,不改变刻度位置)
function nextHour() {
currentHour.value = (currentHour.value + 1) % 24;
}
function prevHour() {
currentHour.value = (currentHour.value - 1 + 24) % 24;
}
// 清理定时器
onBeforeUnmount(() => {
if (interval) clearInterval(interval);
if (forecastInterval) clearInterval(forecastInterval);
});
// 当前时间计算
const currentTime = computed(() => {
return baseTime.value.set("hour", currentHour.value);
});
// 刻度轴计算
const tickSpacing = 40; // px
const tickCount = 240; // 24小时 * 10刻度线
// 当前刻度数据(动态计算当前高亮)
const tickList = computed(() => {
return fixedTicks.value.map((tick) => ({
...tick,
isCurrentHour: tick.isHour && tick.hour === currentHour.value,
}));
});
const hourTickList = computed(() => {
return fixedHourTicks.value.map((tick) => ({
...tick,
isCurrent: tick.hour === currentHour.value,
}));
});
// 跨天分隔
const daySplitList = computed(() => {
return tickList.value
.filter((tick) => tick.isHour && tick.hour === 0 && tick.left > 0)
.map((tick) => ({
...tick,
day: baseTime.value.add(tick.hour, "hour").format("MM月DD日"),
left: tick.left,
}));
});
// 当前时间刻度
const currentTick = computed(() => {
return tickList.value.find((tick) => tick.isCurrentHour);
});
</script>
<style scoped>
.timeline-wrapper {
background: #2a4a7b;
padding: 20px;
border-radius: 8px;
font-family: monospace;
width: 100%;
overflow-x: auto;
}
/* 预报时效控制样式 */
.forecast-controls {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
padding: 10px;
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.forecast-title {
color: white;
font-weight: bold;
margin-right: 10px;
}
.forecast-scale {
display: flex;
margin-left: 20px;
gap: 15px;
}
.forecast-hour {
color: white;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
min-width: 40px; /* 统一宽度 */
text-align: center;
}
.current-forecast {
background-color: red;
font-weight: bold;
}
/* 时间轴控制样式 */
.time-controls {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
}
.current-time-display {
color: white;
font-size: 16px;
font-weight: bold;
min-width: 200px;
}
/* 刻度尺样式 */
.ruler {
position: relative;
height: 60px;
background: #345;
border-top: 2px solid #4e6aa6;
}
.tick {
position: absolute;
width: 1px;
background: white;
bottom: 0;
margin-left: 50px;
}
.tick.full {
height: 20px;
}
.tick.short {
height: 10px;
}
.tick.current-tick {
background: #feac4c;
width: 2px;
height: 25px;
z-index: 2;
margin-left: 50px;
}
.time-label {
position: absolute;
top: 22px;
font-size: 12px;
color: white;
transform: translateX(-50%);
margin-left: 50px;
}
.current-time-marker {
position: absolute;
top: -20px;
background: #feac4c;
color: white;
font-weight: bold;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
transform: translateX(-50%);
white-space: nowrap;
margin-left: 50px;
}
/* 添加"当前"状态的样式 */
.forecast-scale {
display: flex;
margin-left: 20px;
gap: 10px; /* 缩小间距 */
}
</style>

从08:00时刻开始展示:
js
<template>
<div class="timeline-wrapper">
<!-- 预报时效控制行 -->
<div class="forecast-controls">
<div class="forecast-title">预报时效:</div>
<el-button @click="prevForecast" icon="ArrowLeftBold" circle />
<el-button
@click="toggleForecastPlay"
:icon="isForecastPlaying ? 'VideoPause' : 'VideoPlay'"
circle
/>
<el-button @click="nextForecast" icon="ArrowRightBold" circle />
<div class="forecast-scale">
<span
class="forecast-hour"
:class="{ 'current-forecast': currentForecast === 'now' }"
@click="currentForecast = 'now'"
>
当前
</span>
<span
v-for="hour in forecastHours"
:key="hour"
class="forecast-hour"
:class="{ 'current-forecast': hour === currentForecast }"
@click="currentForecast = hour"
>
{{ hour }}时
</span>
</div>
</div>
<!-- 时间轴控制行 -->
<div class="time-controls">
<div class="current-time-display">
{{ currentTime.format("YYYY-MM-DD HH:mm:ss") }}
</div>
<el-button @click="prevHour" icon="ArrowLeftBold" circle />
<el-button
@click="togglePlay"
:icon="isPlaying ? 'VideoPause' : 'VideoPlay'"
circle
/>
<el-button @click="nextHour" icon="ArrowRightBold" circle />
</div>
<!-- 刻度尺部分 -->
<div class="ruler" ref="rulerRef">
<!-- 刻度线 -->
<div
v-for="(tick, idx) in tickList"
:key="'tick_' + idx"
class="tick"
:class="[
tick.isHour ? 'full' : 'short',
{ 'current-tick': tick.isCurrentHour },
]"
:style="{ left: tick.left + 'px' }"
></div>
<!-- 时间文本 -->
<div
v-for="(tick, idx) in hourTickList"
:key="'label' + idx"
class="time-label"
:style="{ left: tick.left + 'px' }"
>
{{ tick.label }}
</div>
<!-- 当前时间标记 -->
<div
v-if="currentTick"
class="current-time-marker"
:style="{ left: currentTick.left + 'px' }"
>
{{ currentHour }}:00
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch, onBeforeUnmount } from "vue";
import dayjs from "dayjs";
// 初始时间(基准时间)
const baseTime = ref(dayjs("2025-03-01 20:00"));
const currentHour = ref(baseTime.value.hour()); // 当前选中小时
const isPlaying = ref(false);
let interval = null;
// 预报时效相关
const forecastHours = [6, 12, 24, 36, 48, 72, 96, 120];
// 当前预报时效可以是'now'或小时数
// const currentForecast = ref(6);
const currentForecast = ref("now"); // 默认选中"当前"
const isForecastPlaying = ref(false);
let forecastInterval = null;
// 固定刻度数据(只在初始化时计算一次)
const fixedTicks = ref([]);
const fixedHourTicks = ref([]);
// 初始化固定刻度
onMounted(() => {
const ticks = [];
const hourTicks = [];
const startHour = baseTime.value.hour();
for (let i = 0; i < 240; i++) {
// 24小时*10刻度
const hourOffset = Math.floor(i / 10);
const displayHour = (startHour + hourOffset) % 24;
const minute = (i % 10) * 6;
const isHourTick = minute === 0;
const tick = {
hour: displayHour,
minute,
isHour: isHourTick,
left: i * (40 / 10), // tickSpacing=40
};
ticks.push(tick);
if (isHourTick) {
hourTicks.push({
...tick,
label: `${displayHour.toString().padStart(2, "0")}:00`,
});
}
}
fixedTicks.value = ticks;
fixedHourTicks.value = hourTicks;
});
// 预报时效播放控制
function toggleForecastPlay() {
isForecastPlaying.value = !isForecastPlaying.value;
if (isForecastPlaying.value) {
forecastInterval = setInterval(nextForecast, 1000);
} else {
clearInterval(forecastInterval);
}
}
// function nextForecast() {
// const currentIndex = forecastHours.indexOf(currentForecast.value);
// if (currentIndex < forecastHours.length - 1) {
// currentForecast.value = forecastHours[currentIndex + 1];
// } else {
// // 循环到第一个
// currentForecast.value = forecastHours[0];
// }
// }
// 修改预报时效播放控制 ----加了"当前"状态
function nextForecast() {
if (currentForecast.value === "now") {
currentForecast.value = forecastHours[0];
} else {
const currentIndex = forecastHours.indexOf(currentForecast.value);
if (currentIndex < forecastHours.length - 1) {
currentForecast.value = forecastHours[currentIndex + 1];
} else {
currentForecast.value = "now"; // 循环回到"当前"
}
}
}
// function prevForecast() {
// const currentIndex = forecastHours.indexOf(currentForecast.value);
// if (currentIndex > 0) {
// currentForecast.value = forecastHours[currentIndex - 1];
// } else {
// // 循环到最后一个
// currentForecast.value = forecastHours[forecastHours.length - 1];
// }
// }
// ----加了"当前"状态
function prevForecast() {
if (currentForecast.value === "now") {
currentForecast.value = forecastHours[forecastHours.length - 1];
} else {
const currentIndex = forecastHours.indexOf(currentForecast.value);
if (currentIndex > 0) {
currentForecast.value = forecastHours[currentIndex - 1];
} else {
currentForecast.value = "now"; // 循环回到"当前"
}
}
}
// 时间轴播放控制
function togglePlay() {
isPlaying.value = !isPlaying.value;
if (isPlaying.value) {
interval = setInterval(nextHour, 1000);
} else {
clearInterval(interval);
}
}
// 播放控制(只改变currentHour,不改变刻度位置)
function nextHour() {
currentHour.value = (currentHour.value + 1) % 24;
}
function prevHour() {
currentHour.value = (currentHour.value - 1 + 24) % 24;
}
// 清理定时器
onBeforeUnmount(() => {
if (interval) clearInterval(interval);
if (forecastInterval) clearInterval(forecastInterval);
});
// 当前时间计算
const currentTime = computed(() => {
return baseTime.value.set("hour", currentHour.value);
});
// 刻度轴计算
const tickSpacing = 40; // px
const tickCount = 240; // 24小时 * 10刻度线
// 当前刻度数据(动态计算当前高亮)
const tickList = computed(() => {
return fixedTicks.value.map((tick) => ({
...tick,
isCurrentHour: tick.isHour && tick.hour === currentHour.value,
}));
});
const hourTickList = computed(() => {
return fixedHourTicks.value.map((tick) => ({
...tick,
isCurrent: tick.hour === currentHour.value,
}));
});
// 跨天分隔
const daySplitList = computed(() => {
return tickList.value
.filter((tick) => tick.isHour && tick.hour === 0 && tick.left > 0)
.map((tick) => ({
...tick,
day: baseTime.value.add(tick.hour, "hour").format("MM月DD日"),
left: tick.left,
}));
});
// 当前时间刻度
const currentTick = computed(() => {
return tickList.value.find((tick) => tick.isCurrentHour);
});
</script>
<style scoped>
.timeline-wrapper {
background: #2a4a7b;
padding: 20px;
border-radius: 8px;
font-family: monospace;
width: 100%;
overflow-x: auto;
}
/* 预报时效控制样式 */
.forecast-controls {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
padding: 10px;
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.forecast-title {
color: white;
font-weight: bold;
margin-right: 10px;
}
.forecast-scale {
display: flex;
margin-left: 20px;
gap: 15px;
}
.forecast-hour {
color: white;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
min-width: 40px; /* 统一宽度 */
text-align: center;
}
.current-forecast {
background-color: red;
font-weight: bold;
}
/* 时间轴控制样式 */
.time-controls {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
}
.current-time-display {
color: white;
font-size: 16px;
font-weight: bold;
min-width: 200px;
}
/* 刻度尺样式 */
.ruler {
position: relative;
height: 60px;
background: #345;
border-top: 2px solid #4e6aa6;
}
.tick {
position: absolute;
width: 1px;
background: white;
bottom: 0;
}
.tick.full {
height: 20px;
}
.tick.short {
height: 10px;
}
.tick.current-tick {
background: #feac4c;
width: 2px;
height: 25px;
z-index: 2;
}
.time-label {
position: absolute;
top: 22px;
font-size: 12px;
color: white;
transform: translateX(-50%);
}
.current-time-marker {
position: absolute;
top: -20px;
background: #feac4c;
color: white;
font-weight: bold;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
transform: translateX(-50%);
white-space: nowrap;
}
/* 添加"当前"状态的样式 */
.forecast-scale {
display: flex;
margin-left: 20px;
gap: 10px; /* 缩小间距 */
}
</style>