@sugarat/theme 0.1.0

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.
@@ -0,0 +1,154 @@
1
+ <template>
2
+ <div class="doc-analyze" v-if="showAnalyze">
3
+ <span>
4
+ <el-icon><EditPen /></el-icon>
5
+ 字数:{{ wordCount }} 个字
6
+ </span>
7
+ <span>
8
+ <el-icon><AlarmClock /></el-icon>
9
+ 预计:{{ readTime }} 分钟
10
+ </span>
11
+ </div>
12
+ <div class="meta-des" ref="$des" id="hack-article-des">
13
+ <span v-if="author">
14
+ <el-icon><UserFilled /></el-icon>
15
+ {{ author }}
16
+ </span>
17
+ <span>
18
+ <el-icon><Clock /></el-icon>
19
+ {{ publishDate }}
20
+ </span>
21
+ </div>
22
+ </template>
23
+
24
+ <script lang="ts" setup>
25
+ // 阅读时间计算方式参考
26
+ // https://zhuanlan.zhihu.com/p/36375802
27
+ import { useData, useRoute } from 'vitepress'
28
+ import { computed, onMounted, ref, watch } from 'vue'
29
+ import { ElIcon } from 'element-plus'
30
+ import { UserFilled, Clock, EditPen, AlarmClock } from '@element-plus/icons-vue'
31
+ import { useBlogConfig, useCurrentArticle } from '../composables/config/blog'
32
+ import countWord, { formatShowDate } from '../utils/index'
33
+ import { Theme } from '../composables/config'
34
+
35
+ const { article } = useBlogConfig()
36
+ const { frontmatter } = useData()
37
+ const showAnalyze = computed(
38
+ () => frontmatter.value?.readingTime ?? article?.readingTime ?? true
39
+ )
40
+
41
+ const wordCount = ref(0)
42
+ const imageCount = ref(0)
43
+ const wordTime = computed(() => {
44
+ return ~~((wordCount.value / 275) * 60)
45
+ })
46
+
47
+ const imageTime = computed(() => {
48
+ const n = imageCount.value
49
+ if (imageCount.value <= 10) {
50
+ // 等差数列求和
51
+ return n * 13 + (n * (n - 1)) / 2
52
+ }
53
+ return 175 + (n - 10) * 3
54
+ })
55
+
56
+ const readTime = computed(() => {
57
+ return Math.ceil((wordTime.value + imageTime.value) / 60)
58
+ })
59
+
60
+ const route = useRoute()
61
+ const $des = ref<HTMLDivElement>()
62
+
63
+ const analyze = () => {
64
+ if (!$des.value) {
65
+ return
66
+ }
67
+ const docDomContainer = window.document.querySelector('#VPContent')
68
+ const imgs = docDomContainer?.querySelectorAll<HTMLImageElement>(
69
+ '.content-container .main img'
70
+ )
71
+ imageCount.value = imgs?.length || 0
72
+
73
+ const words =
74
+ docDomContainer?.querySelector('.content-container .main')?.textContent ||
75
+ ''
76
+
77
+ wordCount.value = countWord(words)
78
+ docDomContainer?.querySelector('h1')?.after($des.value!)
79
+ }
80
+
81
+ onMounted(() => {
82
+ const observer = new MutationObserver(() => {
83
+ const targetInstance = document.querySelector('#hack-article-des')
84
+ if (!targetInstance) {
85
+ analyze()
86
+ }
87
+ })
88
+ observer.observe(document.body, {
89
+ childList: true, // 观察目标子节点的变化,是否有添加或者删除
90
+ subtree: true // 观察后代节点,默认为 false
91
+ })
92
+
93
+ // 初始化时执行一次
94
+ analyze()
95
+ })
96
+
97
+ // 阅读量
98
+ const pv = ref(6666)
99
+
100
+ const currentArticle = useCurrentArticle()
101
+ const publishDate = computed(() => {
102
+ return formatShowDate(currentArticle.value?.meta?.date || '')
103
+ })
104
+
105
+ const { theme } = useData<Theme.Config>()
106
+ const globalAuthor = computed(() => theme.value.blog.author || '')
107
+ const author = computed(
108
+ () => currentArticle.value?.meta.author || globalAuthor.value
109
+ )
110
+
111
+ watch(
112
+ () => route.path,
113
+ () => {
114
+ // TODO: 调用接口取数据
115
+ pv.value = 123
116
+ },
117
+ {
118
+ immediate: true
119
+ }
120
+ )
121
+ </script>
122
+
123
+ <style lang="scss" scoped>
124
+ .doc-analyze {
125
+ color: var(--vp-c-text-2);
126
+ font-size: 14px;
127
+ margin-bottom: 20px;
128
+ display: flex;
129
+ justify-content: center;
130
+ span {
131
+ margin-right: 16px;
132
+ display: flex;
133
+ align-items: center;
134
+ .el-icon {
135
+ margin-right: 4px;
136
+ }
137
+ }
138
+ }
139
+ .meta-des {
140
+ text-align: left;
141
+ color: var(--vp-c-text-2);
142
+ font-size: 14px;
143
+ margin-top: 6px;
144
+ display: flex;
145
+ span {
146
+ margin-right: 16px;
147
+ display: flex;
148
+ align-items: center;
149
+ .el-icon {
150
+ margin-right: 4px;
151
+ }
152
+ }
153
+ }
154
+ </style>
@@ -0,0 +1,131 @@
1
+ <template>
2
+ <div class="comment" v-if="show" id="giscus-comment">
3
+ <el-affix
4
+ :class="{ hidden: !showCommnetAffix }"
5
+ class="comment-btn"
6
+ target="main"
7
+ position="bottom"
8
+ @change="handleVisibleChange"
9
+ :offset="40"
10
+ >
11
+ <el-button
12
+ @click="handleScrollToComment"
13
+ plain
14
+ :icon="Comment"
15
+ type="primary"
16
+ >评论</el-button
17
+ >
18
+ </el-affix>
19
+ <component
20
+ v-if="showComment"
21
+ :is="'script'"
22
+ src="https://giscus.app/client.js"
23
+ :data-repo="commentConfig.repo"
24
+ :data-repo-id="commentConfig.repoId"
25
+ :data-category="commentConfig.category"
26
+ :data-category-id="commentConfig.categoryId"
27
+ :data-mapping="commentConfig.mapping || 'pathname'"
28
+ data-reactions-enabled="1"
29
+ data-emit-metadata="0"
30
+ :data-input-position="commentConfig.inputPosition || 'top'"
31
+ :data-theme="isDark ? 'dark' : 'light'"
32
+ :data-lang="commentConfig.lang || 'zh-CN'"
33
+ crossorigin="anonymous"
34
+ :data-loading="commentConfig.loading || ''"
35
+ async
36
+ >
37
+ </component>
38
+ </div>
39
+ </template>
40
+ <script setup lang="ts">
41
+ import { useDark } from '@vueuse/core'
42
+ import { useData, useRoute } from 'vitepress'
43
+ import { computed, ref, watch } from 'vue'
44
+ import { ElAffix, ElButton } from 'element-plus'
45
+ import { Comment } from '@element-plus/icons-vue'
46
+ import { useGiscusConfig } from '../composables/config/blog'
47
+ import { Theme } from '../composables/config/index'
48
+
49
+ const { frontmatter } = useData()
50
+ const showCommnetAffix = ref(true)
51
+ const handleVisibleChange = (v: boolean) => {
52
+ showCommnetAffix.value = v
53
+ }
54
+ const handleScrollToComment = () => {
55
+ document.querySelector('#giscus-comment')?.scrollIntoView({
56
+ behavior: 'smooth',
57
+ block: 'start'
58
+ })
59
+ }
60
+ const giscusConfig = useGiscusConfig()
61
+
62
+ const commentConfig = computed<Partial<Theme.GiscusConfig>>(() => {
63
+ if (!giscusConfig) {
64
+ return {}
65
+ }
66
+ return giscusConfig
67
+ })
68
+
69
+ const show = computed(() => {
70
+ if (frontmatter.value.comment === false) {
71
+ return frontmatter.value.comment
72
+ }
73
+ if (!giscusConfig) {
74
+ return giscusConfig
75
+ }
76
+ return (
77
+ giscusConfig.repo &&
78
+ giscusConfig.repoId &&
79
+ giscusConfig.category &&
80
+ giscusConfig.categoryId
81
+ )
82
+ })
83
+
84
+ const isDark = useDark({
85
+ storageKey: 'vitepress-theme-appearance'
86
+ })
87
+
88
+ const route = useRoute()
89
+ const showComment = ref(true)
90
+ watch(
91
+ () => route.path,
92
+ () => {
93
+ showComment.value = false
94
+ setTimeout(() => {
95
+ showComment.value = true
96
+ }, 100)
97
+ }
98
+ )
99
+ </script>
100
+ <style scoped lang="scss">
101
+ .comment {
102
+ width: 100%;
103
+ text-align: center;
104
+ padding: 40px 0;
105
+ }
106
+
107
+ .hidden {
108
+ opacity: 0;
109
+ pointer-events: none;
110
+ }
111
+ .comment-btn {
112
+ :deep(.el-affix--fixed) {
113
+ text-align: right;
114
+ .el-button {
115
+ position: relative;
116
+ right: -100px;
117
+ }
118
+ }
119
+ }
120
+
121
+ @media screen and (max-width: 1200px) {
122
+ .comment-btn {
123
+ :deep(.el-affix--fixed) {
124
+ opacity: 0.7;
125
+ .el-button {
126
+ position: static;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ </style>
@@ -0,0 +1,94 @@
1
+ <template>
2
+ <div class="card friend-wrapper" v-if="friend?.length">
3
+ <!-- 头部 -->
4
+ <div class="card-header">
5
+ <span class="title">🤝 友情链接</span>
6
+ </div>
7
+ <!-- 文章列表 -->
8
+ <ol class="friend-list">
9
+ <li v-for="v in friend" :key="v.nickname">
10
+ <a :href="v.url" target="_blank">
11
+ <el-avatar :size="50" :src="v.avatar" />
12
+ <div>
13
+ <span class="nickname">{{ v.nickname }}</span>
14
+ <p class="des">{{ v.des }}</p>
15
+ </div>
16
+ </a>
17
+ </li>
18
+ </ol>
19
+ </div>
20
+ </template>
21
+
22
+ <script lang="ts" setup>
23
+ import { ElAvatar } from 'element-plus'
24
+ import { useBlogConfig } from '../composables/config/blog'
25
+
26
+ const { friend } = useBlogConfig()
27
+ </script>
28
+
29
+ <style lang="scss" scoped>
30
+ .card {
31
+ position: relative;
32
+ margin: 0 auto 10px;
33
+ padding: 10px;
34
+ width: 100%;
35
+ overflow: hidden;
36
+ border-radius: 0.25rem;
37
+ box-shadow: var(--box-shadow);
38
+ box-sizing: border-box;
39
+ transition: all 0.3s;
40
+ background-color: rgba(var(--bg-gradient));
41
+ display: flex;
42
+
43
+ &:hover {
44
+ box-shadow: var(--box-shadow-hover);
45
+ }
46
+ }
47
+
48
+ .card-header {
49
+ display: flex;
50
+ width: 100%;
51
+ justify-content: space-between;
52
+ align-items: center;
53
+
54
+ .title {
55
+ font-size: 12px;
56
+ }
57
+ }
58
+
59
+ .friend-wrapper {
60
+ flex-direction: column;
61
+ }
62
+
63
+ .friend-list {
64
+ display: flex;
65
+ flex-direction: column;
66
+ list-style: none;
67
+ margin: 0;
68
+ padding: 0 10px 0 0px;
69
+ width: 100%;
70
+
71
+ li {
72
+ padding: 6px;
73
+ margin-top: 10px;
74
+ .el-avatar {
75
+ min-width: 50px;
76
+ }
77
+ a {
78
+ display: flex;
79
+ }
80
+ div {
81
+ padding-left: 10px;
82
+ }
83
+ .nickname {
84
+ font-size: 16px;
85
+ font-weight: 450;
86
+ }
87
+
88
+ .des {
89
+ color: var(--vp-c-text-2);
90
+ font-size: 14px;
91
+ }
92
+ }
93
+ }
94
+ </style>
@@ -0,0 +1,105 @@
1
+ <template>
2
+ <div>
3
+ <h1>
4
+ <span class="name">{{ name }}</span>
5
+ <span class="motto" v-show="motto">{{ motto }}</span>
6
+ </h1>
7
+ <div class="inspiring-wrapper">
8
+ <h2 @click="changeSlogan" v-show="!!inspiring">{{ inspiring }}</h2>
9
+ </div>
10
+ </div>
11
+ </template>
12
+ <script setup lang="ts">
13
+ import { computed, ref } from 'vue'
14
+ import { useData } from 'vitepress'
15
+ import { useHomeConfig, useBlogConfig } from '../composables/config/blog'
16
+
17
+ const { site, frontmatter } = useData()
18
+ const { home } = useBlogConfig()
19
+
20
+ const name = computed(
21
+ () => (frontmatter.value.blog?.name ?? site.value.title) || home?.name || ''
22
+ )
23
+ const motto = computed(() => frontmatter.value.blog?.motto || home?.motto || '')
24
+ const initInspiring = ref<string>(
25
+ frontmatter.value.blog?.inspiring || home?.inspiring || ''
26
+ )
27
+ const inspiring = computed({
28
+ get() {
29
+ return initInspiring.value
30
+ },
31
+ set(newValue) {
32
+ initInspiring.value = newValue
33
+ }
34
+ })
35
+
36
+ const homeConfig = useHomeConfig()
37
+
38
+ const changeSlogan = async () => {
39
+ if (typeof homeConfig?.handleChangeSlogan !== 'function') {
40
+ return
41
+ }
42
+ const newSlogan = await homeConfig.handleChangeSlogan(inspiring.value)
43
+ if (typeof newSlogan !== 'string' || !newSlogan.trim()) {
44
+ return
45
+ }
46
+
47
+ // 重新渲染数据,同时触发动画
48
+ inspiring.value = ''
49
+ setTimeout(async () => {
50
+ inspiring.value = newSlogan
51
+ })
52
+ }
53
+ </script>
54
+ <style lang="scss" scoped>
55
+ h1 {
56
+ text-align: center;
57
+ .name {
58
+ transition: all 0.25s ease-in-out 0.04s;
59
+ transform: translateY(0px);
60
+ opacity: 1;
61
+ font-weight: bold;
62
+ margin: 0 auto;
63
+ font-size: 36px;
64
+ }
65
+
66
+ .motto {
67
+ position: relative;
68
+ bottom: 0px;
69
+ font-size: 14px;
70
+ margin-left: 10px;
71
+
72
+ &::before {
73
+ content: '- ';
74
+ }
75
+ }
76
+ }
77
+
78
+ @media screen and (max-width: 500px) {
79
+ .motto {
80
+ display: none;
81
+ }
82
+ }
83
+ @keyframes fade-in {
84
+ 0% {
85
+ opacity: 0;
86
+ }
87
+
88
+ 100% {
89
+ opacity: 1;
90
+ }
91
+ }
92
+
93
+ .inspiring-wrapper {
94
+ margin: 16px 0;
95
+ height: 24px;
96
+ width: auto;
97
+ h2 {
98
+ animation: fade-in 0.5s ease-in-out;
99
+ cursor: pointer;
100
+ text-align: center;
101
+ font-size: 20px;
102
+ line-height: 1.6;
103
+ }
104
+ }
105
+ </style>
@@ -0,0 +1,38 @@
1
+ <script lang="ts" setup>
2
+ import BlogHomeOverview from './BlogHomeOverview.vue'
3
+ import BlogHotArticle from './BlogHotArticle.vue'
4
+ import BlogHomeTags from './BlogHomeTags.vue'
5
+ import BlogFriendLink from './BlogFriendLink.vue'
6
+ </script>
7
+
8
+ <template>
9
+ <div class="blog-info">
10
+ <!-- 统计数据,日后支持,点击筛选出左侧的数据 -->
11
+ <BlogHomeOverview />
12
+
13
+ <!-- 置顶的一些文章 -->
14
+ <BlogHotArticle />
15
+
16
+ <!-- 友链 -->
17
+ <BlogFriendLink />
18
+
19
+ <!-- 标签 -->
20
+ <BlogHomeTags />
21
+ </div>
22
+ </template>
23
+
24
+ <style lang="scss" scoped>
25
+ .blog-info {
26
+ display: flex;
27
+ flex-direction: column;
28
+ min-width: 240px;
29
+ position: relative;
30
+ box-sizing: border-box;
31
+ }
32
+
33
+ @media screen and (min-width: 767px) {
34
+ .blog-info {
35
+ max-width: 300px;
36
+ }
37
+ }
38
+ </style>
@@ -0,0 +1,97 @@
1
+ <template>
2
+ <div class="card overview-data">
3
+ <div class="overview-item">
4
+ <span class="count">{{ notHiddenArticles.length }}</span>
5
+ <span class="label">博客文章</span>
6
+ </div>
7
+ <div class="split"></div>
8
+ <div class="overview-item">
9
+ <span class="count">+{{ currentMonth?.length }}</span>
10
+ <span class="label">本月更新</span>
11
+ </div>
12
+ <div class="split"></div>
13
+ <div class="overview-item">
14
+ <span class="count">+{{ currentWeek?.length }}</span>
15
+ <span class="label">本周更新</span>
16
+ </div>
17
+ </div>
18
+ </template>
19
+
20
+ <script lang="ts" setup>
21
+ import { computed } from 'vue'
22
+ import { isCurrentWeek } from '../utils'
23
+ import { useArticles } from '../composables/config/blog'
24
+
25
+ const docs = useArticles()
26
+ const notHiddenArticles = computed(() => {
27
+ return docs.value.filter((v) => !v.meta.hidden)
28
+ })
29
+ const nowMonth = new Date().getMonth()
30
+ const nowYear = new Date().getFullYear()
31
+ const currentMonth = computed(() => {
32
+ return notHiddenArticles.value.filter((v) => {
33
+ const pubDate = new Date(v.meta?.date)
34
+ return pubDate?.getMonth() === nowMonth && pubDate.getFullYear() === nowYear
35
+ })
36
+ })
37
+
38
+ const currentWeek = computed(() => {
39
+ return notHiddenArticles.value.filter((v) => {
40
+ const pubDate = new Date(v.meta?.date)
41
+ return isCurrentWeek(pubDate)
42
+ })
43
+ })
44
+ </script>
45
+
46
+ <style lang="scss" scoped>
47
+ .card {
48
+ position: relative;
49
+ margin: 0 auto 10px;
50
+ padding: 10px;
51
+ width: 100%;
52
+ overflow: hidden;
53
+ border-radius: 0.25rem;
54
+ box-shadow: var(--box-shadow);
55
+ box-sizing: border-box;
56
+ transition: all 0.3s;
57
+ background-color: rgba(var(--bg-gradient));
58
+ display: flex;
59
+
60
+ &:hover {
61
+ box-shadow: var(--box-shadow-hover);
62
+ }
63
+ }
64
+
65
+ .overview-data {
66
+ width: 100%;
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: space-around;
70
+ }
71
+
72
+ .split {
73
+ width: 1px;
74
+ opacity: 0.8;
75
+ height: 10px;
76
+ background-color: var(--badge-font-color);
77
+ }
78
+
79
+ .overview-item {
80
+ display: flex;
81
+ flex-direction: column;
82
+ justify-content: center;
83
+ align-items: center;
84
+ position: relative;
85
+ margin: 0 10px;
86
+
87
+ .count {
88
+ font-size: 18px;
89
+ }
90
+
91
+ .label {
92
+ margin-top: 6px;
93
+ font-size: 12px;
94
+ color: var(--description-font-color);
95
+ }
96
+ }
97
+ </style>