需求:时间轴组件

效果:

根据当前时间: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>
相关推荐
Qrun1 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp1 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.2 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl4 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫5 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友5 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理7 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻7 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
mapbar_front8 小时前
在职场生存中如何做个不好惹的人
前端
牧杉-惊蛰8 小时前
纯flex布局来写瀑布流
前端·javascript·css