博客侧边栏倒计时组件改进

前言

起因是在刚开始实装梦爱吃鱼的这个侧边栏倒计时时,发现必须填写准确的节日日期,比如如果填写春节的话,每年都需要修改一次第二年的春节日期,虽然不麻烦但也容易忘记,既然如此不如做成全自动的,省事省心

由于我能力不够所以使用了AI,关于这点还请原谅>_<,代码不一定最好,如果有问题和建议请在评论区指出,谢谢 qwq
封面图:https://www.pixiv.net/artworks/76954172

该教程适用于 anzhiyu 主题,其他主题未测试

修改的功能

  1. 支持跨年计算
    原本只是计算当前年份的节日,实际测试当节日已经过去时会出现负数
    改为自动判断节日是否已过,如果已过则自动计算下一年的节日时间
  2. 使用 API 获取节假日
    原教程需要手动在代码中维护节日日期
    优化后通过节假日 API 自动获取节日信息,实现每年节假日更新

使用 API 而非本地填写日期的话会有 API 失效导致无法使用的风险,请在实装前明确这点,担心失效的话可以用回原本的方法

  1. 自动寻找最近节日
    原本只能固定倒计时某一个节日,比如春节
    现在则是遍历节日列表,自动计算距离当前时间最近的还没过的节日并显示倒计时

教程

以下教程基于原教程结构进行,仅修改倒计时逻辑部分

创建 countdown.js 文件

  • 在博客根目录source文件夹下创建js文件夹,然后在js文件夹下创建countdown.js文件(也可以放在其他地方)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
const CountdownTimer = (() => {

let timer = null
let cachedHoliday = null
let dom = {}
let styleInjected = false

const config = {
units: {
day: { text: "今日", unit: "小时" },
week: { text: "本周", unit: "天" },
month: { text: "本月", unit: "天" },
year: { text: "本年", unit: "天" }
}
}

function initDOM() {
dom = {
eventName: document.getElementById("eventName"),
eventDate: document.getElementById("eventDate"),
daysUntil: document.getElementById("daysUntil"),
countRight: document.getElementById("countRight")
}
}

function isLeapYear(year) {
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0)
}

function getCalculators(now) {

return {

day: () => {
const hours = now.getHours()
return {
remaining: 24 - hours,
percentage: (hours / 24) * 100
}
},

week: () => {
const day = now.getDay()
const passed = day === 0 ? 6 : day - 1
return {
remaining: 6 - passed,
percentage: ((passed + 1) / 7) * 100
}
},

month: () => {
const total = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()
const passed = now.getDate() - 1
return {
remaining: total - passed,
percentage: (passed / total) * 100
}
},

year: () => {
const year = now.getFullYear()
const start = new Date(year, 0, 1)
const total = isLeapYear(year) ? 366 : 365
const passed = Math.floor((now - start) / 86400000)
return {
remaining: total - passed,
percentage: (passed / total) * 100
}
}

}
}

async function fetchHoliday(year) {

const res = await fetch(`https://timor.tech/api/holiday/year/${year}/`)
const data = await res.json()

const holidays = []

for (const key in data.holiday) {

const h = data.holiday[key]

if (h.holiday) {
holidays.push({
name: h.name,
date: new Date(h.date)
})
}

}

holidays.sort((a, b) => a.date - b.date)

return holidays
}

async function getNextHoliday() {

if (cachedHoliday) return cachedHoliday

const today = new Date()
today.setHours(0,0,0,0)

const year = today.getFullYear()

const list = await fetchHoliday(year)

for (const h of list) {

const d = new Date(h.date)
d.setHours(0,0,0,0)

if (d >= today) {
cachedHoliday = h
return h
}

}

const nextYearList = await fetchHoliday(year + 1)

cachedHoliday = nextYearList[0]

return cachedHoliday
}

function injectStyles() {

if (styleInjected) return

const styles = `
.card-countdown .item-content { display:flex; }
.cd-count-left { position:relative; display:flex; flex-direction:column; margin-right:.8rem; line-height:1.5; align-items:center; justify-content:center; }
.cd-count-left .cd-text { font-size:14px; }
.cd-count-left .cd-name { font-weight:bold; font-size:18px; }
.cd-count-left .cd-time { font-size:30px; font-weight:bold; color:var(--anzhiyu-main); }
.cd-count-left .cd-date { font-size:12px; opacity:.6; }
.cd-count-left::after { content:""; position:absolute; right:-.8rem; width:2px; height:80%; background-color:var(--anzhiyu-main); opacity:.5; }

.cd-count-right { flex:1; margin-left:.8rem; display:flex; flex-direction:column; justify-content:space-between; }
.cd-count-item { display:flex; flex-direction:row; align-items:center; height:24px; }
.cd-item-name { font-size:14px; margin-right:.8rem; white-space:nowrap; }

.cd-item-progress { position:relative; display:flex; flex-direction:row; align-items:center; justify-content:space-between; height:100%; width:100%; border-radius:8px; background-color:var(--anzhiyu-background); overflow:hidden; }

.cd-progress-bar { height:100%; border-radius:8px; background-color:var(--anzhiyu-main); }

.cd-percentage, .cd-remaining { position:absolute; font-size:12px; margin:0 6px; transition:opacity .3s ease-in-out, transform .3s ease-in-out; }

.cd-many { color:#fff; }

.cd-remaining { opacity:0; transform:translateX(10px); }

.card-countdown .item-content:hover .cd-remaining { transform:translateX(0); opacity:1; }

.card-countdown .item-content:hover .cd-percentage { transform:translateX(-10px); opacity:0; }
`

const styleSheet = document.createElement("style")
styleSheet.id = "countdown-style"
styleSheet.textContent = styles
document.head.appendChild(styleSheet)

styleInjected = true
}

async function updateCountdown() {

if (!dom.eventName) return

try {

const now = new Date()

const target = await getNextHoliday()

const today = new Date()
today.setHours(0,0,0,0)

const targetDate = new Date(target.date)
targetDate.setHours(0,0,0,0)

const days = Math.round((targetDate - today) / 86400000)

dom.eventName.textContent = target.name
dom.eventDate.textContent = targetDate.toLocaleDateString('zh-CN')
dom.daysUntil.textContent = days

const calculators = getCalculators(now)

dom.countRight.innerHTML = Object.entries(config.units)
.map(([key,{text,unit}])=>{

const {remaining,percentage} = calculators[key]()

return `
<div class="cd-count-item">
<div class="cd-item-name">${text}</div>
<div class="cd-item-progress">
<div class="cd-progress-bar" style="width:${percentage}%;opacity:${percentage/100}"></div>
<span class="cd-percentage ${percentage>=46?'cd-many':''}">${percentage.toFixed(2)}%</span>
<span class="cd-remaining ${percentage>=60?'cd-many':''}">
<span class="cd-tip">还剩</span>${remaining}<span class="cd-tip">${unit}</span>
</span>
</div>
</div>
`
}).join('')

} catch (e) {

dom.eventName.textContent = "节日"
dom.eventDate.textContent = "--"
dom.daysUntil.textContent = "--"

}

}

function start() {

if (timer) clearInterval(timer)

initDOM()

injectStyles()

updateCountdown()

timer = setInterval(updateCountdown,600000)

}

document.addEventListener("DOMContentLoaded", start)
document.addEventListener("pjax:complete", start)
document.addEventListener("pjax:send", ()=> timer && clearInterval(timer))

return { start }

})()

添加组件配置

  • source中创建_data/widget.yml文件,复制粘贴下面的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
top:
- class_name: card-countdown
id_name:
name:
icon:
html: |
<div class="cd-count-left">
<span class="cd-text">距离</span>
<span class="cd-name" id="eventName"></span>
<span class="cd-time" id="daysUntil"></span>
<span class="cd-date" id="eventDate"></span>
</div>
<div id="countRight" class="cd-count-right"></div>

引入 JS 文件

_config.anzhiyu.yml文件的inject配置项的bottom中引入 JS

1
2
3
4
5
6
7
inject:
head:
# 自定义css

bottom:
# 自定义js
- <script src="/js/countdown.js"></script>