[{"data":1,"prerenderedAt":239},["ShallowReactive",2],{"content:\u002Fposts\u002Fschedule-progress":3,"surround:\u002Fposts\u002Fschedule-progress":229},{"id":4,"title":5,"body":6,"categories":202,"date":204,"description":205,"draft":206,"extension":207,"image":208,"meta":209,"navigation":211,"path":212,"permalink":213,"pinned":206,"published":213,"readingTime":214,"recommend":213,"references":213,"seo":219,"sitemap":220,"stem":221,"tags":222,"type":226,"updated":227,"__hash__":228},"content\u002Fposts\u002Fposts\u002Fschedule-progress.md","Firefly 魔改：时间进度与节假日倒计时组件",{"type":7,"value":8,"toc":189},"minimark",[9,19,22,25,30,37,40,63,66,70,74,86,90,97,108,115,123,127,133,144,148,153,160,164,174,182,186],[10,11,15],"alert",{"title":12,"type":13,"icon":14},"AI 迁移提示","info","tabler:robot",[16,17,18],"p",{},"本文由 AI 协助从旧站迁移，尚未完成逐篇人工审校；内容如有疏漏，将在复核后修订。",[10,20],{"title":21,"type":13},"> 本文部分内容由 AI 辅助整理，已结合实际操作进行校验，请根据自身环境谨慎使用。",[16,23,24],{},"时间进度条是一个让人又爱又恨的小组件——它直观地告诉你这一年、这一个月、这一周已经过去了多少，还贴心地显示距离下一个法定节假日还有几天。",[26,27,29],"h2",{"id":28},"一功能概览","一、功能概览",[16,31,32],{},[33,34],"img",{"alt":35,"src":36},"时间进度条组件","https:\u002F\u002Fimg.olinl.com\u002Ffile\u002Fpost-img\u002Fschedule-progress\u002F0001.webp",[16,38,39],{},"三个维度的进度条：",[41,42,43,51,57],"ul",{},[44,45,46,50],"li",{},[47,48,49],"strong",{},"本年进度"," — 今年已经过去了多少天",[44,52,53,56],{},[47,54,55],{},"本月进度"," — 本月已经过去了多少天",[44,58,59,62],{},[47,60,61],{},"本周进度"," — 本周已经过去了多少天",[16,64,65],{},"底部还有法定节假日倒计时。",[26,67,69],{"id":68},"二核心实现","二、核心实现",[71,72,73],"h3",{"id":73},"组件完整源码",[75,76,82],"pre",{"className":77,"code":79,"language":80,"meta":81},[78],"language-astro","---\nimport WidgetLayout from \"@\u002Fcomponents\u002Fcommon\u002FWidgetLayout.astro\";\n\ninterface Props {\n    class?: string;\n    style?: string;\n}\n\nconst { class: className, style } = Astro.props;\n---\n\n\u003CWidgetLayout id=\"schedule-widget\" class={className} style={style}>\n  \u003Cdiv class=\"schedule-container\">\n    \u003Cdiv class=\"progress-section space-y-4\">\n      \u003Cdiv class=\"progress-item flex items-center gap-3\">\n        \u003Cspan class=\"text-[14px] font-semibold text-(--primary) shrink-0 w-[60px] year-progress\">--%\u003C\u002Fspan>\n        \u003Cdiv class=\"flex-1 min-w-0\">\n          \u003Cspan class=\"text-sm text-neutral-700 dark:text-neutral-300 year-days-left\">本年还剩 -- 天\u003C\u002Fspan>\n          \u003Cprogress max=\"365\" class=\"schedule-progress-bar pBar_year\" value=\"0\">\u003C\u002Fprogress>\n        \u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n      \u003Cdiv class=\"progress-item flex items-center gap-3\">\n        \u003Cspan class=\"text-[14px] font-semibold text-(--primary) shrink-0 w-[60px] month-progress\">--%\u003C\u002Fspan>\n        \u003Cdiv class=\"flex-1 min-w-0\">\n          \u003Cspan class=\"text-sm text-neutral-700 dark:text-neutral-300 month-days-left\">本月还剩 -- 天\u003C\u002Fspan>\n          \u003Cprogress max=\"31\" class=\"schedule-progress-bar pBar_month\" value=\"0\">\u003C\u002Fprogress>\n        \u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n      \u003Cdiv class=\"progress-item flex items-center gap-3\">\n        \u003Cspan class=\"text-[14px] font-semibold text-(--primary) shrink-0 w-[60px] week-progress\">--%\u003C\u002Fspan>\n        \u003Cdiv class=\"flex-1 min-w-0\">\n          \u003Cspan class=\"text-sm text-neutral-700 dark:text-neutral-300 week-days-left\">本周还剩 -- 天\u003C\u002Fspan>\n          \u003Cprogress max=\"7\" class=\"schedule-progress-bar pBar_week\" value=\"0\">\u003C\u002Fprogress>\n        \u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n    \u003Cdiv class=\"holiday-section mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-700\">\n      \u003Cdiv class=\"text-center\">\n        \u003Cp class=\"text-base font-semibold text-neutral-700 dark:text-neutral-300 mb-0 nearest-holiday-full\">距离 --\u003C\u002Fp>\n        \u003Cp class=\"text-4xl font-bold text-(--primary) mb-2 nearest-holiday-days\">--\u003C\u002Fp>\n        \u003Cp class=\"text-xs text-neutral-500 dark:text-neutral-400 nearest-holiday-date\">--\u003C\u002Fp>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002FWidgetLayout>\n\n\u003Cscript is:inline>\n  (function () {\n    const holidays = [\n      { name: \"元旦\", month: 0, day: 1 },\n      { name: \"清明\", month: 3, day: 4 },\n      { name: \"劳动节\", month: 4, day: 1 },\n      { name: \"端午\", month: 5, day: 19 },\n      { name: \"中秋\", month: 8, day: 25 },\n      { name: \"国庆\", month: 9, day: 1 },\n    ];\n    const formatDate = (date) => {\n      const year = date.getFullYear();\n      const month = String(date.getMonth() + 1).padStart(2, \"0\");\n      const day = String(date.getDate()).padStart(2, \"0\");\n      return `${year}-${month}-${day}`;\n    };\n    const getNearestHoliday = (now) => {\n      const year = now.getFullYear();\n      const dates = holidays.map((h) => ({ name: h.name, date: new Date(year, h.month, h.day) }))\n        .concat({ name: \"元旦\", date: new Date(year + 1, 0, 1) })\n        .filter((h) => h.date >= now).sort((a, b) => a.date - b.date);\n      const nearest = dates[0];\n      const days = Math.ceil((nearest.date - now) \u002F 86400000);\n      return { name: nearest.name, days: days === 0 ? \"今天\" : String(days), date: formatDate(nearest.date) };\n    };\n    const setText = (el, sel, val) => { const e = el.querySelector(sel); if (e) e.textContent = val; };\n    const updateSchedule = () => {\n      const now = new Date(); const year = now.getFullYear();\n      const month = now.getMonth(); const day = now.getDate(); const weekDay = now.getDay() || 7;\n      const isLeap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;\n      const daysInYear = isLeap ? 366 : 365;\n      const yearPassed = Math.floor((now - new Date(year, 0, 0)) \u002F 86400000);\n      const daysInMonth = new Date(year, month + 1, 0).getDate();\n      const h = getNearestHoliday(now);\n      document.querySelectorAll(\".schedule-container\").forEach((c) => {\n        const yp = ((yearPassed \u002F daysInYear) * 100).toFixed(1);\n        const mp = ((day \u002F daysInMonth) * 100).toFixed(1);\n        const wp = ((weekDay \u002F 7) * 100).toFixed(1);\n        const pY = c.querySelector(\".pBar_year\"); if (pY) { pY.max = daysInYear; pY.value = yearPassed; }\n        const pM = c.querySelector(\".pBar_month\"); if (pM) { pM.max = daysInMonth; pM.value = day; }\n        const pW = c.querySelector(\".pBar_week\"); if (pW) pW.value = weekDay;\n        setText(c, \".year-progress\", `${yp}%`); setText(c, \".month-progress\", `${mp}%`); setText(c, \".week-progress\", `${wp}%`);\n        setText(c, \".year-days-left\", `本年还剩 ${daysInYear - yearPassed} 天`);\n        setText(c, \".month-days-left\", `本月还剩 ${daysInMonth - day} 天`);\n        setText(c, \".week-days-left\", `本周还剩 ${7 - weekDay} 天`);\n        setText(c, \".nearest-holiday-full\", `距离${h.name}`);\n        setText(c, \".nearest-holiday-days\", h.days);\n        setText(c, \".nearest-holiday-date\", h.date);\n      });\n    };\n    const init = () => { updateSchedule(); const n = new Date(); const nxt = new Date(n.getFullYear(), n.getMonth(), n.getDate() + 1); setTimeout(() => { updateSchedule(); setInterval(updateSchedule, 86400000); }, nxt - n); };\n    if (document.readyState === \"loading\") document.addEventListener(\"DOMContentLoaded\", init, { once: true }); else init();\n    document.addEventListener(\"astro:page-load\", updateSchedule);\n  })();\n\u003C\u002Fscript>\n\n\u003Cstyle>\n  .schedule-progress-bar { -webkit-appearance: none; appearance: none; border: none; width: 100%; height: 10px; border-radius: 5px; background-color: #f0f0f0; margin-top: 4px; }\n  .schedule-progress-bar::-webkit-progress-bar { background-color: #f0f0f0; border-radius: 5px; }\n  .schedule-progress-bar::-webkit-progress-value { background: var(--primary); border-radius: 5px; transition: width 0.3s ease; }\n  .schedule-progress-bar::-moz-progress-bar { background: var(--primary); border-radius: 5px; }\n  .dark .schedule-progress-bar, .dark .schedule-progress-bar::-webkit-progress-bar { background-color: #374151; }\n\u003C\u002Fstyle>\n","astro","title=\"src\u002Fcomponents\u002Fwidget\u002FSchedule.astro\"",[83,84,79],"code",{"__ignoreMap":85},"",[26,87,89],{"id":88},"三注册组件","三、注册组件",[16,91,92,93,96],{},"在 ",[83,94,95],{"code":95},"src\u002Fcomponents\u002Flayout\u002FSideBar.astro"," 中导入：",[75,98,106],{"className":99,"code":101,"highlights":102,"language":104,"meta":105},[100],"language-js","import Advertisement from \"@\u002Fcomponents\u002Fwidget\u002FAdvertisement.astro\";\nimport Announcement from \"@\u002Fcomponents\u002Fwidget\u002FAnnouncement.astro\";\nimport Calendar from \"@\u002Fcomponents\u002Fwidget\u002FCalendar.astro\";\n\u002F\u002F...\nimport Schedule from \"@\u002Fcomponents\u002Fwidget\u002FSchedule.astro\";\n\u002F\u002F ...\n",[103],5,"js","title=\"src\u002Fcomponents\u002Flayout\u002FSideBar.astro\" ins=",[83,107,101],{"__ignoreMap":85},[16,109,110,111,114],{},"同时在 ",[83,112,113],{"code":113},"componentMap"," 中注册：",[75,116,121],{"className":117,"code":118,"highlights":119,"language":104,"meta":105},[100],"const componentMap = {\n    profile: Profile,\n    announcement: Announcement,\n    categories: Categories,\n    tags: Tags,\n    \u002F\u002F ...\n    schedule: Schedule,\n    quoteOfTheDay: QuoteOfTheDay,\n};\n",[120],7,[83,122,118],{"__ignoreMap":85},[26,124,126],{"id":125},"四注册类型","四、注册类型",[16,128,92,129,132],{},[83,130,131],{"code":131},"src\u002Ftypes\u002FsidebarConfig.ts"," 的类型联合中添加组件名：",[75,134,142],{"className":135,"code":137,"highlights":138,"language":140,"meta":141},[136],"language-typescript","export type WidgetComponentType =\n    | \"githubHeatmap\"\n    | \"timeGreeting\"\n    | \"dynamic\"\n    | \"schedule\"\n    | \"quoteOfTheDay\";\n",[139],4,"typescript","title=\"src\u002Ftypes\u002FsidebarConfig.ts\" del= ins={5-6}",[83,143,137],{"__ignoreMap":85},[26,145,147],{"id":146},"五配置方式","五、配置方式",[16,149,92,150,114],{},[83,151,152],{"code":152},"sidebarConfig.ts",[75,154,158],{"className":155,"code":156,"language":140,"meta":157},[136],"{\n  type: \"schedule\",\n  enable: true,\n  position: \"sticky\",\n  showOnPostPage: false,\n}\n","title=\"src\u002Fconfig\u002FsidebarConfig.ts\"",[83,159,156],{"__ignoreMap":85},[26,161,163],{"id":162},"六相关文件","六、相关文件",[16,165,166,167],{},"组件：",[168,169,173],"a",{"href":170,"rel":171},"https:\u002F\u002Fgithub.com\u002Folinll\u002Ffirefly-blog\u002Fblob\u002Fmaster\u002Fsrc\u002Fcomponents\u002Fwidget\u002FSchedule.astro",[172],"nofollow","\u002Fsrc\u002Fcomponents\u002Fwidget\u002FSchedule.astro",[16,175,176,177],{},"相关源码：",[168,178,181],{"href":179,"rel":180},"https:\u002F\u002Fgithub.com\u002Folinll\u002Ffirefly-blog",[172],"olinll\u002Ffirefly-blog",[26,183,185],{"id":184},"最后","🔗 最后",[16,187,188],{},"这个组件虽然代码不多，但非常实用——把它放在侧边栏或移动端底部，每次浏览博客时瞄一眼，对时间的感知会更强烈。",{"title":85,"searchDepth":139,"depth":139,"links":190},[191,193,197,198,199,200,201],{"id":28,"depth":192,"text":29},2,{"id":68,"depth":192,"text":69,"children":194},[195],{"id":73,"depth":196,"text":73},3,{"id":88,"depth":192,"text":89},{"id":125,"depth":192,"text":126},{"id":146,"depth":192,"text":147},{"id":162,"depth":192,"text":163},{"id":184,"depth":192,"text":185},[203],"Firefly","2026-07-21 19:41:29","年\u002F月\u002F周进度条，搭配节假日倒计时，帮你感知时间的流逝。",false,"md","https:\u002F\u002Fimg.olinl.com\u002Ffile\u002Fpost-img\u002Fschedule-progress\u002Fcover.webp",{"slots":210},{},true,"\u002Fposts\u002Fschedule-progress",null,{"text":215,"minutes":216,"time":217,"words":218},"5 min read",5.005,300300,1001,{"title":5,"description":205},{"loc":212},"posts\u002Fposts\u002Fschedule-progress",[203,223,224,225],"博客","二开","小部件","tech","2026-07-22 19:30:00","7DFjlGgVrDLh63jhjhgaIS-P6-ZQQW515-ev0ElQvVQ",[230,234],{"title":231,"path":232,"stem":233,"date":204,"type":226,"children":-1},"Firefly 魔改：每日一言侧边栏组件","\u002Fposts\u002Fquote-of-the-day","posts\u002Fposts\u002Fquote-of-the-day",{"title":235,"path":236,"stem":237,"date":238,"type":226,"children":-1},"Firefly 魔改：全局复制事件监听与提示浮层","\u002Fposts\u002Fcopy-toast","posts\u002Fposts\u002Fcopy-toast","2026-07-21 19:41:32",1788712221314]