valaxy-theme-hairy 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- package/@types/markdown-it.d.ts +1 -0
- package/@types/markdown-toc.d.ts +1 -0
- package/@types/types.d.ts +1 -0
- package/@types/valaxy.d.ts +10 -0
- package/LICENSE +21 -0
- package/components/HairyAlgoliaSearchBox.vue +118 -0
- package/components/HairyArticleImage.vue +17 -0
- package/components/HairyArticleImageDefault.vue +127 -0
- package/components/HairyArticleImageSmall.vue +49 -0
- package/components/HairyArticleSeries.vue +73 -0
- package/components/HairyArticleText.vue +38 -0
- package/components/HairyArticleTop.vue +0 -0
- package/components/HairyBackToTop.vue +72 -0
- package/components/HairyBody.vue +55 -0
- package/components/HairyBreadcrumb.vue +51 -0
- package/components/HairyBreadcrumbItem.vue +14 -0
- package/components/HairyCarousel.vue +45 -0
- package/components/HairyDivider.vue +0 -0
- package/components/HairyHeader.vue +31 -0
- package/components/HairyImage.vue +14 -0
- package/components/HairyImageGroup.vue +69 -0
- package/components/HairyImageViewer.vue +22 -0
- package/components/HairyLayout.vue +29 -0
- package/components/HairyLink.vue +10 -0
- package/components/HairyMenu.vue +15 -0
- package/components/HairyMenuItem.vue +40 -0
- package/components/HairyMeting.vue +19 -0
- package/components/HairyNav.vue +39 -0
- package/components/HairyNavBackground.vue +7 -0
- package/components/HairyNavSearch.vue +265 -0
- package/components/HairyNavTitle.vue +13 -0
- package/components/HairyNavToggleDark.vue +16 -0
- package/components/HairyPostImageList.vue +28 -0
- package/components/HairyPostList.vue +24 -0
- package/components/HairyPostTitle.vue +33 -0
- package/components/HairySocialLinks.vue +27 -0
- package/components/HairyTimelinePostItem.vue +40 -0
- package/components/HairyToc.vue +135 -0
- package/components/HairyUserCard.vue +30 -0
- package/components/HairyUserNav.vue +68 -0
- package/components/HairyUserTab.vue +40 -0
- package/components/HairyWaline.vue +25 -0
- package/components/HairyWaves.vue +67 -0
- package/components/ValaxyMain.vue +45 -0
- package/hooks/useCategory.ts +18 -0
- package/hooks/useCategoryPost.ts +21 -0
- package/hooks/useContext.ts +16 -0
- package/hooks/useHeaderHeight.ts +9 -0
- package/hooks/usePostLayout.ts +9 -0
- package/hooks/useYearArchives.ts +43 -0
- package/images.json +101 -0
- package/index.d.ts +54 -0
- package/layouts/archives.vue +11 -0
- package/layouts/categories.vue +6 -0
- package/layouts/default.vue +9 -0
- package/layouts/home.vue +23 -0
- package/layouts/month.vue +6 -0
- package/layouts/post.vue +26 -0
- package/layouts/tag.vue +6 -0
- package/layouts/tags.vue +6 -0
- package/layouts/year.vue +6 -0
- package/locales/en.yml +1 -0
- package/locales/zh-CN.yml +3 -0
- package/modules/context.ts +5 -0
- package/modules/loading.scss +531 -0
- package/modules/loading.ts +42 -0
- package/modules/scroll.ts +21 -0
- package/node/addon-hairy.ts +18 -0
- package/node/addon-images.ts +61 -0
- package/node/addon-meting.ts +13 -0
- package/node/addon-statistics.ts +19 -0
- package/node/addon-toc.ts +20 -0
- package/node/utils.ts +20 -0
- package/package.json +47 -0
- package/pages/archives/[year]/[month]/index.vue +52 -0
- package/pages/archives/[year]/index.vue +73 -0
- package/pages/archives/index.vue +53 -0
- package/pages/categories/[...categories].vue +115 -0
- package/pages/index.vue +14 -0
- package/pages/tags/[tag].vue +40 -0
- package/pages/tags/index.vue +31 -0
- package/setup/main.ts +11 -0
- package/shims.d.ts +8 -0
- package/styles/css-vars.scss +161 -0
- package/styles/element-plus/index.scss +2 -0
- package/styles/element-plus/tabs.scss +26 -0
- package/styles/element-plus/timeline.scss +19 -0
- package/styles/font-face.scss +20 -0
- package/styles/fonts/FrederickatheGreat.ttf +0 -0
- package/styles/fonts/Modesty.ttf +0 -0
- package/styles/fonts/MountainsofChristmas-Bold.ttf +0 -0
- package/styles/fonts/MountainsofChristmas-Regular.ttf +0 -0
- package/styles/fonts/Seto.ttf +0 -0
- package/styles/index.scss +65 -0
- package/styles/markdown.scss +60 -0
- package/styles/scrollbar.scss +26 -0
- package/unocss.config.ts +29 -0
- package/utils/createContext.ts +40 -0
- package/utils/index.ts +28 -0
- package/utils/loading.scss +531 -0
- package/utils/loading.ts +30 -0
- package/valaxy.config.ts +26 -0
@@ -0,0 +1 @@
|
|
1
|
+
declare module "*-markdown-it"
|
@@ -0,0 +1 @@
|
|
1
|
+
declare module "markdown-toc"
|
@@ -0,0 +1 @@
|
|
1
|
+
declare const __ALGOLIA__: boolean
|
package/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2022 云游君
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,118 @@
|
|
1
|
+
<script setup lang="ts">
|
2
|
+
import docsearch from '@docsearch/js'
|
3
|
+
import type { DocSearchHit } from '@docsearch/react/dist/esm/types'
|
4
|
+
import { onMounted } from 'vue'
|
5
|
+
import type { AlgoliaSearchOptions } from 'valaxy'
|
6
|
+
import { useConfig } from 'valaxy'
|
7
|
+
import { useRoute, useRouter } from 'vue-router'
|
8
|
+
|
9
|
+
const router = useRouter()
|
10
|
+
const route = useRoute()
|
11
|
+
const config = useConfig()
|
12
|
+
|
13
|
+
onMounted(() => {
|
14
|
+
initialize(config.value.search.algolia)
|
15
|
+
setTimeout(poll, 16)
|
16
|
+
})
|
17
|
+
|
18
|
+
/**
|
19
|
+
* poll until open
|
20
|
+
*/
|
21
|
+
function poll() {
|
22
|
+
// programmatically open the search box after initialize
|
23
|
+
const e = new Event('keydown') as any
|
24
|
+
|
25
|
+
e.key = 'k'
|
26
|
+
e.metaKey = true
|
27
|
+
|
28
|
+
window.dispatchEvent(e)
|
29
|
+
|
30
|
+
setTimeout(() => {
|
31
|
+
if (!document.querySelector('.DocSearch-Modal'))
|
32
|
+
poll()
|
33
|
+
}, 16)
|
34
|
+
}
|
35
|
+
|
36
|
+
function initialize(userOptions: AlgoliaSearchOptions) {
|
37
|
+
// note: multi-lang search support is removed since the theme
|
38
|
+
// doesn't support multiple locales as of now.
|
39
|
+
const options = Object.assign({}, userOptions, {
|
40
|
+
container: '#docsearch',
|
41
|
+
navigator: {
|
42
|
+
navigate({ itemUrl }: { itemUrl: string }) {
|
43
|
+
const { pathname: hitPathname } = new URL(
|
44
|
+
window.location.origin + itemUrl,
|
45
|
+
)
|
46
|
+
// router doesn't handle same-page navigation so we use the native
|
47
|
+
// browser location API for anchor navigation
|
48
|
+
if (route.path === hitPathname)
|
49
|
+
window.location.assign(window.location.origin + itemUrl)
|
50
|
+
|
51
|
+
else
|
52
|
+
router.push(itemUrl)
|
53
|
+
},
|
54
|
+
},
|
55
|
+
transformItems(items: DocSearchHit[]) {
|
56
|
+
return items.map((item) => {
|
57
|
+
return Object.assign({}, item, {
|
58
|
+
url: getRelativePath(item.url),
|
59
|
+
})
|
60
|
+
})
|
61
|
+
},
|
62
|
+
hitComponent({ hit, children }: { hit: DocSearchHit; children: any }) {
|
63
|
+
const relativeHit = hit.url.startsWith('http')
|
64
|
+
? getRelativePath(hit.url as string)
|
65
|
+
: hit.url
|
66
|
+
return {
|
67
|
+
__v: null,
|
68
|
+
type: 'a',
|
69
|
+
ref: undefined,
|
70
|
+
constructor: undefined,
|
71
|
+
key: undefined,
|
72
|
+
props: {
|
73
|
+
href: hit.url,
|
74
|
+
onClick(event: MouseEvent) {
|
75
|
+
if (isSpecialClick(event))
|
76
|
+
return
|
77
|
+
|
78
|
+
// we rely on the native link scrolling when user is already on
|
79
|
+
// the right anchor because Router doesn't support duplicated
|
80
|
+
// history entries.
|
81
|
+
if (route.path === relativeHit)
|
82
|
+
return
|
83
|
+
|
84
|
+
// if the hits goes to another page, we prevent the native link
|
85
|
+
// behavior to leverage the Router loading feature.
|
86
|
+
if (route.path !== relativeHit)
|
87
|
+
event.preventDefault()
|
88
|
+
|
89
|
+
router.push(relativeHit)
|
90
|
+
},
|
91
|
+
children,
|
92
|
+
},
|
93
|
+
}
|
94
|
+
},
|
95
|
+
})
|
96
|
+
docsearch(options as any)
|
97
|
+
}
|
98
|
+
|
99
|
+
function isSpecialClick(event: MouseEvent) {
|
100
|
+
return (
|
101
|
+
event.button === 1
|
102
|
+
|| event.altKey
|
103
|
+
|| event.ctrlKey
|
104
|
+
|| event.metaKey
|
105
|
+
|| event.shiftKey
|
106
|
+
)
|
107
|
+
}
|
108
|
+
|
109
|
+
function getRelativePath(absoluteUrl: string) {
|
110
|
+
const { pathname, hash } = new URL(absoluteUrl)
|
111
|
+
return pathname + hash
|
112
|
+
}
|
113
|
+
</script>
|
114
|
+
|
115
|
+
<template>
|
116
|
+
<div id="docsearch" />
|
117
|
+
</template>
|
118
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<script lang="ts" setup>
|
2
|
+
import type { Post } from 'valaxy'
|
3
|
+
|
4
|
+
defineProps<{
|
5
|
+
post: Post
|
6
|
+
reverse?: boolean
|
7
|
+
}>()
|
8
|
+
</script>
|
9
|
+
|
10
|
+
<template>
|
11
|
+
<HairyArticleImageDefault :post="post" :reverse="reverse" class="hidden sm:block" />
|
12
|
+
<HairyArticleImageSmall :post="post" :reverse="reverse" class="hidden lt-sm:block" />
|
13
|
+
</template>
|
14
|
+
|
15
|
+
<style lang="scss">
|
16
|
+
|
17
|
+
</style>
|
@@ -0,0 +1,127 @@
|
|
1
|
+
<script lang="ts" setup>
|
2
|
+
import type { Post } from 'valaxy'
|
3
|
+
import { computed, defineProps } from 'vue'
|
4
|
+
import dayjs from 'dayjs'
|
5
|
+
import { useRouter } from 'vue-router'
|
6
|
+
import { useI18n } from 'vue-i18n'
|
7
|
+
import last from 'lodash/last'
|
8
|
+
import { usePostLayout } from '../hooks/usePostLayout'
|
9
|
+
import { toArr } from '../utils'
|
10
|
+
|
11
|
+
const props = defineProps<{
|
12
|
+
post: Post
|
13
|
+
reverse?: boolean
|
14
|
+
}>()
|
15
|
+
const router = useRouter()
|
16
|
+
const layout = usePostLayout()
|
17
|
+
const slice = computed(() => layout.value.includes('slice'))
|
18
|
+
const image = computed(() => props.post.image)
|
19
|
+
|
20
|
+
const i18n = useI18n()
|
21
|
+
|
22
|
+
const onReadMore = () => {
|
23
|
+
if (props.post.path)
|
24
|
+
router.push(props.post.path)
|
25
|
+
}
|
26
|
+
|
27
|
+
const displayCategory = (keys: string | string[] = []) => {
|
28
|
+
router.push({ path: `/categories/${toArr(keys).join('/')}` })
|
29
|
+
}
|
30
|
+
</script>
|
31
|
+
|
32
|
+
<template>
|
33
|
+
<li class="HairyArticleImage my-10 py-2" :class="[slice && 'slice', reverse && 'reverse']">
|
34
|
+
<article>
|
35
|
+
<div class="flex justify-between items-center">
|
36
|
+
<a class="text-size-2xl font-bold truncate cursor-pointer" :class="[reverse ? 'order-first' : 'order-last']" @click="onReadMore">{{ post.title }}</a>
|
37
|
+
<div class="flex justify-end gap-2 text-size-sm">
|
38
|
+
<span>{{ dayjs(post.date).format('YYYY-MM-DD') }}</span>
|
39
|
+
<span>{{ (post.length / 1000).toFixed(1) }}k字</span>
|
40
|
+
<span>{{ post.durations.minutes.toFixed(2) }}分钟</span>
|
41
|
+
</div>
|
42
|
+
</div>
|
43
|
+
<div class="h-200px flex">
|
44
|
+
<div class="flex-1 post-image-content" :class="[reverse ? 'order-last' : 'order-first']">
|
45
|
+
<img
|
46
|
+
class="post-image rounded-1 w-full h-full object-cover cursor-pointer" :src="image"
|
47
|
+
@click="onReadMore"
|
48
|
+
/>
|
49
|
+
</div>
|
50
|
+
<div class="flex flex-col justify-between flex-1">
|
51
|
+
<div class="flex-1 text-size-sm">
|
52
|
+
<div class="line-clamp-text">
|
53
|
+
{{ post.text }}
|
54
|
+
</div>
|
55
|
+
</div>
|
56
|
+
<div class="flex justify-between items-center">
|
57
|
+
<a class="cursor-pointer" :class="[reverse && 'order-1']">
|
58
|
+
<span v-if="post.categories?.length" @click="displayCategory(post.categories)">
|
59
|
+
{{ i18n.t(last(toArr(post.categories)) || '') }}
|
60
|
+
</span>
|
61
|
+
</a>
|
62
|
+
<div class="text-base leading-6 font-medium">
|
63
|
+
<a class="link cursor-pointer" aria-label="read more" @click="onReadMore">
|
64
|
+
<span v-if="reverse">←</span>
|
65
|
+
Read more
|
66
|
+
<span v-if="!reverse">→</span>
|
67
|
+
</a>
|
68
|
+
</div>
|
69
|
+
</div>
|
70
|
+
</div>
|
71
|
+
</div>
|
72
|
+
</article>
|
73
|
+
</li>
|
74
|
+
</template>
|
75
|
+
|
76
|
+
<style lang="scss" scoped>
|
77
|
+
.line-clamp-text {
|
78
|
+
word-break: break-all;
|
79
|
+
overflow: hidden;
|
80
|
+
text-overflow: ellipsis;
|
81
|
+
display: -webkit-box;
|
82
|
+
text-overflow: ellipsis;
|
83
|
+
-webkit-box-orient: vertical;
|
84
|
+
-webkit-line-clamp: 5;
|
85
|
+
}
|
86
|
+
|
87
|
+
.dark {
|
88
|
+
.post-image {
|
89
|
+
@apply opacity-75 hover: opacity-90 duration-200;
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
.post-image-content {
|
94
|
+
margin-right: 1rem;
|
95
|
+
}
|
96
|
+
|
97
|
+
.slice {
|
98
|
+
.post-image-content {
|
99
|
+
webkit-clip-path: polygon(0 0, 92% 0, 100% 100%, 0 100%);
|
100
|
+
clip-path: polygon(0 0, 92% 0, 100% 100%, 0 100%);
|
101
|
+
border-radius: 0.625rem 0 0 0.625rem;
|
102
|
+
overflow: hidden;
|
103
|
+
}
|
104
|
+
}
|
105
|
+
|
106
|
+
.reverse {
|
107
|
+
.post-image-content {
|
108
|
+
margin-right: 0;
|
109
|
+
margin-left: 1rem;
|
110
|
+
clip-path: polygon(0 0,100% 0,100% 100%,8% 100%);
|
111
|
+
border-radius: 0 0.625rem 0.625rem 0;
|
112
|
+
overflow: hidden;
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
116
|
+
.HairyArticleImage.slice {
|
117
|
+
.post-image {
|
118
|
+
@apply transition-all;
|
119
|
+
}
|
120
|
+
|
121
|
+
&:hover {
|
122
|
+
.post-image {
|
123
|
+
transform: scale(1.05) rotate(1deg);
|
124
|
+
}
|
125
|
+
}
|
126
|
+
}
|
127
|
+
</style>
|
@@ -0,0 +1,49 @@
|
|
1
|
+
<script lang="ts" setup>
|
2
|
+
import type { Post } from 'valaxy'
|
3
|
+
import { computed, defineProps } from 'vue'
|
4
|
+
import dayjs from 'dayjs'
|
5
|
+
import { useRouter } from 'vue-router'
|
6
|
+
import { useI18n } from 'vue-i18n'
|
7
|
+
import last from 'lodash/last'
|
8
|
+
import { usePostLayout } from '../hooks/usePostLayout'
|
9
|
+
import { toArr } from '../utils'
|
10
|
+
|
11
|
+
const props = defineProps<{
|
12
|
+
post: Post
|
13
|
+
reverse?: boolean
|
14
|
+
}>()
|
15
|
+
const router = useRouter()
|
16
|
+
const layout = usePostLayout()
|
17
|
+
const slice = computed(() => layout.value.includes('slice'))
|
18
|
+
const image = computed(() => props.post.image)
|
19
|
+
|
20
|
+
const i18n = useI18n()
|
21
|
+
|
22
|
+
const onReadMore = () => {
|
23
|
+
if (props.post.path)
|
24
|
+
router.push(props.post.path)
|
25
|
+
}
|
26
|
+
|
27
|
+
const displayCategory = (keys: string | string[] = []) => {
|
28
|
+
router.push({ path: `/categories/${toArr(keys).join('/')}` })
|
29
|
+
}
|
30
|
+
</script>
|
31
|
+
|
32
|
+
<template>
|
33
|
+
<li class="HairyArticleImage my-10 py-2" :class="[slice && 'slice', reverse && 'reverse']">
|
34
|
+
<article>
|
35
|
+
<div class="flex justify-between items-center">
|
36
|
+
<a class="text-size-2xl font-bold truncate cursor-pointer" :class="[reverse ? 'order-first' : 'order-last']" @click="onReadMore">{{ post.title }}</a>
|
37
|
+
<div class="flex justify-end gap-2 text-size-sm">
|
38
|
+
<span>{{ dayjs(post.date).format('YYYY-MM-DD') }}</span>
|
39
|
+
<span>{{ (post.length / 1000).toFixed(1) }}k字</span>
|
40
|
+
<span>{{ post.durations.minutes.toFixed(2) }}分钟</span>
|
41
|
+
</div>
|
42
|
+
</div>
|
43
|
+
</article>
|
44
|
+
</li>
|
45
|
+
</template>
|
46
|
+
|
47
|
+
<style lang="scss">
|
48
|
+
|
49
|
+
</style>
|
@@ -0,0 +1,73 @@
|
|
1
|
+
<script lang="ts" setup>
|
2
|
+
import { useFrontmatter } from 'valaxy'
|
3
|
+
import { computed } from 'vue'
|
4
|
+
import { toArr } from '../utils'
|
5
|
+
import { useCurrentCategory } from '../hooks/useCategory'
|
6
|
+
|
7
|
+
const frontmatter = useFrontmatter()
|
8
|
+
const paths = computed(() => toArr(frontmatter.value.categories).filter(Boolean) as string[])
|
9
|
+
const category = useCurrentCategory(paths)
|
10
|
+
const posts = computed(() => category.value.posts || [])
|
11
|
+
|
12
|
+
function isCurrent(title = '') {
|
13
|
+
return frontmatter.value.title === title
|
14
|
+
}
|
15
|
+
</script>
|
16
|
+
|
17
|
+
<template>
|
18
|
+
<div class="pl-16px text-14px relative overflow-hidden">
|
19
|
+
<div class="outline-title">
|
20
|
+
On this Series
|
21
|
+
</div>
|
22
|
+
<ul class="va-toc relative z-1">
|
23
|
+
<a v-for="(item, index) of posts" :key="index" class="va-toc-item" @click="$router.push(item.path || '')">
|
24
|
+
<a class="outline-link" :class="[isCurrent(item.title) && 'active']">{{ index + 1 }}.{{ item.title }}</a>
|
25
|
+
</a>
|
26
|
+
</ul>
|
27
|
+
</div>
|
28
|
+
</template>
|
29
|
+
|
30
|
+
<style lang="scss" scoped>
|
31
|
+
.outline-title {
|
32
|
+
letter-spacing: 0.4px;
|
33
|
+
line-height: 28px;
|
34
|
+
font-size: 14px;
|
35
|
+
font-weight: 600;
|
36
|
+
}
|
37
|
+
|
38
|
+
.outline-link {
|
39
|
+
display: block;
|
40
|
+
position: relative;
|
41
|
+
line-height: 28px;
|
42
|
+
color: var(--va-c-text-light);
|
43
|
+
white-space: nowrap;
|
44
|
+
text-overflow: ellipsis;
|
45
|
+
transition: color 0.5s;
|
46
|
+
cursor: pointer;
|
47
|
+
|
48
|
+
&:hover {
|
49
|
+
color: var(--va-c-brand);
|
50
|
+
transition: color 0.25s;
|
51
|
+
}
|
52
|
+
|
53
|
+
&.active {
|
54
|
+
color: var(--va-c-brand);
|
55
|
+
transition: color .25s;
|
56
|
+
|
57
|
+
&::after {
|
58
|
+
position: absolute;
|
59
|
+
content: '';
|
60
|
+
left: -1.12rem;
|
61
|
+
top: 0;
|
62
|
+
bottom: 0;
|
63
|
+
margin: auto;
|
64
|
+
width: 4px;
|
65
|
+
height: 18px;
|
66
|
+
background-color: var(--va-c-brand);
|
67
|
+
transition: top 0.25s cubic-bezier(0, 1, 0.5, 1), background-color 0.5s, opacity 0.25s;
|
68
|
+
border-top-right-radius: 2px;
|
69
|
+
border-bottom-right-radius: 2px;
|
70
|
+
}
|
71
|
+
}
|
72
|
+
}
|
73
|
+
</style>
|
@@ -0,0 +1,38 @@
|
|
1
|
+
<script lang="ts" setup>
|
2
|
+
import { useThemeConfig } from 'valaxy'
|
3
|
+
import type { Post } from 'valaxy'
|
4
|
+
import { computed, defineProps } from 'vue'
|
5
|
+
import type { HairyTheme } from '..'
|
6
|
+
const props = defineProps<{
|
7
|
+
post: Post
|
8
|
+
}>()
|
9
|
+
const themeConfig = useThemeConfig<HairyTheme>()
|
10
|
+
const layout = computed(() => themeConfig.value.post?.layout || 'text')
|
11
|
+
const text = computed(() => {
|
12
|
+
if (layout.value === 'text')
|
13
|
+
return props.post.text
|
14
|
+
return props.post.excerpt
|
15
|
+
})
|
16
|
+
</script>
|
17
|
+
|
18
|
+
<template>
|
19
|
+
<li class="py-12">
|
20
|
+
<article class="space-y-2 xl:grid xl:grid-cols-4 xl:space-y-0 xl:items-baseline">
|
21
|
+
<div class="space-y-5 xl:col-span-4">
|
22
|
+
<div class="space-y-6">
|
23
|
+
<h2 class="text-2xl leading-8 font-bold tracking-tight">
|
24
|
+
<a class="st-text" :href="post.path">{{ post.title }}</a>
|
25
|
+
</h2>
|
26
|
+
<div
|
27
|
+
v-if="text"
|
28
|
+
class="prose max-w-none text-gray-500"
|
29
|
+
v-html="text"
|
30
|
+
/>
|
31
|
+
</div>
|
32
|
+
<div class="text-base leading-6 font-medium">
|
33
|
+
<a class="link" aria-label="read more" :href="post.path">Read more →</a>
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
</article>
|
37
|
+
</li>
|
38
|
+
</template>
|
File without changes
|
@@ -0,0 +1,72 @@
|
|
1
|
+
<script lang="ts" setup>
|
2
|
+
import { computed } from 'vue'
|
3
|
+
import { useBackToTop } from 'valaxy'
|
4
|
+
|
5
|
+
const { show, percentage } = useBackToTop({ offset: 100 })
|
6
|
+
|
7
|
+
const radius = 48
|
8
|
+
const circumference = 2 * radius * Math.PI
|
9
|
+
|
10
|
+
const strokeOffset = computed(() => {
|
11
|
+
// 周长
|
12
|
+
const val = (1 - percentage.value) * circumference
|
13
|
+
return val < 0 ? 0 : val
|
14
|
+
})
|
15
|
+
|
16
|
+
function toTop() {
|
17
|
+
window.scrollTo({ top: 0, behavior: 'smooth' })
|
18
|
+
}
|
19
|
+
</script>
|
20
|
+
|
21
|
+
<template>
|
22
|
+
<a class="back-to-top cursor-pointer" :class="show && 'show'" @click="toTop">
|
23
|
+
<div w="8" h="8" i-ri-arrow-up-s-line />
|
24
|
+
<svg class="progress-circle-container" viewBox="0 0 100 100">
|
25
|
+
<circle :stroke-dasharray="`${circumference} ${circumference}`" :stroke-dashoffset="strokeOffset" class="progress-circle" cx="50" cy="50" :r="radius" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
26
|
+
</svg>
|
27
|
+
</a>
|
28
|
+
</template>
|
29
|
+
|
30
|
+
<style lang="scss">
|
31
|
+
@use "sass:map";
|
32
|
+
|
33
|
+
.back-to-top {
|
34
|
+
position: fixed;
|
35
|
+
width: 32px;
|
36
|
+
height: 32px;
|
37
|
+
right: 0rem;
|
38
|
+
bottom: 1.5rem;
|
39
|
+
z-index: 10;
|
40
|
+
opacity: 0;
|
41
|
+
pointer-events: none;
|
42
|
+
|
43
|
+
color: var(--hy-c-primary);
|
44
|
+
transform: translateX(0) rotate(270deg);
|
45
|
+
// override yun-icon-btn transition
|
46
|
+
transition: transform var(--va-transition-duration), opacity var(--va-transition-duration-fast) !important;
|
47
|
+
|
48
|
+
&.show {
|
49
|
+
transform: translateX(-32px) rotate(360deg);
|
50
|
+
opacity: 1;
|
51
|
+
pointer-events: fill;
|
52
|
+
}
|
53
|
+
|
54
|
+
.icon {
|
55
|
+
width: 2.5rem;
|
56
|
+
height: 2.5rem;
|
57
|
+
}
|
58
|
+
> div {
|
59
|
+
margin-bottom: -2em;
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
.progress-circle {
|
64
|
+
transition: 0.3s stroke-dashoffset;
|
65
|
+
transform: rotate(-90deg);
|
66
|
+
transform-origin: 50% 50%;
|
67
|
+
|
68
|
+
&-container {
|
69
|
+
position: absolute;
|
70
|
+
}
|
71
|
+
}
|
72
|
+
</style>
|
@@ -0,0 +1,55 @@
|
|
1
|
+
<script lang="ts" setup>
|
2
|
+
import { useFrontmatter } from 'valaxy'
|
3
|
+
import { computed } from 'vue'
|
4
|
+
import { useRoute } from 'vue-router'
|
5
|
+
const fr = useFrontmatter()
|
6
|
+
const route = useRoute()
|
7
|
+
|
8
|
+
const showWaline = computed(() => route.path.includes('/posts/') || fr.value.waline)
|
9
|
+
</script>
|
10
|
+
|
11
|
+
<template>
|
12
|
+
<div class="HairyBody min-h-50vh relative">
|
13
|
+
<div class="mx-auto breakpoint flex z-1 relative">
|
14
|
+
<div class="relative flex-1 pt-2">
|
15
|
+
<slot />
|
16
|
+
<HairyWaline v-if="showWaline" />
|
17
|
+
</div>
|
18
|
+
<div class="ml-4 w-60 lg:block hidden">
|
19
|
+
<div class="sticky top-3.125rem z-1">
|
20
|
+
<slot v-if="$slots.slide" name="slide" />
|
21
|
+
<HairyUserCard v-else />
|
22
|
+
</div>
|
23
|
+
</div>
|
24
|
+
</div>
|
25
|
+
<div class="HairyBodyBackground" />
|
26
|
+
</div>
|
27
|
+
</template>
|
28
|
+
|
29
|
+
<style lang="scss">
|
30
|
+
.a {
|
31
|
+
overflow: hidden;
|
32
|
+
}
|
33
|
+
|
34
|
+
.HairyBodyBackground {
|
35
|
+
@apply transition-all duration-200;
|
36
|
+
@apply absolute top-0 max-h-150vh top-5 bottom-0 w-full transition-opacity;
|
37
|
+
opacity: 0;
|
38
|
+
}
|
39
|
+
|
40
|
+
.dark {
|
41
|
+
.HairyBodyBackground {
|
42
|
+
transition-delay: 200ms;
|
43
|
+
transition-delay: 0;
|
44
|
+
opacity: 1;
|
45
|
+
background-image:
|
46
|
+
linear-gradient(to bottom, var(--hy-c-waves-dimm) 0%, transparent 60%, var(--hy-c-waves-dimm) 100%),
|
47
|
+
url(https://tva2.sinaimg.cn/large/008ugSUaly8h4mt4lbuc0j31hc0u040c.jpg);
|
48
|
+
background-position: center;
|
49
|
+
opacity: 0.4;
|
50
|
+
background-repeat: no-repeat;
|
51
|
+
filter: blur(0px);
|
52
|
+
background-size: cover;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
</style>
|
@@ -0,0 +1,51 @@
|
|
1
|
+
<script lang="ts" setup>
|
2
|
+
import { ElBreadcrumb, breadcrumbProps } from 'element-plus/es/components/breadcrumb/index'
|
3
|
+
import 'element-plus/es/components/breadcrumb/style/index'
|
4
|
+
import type { PropType } from 'vue'
|
5
|
+
import { computed } from 'vue'
|
6
|
+
|
7
|
+
const props = defineProps({
|
8
|
+
...breadcrumbProps,
|
9
|
+
size: String as PropType<'default' | 'large'>,
|
10
|
+
after: String,
|
11
|
+
})
|
12
|
+
const text = computed(() => `"${props.after || ''}"`)
|
13
|
+
</script>
|
14
|
+
|
15
|
+
<template>
|
16
|
+
<ElBreadcrumb :class="[size]" :style="{ '--after-text': text }" v-bind="$props">
|
17
|
+
<slot />
|
18
|
+
</ElBreadcrumb>
|
19
|
+
</template>
|
20
|
+
|
21
|
+
<style lang="scss">
|
22
|
+
.el-breadcrumb {
|
23
|
+
overflow: hidden;
|
24
|
+
display: inline-block;
|
25
|
+
position: relative;
|
26
|
+
}
|
27
|
+
.el-breadcrumb .el-breadcrumb__inner,
|
28
|
+
.el-breadcrumb .el-breadcrumb__item:last-child .el-breadcrumb__inner {
|
29
|
+
color: initial;
|
30
|
+
}
|
31
|
+
.el-breadcrumb .el-breadcrumb__inner.is-link {
|
32
|
+
@apply border-b border-dashed hover:border-primary hover:text-primary transition-all cursor-pointer;
|
33
|
+
font-weight: normal;
|
34
|
+
}
|
35
|
+
|
36
|
+
.el-breadcrumb::after {
|
37
|
+
@apply text-gray-5 text-size-5;
|
38
|
+
content: var(--after-text);
|
39
|
+
clear: none;
|
40
|
+
transform: translateX(2px);
|
41
|
+
}
|
42
|
+
.el-breadcrumb.large {
|
43
|
+
margin-top: 3rem;
|
44
|
+
font-size: 2.5em;
|
45
|
+
@apply lt-md:text-3xl lt-sm:text-xl;
|
46
|
+
.el-breadcrumb__separator {
|
47
|
+
@apply text-size-5 text-gray-5;
|
48
|
+
font-weight: normal;
|
49
|
+
}
|
50
|
+
}
|
51
|
+
</style>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<script lang="ts" setup>
|
2
|
+
import { ElBreadcrumbItem, breadcrumbItemProps } from 'element-plus/es/components/breadcrumb/index'
|
3
|
+
defineProps(breadcrumbItemProps)
|
4
|
+
</script>
|
5
|
+
|
6
|
+
<template>
|
7
|
+
<ElBreadcrumbItem v-bind="$props">
|
8
|
+
<slot />
|
9
|
+
</ElBreadcrumbItem>
|
10
|
+
</template>
|
11
|
+
|
12
|
+
<style lang="scss" scoped>
|
13
|
+
|
14
|
+
</style>
|