clarity-theme 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -20
- package/README.md +70 -4
- package/README.zh-CN.md +67 -4
- package/nuxt.config.ts +217 -214
- package/package.json +35 -37
- package/{app → src}/app.config.ts +110 -110
- package/{app → src}/app.vue +5 -5
- package/{app → src}/components/blog/BlogAside.vue +25 -25
- package/{app → src}/components/blog/BlogFooter.vue +25 -25
- package/{app → src}/components/blog/BlogHeader.global.vue +48 -48
- package/{app → src}/components/blog/BlogPanel.vue +43 -43
- package/{app → src}/components/blog/BlogSidebar.vue +53 -53
- package/{app → src}/components/blog/BlogWidget.vue +42 -42
- package/{app → src}/components/blog/Mask.vue +25 -25
- package/{app → src}/components/blog/SkipToContent.vue +9 -9
- package/{app → src}/components/blog/ThemeToggle.vue +22 -22
- package/{app → src}/components/content/Badge.vue +45 -45
- package/{app → src}/components/content/Blur.vue +14 -14
- package/{app → src}/components/content/CardList.vue +8 -8
- package/{app → src}/components/content/Chat.vue +41 -41
- package/{app → src}/components/content/Copy.vue +71 -71
- package/{app → src}/components/content/EmojiClock.vue +49 -49
- package/{app → src}/components/content/FeedCard.vue +96 -96
- package/{app → src}/components/content/FeedGroup.vue +61 -61
- package/{app → src}/components/content/Folding.vue +19 -19
- package/{app → src}/components/content/Key.vue +143 -143
- package/{app → src}/components/content/LinkBanner.vue +27 -27
- package/{app → src}/components/content/LinkCard.vue +28 -28
- package/{app → src}/components/content/MdTitle.vue +8 -8
- package/{app → src}/components/content/Mermaid.vue +99 -99
- package/{app → src}/components/content/MusicScore.vue +63 -63
- package/{app → src}/components/content/Pic.vue +49 -49
- package/{app → src}/components/content/Poetry.vue +27 -27
- package/{app → src}/components/content/ProseA.vue +24 -24
- package/{app → src}/components/content/ProseCode.vue +16 -16
- package/{app → src}/components/content/ProsePre.vue +117 -117
- package/{app → src}/components/content/ProseTable.vue +23 -23
- package/{app → src}/components/content/Quote.vue +21 -21
- package/{app → src}/components/content/Tab.vue +32 -32
- package/{app → src}/components/content/Timeline.vue +33 -33
- package/{app → src}/components/content/Tip.vue +40 -40
- package/{app → src}/components/content/VideoEmbed.vue +112 -112
- package/{app → src}/components/partial/Button.vue +36 -36
- package/{app → src}/components/partial/DlGroup.vue +31 -31
- package/{app → src}/components/partial/Dropdown.vue +21 -21
- package/{app → src}/components/partial/Error.vue +39 -39
- package/{app → src}/components/partial/Expand.vue +26 -26
- package/{app → src}/components/partial/IconNavList.vue +24 -24
- package/{app → src}/components/partial/Pagination.vue +62 -62
- package/{app → src}/components/partial/RadioGroup.vue +30 -30
- package/{app → src}/components/partial/Secret.vue +11 -11
- package/{app → src}/components/partial/Slider.vue +48 -48
- package/{app → src}/components/partial/Toggle.vue +17 -17
- package/{app → src}/components/popover/Lightbox.vue +44 -44
- package/{app → src}/components/popover/Search.vue +143 -143
- package/{app → src}/components/popover/SearchItem.vue +28 -28
- package/{app → src}/components/post/Archive.vue +40 -40
- package/{app → src}/components/post/Article.vue +49 -49
- package/{app → src}/components/post/Comment.vue +119 -119
- package/{app → src}/components/post/Excerpt.vue +40 -40
- package/{app → src}/components/post/OrderToggle.vue +70 -70
- package/{app → src}/components/post/PostFooter.vue +52 -52
- package/{app → src}/components/post/PostHeader.vue +66 -66
- package/{app → src}/components/post/PostSurround.vue +51 -51
- package/{app → src}/components/post/Slide.vue +85 -85
- package/{app → src}/components/util/Date.vue +62 -62
- package/{app → src}/components/util/HydrateSafe.vue +10 -10
- package/{app → src}/components/util/Img.vue +38 -38
- package/{app → src}/components/util/Link.vue +11 -11
- package/{app → src}/components/widget/BlogLog.vue +23 -23
- package/{app → src}/components/widget/BlogStats.vue +38 -38
- package/{app → src}/components/widget/BlogTech.vue +57 -57
- package/{app → src}/components/widget/Empty.vue +3 -3
- package/{app → src}/components/widget/Toc.vue +67 -67
- package/{app → src}/composables/useArticle.ts +115 -115
- package/{app → src}/composables/useAvoid.ts +87 -87
- package/{app → src}/composables/useCopy.ts +21 -21
- package/{app → src}/composables/usePagination.ts +70 -70
- package/{app → src}/composables/useShiki.ts +129 -129
- package/{app → src}/composables/useToc.ts +57 -57
- package/{app → src}/composables/useWidgets.ts +56 -56
- package/{config → src/config}/app.ts +62 -62
- package/{config → src/config}/content.ts +75 -75
- package/{config → src/config}/define.ts +3 -3
- package/src/config/feeds.empty.ts +6 -0
- package/{config → src/config}/index.ts +1 -1
- package/{config → src/config}/schema.ts +89 -89
- package/{app → src}/error.vue +31 -31
- package/{img → src/img}/index.ts +2 -2
- package/{app → src}/layouts/default.vue +25 -25
- package/{modules → src/modules}/clarity-config/index.ts +168 -162
- package/src/modules/clarity-source-layout/index.ts +78 -0
- package/{app → src}/pages/[...slug].vue +59 -59
- package/{app → src}/pages/archive.vue +129 -129
- package/{app → src}/pages/index.vue +76 -76
- package/{app → src}/pages/link.vue +62 -62
- package/{app → src}/pages/preview.vue +46 -46
- package/{app → src}/plugins/easter-egg.ts +23 -23
- package/{app → src}/plugins/init.ts +6 -6
- package/{app → src}/plugins/tippy.ts +22 -22
- package/{public → src/public}/assets/atom.css +274 -274
- package/{public → src/public}/assets/atom.xsl +109 -109
- package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.ts +58 -58
- package/{remark-plugins → src/remark-plugins}/remark-code-component.ts +41 -41
- package/{server → src/server}/api/stats.get.ts +84 -83
- package/{server → src/server}/routes/atom.xml.get.ts +85 -85
- package/{server → src/server}/routes/subscriptions.opml.get.ts +64 -64
- package/{shared → src/shared}/utils/clarity.ts +24 -24
- package/{shared → src/shared}/utils/icon.ts +218 -218
- package/{shared → src/shared}/utils/link.ts +52 -52
- package/{shared → src/shared}/utils/str.ts +88 -88
- package/{shared → src/shared}/utils/time.ts +115 -115
- package/{app → src}/shiki.config.ts +8 -8
- package/{app → src}/stores/layout.ts +34 -34
- package/{app → src}/stores/search.ts +37 -37
- package/{app → src}/types/article.ts +16 -16
- package/src/types/feed.ts +1 -0
- package/{app → src}/types/index.ts +14 -14
- package/{app → src}/types/nav.ts +11 -11
- package/{app → src}/utils/anim.ts +37 -37
- package/{app → src}/utils/img.ts +83 -83
- package/app/types/feed.ts +0 -1
- package/config/feeds.empty.ts +0 -4
- package/{app → src}/assets/css/_variable.scss +0 -0
- package/{app → src}/assets/css/animation.scss +0 -0
- package/{app → src}/assets/css/article.scss +0 -0
- package/{app → src}/assets/css/color.scss +0 -0
- package/{app → src}/assets/css/font.scss +0 -0
- package/{app → src}/assets/css/main.scss +0 -0
- package/{app → src}/assets/css/reusable.scss +0 -0
- package/{app → src}/assets/icons/solar-rewind-back-bold-duotone.svg +0 -0
- package/{app → src}/assets/icons/solar-rewind-forward-bold-duotone.svg +0 -0
- package/{app → src}/components/content/Alert.vue +0 -0
- package/{config → src/config}/content.d.mts +0 -0
- package/{config → src/config}/content.mjs +0 -0
- package/{config → src/config}/define.mjs +0 -0
- package/{config → src/config}/feed.ts +0 -0
- package/{config → src/config}/index.d.mts +0 -0
- package/{config → src/config}/index.mjs +0 -0
- package/{config → src/config}/public.ts +1 -1
- package/{config → src/config}/schema.d.mts +0 -0
- package/{config → src/config}/schema.mjs +0 -0
- package/{config → src/config}/server.ts +1 -1
- /package/{img → src/img}/index.d.mts +0 -0
- /package/{img → src/img}/index.mjs +0 -0
- /package/{modules → src/modules}/clarity-config/anti-mirror-client.ts +0 -0
- /package/{public → src/public}/fonts/AlimamaFangYuanTi.woff2 +0 -0
- /package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.d.mts +0 -0
- /package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.mjs +0 -0
- /package/{remark-plugins → src/remark-plugins}/remark-code-component.d.mts +0 -0
- /package/{remark-plugins → src/remark-plugins}/remark-code-component.mjs +0 -0
- /package/{server → src/server}/utils/clarity.ts +0 -0
- /package/{shared → src/shared}/types/content.ts +0 -0
- /package/{app → src}/types/modal.ts +0 -0
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
import { toArray } from '@vueuse/core'
|
|
2
|
-
import { escape, escapeRegExp } from 'es-toolkit/string'
|
|
3
|
-
|
|
4
|
-
// @keep-sorted
|
|
5
|
-
const promptLanguageMap: Record<string, string> = {
|
|
6
|
-
'#': 'sh',
|
|
7
|
-
'$': 'sh',
|
|
8
|
-
'CMD': 'bat',
|
|
9
|
-
'PS': 'powershell',
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function formatNumber(num?: number) {
|
|
13
|
-
if (typeof num !== 'number')
|
|
14
|
-
return ''
|
|
15
|
-
const intervals = [
|
|
16
|
-
{ label: '万亿', threshold: 1e12 },
|
|
17
|
-
{ label: '亿', threshold: 1e8 },
|
|
18
|
-
{ label: '万', threshold: 1e4 },
|
|
19
|
-
]
|
|
20
|
-
for (const interval of intervals) {
|
|
21
|
-
if (num >= interval.threshold)
|
|
22
|
-
return `${(num / interval.threshold).toFixed(2)}${interval.label}`
|
|
23
|
-
}
|
|
24
|
-
return num.toString()
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface FormatBytesOptions {
|
|
28
|
-
decimals?: number
|
|
29
|
-
binary?: boolean
|
|
30
|
-
unitSeparator?: string
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function formatBytes(bytes: number, options: FormatBytesOptions = {}) {
|
|
34
|
-
const {
|
|
35
|
-
decimals = 2,
|
|
36
|
-
binary = true,
|
|
37
|
-
unitSeparator = ' ',
|
|
38
|
-
} = options
|
|
39
|
-
|
|
40
|
-
if (bytes === 0)
|
|
41
|
-
return `0${unitSeparator}Bytes`
|
|
42
|
-
|
|
43
|
-
const base = binary ? 1024 : 1000
|
|
44
|
-
const units = binary
|
|
45
|
-
? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
|
46
|
-
: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
|
47
|
-
|
|
48
|
-
const i = Math.floor(Math.log(bytes) / Math.log(base))
|
|
49
|
-
const value = Number.parseFloat((bytes / base ** i).toFixed(decimals))
|
|
50
|
-
|
|
51
|
-
return `${value}${unitSeparator}${units[i]}`
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function getPromptLanguage(prompt: string | boolean) {
|
|
55
|
-
if (typeof prompt === 'boolean')
|
|
56
|
-
return 'text'
|
|
57
|
-
for (const promptPrefix in promptLanguageMap) {
|
|
58
|
-
if (prompt.startsWith(promptPrefix))
|
|
59
|
-
return promptLanguageMap[promptPrefix] ?? 'text'
|
|
60
|
-
}
|
|
61
|
-
return 'text'
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function joinWith(strings: (string | undefined)[], separator = '\n') {
|
|
65
|
-
return strings.filter(Boolean).join(separator)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function highlightHtml(text: string, words: string | string[] | undefined, className?: string) {
|
|
69
|
-
const validTerms = toArray(words)
|
|
70
|
-
.filter((t): t is string => !!t?.trim())
|
|
71
|
-
.map(t => t.toLowerCase())
|
|
72
|
-
|
|
73
|
-
const highlightRegex = new RegExp(`(${Array.from(validTerms, escapeRegExp).join('|')})`, 'gi')
|
|
74
|
-
|
|
75
|
-
return text
|
|
76
|
-
.split(highlightRegex)
|
|
77
|
-
.map(part => part && validTerms.includes(part.toLowerCase())
|
|
78
|
-
? `<mark${className ? ` class="${className}"` : ''}>${escape(part)}</mark>`
|
|
79
|
-
: escape(part))
|
|
80
|
-
.join('')
|
|
81
|
-
.replace(/\n+/g, '<br>')
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function removeHtmlTags(str?: string) {
|
|
85
|
-
if (typeof str !== 'string')
|
|
86
|
-
return ''
|
|
87
|
-
return str.replace(/<[^>]+(>|$)/g, '')
|
|
88
|
-
}
|
|
1
|
+
import { toArray } from '@vueuse/core'
|
|
2
|
+
import { escape, escapeRegExp } from 'es-toolkit/string'
|
|
3
|
+
|
|
4
|
+
// @keep-sorted
|
|
5
|
+
const promptLanguageMap: Record<string, string> = {
|
|
6
|
+
'#': 'sh',
|
|
7
|
+
'$': 'sh',
|
|
8
|
+
'CMD': 'bat',
|
|
9
|
+
'PS': 'powershell',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatNumber(num?: number) {
|
|
13
|
+
if (typeof num !== 'number')
|
|
14
|
+
return ''
|
|
15
|
+
const intervals = [
|
|
16
|
+
{ label: '万亿', threshold: 1e12 },
|
|
17
|
+
{ label: '亿', threshold: 1e8 },
|
|
18
|
+
{ label: '万', threshold: 1e4 },
|
|
19
|
+
]
|
|
20
|
+
for (const interval of intervals) {
|
|
21
|
+
if (num >= interval.threshold)
|
|
22
|
+
return `${(num / interval.threshold).toFixed(2)}${interval.label}`
|
|
23
|
+
}
|
|
24
|
+
return num.toString()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface FormatBytesOptions {
|
|
28
|
+
decimals?: number
|
|
29
|
+
binary?: boolean
|
|
30
|
+
unitSeparator?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatBytes(bytes: number, options: FormatBytesOptions = {}) {
|
|
34
|
+
const {
|
|
35
|
+
decimals = 2,
|
|
36
|
+
binary = true,
|
|
37
|
+
unitSeparator = ' ',
|
|
38
|
+
} = options
|
|
39
|
+
|
|
40
|
+
if (bytes === 0)
|
|
41
|
+
return `0${unitSeparator}Bytes`
|
|
42
|
+
|
|
43
|
+
const base = binary ? 1024 : 1000
|
|
44
|
+
const units = binary
|
|
45
|
+
? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
|
46
|
+
: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
|
47
|
+
|
|
48
|
+
const i = Math.floor(Math.log(bytes) / Math.log(base))
|
|
49
|
+
const value = Number.parseFloat((bytes / base ** i).toFixed(decimals))
|
|
50
|
+
|
|
51
|
+
return `${value}${unitSeparator}${units[i]}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getPromptLanguage(prompt: string | boolean) {
|
|
55
|
+
if (typeof prompt === 'boolean')
|
|
56
|
+
return 'text'
|
|
57
|
+
for (const promptPrefix in promptLanguageMap) {
|
|
58
|
+
if (prompt.startsWith(promptPrefix))
|
|
59
|
+
return promptLanguageMap[promptPrefix] ?? 'text'
|
|
60
|
+
}
|
|
61
|
+
return 'text'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function joinWith(strings: (string | undefined)[], separator = '\n') {
|
|
65
|
+
return strings.filter(Boolean).join(separator)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function highlightHtml(text: string, words: string | string[] | undefined, className?: string) {
|
|
69
|
+
const validTerms = toArray(words)
|
|
70
|
+
.filter((t): t is string => !!t?.trim())
|
|
71
|
+
.map(t => t.toLowerCase())
|
|
72
|
+
|
|
73
|
+
const highlightRegex = new RegExp(`(${Array.from(validTerms, escapeRegExp).join('|')})`, 'gi')
|
|
74
|
+
|
|
75
|
+
return text
|
|
76
|
+
.split(highlightRegex)
|
|
77
|
+
.map(part => part && validTerms.includes(part.toLowerCase())
|
|
78
|
+
? `<mark${className ? ` class="${className}"` : ''}>${escape(part)}</mark>`
|
|
79
|
+
: escape(part))
|
|
80
|
+
.join('')
|
|
81
|
+
.replace(/\n+/g, '<br>')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function removeHtmlTags(str?: string) {
|
|
85
|
+
if (typeof str !== 'string')
|
|
86
|
+
return ''
|
|
87
|
+
return str.replace(/<[^>]+(>|$)/g, '')
|
|
88
|
+
}
|
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
import { Temporal } from 'temporal-polyfill'
|
|
2
|
-
|
|
1
|
+
import { Temporal } from 'temporal-polyfill'
|
|
2
|
+
|
|
3
3
|
export function isSameUnit(date1: string, date2: string, unit: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second', timeZone = 'UTC') {
|
|
4
|
-
try {
|
|
5
|
-
const p1 = toZonedTemporal(date1, timeZone).toPlainDateTime()
|
|
6
|
-
const p2 = toZonedTemporal(date2, timeZone).toPlainDateTime()
|
|
7
|
-
return p1.until(p2, {
|
|
8
|
-
largestUnit: unit,
|
|
9
|
-
smallestUnit: unit,
|
|
10
|
-
roundingMode: 'trunc',
|
|
11
|
-
}).blank
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return false
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** 检查两个时间相对现在是否相差显著 */
|
|
19
|
-
export function isTimeDiffSignificant(
|
|
20
|
-
date1?: string,
|
|
21
|
-
date2?: string,
|
|
22
|
-
timeZone = 'UTC',
|
|
23
|
-
/** 对于时间差的敏感程度,0~1 之间,1:不同则认为显著,>1:始终认为显著 */
|
|
24
|
-
threshold = 0.6,
|
|
25
|
-
) {
|
|
26
|
-
if (!date1 || !date2 || threshold <= 0)
|
|
27
|
-
return false
|
|
28
|
-
if (threshold > 1)
|
|
29
|
-
return true
|
|
30
|
-
try {
|
|
31
|
-
const now = Temporal.Now.instant().epochMilliseconds
|
|
32
|
-
const diff1 = now - toZonedTemporal(date1, timeZone).epochMilliseconds
|
|
33
|
-
const diff2 = now - toZonedTemporal(date2, timeZone).epochMilliseconds
|
|
34
|
-
return diff1 / diff2 < threshold || diff2 / diff1 < threshold
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return true
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const timeIntervals = [
|
|
42
|
-
{ label: '世纪', threshold: 60 * 60 * 24 * 365.2422 * 100 },
|
|
43
|
-
{ label: '年', threshold: 60 * 60 * 24 * 365.2422 },
|
|
44
|
-
{ label: '个月', threshold: 60 * 60 * 24 * 30.44 },
|
|
45
|
-
{ label: '天', threshold: 60 * 60 * 24 },
|
|
46
|
-
{ label: '小时', threshold: 60 * 60 },
|
|
47
|
-
{ label: '分', threshold: 60 },
|
|
48
|
-
{ label: '秒', threshold: 1 },
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
export function timeElapse(date: string | Temporal.PlainDateTime, maxDepth = 2) {
|
|
52
|
-
let timeString = ''
|
|
53
|
-
let secRemained = Temporal.Now.plainDateTimeISO().since(date, { largestUnit: 'second' }).seconds
|
|
54
|
-
for (const interval of timeIntervals) {
|
|
55
|
-
const count = Math.floor(secRemained / interval.threshold)
|
|
56
|
-
if (count <= 0)
|
|
57
|
-
continue
|
|
58
|
-
timeString += `${count}${interval.label}`
|
|
59
|
-
secRemained -= count * interval.threshold
|
|
60
|
-
if (--maxDepth <= 0)
|
|
61
|
-
break
|
|
62
|
-
}
|
|
63
|
-
return timeString || '刚刚'
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function toInstantString(date: string | Temporal.ZonedDateTime, timeZone = 'UTC') {
|
|
67
|
-
return (typeof date === 'string' ? toZonedTemporal(date, timeZone) : date).toInstant().toString()
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 将日期字符串解析为带时区的 Temporal 对象
|
|
72
|
-
* @param date 日期字符串
|
|
73
|
-
* @param timeZone 无时区信息时使用的默认时区(来自站点配置)
|
|
74
|
-
*/
|
|
75
|
-
export function toZonedTemporal(date: string, timeZone = 'UTC') {
|
|
76
|
-
try {
|
|
77
|
-
return Temporal.ZonedDateTime.from(date)
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
try {
|
|
81
|
-
return Temporal.Instant.from(date).toZonedDateTimeISO(timeZone)
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
return Temporal.PlainDateTime.from(date).toZonedDateTime(timeZone)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export const dateTimeFormat = {
|
|
90
|
-
date: {
|
|
91
|
-
year: 'numeric',
|
|
92
|
-
month: '2-digit',
|
|
93
|
-
day: '2-digit',
|
|
94
|
-
},
|
|
95
|
-
monthDay: {
|
|
96
|
-
month: '2-digit',
|
|
97
|
-
day: '2-digit',
|
|
98
|
-
},
|
|
99
|
-
full: {
|
|
100
|
-
year: 'numeric',
|
|
101
|
-
month: '2-digit',
|
|
102
|
-
day: '2-digit',
|
|
103
|
-
weekday: 'long',
|
|
104
|
-
hour: '2-digit',
|
|
105
|
-
minute: '2-digit',
|
|
106
|
-
second: '2-digit',
|
|
107
|
-
timeZoneName: 'long',
|
|
108
|
-
},
|
|
109
|
-
} satisfies Record<string, Intl.DateTimeFormatOptions>
|
|
110
|
-
|
|
111
|
-
export type dateTimeFormatOptions = keyof typeof dateTimeFormat | Intl.DateTimeFormatOptions
|
|
112
|
-
|
|
113
|
-
export function toZdtLocaleString(date: string | Temporal.ZonedDateTime, format: dateTimeFormatOptions = 'full', timeZone = 'UTC') {
|
|
114
|
-
return (typeof date === 'string' ? toZonedTemporal(date, timeZone) : date)
|
|
115
|
-
.toLocaleString(undefined, typeof format === 'string' ? dateTimeFormat[format] : format)
|
|
116
|
-
}
|
|
4
|
+
try {
|
|
5
|
+
const p1 = toZonedTemporal(date1, timeZone).toPlainDateTime()
|
|
6
|
+
const p2 = toZonedTemporal(date2, timeZone).toPlainDateTime()
|
|
7
|
+
return p1.until(p2, {
|
|
8
|
+
largestUnit: unit,
|
|
9
|
+
smallestUnit: unit,
|
|
10
|
+
roundingMode: 'trunc',
|
|
11
|
+
}).blank
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 检查两个时间相对现在是否相差显著 */
|
|
19
|
+
export function isTimeDiffSignificant(
|
|
20
|
+
date1?: string,
|
|
21
|
+
date2?: string,
|
|
22
|
+
timeZone = 'UTC',
|
|
23
|
+
/** 对于时间差的敏感程度,0~1 之间,1:不同则认为显著,>1:始终认为显著 */
|
|
24
|
+
threshold = 0.6,
|
|
25
|
+
) {
|
|
26
|
+
if (!date1 || !date2 || threshold <= 0)
|
|
27
|
+
return false
|
|
28
|
+
if (threshold > 1)
|
|
29
|
+
return true
|
|
30
|
+
try {
|
|
31
|
+
const now = Temporal.Now.instant().epochMilliseconds
|
|
32
|
+
const diff1 = now - toZonedTemporal(date1, timeZone).epochMilliseconds
|
|
33
|
+
const diff2 = now - toZonedTemporal(date2, timeZone).epochMilliseconds
|
|
34
|
+
return diff1 / diff2 < threshold || diff2 / diff1 < threshold
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const timeIntervals = [
|
|
42
|
+
{ label: '世纪', threshold: 60 * 60 * 24 * 365.2422 * 100 },
|
|
43
|
+
{ label: '年', threshold: 60 * 60 * 24 * 365.2422 },
|
|
44
|
+
{ label: '个月', threshold: 60 * 60 * 24 * 30.44 },
|
|
45
|
+
{ label: '天', threshold: 60 * 60 * 24 },
|
|
46
|
+
{ label: '小时', threshold: 60 * 60 },
|
|
47
|
+
{ label: '分', threshold: 60 },
|
|
48
|
+
{ label: '秒', threshold: 1 },
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
export function timeElapse(date: string | Temporal.PlainDateTime, maxDepth = 2) {
|
|
52
|
+
let timeString = ''
|
|
53
|
+
let secRemained = Temporal.Now.plainDateTimeISO().since(date, { largestUnit: 'second' }).seconds
|
|
54
|
+
for (const interval of timeIntervals) {
|
|
55
|
+
const count = Math.floor(secRemained / interval.threshold)
|
|
56
|
+
if (count <= 0)
|
|
57
|
+
continue
|
|
58
|
+
timeString += `${count}${interval.label}`
|
|
59
|
+
secRemained -= count * interval.threshold
|
|
60
|
+
if (--maxDepth <= 0)
|
|
61
|
+
break
|
|
62
|
+
}
|
|
63
|
+
return timeString || '刚刚'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function toInstantString(date: string | Temporal.ZonedDateTime, timeZone = 'UTC') {
|
|
67
|
+
return (typeof date === 'string' ? toZonedTemporal(date, timeZone) : date).toInstant().toString()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 将日期字符串解析为带时区的 Temporal 对象
|
|
72
|
+
* @param date 日期字符串
|
|
73
|
+
* @param timeZone 无时区信息时使用的默认时区(来自站点配置)
|
|
74
|
+
*/
|
|
75
|
+
export function toZonedTemporal(date: string, timeZone = 'UTC') {
|
|
76
|
+
try {
|
|
77
|
+
return Temporal.ZonedDateTime.from(date)
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
try {
|
|
81
|
+
return Temporal.Instant.from(date).toZonedDateTimeISO(timeZone)
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return Temporal.PlainDateTime.from(date).toZonedDateTime(timeZone)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const dateTimeFormat = {
|
|
90
|
+
date: {
|
|
91
|
+
year: 'numeric',
|
|
92
|
+
month: '2-digit',
|
|
93
|
+
day: '2-digit',
|
|
94
|
+
},
|
|
95
|
+
monthDay: {
|
|
96
|
+
month: '2-digit',
|
|
97
|
+
day: '2-digit',
|
|
98
|
+
},
|
|
99
|
+
full: {
|
|
100
|
+
year: 'numeric',
|
|
101
|
+
month: '2-digit',
|
|
102
|
+
day: '2-digit',
|
|
103
|
+
weekday: 'long',
|
|
104
|
+
hour: '2-digit',
|
|
105
|
+
minute: '2-digit',
|
|
106
|
+
second: '2-digit',
|
|
107
|
+
timeZoneName: 'long',
|
|
108
|
+
},
|
|
109
|
+
} satisfies Record<string, Intl.DateTimeFormatOptions>
|
|
110
|
+
|
|
111
|
+
export type dateTimeFormatOptions = keyof typeof dateTimeFormat | Intl.DateTimeFormatOptions
|
|
112
|
+
|
|
113
|
+
export function toZdtLocaleString(date: string | Temporal.ZonedDateTime, format: dateTimeFormatOptions = 'full', timeZone = 'UTC') {
|
|
114
|
+
return (typeof date === 'string' ? toZonedTemporal(date, timeZone) : date)
|
|
115
|
+
.toLocaleString(undefined, typeof format === 'string' ? dateTimeFormat[format] : format)
|
|
116
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { defineConfig } from '#shiki/config'
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
themes: {
|
|
5
|
-
light: () => import('shiki/themes/catppuccin-latte.mjs'),
|
|
6
|
-
dark: () => import('shiki/themes/one-dark-pro.mjs'),
|
|
7
|
-
},
|
|
8
|
-
})
|
|
1
|
+
import { defineConfig } from '#shiki/config'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
themes: {
|
|
5
|
+
light: () => import('shiki/themes/catppuccin-latte.mjs'),
|
|
6
|
+
dark: () => import('shiki/themes/one-dark-pro.mjs'),
|
|
7
|
+
},
|
|
8
|
+
})
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
export type LayoutState = 'none' | 'sidebar' | 'aside' | 'search' | 'lightbox'
|
|
2
|
-
|
|
3
|
-
export const useLayoutStore = defineStore('layout', () => {
|
|
4
|
-
const router = useRouter()
|
|
5
|
-
|
|
6
|
-
const state = ref<LayoutState>('none')
|
|
7
|
-
const avoidTargets = ref<AvoidTarget[]>([])
|
|
8
|
-
|
|
9
|
-
const close = () => state.value = 'none'
|
|
10
|
-
|
|
11
|
-
const toggle = (key: LayoutState) => {
|
|
12
|
-
if (state.value === key)
|
|
13
|
-
return close()
|
|
14
|
-
state.value = key
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
useEventListener('keydown', (e) => {
|
|
18
|
-
if (state.value !== 'none' && e.key === 'Escape') {
|
|
19
|
-
e.preventDefault()
|
|
20
|
-
close()
|
|
21
|
-
}
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
router.beforeEach(() => {
|
|
25
|
-
close()
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
state,
|
|
30
|
-
avoidTargets,
|
|
31
|
-
close,
|
|
32
|
-
toggle,
|
|
33
|
-
}
|
|
34
|
-
})
|
|
1
|
+
export type LayoutState = 'none' | 'sidebar' | 'aside' | 'search' | 'lightbox'
|
|
2
|
+
|
|
3
|
+
export const useLayoutStore = defineStore('layout', () => {
|
|
4
|
+
const router = useRouter()
|
|
5
|
+
|
|
6
|
+
const state = ref<LayoutState>('none')
|
|
7
|
+
const avoidTargets = ref<AvoidTarget[]>([])
|
|
8
|
+
|
|
9
|
+
const close = () => state.value = 'none'
|
|
10
|
+
|
|
11
|
+
const toggle = (key: LayoutState) => {
|
|
12
|
+
if (state.value === key)
|
|
13
|
+
return close()
|
|
14
|
+
state.value = key
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
useEventListener('keydown', (e) => {
|
|
18
|
+
if (state.value !== 'none' && e.key === 'Escape') {
|
|
19
|
+
e.preventDefault()
|
|
20
|
+
close()
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
router.beforeEach(() => {
|
|
25
|
+
close()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
state,
|
|
30
|
+
avoidTargets,
|
|
31
|
+
close,
|
|
32
|
+
toggle,
|
|
33
|
+
}
|
|
34
|
+
})
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import { LazyPopoverSearch } from '#components'
|
|
2
|
-
|
|
3
|
-
export const useSearchStore = defineStore('search', () => {
|
|
4
|
-
// 搜索框应和侧边栏状态联动
|
|
5
|
-
const layoutStore = useLayoutStore()
|
|
6
|
-
const modalStore = useModalStore()
|
|
7
|
-
|
|
8
|
-
const word = ref('')
|
|
9
|
-
const debouncedWord = refDebounced(word)
|
|
10
|
-
|
|
11
|
-
const {
|
|
12
|
-
open: _open,
|
|
13
|
-
close: _close,
|
|
14
|
-
} = modalStore.use(() => h(LazyPopoverSearch, {
|
|
15
|
-
onClose: () => {
|
|
16
|
-
_close()
|
|
17
|
-
layoutStore.close()
|
|
18
|
-
},
|
|
19
|
-
}), {
|
|
20
|
-
unique: true,
|
|
21
|
-
duration: 200,
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
// 从外部调用时应该操作 layoutStore
|
|
25
|
-
watch(() => layoutStore.state, (state) => {
|
|
26
|
-
if (state !== 'search')
|
|
27
|
-
return _close()
|
|
28
|
-
|
|
29
|
-
word.value = window.getSelection()?.toString().trim() || word.value
|
|
30
|
-
_open()
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
word,
|
|
35
|
-
debouncedWord,
|
|
36
|
-
}
|
|
37
|
-
})
|
|
1
|
+
import { LazyPopoverSearch } from '#components'
|
|
2
|
+
|
|
3
|
+
export const useSearchStore = defineStore('search', () => {
|
|
4
|
+
// 搜索框应和侧边栏状态联动
|
|
5
|
+
const layoutStore = useLayoutStore()
|
|
6
|
+
const modalStore = useModalStore()
|
|
7
|
+
|
|
8
|
+
const word = ref('')
|
|
9
|
+
const debouncedWord = refDebounced(word)
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
open: _open,
|
|
13
|
+
close: _close,
|
|
14
|
+
} = modalStore.use(() => h(LazyPopoverSearch, {
|
|
15
|
+
onClose: () => {
|
|
16
|
+
_close()
|
|
17
|
+
layoutStore.close()
|
|
18
|
+
},
|
|
19
|
+
}), {
|
|
20
|
+
unique: true,
|
|
21
|
+
duration: 200,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// 从外部调用时应该操作 layoutStore
|
|
25
|
+
watch(() => layoutStore.state, (state) => {
|
|
26
|
+
if (state !== 'search')
|
|
27
|
+
return _close()
|
|
28
|
+
|
|
29
|
+
word.value = window.getSelection()?.toString().trim() || word.value
|
|
30
|
+
_open()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
word,
|
|
35
|
+
debouncedWord,
|
|
36
|
+
}
|
|
37
|
+
})
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import type { ClarityAppConfig } from '
|
|
2
|
-
import type { ArticleSchema } from '
|
|
3
|
-
import type { MetaSlotsTree } from '
|
|
4
|
-
|
|
5
|
-
export type ArticleOrderType = keyof ClarityAppConfig['article']['order']
|
|
6
|
-
|
|
7
|
-
export interface ArticleProps extends ArticleSchema {
|
|
8
|
-
path: string
|
|
9
|
-
|
|
10
|
-
meta?: {
|
|
11
|
-
coverDim?: boolean
|
|
12
|
-
coverFilter?: string
|
|
13
|
-
hideInfo?: boolean
|
|
14
|
-
slots?: Record<string, MetaSlotsTree>
|
|
15
|
-
}
|
|
16
|
-
}
|
|
1
|
+
import type { ClarityAppConfig } from '../config/app'
|
|
2
|
+
import type { ArticleSchema } from '../config/content'
|
|
3
|
+
import type { MetaSlotsTree } from '../remark-plugins/rehype-meta-slots'
|
|
4
|
+
|
|
5
|
+
export type ArticleOrderType = keyof ClarityAppConfig['article']['order']
|
|
6
|
+
|
|
7
|
+
export interface ArticleProps extends ArticleSchema {
|
|
8
|
+
path: string
|
|
9
|
+
|
|
10
|
+
meta?: {
|
|
11
|
+
coverDim?: boolean
|
|
12
|
+
coverFilter?: string
|
|
13
|
+
hideInfo?: boolean
|
|
14
|
+
slots?: Record<string, MetaSlotsTree>
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { FeedEntry, FeedGroup } from '../config/feed'
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
declare global {
|
|
2
|
-
interface Window {
|
|
3
|
-
twikoo?: {
|
|
4
|
-
init: (options: {
|
|
5
|
-
envId: string
|
|
6
|
-
el: string
|
|
7
|
-
region?: string
|
|
8
|
-
path?: string
|
|
9
|
-
lang?: string
|
|
10
|
-
}) => void
|
|
11
|
-
version: string
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
}
|
|
1
|
+
declare global {
|
|
2
|
+
interface Window {
|
|
3
|
+
twikoo?: {
|
|
4
|
+
init: (options: {
|
|
5
|
+
envId: string
|
|
6
|
+
el: string
|
|
7
|
+
region?: string
|
|
8
|
+
path?: string
|
|
9
|
+
lang?: string
|
|
10
|
+
}) => void
|
|
11
|
+
version: string
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
package/{app → src}/types/nav.ts
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export interface NavItem {
|
|
2
|
-
icon: string
|
|
3
|
-
text: string
|
|
4
|
-
url: string
|
|
5
|
-
external?: boolean
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export type Nav = {
|
|
9
|
-
title: string
|
|
10
|
-
items: NavItem[]
|
|
11
|
-
}[]
|
|
1
|
+
export interface NavItem {
|
|
2
|
+
icon: string
|
|
3
|
+
text: string
|
|
4
|
+
url: string
|
|
5
|
+
external?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type Nav = {
|
|
9
|
+
title: string
|
|
10
|
+
items: NavItem[]
|
|
11
|
+
}[]
|