倒计时器
2025年8月15日
修订 3
评分
1
↑ 1
↓ 0
支持率
100%
总票数 1
Wilson 95% 下界
20.7%
在相同票数下更稳健的支持率估计
争议指数
0.000
评分趋势
按周聚合 加载图表中...
最近修订
1 / 2
最近投票
1 / 1
2025-08-15
相关页面
暂无推荐
页面源码
[[iftags +组件]]
[[include :scp-wiki-cn:component:croqstyle -=- ]]
[[module CSS]]
.croqstyle__documentation :is(h1, h2) {
font-family: Inter, sans-serif;
font-weight: bold;
}
.croqstyle__documentation h2 { margin-top: 2rem }
.croqstyle__documentation h3 { margin-top: 2.5rem }
.croqstyle__documentation #toc,
.croqstyle__description {
border: thin solid #ddd;
padding: 1em;
background-color: transparent;
}
td { vertical-align: top; }
.options table.wiki-content-table td { border: none; padding: 0.7em 1em; }
[[/module]]
[[div class="croqstyle__documentation"]]
[[div class="croqstyle__description"]]
这是一个会显示倒计时器的组件。给它一个时间戳它就会倒计时到那个时刻。
++ 演示
[[embed]]
<iframe src="https://scp-sandbox-3.wdfiles.com/local--html/demo%3Acountdown-timer/2f51a331ec18aba963d57dd2a1cea2f7065d0298-686842511317953009/scp-sandbox-3.wikidot.com/" allowtransparency="true" class="html-block-iframe" style="height: 3rem"></iframe>
[[/embed]]
更多展示例:https://scp-sandbox-3.wikidot.com/demo:countdown-timer
++ 用法
将以下代码放在页面里:
[[div class="code terminal"]]
##var(--c-keyword)|[[####var(--c-builtin)|include## ##var(--c-symbol)|:scp-wiki-cn:component:countdown##
##var(--c-keyword)||## ##var(--c-builtin)|timestamp####var(--c-keyword)| = ####var(--c-string)|1752897600##
##var(--c-keyword)||## ##var(--c-builtin)|format####var(--c-keyword)| = ####var(--c-string)|DDd HH:MM:SS##
##var(--c-keyword)||## ##var(--c-builtin)|text_when_elapsed####var(--c-keyword)| = ####var(--c-string)|Timer ended.##
##var(--c-keyword)||## ##var(--c-builtin)|css####var(--c-keyword)| = ##
##var(--c-string)|.countdown {##
##var(--c-string)|@@ @@font-size: 2em;##
##var(--c-string)|@@ @@font-weight: bold;##
##var(--c-string)|@@ @@text-align: center;##
##var(--c-string)|}##
##var(--c-keyword)|]]##
[[/div]]
支持以下参数。如果你省略了一个可选设置,那么它将会使用其默认值。
[[div class="options"]]
|| **timestamp** || 需计时至的时刻,形式为UNIX时间戳。 _
或者,一个以英文逗号分隔的UNIX时间戳列表。 _
或者,一个表达式(参见下方)。 ||
|| **//format//** _
//(可选)// || 计时器的输出格式。可应用自定义HTML。 _
占位符{{WW}},{{DD}},{{HH}},{{MM}}以及{{SS}}将在倒计时中被替换为周数/日数/小时数/分钟数/秒数。 _
//默认值:HH:MM:SS// ||
|| **//text_when_elapsed//** _
//(可选)// || 在时间走完后计时器停止倒数时显示的文本。可应用自定义HTML。 _
//默认值:00:00:00 (你的格式下所有数值达到0时的内容)// ||
|| **//css//** _
//(可选)// || 用来为组件内部的iframe添加样式的CSS。若要为外部添加样式,请使用通常的CSS模块。 _
_
例如:在应用暗色版式的页面中,可以尝试 {{.countdown { color: white }}} ||
[[/div]]
在倒计时器永久结束计时后,其内容将几乎保持静态,倒计时功能也不再可用。在你方便时,你应当从页面上移除本倒计时器并以正常内容替换。
+++ 选择一个时间戳
该组件使用UNIX时间戳,也即自UTC时间1970年起经过的秒数。一个给定的UNIX时间戳对所有人都意味着相同的时间,无论人们在世界上所处的位置如何——不需要处理时区与本地化了。
例如:
* {{1767225600}} - 倒数至UTC 2026元旦午夜,
* {{1863561600}} - 倒数至UTC 2029-01-20午夜
有很多免费资源可以得到你脑中的截止时间所对应的UNIX时间戳,比如https://www.unixtimestamp.com/。
如果你//真的//需要时区本地化,我们有这个功能的组件:[[[component:tz|]]]
+++ 时间戳列表
你可以传入一个以逗号分隔开的时间戳列表。组件将选择第一个不在过去的时间。
在当前时间达到该时间戳时,计时器将停止。它不会自动跳至列表中的下一个时间戳,而是展示{{text_when_elapsed}}的内容。在该页面下次加载时它将开始向列表中的下一个时间戳倒数。
+++ 时间戳表达式
假使你需要一个自适应的倒计时,例如:不断重复,或取决于页面加载时的时间,你可提供一表达式而非固定的时间戳。
表达式中,字母“t”将被替换为页面加载完成时的时间戳,余下部分被传至Javascript的计算中。输出必须为需要倒计时至的另一个时间戳。
例如:
* {{t + 60}} - 自页面加载完成后倒数60秒
* {{t - (t % 60) + 60}} - 倒计时至下一个整分钟时。(就是把当前的时间戳,减去其本身对60取余之结果以向下取整至一个分钟开始处。这将返回一个处于过去的时间戳,故而加60将之推向未来)
* {{t - (t % 3600) + 3600}} - 倒计时至下一个整点时
* {{Math.floor(t / 86400) * 86400 + 86400}} - 倒计时至UTC午夜
* {{Math.floor(t / 86400) * 86400 + 86400 + 21600}} - 倒计时至UTC6点
除非你是天才,试图理解这里的算术要花一点时间。我每次看到它的时候就会花上一点时间。
++ CSS结构
就CSS改变样式而言,这就是iframe里内容的结构。检查页面源代码以获取全部细节。
[[div class="code"]]
@@ <div class="countdown">@@
@@ <span class="timer timer--active">00:00:00</span>@@
@@ <!-- 只在计时器停止时显示 -->@@
@@ <span class="timer timer--elapsed">Timer ended.</span>@@
@@ </div>@@
[[/div]]
iframe本身位于带有类{{.countdown-wrapper}}的div之中。
+++ 进阶CSS
你可以使用CSS在计时器停止时令页面发生变化。选择器{{.countdown-wrapper:has(iframe[style*=".01px"])}}会匹配一个已结束的倒计时。在[https://scp-sandbox-3.wikidot.com/demo:countdown-timer 展示页]上有个例子。
++ 无障碍
构建本倒计时器时已考虑到对无障碍性之最佳实践。使用本组件前考虑先阅读下文,这样你就不会因为做了蠢事搞砸我完美的造物。
**太长不看:**避免在读者仍在阅读时使用本组件移除内容,因为这将造成含障碍的体验。
ARIA属性:
* 本计时器使用了[https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/timer_role ARIA角色"timer"],这代表它对于屏幕阅读器用户而言是计时器但无需随着计时器每秒变换时重复将其读出。
* 当计时器结束倒计时时,它使用[https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-live aria-live="polite"]以在一段时间的静默后彬彬有礼地向屏幕阅读器用户宣读你的{{text_when_elapsed}}——只要这段文字不是形如“00:00:00”就会被读出。
遵从[https://www.w3.org/TR/WCAG22/ WCAG 2.2]:
* [https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable.html 成功标准2.2.1 定时可调](A级):若该计时器被用于与一真实事件同步(例如:竞赛结束,系列文章发布),则此要求被免除。若该计时器被用于在读者继续阅读前人为延迟之,则该行为违背了指南之字面意义而未违反其精神,因其意图为给予用户足够时间以完成一行为,并且用户在计时终止后便可继续阅读。若你将本计时器用于在读者有完整阅读之可能前//移除//内容,则违反了本指南。
* [https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html 成功标准2.2.2 暂停,停止,隐藏](A级):在悬停或获得焦点时,应显示一个可通过点击或键盘交互暂停计时器的按钮。
* [https://www.w3.org/WAI/WCAG22/Understanding/no-timing.html 成功标准2.2.3 无定时](AAA级):只要该计时器不被用于自用户处//移除//内容,这被视为可行的。若该组件被用于设定一个时间//限制//而非只是倒计时至某一时刻,则不适用此条。
* [https://www.w3.org/WAI/WCAG22/Understanding/three-flashes.html 成功标准2.3.2 闪烁三次] (AAA级):即便含有大号的花哨字体,该计时器每秒最多闪烁一次,低于最大阈值(每秒三次闪烁)。
[[/div]]
[[/div]]
[[/iftags]]
[[div_ class="countdown-wrapper"]]
[[html]]
<head>
<script>
const inputs = {
timestamp: `{$timestamp}`,
format: `{$format}`,
}
</script>
<link href="https://d3g0gp89917ko0.cloudfront.net/v--edac79f846ba/common--theme/base/css/style.css" rel="stylesheet">
<link href="https://scp-wiki.wikidot.com/local--code/component%3Atheme/1" rel="stylesheet">
<script src="https://interwiki.scpwiki.com/resizeIframe.js" defer></script>
<script>
window.resize = () => {}
</script>
<style>
.countdown {
padding-block: 0.2em;
font-variant-numeric: tabular-nums;
}
.timer--elapsed, .countdown--elapsed .timer--active {
display: none;
}
.countdown--elapsed .timer--elapsed {
display: inline;
}
.timer-pause {
mask-size: contain;
font-size: 0;
height: 1rem;
width: 1rem;
border: none;
vertical-align: middle;
color: inherit !important;
background-color: currentColor !important;
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M2 5.27L3.28 4L20 20.72L18.73 22l-3.08-3.08c-1.15.38-2.37.58-3.65.58c-5 0-9.27-3.11-11-7.5c.69-1.76 1.79-3.31 3.19-4.54zM12 9a3 3 0 0 1 3 3a3 3 0 0 1-.17 1L11 9.17A3 3 0 0 1 12 9m0-4.5c5 0 9.27 3.11 11 7.5a11.8 11.8 0 0 1-4 5.19l-1.42-1.43A9.86 9.86 0 0 0 20.82 12A9.82 9.82 0 0 0 12 6.5c-1.09 0-2.16.18-3.16.5L7.3 5.47c1.44-.62 3.03-.97 4.7-.97M3.18 12A9.82 9.82 0 0 0 12 17.5c.69 0 1.37-.07 2-.21L11.72 15A3.064 3.064 0 0 1 9 12.28L5.6 8.87c-.99.85-1.82 1.91-2.42 3.13'/%3E%3C/svg%3E");
opacity: 0;
margin-right: -1rem;
}
.countdown--paused .timer-pause {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M12 9a3 3 0 0 1 3 3a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3m0-4.5c5 0 9.27 3.11 11 7.5c-1.73 4.39-6 7.5-11 7.5S2.73 16.39 1 12c1.73-4.39 6-7.5 11-7.5M3.18 12a9.821 9.821 0 0 0 17.64 0a9.821 9.821 0 0 0-17.64 0'/%3E%3C/svg%3E");
}
.countdown--paused .timer--active {
opacity: 0.5;
}
:where(.countdown:hover, .countdown:focus-within, .countdown--paused) .timer-pause {
opacity: 1;
}
</style>
<style>{$css}</style>
</head>
<body>
<div class="countdown">
<span class="timer timer--active" role="timer" id="timer"></span><button class="timer-pause" type="button" aria-controls="timer" title="Hide timer. The countdown will still end at the correct time.">Hide</button>
<!-- Only shows when the timer has elapsed -->
<span class="timer timer--elapsed">{$text_when_elapsed}</span>
</div>
</body>
<script>
function log() {
console.log("component:countdown:", ...arguments)
}
let interval = 0
let paused = false
let elapsed = false
const timeTokens = { WW: 604800, DD: 86400, HH: 3600, MM: 60, SS: 1 }
const emptyDelta = { WW: 0, DD: 0, HH: 0, MM: 0, SS: 0 }
const defaultFormatString = "HH:MM:SS"
function getTargetTime(inputString, now) {
// If the input is literally a timestamp, return it directly
if (!Number.isNaN(Number(inputString))) {
log("Received specific timestamp", inputString)
return Number(inputString)
}
// If it's a list of timestamps, get the next one
if (inputString.includes(",")) {
log("Received timestamp list", inputString)
const timestamps = inputString.split(",").map(Number)
for (const timestamp of timestamps) {
if (!Number.isNaN(timestamp) && timestamp > now) {
return timestamp
}
}
}
// If it's a formula, parse it
if (inputString.includes("t")) {
log("Received timestamp formula", inputString)
const expression = inputString.replaceAll("t", String(now))
log("Evaluating", expression)
let result
try {
result = eval?.(expression)
log("Evaluated to", result, `(${typeof result})`)
if (!Number.isNaN(Number(result))) return Number(result)
} catch (error) {
log("Evaluation failed with error", error)
throw error
}
}
log("Failed to get a valid future timestamp")
return 0
}
function parseDelta(delta, formatString) {
const result = {...emptyDelta}
for (const [token, tokenSize] of Object.entries(timeTokens)) {
if (formatString.includes(token)) {
const tokenCount = Math.floor(delta / tokenSize)
delta = delta - tokenSize * tokenCount
result[token] = tokenCount
}
}
return result
}
function formatDelta(delta, formatString) {
let formatted = formatString
for (let [token, tokenCount] of Object.entries(delta)) {
if (["HH", "MM", "SS"].includes(token)) {
tokenCount = String(tokenCount).padStart(2, "0")
}
formatted = formatted.replace(token, tokenCount)
}
return formatted
}
function processCountdown(target) {
const now = Math.floor(Date.now() / 1000)
const deltaSecs = target - now
const formatString = inputs.format === "{"+"$format}"
? defaultFormatString
: inputs.format
if (deltaSecs <= 0) {
setCountdownDone(formatString)
return true
}
if (paused) return false
const delta = parseDelta(deltaSecs, formatString)
const formattedDelta = formatDelta(delta, formatString)
document.querySelector(".countdown .timer").innerHTML = formattedDelta
window.resize(document.body.scrollHeight)
return false
}
function setCountdownDone(formatString) {
elapsed = true
clearInterval(interval)
document.querySelector(".timer-pause").remove()
const countdownEl = document.querySelector(".countdown")
const doneEl = countdownEl.querySelector(".timer--elapsed")
if (doneEl.textContent === "{"+"$text_when_elapsed}") {
doneEl.textContent = formatDelta({...emptyDelta}, formatString)
} else {
countdownEl.setAttribute("aria-live", "polite")
countdownEl.setAttribute("aria-atomic", "true")
}
document.querySelector(".timer").remove()
countdownEl.classList.add("countdown--elapsed")
window.resize(document.body.scrollHeight)
}
function reimplementResize() {
// [[html]] iframes come with their own resize script
// Remove this so that we can use our own to control the value
document.querySelectorAll(
"script[src*='common--javascript/html-block-iframe.js']"
).forEach(s => s.remove())
addEventListener("load", () => {
document.querySelectorAll(
"iframe[src*='common--javascript/resize-iframe.html']"
).forEach(i => i.remove())
const resizeRaw = window.resizeIframe.createResizeIframe(
document.referrer,
// Extract last 2 segments of [[html]] pseudopath
location.href.replace(/^.*\/(.*\/.*\/)/, "/$1"),
100,
)
window.resize = val => {
resizeRaw(val + (elapsed ? 0.01 : 0))
}
window.resize(document.body.scrollHeight)
})
}
addEventListener("DOMContentLoaded", () => {
reimplementResize()
const now = Math.floor(Date.now() / 1000)
const target = getTargetTime(inputs.timestamp, now)
const alreadyEnded = processCountdown(target)
if (alreadyEnded) return
interval = setInterval(() => processCountdown(target), 1000)
document.querySelector(".timer-pause")?.addEventListener("click", () => {
paused = !paused
document.querySelector(".countdown")?.classList.toggle("countdown--paused", paused)
processCountdown(target)
})
})
</script>
[[/html]]
[[/div]]
[!-- --]