|
| 1 | +<script setup lang="ts"> |
| 2 | +import { useData } from 'vitepress' |
| 3 | +import { |
| 4 | + computed, |
| 5 | + onBeforeUnmount, |
| 6 | + onMounted, |
| 7 | + ref, |
| 8 | +} from 'vue' |
| 9 | +
|
| 10 | +const props = withDefaults(defineProps<{ |
| 11 | + locale?: string |
| 12 | + datetime: string | number | Date |
| 13 | + localeMatcher?: 'best fit' | 'lookup' |
| 14 | + weekday?: 'long' | 'short' | 'narrow' |
| 15 | + era?: 'long' | 'short' | 'narrow' |
| 16 | + year?: 'numeric' | '2-digit' |
| 17 | + month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow' |
| 18 | + day?: 'numeric' | '2-digit' |
| 19 | + hour?: 'numeric' | '2-digit' |
| 20 | + minute?: 'numeric' | '2-digit' |
| 21 | + second?: 'numeric' | '2-digit' |
| 22 | + timeZoneName?: 'short' | 'long' | 'shortOffset' | 'longOffset' | 'shortGeneric' | 'longGeneric' |
| 23 | + formatMatcher?: 'best fit' | 'basic' |
| 24 | + hour12?: boolean |
| 25 | + timeZone?: string |
| 26 | +
|
| 27 | + calendar?: string |
| 28 | + dayPeriod?: 'narrow' | 'short' | 'long' |
| 29 | + numberingSystem?: string |
| 30 | +
|
| 31 | + dateStyle?: 'full' | 'long' | 'medium' | 'short' |
| 32 | + timeStyle?: 'full' | 'long' | 'medium' | 'short' |
| 33 | + hourCycle?: 'h11' | 'h12' | 'h23' | 'h24' |
| 34 | + relative?: boolean |
| 35 | + title?: boolean | string |
| 36 | +}>(), { |
| 37 | + hour12: undefined, |
| 38 | +}) |
| 39 | +
|
| 40 | +// Get current VitePress locale |
| 41 | +const { lang } = useData() |
| 42 | +
|
| 43 | +const date = computed(() => { |
| 44 | + const rawDate = props.datetime |
| 45 | + if (!rawDate) |
| 46 | + return new Date() |
| 47 | + return new Date(rawDate) |
| 48 | +}) |
| 49 | +
|
| 50 | +const now = ref(new Date()) |
| 51 | +let interval: ReturnType<typeof setInterval> | null = null |
| 52 | +
|
| 53 | +onMounted(() => { |
| 54 | + if (props.relative) { |
| 55 | + now.value = new Date() |
| 56 | + interval = setInterval(() => { |
| 57 | + now.value = new Date() |
| 58 | + }, 1000) |
| 59 | + } |
| 60 | +}) |
| 61 | +
|
| 62 | +onBeforeUnmount(() => { |
| 63 | + if (interval) |
| 64 | + clearInterval(interval) |
| 65 | +}) |
| 66 | +
|
| 67 | +const formatter = computed(() => { |
| 68 | + const { locale: propsLocale, relative, ...rest } = props |
| 69 | + // Use VitePress locale first, then fallback to prop locale, then browser/SSR default |
| 70 | + const locale = propsLocale || lang.value || (import.meta.env.SSR ? 'zh-CN' : navigator.language) |
| 71 | + if (relative) { |
| 72 | + return new Intl.RelativeTimeFormat(locale, rest) |
| 73 | + } |
| 74 | + return new Intl.DateTimeFormat(locale, rest) |
| 75 | +}) |
| 76 | +
|
| 77 | +const formattedDate = computed(() => { |
| 78 | + if (props.relative) { |
| 79 | + const diffInSeconds = (date.value.getTime() - now.value.getTime()) / 1000 |
| 80 | + const units: Array<{ unit: Intl.RelativeTimeFormatUnit, value: number }> = [ |
| 81 | + { unit: 'second', value: diffInSeconds }, |
| 82 | + { unit: 'minute', value: diffInSeconds / 60 }, |
| 83 | + { unit: 'hour', value: diffInSeconds / 3600 }, |
| 84 | + { unit: 'day', value: diffInSeconds / 86400 }, |
| 85 | + { unit: 'month', value: diffInSeconds / 2592000 }, |
| 86 | + { unit: 'year', value: diffInSeconds / 31536000 }, |
| 87 | + ] |
| 88 | + const { unit, value } = units.find(({ value }) => Math.abs(value) < 60) || units[units.length - 1]! |
| 89 | + return formatter.value.format(Math.round(value), unit) |
| 90 | + } |
| 91 | +
|
| 92 | + return (formatter.value as Intl.DateTimeFormat).format(date.value) |
| 93 | +}) |
| 94 | +
|
| 95 | +const isoDate = computed(() => date.value.toISOString()) |
| 96 | +
|
| 97 | +const title = computed(() => { |
| 98 | + if (props.title === true) |
| 99 | + return isoDate.value |
| 100 | + if (typeof props.title === 'string') |
| 101 | + return props.title |
| 102 | + return undefined |
| 103 | +}) |
| 104 | +
|
| 105 | +const dataset: Record<string, string | number | boolean | Date | undefined> = {} |
| 106 | +for (const prop in props) { |
| 107 | + if (prop !== 'datetime') { |
| 108 | + const value = props[prop as keyof typeof props] |
| 109 | + if (value != null) { |
| 110 | + const kebab = prop.replace(/([A-Z])/g, '-$1').toLowerCase() |
| 111 | + dataset[`data-${kebab}`] = value |
| 112 | + } |
| 113 | + } |
| 114 | +} |
| 115 | +</script> |
| 116 | + |
| 117 | +<template> |
| 118 | + <time |
| 119 | + v-bind="dataset" |
| 120 | + :datetime="isoDate" |
| 121 | + :title="title" |
| 122 | + >{{ formattedDate }}</time> |
| 123 | +</template> |
0 commit comments