clarity-theme 0.1.0 → 0.1.1
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 +18 -4
- package/README.zh-CN.md +18 -4
- package/app/app.config.ts +110 -110
- package/app/app.vue +5 -5
- package/app/components/blog/BlogAside.vue +25 -25
- package/app/components/blog/BlogFooter.vue +25 -25
- package/app/components/blog/BlogHeader.global.vue +48 -48
- package/app/components/blog/BlogPanel.vue +43 -43
- package/app/components/blog/BlogSidebar.vue +53 -53
- package/app/components/blog/BlogWidget.vue +42 -42
- package/app/components/blog/Mask.vue +25 -25
- package/app/components/blog/SkipToContent.vue +9 -9
- package/app/components/blog/ThemeToggle.vue +22 -22
- package/app/components/content/Badge.vue +45 -45
- package/app/components/content/Blur.vue +14 -14
- package/app/components/content/CardList.vue +8 -8
- package/app/components/content/Chat.vue +41 -41
- package/app/components/content/Copy.vue +71 -71
- package/app/components/content/EmojiClock.vue +49 -49
- package/app/components/content/FeedCard.vue +96 -96
- package/app/components/content/FeedGroup.vue +61 -61
- package/app/components/content/Folding.vue +19 -19
- package/app/components/content/Key.vue +143 -143
- package/app/components/content/LinkBanner.vue +27 -27
- package/app/components/content/LinkCard.vue +28 -28
- package/app/components/content/MdTitle.vue +8 -8
- package/app/components/content/Mermaid.vue +99 -99
- package/app/components/content/MusicScore.vue +63 -63
- package/app/components/content/Pic.vue +49 -49
- package/app/components/content/Poetry.vue +27 -27
- package/app/components/content/ProseA.vue +24 -24
- package/app/components/content/ProseCode.vue +16 -16
- package/app/components/content/ProsePre.vue +117 -117
- package/app/components/content/ProseTable.vue +23 -23
- package/app/components/content/Quote.vue +21 -21
- package/app/components/content/Tab.vue +32 -32
- package/app/components/content/Timeline.vue +33 -33
- package/app/components/content/Tip.vue +40 -40
- package/app/components/content/VideoEmbed.vue +112 -112
- package/app/components/partial/Button.vue +36 -36
- package/app/components/partial/DlGroup.vue +31 -31
- package/app/components/partial/Dropdown.vue +21 -21
- package/app/components/partial/Error.vue +39 -39
- package/app/components/partial/Expand.vue +26 -26
- package/app/components/partial/IconNavList.vue +24 -24
- package/app/components/partial/Pagination.vue +62 -62
- package/app/components/partial/RadioGroup.vue +30 -30
- package/app/components/partial/Secret.vue +11 -11
- package/app/components/partial/Slider.vue +48 -48
- package/app/components/partial/Toggle.vue +17 -17
- package/app/components/popover/Lightbox.vue +44 -44
- package/app/components/popover/Search.vue +143 -143
- package/app/components/popover/SearchItem.vue +28 -28
- package/app/components/post/Archive.vue +40 -40
- package/app/components/post/Article.vue +49 -49
- package/app/components/post/Comment.vue +119 -119
- package/app/components/post/Excerpt.vue +40 -40
- package/app/components/post/OrderToggle.vue +70 -70
- package/app/components/post/PostFooter.vue +52 -52
- package/app/components/post/PostHeader.vue +66 -66
- package/app/components/post/PostSurround.vue +51 -51
- package/app/components/post/Slide.vue +85 -85
- package/app/components/util/Date.vue +62 -62
- package/app/components/util/HydrateSafe.vue +10 -10
- package/app/components/util/Img.vue +38 -38
- package/app/components/util/Link.vue +11 -11
- package/app/components/widget/BlogLog.vue +23 -23
- package/app/components/widget/BlogStats.vue +38 -38
- package/app/components/widget/BlogTech.vue +57 -57
- package/app/components/widget/Empty.vue +3 -3
- package/app/components/widget/Toc.vue +67 -67
- package/app/composables/useArticle.ts +113 -113
- package/app/composables/useAvoid.ts +87 -87
- package/app/composables/useCopy.ts +21 -21
- package/app/composables/usePagination.ts +70 -70
- package/app/composables/useShiki.ts +129 -129
- package/app/composables/useToc.ts +57 -57
- package/app/composables/useWidgets.ts +56 -56
- package/app/error.vue +31 -31
- package/app/layouts/default.vue +25 -25
- package/app/pages/[...slug].vue +59 -59
- package/app/pages/archive.vue +129 -129
- package/app/pages/index.vue +76 -76
- package/app/pages/link.vue +62 -62
- package/app/pages/preview.vue +46 -46
- package/app/plugins/easter-egg.ts +23 -23
- package/app/plugins/init.ts +6 -6
- package/app/plugins/tippy.ts +22 -22
- package/app/shiki.config.ts +8 -8
- package/app/stores/layout.ts +34 -34
- package/app/stores/search.ts +37 -37
- package/app/types/article.ts +16 -16
- package/app/types/index.ts +14 -14
- package/app/types/nav.ts +11 -11
- package/app/utils/anim.ts +37 -37
- package/app/utils/img.ts +83 -83
- package/config/app.ts +61 -61
- package/config/content.ts +75 -75
- package/config/define.ts +3 -3
- package/config/feeds.empty.ts +6 -4
- package/config/public.ts +1 -1
- package/config/schema.ts +89 -89
- package/config/server.ts +1 -1
- package/modules/clarity-config/index.ts +162 -159
- package/nuxt.config.ts +212 -212
- package/package.json +25 -21
- package/public/assets/atom.css +274 -274
- package/public/assets/atom.xsl +109 -109
- package/remark-plugins/rehype-meta-slots.ts +58 -58
- package/remark-plugins/remark-code-component.ts +41 -41
- package/server/api/stats.get.ts +84 -83
- package/server/routes/atom.xml.get.ts +85 -85
- package/server/routes/subscriptions.opml.get.ts +64 -64
- package/shared/utils/clarity.ts +24 -24
- package/shared/utils/icon.ts +218 -218
- package/shared/utils/link.ts +52 -52
- package/shared/utils/str.ts +88 -88
- package/shared/utils/time.ts +115 -115
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
<script lang="tsx" setup>
|
|
2
|
-
const slots = defineSlots<{
|
|
3
|
-
default: () => VNode[]
|
|
4
|
-
}>()
|
|
5
|
-
|
|
6
|
-
const chatRegex = /^\{(?<control>\.|:)?(?<caption>.*)\}$/
|
|
7
|
-
|
|
8
|
-
function getControlClass(control?: string) {
|
|
9
|
-
if (control === '.')
|
|
10
|
-
return 'chat-myself'
|
|
11
|
-
if (control === ':')
|
|
12
|
-
return 'chat-system'
|
|
13
|
-
return ''
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function render() {
|
|
17
|
-
const slotContent = slots.default()
|
|
18
|
-
if (!slotContent)
|
|
19
|
-
return <span>无会话内容</span>
|
|
20
|
-
|
|
21
|
-
return slotContent.map((node: VNode) => {
|
|
22
|
-
// WARN: 此处使用了非标准的 v-slot:default
|
|
23
|
-
const textContent = (node.children as any)?.default?.()[0].children
|
|
24
|
-
const matchGroups = typeof textContent === 'string'
|
|
25
|
-
? textContent.match(chatRegex)?.groups
|
|
26
|
-
: undefined
|
|
27
|
-
return matchGroups
|
|
28
|
-
? <dt class={`chat-caption ${getControlClass(matchGroups.control)}`}>{matchGroups.caption}</dt>
|
|
29
|
-
: <dd class="chat-body">{node}</dd>
|
|
30
|
-
})
|
|
31
|
-
}
|
|
32
|
-
</script>
|
|
33
|
-
|
|
34
|
-
<template>
|
|
35
|
-
<dl class="chat">
|
|
36
|
-
<render />
|
|
37
|
-
</dl>
|
|
38
|
-
</template>
|
|
39
|
-
|
|
40
|
-
<style lang="scss" scoped>
|
|
1
|
+
<script lang="tsx" setup>
|
|
2
|
+
const slots = defineSlots<{
|
|
3
|
+
default: () => VNode[]
|
|
4
|
+
}>()
|
|
5
|
+
|
|
6
|
+
const chatRegex = /^\{(?<control>\.|:)?(?<caption>.*)\}$/
|
|
7
|
+
|
|
8
|
+
function getControlClass(control?: string) {
|
|
9
|
+
if (control === '.')
|
|
10
|
+
return 'chat-myself'
|
|
11
|
+
if (control === ':')
|
|
12
|
+
return 'chat-system'
|
|
13
|
+
return ''
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function render() {
|
|
17
|
+
const slotContent = slots.default()
|
|
18
|
+
if (!slotContent)
|
|
19
|
+
return <span>无会话内容</span>
|
|
20
|
+
|
|
21
|
+
return slotContent.map((node: VNode) => {
|
|
22
|
+
// WARN: 此处使用了非标准的 v-slot:default
|
|
23
|
+
const textContent = (node.children as any)?.default?.()[0].children
|
|
24
|
+
const matchGroups = typeof textContent === 'string'
|
|
25
|
+
? textContent.match(chatRegex)?.groups
|
|
26
|
+
: undefined
|
|
27
|
+
return matchGroups
|
|
28
|
+
? <dt class={`chat-caption ${getControlClass(matchGroups.control)}`}>{matchGroups.caption}</dt>
|
|
29
|
+
: <dd class="chat-body">{node}</dd>
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<dl class="chat">
|
|
36
|
+
<render />
|
|
37
|
+
</dl>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<style lang="scss" scoped>
|
|
41
41
|
.chat {
|
|
42
42
|
margin-inline: 2vw;
|
|
43
43
|
font-size: 0.9em;
|
|
@@ -76,4 +76,4 @@ function render() {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
-
</style>
|
|
79
|
+
</style>
|
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
const props = withDefaults(defineProps<{
|
|
3
|
-
prompt?: string | boolean
|
|
4
|
-
code?: string
|
|
5
|
-
lang?: string
|
|
6
|
-
}>(), {
|
|
7
|
-
prompt: '$',
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
// prompt 传入空字符串会变成 true
|
|
11
|
-
const showPrompt = computed(() => props.prompt !== true)
|
|
12
|
-
const language = computed(() => props.lang ?? getPromptLanguage(props.prompt))
|
|
13
|
-
|
|
14
|
-
const showUndo = ref(false)
|
|
15
|
-
const codeInput = useTemplateRef('code-input')
|
|
16
|
-
const shiki = useShiki()
|
|
17
|
-
|
|
18
|
-
const { copy, copied } = useCopy(codeInput)
|
|
19
|
-
|
|
20
|
-
function undo() {
|
|
21
|
-
if (!codeInput.value)
|
|
22
|
-
return
|
|
23
|
-
codeInput.value.textContent = props.code ?? ''
|
|
24
|
-
// 触发 shiki 高亮
|
|
25
|
-
codeInput.value.dispatchEvent(new Event('input'))
|
|
26
|
-
showUndo.value = false
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function preventLineBreak(event: InputEvent) {
|
|
30
|
-
const { data, inputType } = event
|
|
31
|
-
if (data?.includes('\n') || inputType === 'insertLineBreak') {
|
|
32
|
-
event.preventDefault()
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function checkUndoable(event: InputEvent) {
|
|
37
|
-
showUndo.value = props.code !== (event.target as Element).textContent
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
onMounted(async () => {
|
|
41
|
-
await shiki.mountPlain(codeInput.value!, language.value)
|
|
42
|
-
})
|
|
43
|
-
</script>
|
|
44
|
-
|
|
45
|
-
<template>
|
|
46
|
-
<code class="copy">
|
|
47
|
-
<span v-if="showPrompt" class="prompt">{{ prompt }}</span>
|
|
48
|
-
|
|
49
|
-
<div
|
|
50
|
-
ref="code-input"
|
|
51
|
-
contenteditable="plaintext-only"
|
|
52
|
-
class="code scrollcheck-x"
|
|
53
|
-
spellcheck="false"
|
|
54
|
-
@beforeinput="preventLineBreak"
|
|
55
|
-
@input="checkUndoable"
|
|
56
|
-
v-text="code"
|
|
57
|
-
/>
|
|
58
|
-
|
|
59
|
-
<button v-if="showUndo" class="operation" aria-label="恢复原始内容" @click="undo">
|
|
60
|
-
<Icon name="tabler:arrow-back-up" />
|
|
61
|
-
</button>
|
|
62
|
-
|
|
63
|
-
<Icon v-show="false" name="tabler:check" />
|
|
64
|
-
<button class="operation" aria-label="复制" @click="copy()">
|
|
65
|
-
<Icon :name="copied ? 'tabler:check' : 'tabler:copy'" />
|
|
66
|
-
</button>
|
|
67
|
-
</code>
|
|
68
|
-
</template>
|
|
69
|
-
|
|
70
|
-
<style lang="scss" scoped>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = withDefaults(defineProps<{
|
|
3
|
+
prompt?: string | boolean
|
|
4
|
+
code?: string
|
|
5
|
+
lang?: string
|
|
6
|
+
}>(), {
|
|
7
|
+
prompt: '$',
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
// prompt 传入空字符串会变成 true
|
|
11
|
+
const showPrompt = computed(() => props.prompt !== true)
|
|
12
|
+
const language = computed(() => props.lang ?? getPromptLanguage(props.prompt))
|
|
13
|
+
|
|
14
|
+
const showUndo = ref(false)
|
|
15
|
+
const codeInput = useTemplateRef('code-input')
|
|
16
|
+
const shiki = useShiki()
|
|
17
|
+
|
|
18
|
+
const { copy, copied } = useCopy(codeInput)
|
|
19
|
+
|
|
20
|
+
function undo() {
|
|
21
|
+
if (!codeInput.value)
|
|
22
|
+
return
|
|
23
|
+
codeInput.value.textContent = props.code ?? ''
|
|
24
|
+
// 触发 shiki 高亮
|
|
25
|
+
codeInput.value.dispatchEvent(new Event('input'))
|
|
26
|
+
showUndo.value = false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function preventLineBreak(event: InputEvent) {
|
|
30
|
+
const { data, inputType } = event
|
|
31
|
+
if (data?.includes('\n') || inputType === 'insertLineBreak') {
|
|
32
|
+
event.preventDefault()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function checkUndoable(event: InputEvent) {
|
|
37
|
+
showUndo.value = props.code !== (event.target as Element).textContent
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onMounted(async () => {
|
|
41
|
+
await shiki.mountPlain(codeInput.value!, language.value)
|
|
42
|
+
})
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<code class="copy">
|
|
47
|
+
<span v-if="showPrompt" class="prompt">{{ prompt }}</span>
|
|
48
|
+
|
|
49
|
+
<div
|
|
50
|
+
ref="code-input"
|
|
51
|
+
contenteditable="plaintext-only"
|
|
52
|
+
class="code scrollcheck-x"
|
|
53
|
+
spellcheck="false"
|
|
54
|
+
@beforeinput="preventLineBreak"
|
|
55
|
+
@input="checkUndoable"
|
|
56
|
+
v-text="code"
|
|
57
|
+
/>
|
|
58
|
+
|
|
59
|
+
<button v-if="showUndo" class="operation" aria-label="恢复原始内容" @click="undo">
|
|
60
|
+
<Icon name="tabler:arrow-back-up" />
|
|
61
|
+
</button>
|
|
62
|
+
|
|
63
|
+
<Icon v-show="false" name="tabler:check" />
|
|
64
|
+
<button class="operation" aria-label="复制" @click="copy()">
|
|
65
|
+
<Icon :name="copied ? 'tabler:check' : 'tabler:copy'" />
|
|
66
|
+
</button>
|
|
67
|
+
</code>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<style lang="scss" scoped>
|
|
71
71
|
.copy {
|
|
72
72
|
contain: paint;
|
|
73
73
|
display: flex;
|
|
@@ -131,4 +131,4 @@ onMounted(async () => {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
-
</style>
|
|
134
|
+
</style>
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { Temporal } from 'temporal-polyfill'
|
|
3
|
-
|
|
4
|
-
const props = defineProps<{
|
|
5
|
-
datetime?: string
|
|
6
|
-
rotate?: boolean
|
|
7
|
-
}>()
|
|
8
|
-
|
|
9
|
-
const emojiStatic = ['🕛', '🕧', '🕐', '🕜', '🕑', '🕝', '🕒', '🕞', '🕓', '🕟', '🕔', '🕠', '🕕', '🕡', '🕖', '🕢', '🕗', '🕣', '🕘', '🕤', '🕙', '🕥', '🕚', '🕦']
|
|
10
|
-
const emojiRotate = ['🕛', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚']
|
|
11
|
-
|
|
12
|
-
const now = ref(Temporal.Now.zonedDateTimeISO())
|
|
13
|
-
|
|
14
|
-
const datetime = computed(() => props.datetime
|
|
15
|
-
? toZonedTemporal(props.datetime)
|
|
16
|
-
: now.value)
|
|
17
|
-
|
|
18
|
-
const status = computed(() => {
|
|
19
|
-
const { hour, minute } = datetime.value
|
|
20
|
-
|
|
21
|
-
if (!props.rotate) {
|
|
22
|
-
const emojiIndex = (hour * 2 + Math.round(minute / 30)) % emojiStatic.length
|
|
23
|
-
return { emoji: emojiStatic[emojiIndex] }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const minuteAt = Math.round(minute / 5)
|
|
27
|
-
const emojiIndex = (hour % 12 - minuteAt + emojiRotate.length) % emojiRotate.length
|
|
28
|
-
return { rotate: minuteAt * 30, emoji: emojiRotate[emojiIndex] }
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
// 定时器只能在客户端运行,否则 nuxt generate 不能自动退出
|
|
32
|
-
const { resume } = useIntervalFn(() => {
|
|
33
|
-
now.value = Temporal.Now.zonedDateTimeISO()
|
|
34
|
-
}, 30000, { immediate: false })
|
|
35
|
-
|
|
36
|
-
whenever(() => !props.datetime, resume)
|
|
37
|
-
</script>
|
|
38
|
-
|
|
39
|
-
<template>
|
|
40
|
-
<span
|
|
41
|
-
class="emoji-clock"
|
|
42
|
-
:class="{ rotate }"
|
|
43
|
-
:style="{ '--deg': rotate ? `${status.rotate}deg` : undefined }"
|
|
44
|
-
v-text="status.emoji"
|
|
45
|
-
/>
|
|
46
|
-
</template>
|
|
47
|
-
|
|
48
|
-
<style lang="scss" scoped>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Temporal } from 'temporal-polyfill'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
datetime?: string
|
|
6
|
+
rotate?: boolean
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const emojiStatic = ['🕛', '🕧', '🕐', '🕜', '🕑', '🕝', '🕒', '🕞', '🕓', '🕟', '🕔', '🕠', '🕕', '🕡', '🕖', '🕢', '🕗', '🕣', '🕘', '🕤', '🕙', '🕥', '🕚', '🕦']
|
|
10
|
+
const emojiRotate = ['🕛', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚']
|
|
11
|
+
|
|
12
|
+
const now = ref(Temporal.Now.zonedDateTimeISO())
|
|
13
|
+
|
|
14
|
+
const datetime = computed(() => props.datetime
|
|
15
|
+
? toZonedTemporal(props.datetime)
|
|
16
|
+
: now.value)
|
|
17
|
+
|
|
18
|
+
const status = computed(() => {
|
|
19
|
+
const { hour, minute } = datetime.value
|
|
20
|
+
|
|
21
|
+
if (!props.rotate) {
|
|
22
|
+
const emojiIndex = (hour * 2 + Math.round(minute / 30)) % emojiStatic.length
|
|
23
|
+
return { emoji: emojiStatic[emojiIndex] }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const minuteAt = Math.round(minute / 5)
|
|
27
|
+
const emojiIndex = (hour % 12 - minuteAt + emojiRotate.length) % emojiRotate.length
|
|
28
|
+
return { rotate: minuteAt * 30, emoji: emojiRotate[emojiIndex] }
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// 定时器只能在客户端运行,否则 nuxt generate 不能自动退出
|
|
32
|
+
const { resume } = useIntervalFn(() => {
|
|
33
|
+
now.value = Temporal.Now.zonedDateTimeISO()
|
|
34
|
+
}, 30000, { immediate: false })
|
|
35
|
+
|
|
36
|
+
whenever(() => !props.datetime, resume)
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<span
|
|
41
|
+
class="emoji-clock"
|
|
42
|
+
:class="{ rotate }"
|
|
43
|
+
:style="{ '--deg': rotate ? `${status.rotate}deg` : undefined }"
|
|
44
|
+
v-text="status.emoji"
|
|
45
|
+
/>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<style lang="scss" scoped>
|
|
49
49
|
.emoji-clock.rotate {
|
|
50
50
|
display: inline-block;
|
|
51
51
|
transform: rotate(var(--deg, 0deg));
|
|
52
52
|
}
|
|
53
|
-
</style>
|
|
53
|
+
</style>
|
|
@@ -1,98 +1,98 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { CSSProperties } from 'vue'
|
|
3
|
-
import type { FeedEntry } from '../../types/feed'
|
|
4
|
-
// eslint-disable-next-line unused-imports/no-unused-imports
|
|
5
|
-
import { Temporal } from 'temporal-polyfill'
|
|
6
|
-
|
|
7
|
-
const props = defineProps<FeedEntry>()
|
|
8
|
-
|
|
9
|
-
const clarity = useClarityConfig()
|
|
10
|
-
const route = useRoute()
|
|
11
|
-
const isInspect = computed(() => import.meta.dev && route.query.inspect !== undefined)
|
|
12
|
-
|
|
13
|
-
const title = computed(() => props.title ?? props.sitenick ?? props.author)
|
|
14
|
-
const domainTip = computed(() => getDomainType(getMainDomain(props.link, true)))
|
|
15
|
-
const domainIcon = computed(() => getDomainIcon(props.link))
|
|
16
|
-
|
|
17
|
-
function getInspectStyle(src: string): CSSProperties {
|
|
18
|
-
src = getMainDomain(src)
|
|
19
|
-
let color = 'red'
|
|
20
|
-
|
|
21
|
-
if (src === getMainDomain(props.link))
|
|
22
|
-
color = 'transparent' // 来自源站
|
|
23
|
-
else if (src === 'gstatic.cn' || src === 'webp.se')
|
|
24
|
-
color = 'yellow' // 来自API获取
|
|
25
|
-
else if (src === 'qlogo.cn')
|
|
26
|
-
color = 'lightblue' // 来自QQ头像
|
|
27
|
-
else if (src === ('wsrv.nl'))
|
|
28
|
-
color = 'lightgreen' // 来自GitHub头像
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
border: `2px solid ${color}`,
|
|
32
|
-
boxSizing: 'content-box',
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
</script>
|
|
36
|
-
|
|
37
|
-
<template>
|
|
38
|
-
<Tooltip :delay="200" interactive ^role="link" hide-on-click="toggle">
|
|
39
|
-
<UtilLink
|
|
40
|
-
class="feed-card gradient-card"
|
|
41
|
-
:to="error ? undefined : link"
|
|
42
|
-
rel="noopener"
|
|
43
|
-
:data-error="error"
|
|
44
|
-
>
|
|
45
|
-
<div class="avatar" :title="feed ? undefined : '无订阅源'">
|
|
46
|
-
<ClientOnly v-if="isInspect">
|
|
47
|
-
<span style="position: absolute; left: 100%; white-space: nowrap;" v-text="title" />
|
|
48
|
-
<NuxtImg :src="icon" :title="icon" :style="getInspectStyle(icon)" />
|
|
49
|
-
<NuxtImg :src="avatar" :title="avatar" :style="getInspectStyle(avatar)" />
|
|
50
|
-
</ClientOnly>
|
|
51
|
-
|
|
52
|
-
<NuxtImg v-else class="round-cobblestone" :src="avatar" :alt="author" loading="lazy" />
|
|
53
|
-
<Icon v-if="clarity.link.remindNoFeed && !feed" class="no-feed" name="tabler:bell-off" />
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
|
-
<span class="author">{{ author }}</span>
|
|
57
|
-
<span class="sitenick">{{ sitenick }}</span>
|
|
58
|
-
</UtilLink>
|
|
59
|
-
|
|
60
|
-
<template #content>
|
|
61
|
-
<div class="site-content">
|
|
62
|
-
<NuxtImg class="site-icon" :src="icon" :alt="title" loading="lazy" />
|
|
63
|
-
|
|
64
|
-
<div class="site-info">
|
|
65
|
-
<h3 class="text-creative">
|
|
66
|
-
{{ title }}
|
|
67
|
-
</h3>
|
|
68
|
-
|
|
69
|
-
<code class="domain" :title="domainTip">
|
|
70
|
-
<span>{{ getDomain(link) }}</span>
|
|
71
|
-
<Icon v-if="domainIcon" class="domain-mark" :name="domainIcon" />
|
|
72
|
-
</code>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
<Icon
|
|
76
|
-
v-for="arch in archs" :key="arch"
|
|
77
|
-
class="arch" :name="getArchIcon(arch)" :title="arch"
|
|
78
|
-
/>
|
|
79
|
-
</div>
|
|
80
|
-
<div class="desc-content">
|
|
81
|
-
<div class="date">
|
|
82
|
-
{{ Temporal.PlainDate.from(date).toLocaleString() }}
|
|
83
|
-
</div>
|
|
84
|
-
|
|
85
|
-
<p>{{ error ?? desc }}</p>
|
|
86
|
-
|
|
87
|
-
<p v-if="comment">
|
|
88
|
-
<Icon name="tabler:message-dots" /> {{ comment }}
|
|
89
|
-
</p>
|
|
90
|
-
</div>
|
|
91
|
-
</template>
|
|
92
|
-
</Tooltip>
|
|
93
|
-
</template>
|
|
94
|
-
|
|
95
|
-
<style lang="scss" scoped>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CSSProperties } from 'vue'
|
|
3
|
+
import type { FeedEntry } from '../../types/feed'
|
|
4
|
+
// eslint-disable-next-line unused-imports/no-unused-imports
|
|
5
|
+
import { Temporal } from 'temporal-polyfill'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<FeedEntry>()
|
|
8
|
+
|
|
9
|
+
const clarity = useClarityConfig()
|
|
10
|
+
const route = useRoute()
|
|
11
|
+
const isInspect = computed(() => import.meta.dev && route.query.inspect !== undefined)
|
|
12
|
+
|
|
13
|
+
const title = computed(() => props.title ?? props.sitenick ?? props.author)
|
|
14
|
+
const domainTip = computed(() => getDomainType(getMainDomain(props.link, true)))
|
|
15
|
+
const domainIcon = computed(() => getDomainIcon(props.link))
|
|
16
|
+
|
|
17
|
+
function getInspectStyle(src: string): CSSProperties {
|
|
18
|
+
src = getMainDomain(src)
|
|
19
|
+
let color = 'red'
|
|
20
|
+
|
|
21
|
+
if (src === getMainDomain(props.link))
|
|
22
|
+
color = 'transparent' // 来自源站
|
|
23
|
+
else if (src === 'gstatic.cn' || src === 'webp.se')
|
|
24
|
+
color = 'yellow' // 来自API获取
|
|
25
|
+
else if (src === 'qlogo.cn')
|
|
26
|
+
color = 'lightblue' // 来自QQ头像
|
|
27
|
+
else if (src === ('wsrv.nl'))
|
|
28
|
+
color = 'lightgreen' // 来自GitHub头像
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
border: `2px solid ${color}`,
|
|
32
|
+
boxSizing: 'content-box',
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<Tooltip :delay="200" interactive ^role="link" hide-on-click="toggle">
|
|
39
|
+
<UtilLink
|
|
40
|
+
class="feed-card gradient-card"
|
|
41
|
+
:to="error ? undefined : link"
|
|
42
|
+
rel="noopener"
|
|
43
|
+
:data-error="error"
|
|
44
|
+
>
|
|
45
|
+
<div class="avatar" :title="feed ? undefined : '无订阅源'">
|
|
46
|
+
<ClientOnly v-if="isInspect">
|
|
47
|
+
<span style="position: absolute; left: 100%; white-space: nowrap;" v-text="title" />
|
|
48
|
+
<NuxtImg :src="icon" :title="icon" :style="getInspectStyle(icon)" />
|
|
49
|
+
<NuxtImg :src="avatar" :title="avatar" :style="getInspectStyle(avatar)" />
|
|
50
|
+
</ClientOnly>
|
|
51
|
+
|
|
52
|
+
<NuxtImg v-else class="round-cobblestone" :src="avatar" :alt="author" loading="lazy" />
|
|
53
|
+
<Icon v-if="clarity.link.remindNoFeed && !feed" class="no-feed" name="tabler:bell-off" />
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<span class="author">{{ author }}</span>
|
|
57
|
+
<span class="sitenick">{{ sitenick }}</span>
|
|
58
|
+
</UtilLink>
|
|
59
|
+
|
|
60
|
+
<template #content>
|
|
61
|
+
<div class="site-content">
|
|
62
|
+
<NuxtImg class="site-icon" :src="icon" :alt="title" loading="lazy" />
|
|
63
|
+
|
|
64
|
+
<div class="site-info">
|
|
65
|
+
<h3 class="text-creative">
|
|
66
|
+
{{ title }}
|
|
67
|
+
</h3>
|
|
68
|
+
|
|
69
|
+
<code class="domain" :title="domainTip">
|
|
70
|
+
<span>{{ getDomain(link) }}</span>
|
|
71
|
+
<Icon v-if="domainIcon" class="domain-mark" :name="domainIcon" />
|
|
72
|
+
</code>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<Icon
|
|
76
|
+
v-for="arch in archs" :key="arch"
|
|
77
|
+
class="arch" :name="getArchIcon(arch)" :title="arch"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="desc-content">
|
|
81
|
+
<div class="date">
|
|
82
|
+
{{ Temporal.PlainDate.from(date).toLocaleString() }}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<p>{{ error ?? desc }}</p>
|
|
86
|
+
|
|
87
|
+
<p v-if="comment">
|
|
88
|
+
<Icon name="tabler:message-dots" /> {{ comment }}
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
92
|
+
</Tooltip>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<style lang="scss" scoped>
|
|
96
96
|
.feed-card {
|
|
97
97
|
display: flex;
|
|
98
98
|
align-items: center;
|
|
@@ -209,4 +209,4 @@ function getInspectStyle(src: string): CSSProperties {
|
|
|
209
209
|
pointer-events: none;
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
-
</style>
|
|
212
|
+
</style>
|
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { FeedEntry, FeedGroup } from '../../types/feed'
|
|
3
|
-
import { shuffle } from 'es-toolkit/array'
|
|
4
|
-
|
|
5
|
-
const props = defineProps<FeedGroup & { shuffle?: boolean }>()
|
|
6
|
-
const route = useRoute()
|
|
7
|
-
const entries = ref(props.entries)
|
|
8
|
-
|
|
9
|
-
// 友链浮现随机延迟
|
|
10
|
-
function getCardDelay(feed: FeedEntry) {
|
|
11
|
-
let hash = 0
|
|
12
|
-
for (const char of feed.link) {
|
|
13
|
-
hash = hash * 31 + char.charCodeAt(0)
|
|
14
|
-
}
|
|
15
|
-
return (hash % 1000) / 1000
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const shuffleEntries = () => entries.value = shuffle(entries.value)
|
|
19
|
-
const unshuffleEntries = () => entries.value = props.entries
|
|
20
|
-
|
|
21
|
-
onMounted(() => {
|
|
22
|
-
if (props.shuffle && route.query.shuffle !== 'false')
|
|
23
|
-
shuffleEntries()
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
if (import.meta.dev) {
|
|
27
|
-
watch(() => props.entries, (newEntries) => {
|
|
28
|
-
entries.value = newEntries
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
</script>
|
|
32
|
-
|
|
33
|
-
<template>
|
|
34
|
-
<section class="feed-group">
|
|
35
|
-
<h3 class="feed-title">
|
|
36
|
-
<button
|
|
37
|
-
v-if="props.shuffle"
|
|
38
|
-
role="button"
|
|
39
|
-
title="点击随机排序,按住修饰键点击可取消随机排序"
|
|
40
|
-
@click="unshuffleEntries"
|
|
41
|
-
@click.exact="shuffleEntries"
|
|
42
|
-
v-text="name"
|
|
43
|
-
/>
|
|
44
|
-
<span v-else v-text="name" />
|
|
45
|
-
</h3>
|
|
46
|
-
<p class="feed-desc" v-text="desc" />
|
|
47
|
-
|
|
48
|
-
<TransitionGroup tag="menu" class="feed-list" name="float-in">
|
|
49
|
-
<li
|
|
50
|
-
v-for="entry in entries"
|
|
51
|
-
:key="entry.link"
|
|
52
|
-
:style="`--delay: ${getCardDelay(entry)}s;`"
|
|
53
|
-
>
|
|
54
|
-
<FeedCard v-bind="entry" />
|
|
55
|
-
</li>
|
|
56
|
-
</TransitionGroup>
|
|
57
|
-
</section>
|
|
58
|
-
</template>
|
|
59
|
-
|
|
60
|
-
<style lang="scss" scoped>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { FeedEntry, FeedGroup } from '../../types/feed'
|
|
3
|
+
import { shuffle } from 'es-toolkit/array'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<FeedGroup & { shuffle?: boolean }>()
|
|
6
|
+
const route = useRoute()
|
|
7
|
+
const entries = ref(props.entries)
|
|
8
|
+
|
|
9
|
+
// 友链浮现随机延迟
|
|
10
|
+
function getCardDelay(feed: FeedEntry) {
|
|
11
|
+
let hash = 0
|
|
12
|
+
for (const char of feed.link) {
|
|
13
|
+
hash = hash * 31 + char.charCodeAt(0)
|
|
14
|
+
}
|
|
15
|
+
return (hash % 1000) / 1000
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const shuffleEntries = () => entries.value = shuffle(entries.value)
|
|
19
|
+
const unshuffleEntries = () => entries.value = props.entries
|
|
20
|
+
|
|
21
|
+
onMounted(() => {
|
|
22
|
+
if (props.shuffle && route.query.shuffle !== 'false')
|
|
23
|
+
shuffleEntries()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if (import.meta.dev) {
|
|
27
|
+
watch(() => props.entries, (newEntries) => {
|
|
28
|
+
entries.value = newEntries
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<section class="feed-group">
|
|
35
|
+
<h3 class="feed-title">
|
|
36
|
+
<button
|
|
37
|
+
v-if="props.shuffle"
|
|
38
|
+
role="button"
|
|
39
|
+
title="点击随机排序,按住修饰键点击可取消随机排序"
|
|
40
|
+
@click="unshuffleEntries"
|
|
41
|
+
@click.exact="shuffleEntries"
|
|
42
|
+
v-text="name"
|
|
43
|
+
/>
|
|
44
|
+
<span v-else v-text="name" />
|
|
45
|
+
</h3>
|
|
46
|
+
<p class="feed-desc" v-text="desc" />
|
|
47
|
+
|
|
48
|
+
<TransitionGroup tag="menu" class="feed-list" name="float-in">
|
|
49
|
+
<li
|
|
50
|
+
v-for="entry in entries"
|
|
51
|
+
:key="entry.link"
|
|
52
|
+
:style="`--delay: ${getCardDelay(entry)}s;`"
|
|
53
|
+
>
|
|
54
|
+
<FeedCard v-bind="entry" />
|
|
55
|
+
</li>
|
|
56
|
+
</TransitionGroup>
|
|
57
|
+
</section>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<style lang="scss" scoped>
|
|
61
61
|
.feed-group {
|
|
62
62
|
container-type: inline-size;
|
|
63
63
|
margin: 2em 1em;
|
|
@@ -127,4 +127,4 @@ if (import.meta.dev) {
|
|
|
127
127
|
contain: paint; // 防止移动时出现滚动条
|
|
128
128
|
pointer-events: none; // 阻止触发错误定位的气泡
|
|
129
129
|
}
|
|
130
|
-
</style>
|
|
130
|
+
</style>
|