需求:时间轴组件

效果:

根据当前时间: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>
相关推荐
极客小俊34 分钟前
粘性定位Position:sticky属性是不是真的没用?
前端
云端看世界37 分钟前
ECMAScript 类型转换 下
前端·javascript
云端看世界39 分钟前
ECMAScript 运算符怪谈 下
前端·javascript
云端看世界40 分钟前
ECMAScript 函数对象实例化
前端·javascript
前端爆冲41 分钟前
基于vue和flex实现页面可配置组件顺序
前端·javascript·vue.js
云端看世界42 分钟前
ECMAScript 中的特异对象
前端·javascript
il44 分钟前
Deepdive into Tanstack Query - 2.1 QueryClient 基础
前端
_十六1 小时前
看完就懂!用最简单的方式带你了解 TypeScript 编译器原理
前端·typescript
云端看世界1 小时前
ECMAScript 运算符怪谈 上
前端·javascript·ecmascript 6
前端涂涂1 小时前
express的介绍,简单使用
前端