惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
博客园 - 司徒正美
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
A
About on SuperTechFans
Google DeepMind News
Google DeepMind News
T
Tailwind CSS Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
M
MIT News - Artificial intelligence
L
LangChain Blog
aimingoo的专栏
aimingoo的专栏
Engineering at Meta
Engineering at Meta
Martin Fowler
Martin Fowler
H
Help Net Security
B
Blog
Y
Y Combinator Blog
小众软件
小众软件
S
SegmentFault 最新的问题
I
InfoQ
爱范儿
爱范儿
Hugging Face - Blog
Hugging Face - Blog
D
Docker
博客园 - 【当耐特】
J
Java Code Geeks
阮一峰的网络日志
阮一峰的网络日志

Hyde Blog

Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog
Hyde Blog
Hyde · 2026-05-19 · via Hyde Blog

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>

astro

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

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