Skip to content
0

时间进度组件

文件目录树

md
src/
└── components/
    └── widget/
        ├── Schedule.astro      # 时间进度组件
        └── ...                 # 其他组件

创建组件

文件路径src/components/widget/Schedule.astro

astro
<!-- 时间进度组件 -->
---
import WidgetLayout from "@/components/common/WidgetLayout.astro";

interface Props {
	class?: string;
	style?: string;
}

const { class: className, style } = Astro.props;
---

<WidgetLayout id="schedule-widget" class={className} style={style}>
  <div class="schedule-container" data-schedule-id="schedule-${Math.random().toString(36).substr(2, 9)}">
    <!-- 进度区域 -->
    <div class="progress-section space-y-4">
      <!-- 本年进度 -->
      <div class="progress-item flex items-center gap-3">
        <span class="text-[14px] font-semibold text-(--primary) shrink-0 w-[60px] year-progress">--%</span>
        <div class="flex-1 min-w-0">
          <span class="text-sm text-neutral-700 dark:text-neutral-300 year-days-left">本年还剩 -- 天</span>
          <progress max="365" class="schedule-progress-bar pBar_year" value="0"></progress>
        </div>
      </div>

      <!-- 本月进度 -->
      <div class="progress-item flex items-center gap-3">
        <span class="text-[14px] font-semibold text-(--primary) shrink-0 w-[60px] month-progress">--%</span>
        <div class="flex-1 min-w-0">
          <span class="text-sm text-neutral-700 dark:text-neutral-300 month-days-left">本月还剩 -- 天</span>
          <progress max="31" class="schedule-progress-bar pBar_month" value="0"></progress>
        </div>
      </div>

      <!-- 本周进度 -->
      <div class="progress-item flex items-center gap-3">
        <span class="text-[14px] font-semibold text-(--primary) shrink-0 w-[60px] week-progress">--%</span>
        <div class="flex-1 min-w-0">
          <span class="text-sm text-neutral-700 dark:text-neutral-300 week-days-left">本周还剩 -- 天</span>
          <progress max="7" class="schedule-progress-bar pBar_week" value="0"></progress>
        </div>
      </div>
    </div>

    <!-- 节假日倒计时区域 -->
    <div class="holiday-section mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-700">
      <div class="text-center">
        <p class="text-base font-semibold text-neutral-700 dark:text-neutral-300 mb-0 nearest-holiday-full">距离 --</p>
        <p class="text-4xl font-bold text-(--primary) mb-2 nearest-holiday-days">--</p>
        <p class="text-xs text-neutral-500 dark:text-neutral-400 nearest-holiday-date">--</p>
      </div>
    </div>
  </div>
</WidgetLayout>

<script is:inline>
(function () {
  // 春节日期数据(1900-2100年)
  const springFestivalData = [
    [2,19],[2,8],[1,28],[2,16],[2,5],[1,25],[2,13],[2,2],[1,22],[2,10],
    [1,30],[2,18],[2,7],[1,26],[2,14],[2,3],[1,23],[2,11],[1,31],[2,19],
    [2,8],[1,28],[2,16],[2,5],[1,24],[2,12],[2,1],[1,21],[2,9],[1,28],
    [2,16],[2,5],[1,24],[2,12],[2,1],[1,21],[2,9],[1,29],[2,17],[2,6],
    [1,26],[2,14],[2,2],[1,22],[2,10],[1,29],[2,17],[2,6],[1,26],[2,13],
    [2,2],[1,22],[2,10],[1,30],[2,17],[2,6],[1,25],[2,13],[2,1],[1,21],
    [2,8],[1,28],[2,15],[2,5],[1,24],[2,12],[1,31],[2,18],[2,7],[1,27],
    [2,15],[2,3],[1,23],[2,11],[1,31],[2,18],[2,6],[1,26],[2,14],[2,3],
    [1,23],[2,10],[1,29],[2,16],[2,5],[1,24],[2,12],[2,1],[1,22],[2,9],
    [1,28],[2,15],[2,4],[1,23],[2,10],[1,30],[2,17],[2,6],[1,26],[2,14],
    [2,2],[1,22],[2,10],[1,29],[2,17],[2,5],[1,24],[2,12],[1,31],[2,18],
    [2,7],[1,26],[2,14],[2,3],[1,23],[2,10],[1,31],[2,18],[2,7],[1,26],
    [2,12],[2,1],[1,22],[2,10],[1,29],[2,17],[2,17],[1,24],[2,12],[2,1],
    [1,22],[2,10],[1,29],[2,17],[2,6],[1,26],[2,14],[2,3],[1,23],[2,10],
    [1,30],[2,17],[2,6],[1,26],[2,13],[2,2],[1,22],[2,10],[1,29],[2,17],
    [2,5],[1,25],[2,13],[2,1],[1,21],[2,9],[1,28],[2,16],[2,5],[1,24],
    [2,12],[2,1],[1,21],[2,9],[1,28],[2,15],[2,4],[1,24],[2,11],[1,31],
    [2,18],[2,7],[1,27],[2,15],[2,3],[1,23],[2,10],[1,30],[2,17],[2,6],
    [1,25],[2,13],[2,2],[1,22],[2,10],[1,29],[2,17],[2,5],[1,25],[2,13],
    [2,1],[1,21],[2,9],[1,29],[2,17],[2,6],[1,26],[2,14],[2,3],[1,23],
    [2,11],[1,30],[2,18],[2,7],[1,27],[2,15],[2,4],[1,24],[2,12],[2,1],
    [1,21],[2,9],[1,28],[2,16],[2,5],[1,24],[2,12],[2,1],[1,21],[2,9]
  ];

  // 端午节日期数据(农历五月初五,1900-2100年公历日期)
  const dragonBoatData = [
    [6,9],[5,30],[6,18],[6,7],[5,28],[6,15],[6,4],[5,24],[6,12],[6,1],
    [5,22],[6,9],[5,30],[6,17],[6,5],[5,25],[6,12],[6,1],[5,22],[6,10],
    [5,30],[6,17],[6,6],[5,27],[6,14],[6,3],[5,23],[6,11],[5,31],[6,18],
    [6,7],[5,28],[6,16],[6,5],[5,26],[6,13],[6,2],[5,23],[6,10],[5,30],
    [6,18],[6,6],[5,27],[6,14],[6,3],[5,24],[6,11],[5,31],[6,19],[6,8],
    [5,29],[6,16],[6,5],[5,26],[6,13],[6,2],[5,22],[6,10],[5,30],[6,17],
    [6,5],[5,26],[6,14],[6,3],[5,24],[6,11],[5,31],[6,19],[6,8],[5,29],
    [6,16],[6,5],[5,26],[6,14],[6,2],[5,22],[6,10],[5,30],[6,17],[6,6],
    [5,27],[6,15],[6,4],[5,24],[6,12],[6,1],[5,22],[6,9],[5,29],[6,17],
    [6,6],[5,27],[6,15],[6,3],[5,24],[6,11],[5,31],[6,19],[6,8],[5,29],
    [6,16],[6,5],[5,25],[6,13],[6,2],[5,22],[6,10],[5,30],[6,18],[6,7],
    [5,28],[6,15],[6,4],[5,24],[6,12],[6,1],[5,21],[6,9],[5,29],[6,17],
    [6,6],[5,27],[6,14],[6,3],[5,23],[6,11],[6,19],[6,18],[6,7],[5,28],
    [6,15],[6,4],[5,25],[6,13],[6,2],[5,22],[6,10],[5,30],[6,17],[6,6],
    [5,27],[6,15],[6,4],[5,24],[6,12],[6,1],[5,22],[6,10],[5,30],[6,18],
    [6,7],[5,28],[6,16],[6,5],[5,25],[6,13],[6,2],[5,23],[6,11],[5,31],
    [6,19],[6,8],[5,29],[6,16],[6,5],[5,26],[6,14],[6,3],[5,24],[6,12],
    [6,1],[5,22],[6,10],[5,30],[6,17],[6,6],[5,27],[6,15],[6,4],[5,24],
    [6,12],[6,1],[5,21],[6,9],[5,29],[6,17],[6,6],[5,26],[6,14],[6,3],
    [5,24],[6,12],[6,1],[5,22],[6,10],[5,30],[6,18],[6,7],[5,28],[6,16],
    [6,5],[5,26],[6,13],[6,2],[5,22],[6,10],[5,30],[6,17],[6,5],[5,26],
    [6,14],[6,3],[5,24],[6,12],[6,1],[5,22],[6,10],[5,29],[6,17],[6,6]
  ];

  const getSpringFestivalDate = (lunarYear) => {
    const index = lunarYear - 1900;
    if (index >= 0 && index < springFestivalData.length) {
      const [month, day] = springFestivalData[index];
      return new Date(lunarYear, month - 1, day);
    }
    return new Date(lunarYear, 1, 1);
  };

  const getDragonBoatDate = (year) => {
    // 端午节是农历五月初五,使用精确的公历日期映射
    const dragonBoatMap = {
      2020: [6, 25], 2021: [6, 14], 2022: [6, 3], 2023: [6, 22], 2024: [6, 10],
      2025: [5, 31], 2026: [6, 19], 2027: [6, 8], 2028: [5, 28], 2029: [6, 16],
      2030: [6, 5], 2031: [5, 25], 2032: [6, 12], 2033: [6, 1], 2034: [6, 20]
    };
    if (dragonBoatMap[year]) {
      const [month, day] = dragonBoatMap[year];
      return new Date(year, month - 1, day);
    }
    // 默认回退到农历五月初五的近似日期
    return new Date(year, 4, 22);
  };

  const getHolidays = () => {
    const now = new Date();
    const year = now.getFullYear();
    
    return [
      { name: '元旦', date: new Date(year, 0, 1) },
      { name: '春节', date: getSpringFestivalDate(year) },
      { name: '清明', date: new Date(year, 3, 4) },
      { name: '劳动节', date: new Date(year, 4, 1) },
      { name: '端午', date: getDragonBoatDate(year) },
      { name: '中秋', date: new Date(year, 8, 15) },
      { name: '国庆', date: new Date(year, 9, 1) },
      { name: '元旦', date: new Date(year + 1, 0, 1) }
    ];
  };

  const calculateProgress = () => {
    const now = new Date();
    const year = now.getFullYear();
    const month = now.getMonth();
    const day = now.getDate();
    const weekDay = now.getDay();

    // 本年进度
    const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
    const daysInYear = isLeapYear ? 366 : 365;
    const startOfYear = new Date(year, 0, 0);
    const yearPassedDays = Math.floor((now - startOfYear) / 86400000);
    const yearDaysLeft = daysInYear - yearPassedDays;
    const yearProgress = ((yearPassedDays / daysInYear) * 100).toFixed(1);

    // 本月进度
    const daysInMonth = new Date(year, month + 1, 0).getDate();
    const monthPassedDays = day;
    const monthDaysLeft = daysInMonth - day;
    const monthProgress = ((monthPassedDays / daysInMonth) * 100).toFixed(1);

    // 本周进度
    const weekPassedDays = weekDay === 0 ? 7 : weekDay;
    const weekDaysLeft = 7 - weekPassedDays;
    const weekProgress = ((weekPassedDays / 7) * 100).toFixed(1);

    // 最近节假日
    const holidays = getHolidays();
    let nearestHoliday = { name: '元旦', days: '--', date: '--' };
    
    for (const holiday of holidays) {
      const diffTime = holiday.date - now;
      if (diffTime >= 0) {
        const days = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
        const year = holiday.date.getFullYear();
        const month = String(holiday.date.getMonth() + 1).padStart(2, '0');
        const day = String(holiday.date.getDate()).padStart(2, '0');
        nearestHoliday = {
          name: holiday.name,
          days: days === 0 ? '今天' : days,
          date: `${year}-${month}-${day}`
        };
        break;
      }
    }

    // 更新所有 Schedule 组件实例
    const containers = document.querySelectorAll('.schedule-container');
    containers.forEach(container => {
      // 更新进度条
      const pBarYear = container.querySelector('.pBar_year');
      const pBarMonth = container.querySelector('.pBar_month');
      const pBarWeek = container.querySelector('.pBar_week');
      
      if (pBarYear) pBarYear.value = yearPassedDays;
      if (pBarMonth) pBarMonth.value = monthPassedDays;
      if (pBarWeek) pBarWeek.value = weekPassedDays;
      
      // 更新进度百分比显示
      const yearProgressEl = container.querySelector('.year-progress');
      const monthProgressEl = container.querySelector('.month-progress');
      const weekProgressEl = container.querySelector('.week-progress');
      
      const yearDaysLeftEl = container.querySelector('.year-days-left');
      const monthDaysLeftEl = container.querySelector('.month-days-left');
      const weekDaysLeftEl = container.querySelector('.week-days-left');

      if (yearProgressEl) yearProgressEl.textContent = yearProgress + '%';
      if (monthProgressEl) monthProgressEl.textContent = monthProgress + '%';
      if (weekProgressEl) weekProgressEl.textContent = weekProgress + '%';
      
      if (yearDaysLeftEl) yearDaysLeftEl.textContent = `本年还剩 ${yearDaysLeft} 天`;
      if (monthDaysLeftEl) monthDaysLeftEl.textContent = `本月还剩 ${monthDaysLeft} 天`;
      if (weekDaysLeftEl) weekDaysLeftEl.textContent = `本周还剩 ${weekDaysLeft} 天`;
      
      const holidayFull = container.querySelector('.nearest-holiday-full');
      const holidayDays = container.querySelector('.nearest-holiday-days');
      const holidayDate = container.querySelector('.nearest-holiday-date');
      
      if (holidayFull) holidayFull.textContent = `距离${nearestHoliday.name}节`;
      if (holidayDays) holidayDays.textContent = nearestHoliday.days;
      if (holidayDate) holidayDate.textContent = nearestHoliday.date;
    });
  };

  // 轮询检查DOM元素是否存在
  const waitForElement = (callback, maxAttempts = 50, interval = 100) => {
    let attempts = 0;
    const check = () => {
      // 检查关键DOM元素是否存在(使用类选择器)
      if (document.querySelector('.schedule-container')) {
        callback();
      } else if (attempts < maxAttempts) {
        attempts++;
        setTimeout(check, interval);
      }
    };
    check();
  };

  // 初始化
  const init = () => {
    calculateProgress();

    // 每天更新
    const now = new Date();
    const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0, 0);
    dayTimer = setTimeout(function updateDaily() {
      calculateProgress();
      const nextDay = new Date();
      nextDay.setDate(nextDay.getDate() + 1);
      nextDay.setHours(0, 0, 0, 0);
      dayTimer = setTimeout(updateDaily, nextDay - new Date());
    }, tomorrow - now);
  };

  // 使用轮询等待DOM元素就绪
  waitForElement(init);

  // 清理
  const cleanup = () => {
    clearTimeout(dayTimer);
  };

  if (window) {
    window.addEventListener('pagehide', cleanup);
  }
})();
</script>

<style>
  .schedule-container {
    padding: 0;
  }

  .progress-section {
    padding: 0;
  }

  .progress-item {
    padding: 0;
  }

  /* 进度条样式 */
  .schedule-progress-bar {
    -webkit-appearance: none;
    appearance: none;
    border: none;
    width: 100%;
    height: 10px;
    border-radius: 5px;
    background-color: #f0f0f0;
    margin-top: 4px;
  }

  .schedule-progress-bar::-webkit-progress-bar {
    background-color: #f0f0f0;
    border-radius: 5px;
  }

  .schedule-progress-bar::-webkit-progress-value {
    background: var(--primary);
    border-radius: 5px;
    transition: width 0.3s ease;
  }

  .schedule-progress-bar::-moz-progress-bar {
    background: var(--primary);
    border-radius: 5px;
  }

  /* 暗色主题 */
  .dark .schedule-progress-bar {
    background-color: #374151;
  }

  .dark .schedule-progress-bar::-webkit-progress-bar {
    background-color: #374151;
  }
</style>

注册组件

文件路径: src/components/layout/SideBar.astro

astro
---
import Schedule from "@/components/widget/Schedule.astro";

// 组件映射表 - 核心注册机制
const componentMap = {
    <!-- 其他组件 -->
    schedule: Schedule,  // ← 注册 Schedule 组件
} satisfies Record<WidgetComponentType, typeof Profile>;
---

开启/关闭

桌面端侧边栏配置

  • 文件路径 :src/config/sidebarConfig.ts

组件默认在 右侧边栏 启用,找到 rightComponents 数组中的 schedule 配置项:

ts
rightComponents: [
    // ... 其他组件
    {
        // 组件类型:时间进度组件
        type: "schedule",
        // 是否启用该组件
        enable: true,  // true = 开启, false = 关闭
        // 组件位置
        position: "sticky",
        // 是否在文章详情页显示
        showOnPostPage: false,
    },
    // ... 其他组件
]

移动端配置

  • 文件路径 :src/config/sidebarConfig.ts

如果需要在移动端(<768px)显示该组件,需在 mobileBottomComponents 数组中添加配置:

ts
mobileBottomComponents: [
    // ... 其他组件
    {
		// 组件类型:时间进度组件
		type: "schedule",
		// 是否启用该组件
		enable: true,
		// 是否在文章详情页显示
		showOnPostPage: false,
	},
    // ... 其他组件
]
最近更新