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

推荐订阅源

J
Java Code Geeks
T
Tailwind CSS Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
L
LangChain Blog
博客园 - 【当耐特】
I
InfoQ
腾讯CDC
人人都是产品经理
人人都是产品经理
H
Help Net Security
Y
Y Combinator Blog
B
Blog
博客园 - Franky
Microsoft Security Blog
Microsoft Security Blog
Stack Overflow Blog
Stack Overflow Blog
The Cloudflare Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 叶小钗
D
Docker
博客园 - 聂微东
B
Blog RSS Feed
G
Google Developers 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 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>;
---