@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,123 @@
1
+ <template>
2
+ <div class="card tags" v-if="tags.length">
3
+ <!-- 头部 -->
4
+ <div class="card-header">
5
+ <span class="title">🏷 标签</span>
6
+ <el-tag
7
+ v-if="activeTag.label"
8
+ :type="(activeTag.type as any)"
9
+ :effect="colorMode"
10
+ closable
11
+ @close="handleCloseTag"
12
+ >
13
+ {{ activeTag.label }}
14
+ </el-tag>
15
+ </div>
16
+ <!-- 标签列表 -->
17
+ <ul class="tag-list">
18
+ <li v-for="(tag, idx) in tags" :key="tag">
19
+ <el-tag
20
+ :type="tagType[idx % tagType.length]"
21
+ @click="handleTagClick(tag, tagType[idx % tagType.length])"
22
+ :effect="colorMode"
23
+ >
24
+ {{ tag }}
25
+ </el-tag>
26
+ </li>
27
+ </ul>
28
+ </div>
29
+ </template>
30
+
31
+ <script lang="ts" setup>
32
+ import { computed, onMounted } from 'vue'
33
+ import { ElTag } from 'element-plus'
34
+ import { useDark } from '@vueuse/core'
35
+ import { useRouter } from 'vitepress'
36
+ import { useActiveTag, useArticles } from '../composables/config/blog'
37
+
38
+ const docs = useArticles()
39
+
40
+ const tags = computed(() => {
41
+ return [...new Set(docs.value.map((v) => v.meta.tag || []).flat(3))]
42
+ })
43
+
44
+ const activeTag = useActiveTag()
45
+
46
+ const isDark = useDark({
47
+ storageKey: 'vitepress-theme-appearance'
48
+ })
49
+
50
+ const colorMode = computed(() => (isDark.value ? 'light' : 'dark'))
51
+
52
+ const tagType: any = ['', 'info', 'success', 'warning', 'danger']
53
+
54
+ const handleCloseTag = () => {
55
+ activeTag.value.label = ''
56
+ activeTag.value.type = ''
57
+ router.go(`${window.location.origin}${router.route.path}`)
58
+ }
59
+
60
+ const router = useRouter()
61
+ const handleTagClick = (tag: string, type: string) => {
62
+ if (tag === activeTag.value.label) {
63
+ handleCloseTag()
64
+ return
65
+ }
66
+ activeTag.value.type = type
67
+ activeTag.value.label = tag
68
+
69
+ router.go(
70
+ `${window.location.origin}${router.route.path}?tag=${tag}&type=${type}`
71
+ )
72
+ }
73
+ onMounted(() => {
74
+ const url = new URL(window.location.href)
75
+ activeTag.value.type = url.searchParams.get('type') || ''
76
+ activeTag.value.label = url.searchParams.get('tag') || ''
77
+ })
78
+ </script>
79
+
80
+ <style lang="scss" scoped>
81
+ .card {
82
+ position: relative;
83
+ margin: 0 auto 10px;
84
+ padding: 10px;
85
+ width: 100%;
86
+ overflow: hidden;
87
+ border-radius: 0.25rem;
88
+ box-shadow: var(--box-shadow);
89
+ box-sizing: border-box;
90
+ transition: all 0.3s;
91
+ background-color: rgba(var(--bg-gradient));
92
+ display: flex;
93
+
94
+ &:hover {
95
+ box-shadow: var(--box-shadow-hover);
96
+ }
97
+ }
98
+ .card-header {
99
+ display: flex;
100
+ width: 100%;
101
+ justify-content: space-between;
102
+ align-items: center;
103
+
104
+ .title {
105
+ font-size: 12px;
106
+ }
107
+ }
108
+
109
+ .tags {
110
+ flex-direction: column;
111
+ }
112
+
113
+ .tag-list {
114
+ display: flex;
115
+ flex-wrap: wrap;
116
+ margin-top: 10px;
117
+ li {
118
+ margin-right: 10px;
119
+ margin-bottom: 10px;
120
+ cursor: pointer;
121
+ }
122
+ }
123
+ </style>
@@ -0,0 +1,177 @@
1
+ <template>
2
+ <div class="card recommend" v-if="recommendList.length || empty">
3
+ <!-- 头部 -->
4
+ <div class="card-header">
5
+ <span class="title">{{ title }}</span>
6
+ <el-button
7
+ v-if="showChangeBtn"
8
+ size="small"
9
+ type="primary"
10
+ text
11
+ @click="changePage"
12
+ >{{ nextText }}</el-button
13
+ >
14
+ </div>
15
+ <!-- 文章列表 -->
16
+ <ol class="recommend-container" v-if="currentWikiData.length">
17
+ <li v-for="(v, idx) in currentWikiData" :key="v.route">
18
+ <!-- 序号 -->
19
+ <i class="num">{{ idx + 1 }}</i>
20
+ <!-- 简介 -->
21
+ <div class="des">
22
+ <!-- title -->
23
+ <el-link type="info" class="title" :href="v.route">{{
24
+ v.meta.title
25
+ }}</el-link>
26
+ <!-- 描述信息 -->
27
+ <div class="suffix">
28
+ <!-- 日期 -->
29
+ <span class="tag">{{ formatShowDate(v.meta.date) }}</span>
30
+ </div>
31
+ </div>
32
+ </li>
33
+ </ol>
34
+ <div class="empty-text" v-else>{{ empty }}</div>
35
+ </div>
36
+ </template>
37
+
38
+ <script lang="ts" setup>
39
+ import { ref, computed } from 'vue'
40
+ import { ElButton, ElLink } from 'element-plus'
41
+ import { useArticles, useBlogConfig } from '../composables/config/blog'
42
+ import { formatShowDate } from '../utils/index'
43
+
44
+ const { hotArticle } = useBlogConfig()
45
+ const title = computed(() => hotArticle?.title || '🔥 精选文章')
46
+ const nextText = computed(() => hotArticle?.nextText || '换一组')
47
+ const pageSize = computed(() => hotArticle?.pageSize || 9)
48
+ const empty = computed(() => hotArticle?.empty ?? '暂无精选内容')
49
+
50
+ const docs = useArticles()
51
+
52
+ const recommendList = computed(() => {
53
+ const data = docs.value.filter((v) => v.meta.sticky)
54
+ data.sort((a, b) => b.meta.sticky! - a.meta.sticky!)
55
+ return [...data]
56
+ })
57
+
58
+ const currentPage = ref(1)
59
+ const changePage = () => {
60
+ const newIdx =
61
+ currentPage.value % Math.ceil(recommendList.value.length / pageSize.value)
62
+ currentPage.value = newIdx + 1
63
+ }
64
+
65
+ const currentWikiData = computed(() => {
66
+ const startIdx = (currentPage.value - 1) * pageSize.value
67
+ const endIdx = startIdx + pageSize.value
68
+ return recommendList.value.slice(startIdx, endIdx)
69
+ })
70
+
71
+ const showChangeBtn = computed(() => {
72
+ return recommendList.value.length > pageSize.value
73
+ })
74
+ </script>
75
+
76
+ <style lang="scss" scoped>
77
+ .card {
78
+ position: relative;
79
+ margin: 0 auto 10px;
80
+ padding: 10px;
81
+ width: 100%;
82
+ overflow: hidden;
83
+ border-radius: 0.25rem;
84
+ box-shadow: var(--box-shadow);
85
+ box-sizing: border-box;
86
+ transition: all 0.3s;
87
+ background-color: rgba(var(--bg-gradient));
88
+ display: flex;
89
+
90
+ &:hover {
91
+ box-shadow: var(--box-shadow-hover);
92
+ }
93
+ }
94
+
95
+ .card-header {
96
+ display: flex;
97
+ width: 100%;
98
+ justify-content: space-between;
99
+ align-items: center;
100
+
101
+ .title {
102
+ font-size: 12px;
103
+ }
104
+ }
105
+
106
+ .recommend {
107
+ flex-direction: column;
108
+ }
109
+
110
+ .recommend-container {
111
+ display: flex;
112
+ flex-direction: column;
113
+ list-style: none;
114
+ margin: 0;
115
+ padding: 0 10px 0 0px;
116
+ width: 100%;
117
+
118
+ li {
119
+ display: flex;
120
+
121
+ &:nth-child(1) .num {
122
+ background-color: #f56c6c;
123
+ color: #fff;
124
+ font-size: 12px;
125
+ border-radius: 8px 0 8px 0;
126
+ }
127
+
128
+ &:nth-child(2) .num {
129
+ background-color: #67c23a;
130
+ color: #fff;
131
+ font-size: 12px;
132
+ border-radius: 0 8px 0 8px;
133
+ }
134
+
135
+ &:nth-child(3) .num {
136
+ background-color: #409eff;
137
+ color: #fff;
138
+ font-size: 12px;
139
+ border-radius: 6px;
140
+ }
141
+
142
+ .num {
143
+ display: block;
144
+ font-size: 14px;
145
+ color: var(--description-font-color);
146
+ font-weight: 600;
147
+ margin: 6px 12px 10px 0;
148
+ width: 18px;
149
+ height: 18px;
150
+ line-height: 18px;
151
+ text-align: center;
152
+ }
153
+
154
+ .des {
155
+ overflow: hidden;
156
+ text-overflow: ellipsis;
157
+ white-space: nowrap;
158
+ }
159
+
160
+ .title {
161
+ font-size: 14px;
162
+ color: var(--vp-c-text-1);
163
+ }
164
+
165
+ .suffix {
166
+ font-size: 12px;
167
+ color: var(--vp-c-text-2);
168
+ }
169
+ }
170
+ }
171
+
172
+ .empty-text {
173
+ padding: 6px;
174
+ font-size: 14px;
175
+ text-align: center;
176
+ }
177
+ </style>
@@ -0,0 +1,52 @@
1
+ <script setup lang="ts">
2
+ import { ElImageViewer } from 'element-plus'
3
+ import { onMounted, onUnmounted, reactive, ref } from 'vue'
4
+
5
+ const show = ref(false)
6
+ const previewImageInfo = reactive<{ url: string; list: string[]; idx: number }>(
7
+ {
8
+ url: '',
9
+ list: [],
10
+ idx: 0
11
+ }
12
+ )
13
+ const previewImage = (e: Event) => {
14
+ const target = e.target as HTMLElement
15
+ const currentTarget = e.currentTarget as HTMLElement
16
+ if (target.tagName.toLowerCase() === 'img') {
17
+ const imgs = currentTarget.querySelectorAll<HTMLImageElement>(
18
+ '.content-container .main img'
19
+ )
20
+ const idx = Array.from(imgs).findIndex((el) => el === target)
21
+ const urls = Array.from(imgs).map((el) => el.src)
22
+
23
+ const url = target.getAttribute('src')
24
+ previewImageInfo.url = url!
25
+ previewImageInfo.list = urls
26
+ previewImageInfo.idx = idx
27
+
28
+ show.value = true
29
+ }
30
+ }
31
+ onMounted(() => {
32
+ const docDomContainer = document.querySelector('#VPContent')
33
+ docDomContainer?.addEventListener('click', previewImage)
34
+ })
35
+
36
+ onUnmounted(() => {
37
+ const docDomContainer = document.querySelector('#VPContent')
38
+ docDomContainer?.removeEventListener('click', previewImage)
39
+ })
40
+ </script>
41
+
42
+ <template>
43
+ <ElImageViewer
44
+ :infinite="false"
45
+ hide-on-click-modal
46
+ teleported
47
+ @close="show = false"
48
+ :url-list="previewImageInfo.list"
49
+ :initial-index="previewImageInfo.idx"
50
+ v-if="show"
51
+ />
52
+ </template>
@@ -0,0 +1,112 @@
1
+ <template>
2
+ <a class="blog-item" :href="route">
3
+ <!-- 左侧信息 -->
4
+ <div class="info-part">
5
+ <!-- 标题 -->
6
+ <p class="title">{{ title }}</p>
7
+ <!-- 简短描述 -->
8
+ <p class="description" v-if="!!description">{{ description }}</p>
9
+ <div class="badge-list">
10
+ <span class="split" v-if="author">{{ author }}</span>
11
+ <span class="split">{{ showTime }}</span>
12
+ <span class="split" v-if="tag?.length">{{ tag.join(' · ') }}</span>
13
+ </div>
14
+ </div>
15
+ <div
16
+ v-if="cover"
17
+ class="cover-img"
18
+ :style="`background-image: url(${cover});`"
19
+ ></div>
20
+ </a>
21
+ </template>
22
+
23
+ <script lang="ts" setup>
24
+ import { computed } from 'vue'
25
+ import { formatShowDate } from '../utils/index'
26
+
27
+ const props = defineProps<{
28
+ route: string
29
+ title: string
30
+ date: string | Date
31
+ sticky?: number
32
+ description?: string
33
+ tag?: string[]
34
+ author?: string
35
+ cover?: string
36
+ }>()
37
+
38
+ const showTime = computed(() => {
39
+ return formatShowDate(props.date)
40
+ })
41
+ </script>
42
+
43
+ <style lang="scss" scoped>
44
+ .blog-item {
45
+ position: relative;
46
+ margin: 0 auto 20px;
47
+ padding: 16px 20px;
48
+ width: 100%;
49
+ overflow: hidden;
50
+ border-radius: 0.25rem;
51
+ box-shadow: var(--box-shadow);
52
+ box-sizing: border-box;
53
+ transition: all 0.3s;
54
+ background-color: rgba(var(--bg-gradient));
55
+ cursor: pointer;
56
+ display: flex;
57
+ align-items: center;
58
+ &:hover {
59
+ box-shadow: var(--box-shadow-hover);
60
+ }
61
+ }
62
+
63
+ .info-part {
64
+ flex: 1;
65
+ }
66
+ .title {
67
+ font-size: 18px;
68
+ font-weight: 600;
69
+ margin-bottom: 8px;
70
+ }
71
+ .description {
72
+ color: var(--description-font-color);
73
+ font-size: 14px;
74
+ margin-bottom: 8px;
75
+ // 多行换行
76
+ overflow: hidden;
77
+ text-overflow: ellipsis;
78
+ display: -webkit-box;
79
+ -webkit-line-clamp: 2;
80
+ -webkit-box-orient: vertical;
81
+ }
82
+ .badge-list {
83
+ font-size: 13px;
84
+ color: var(--badge-font-color);
85
+ .split:not(:last-child) {
86
+ &::after {
87
+ content: '';
88
+ display: inline-block;
89
+ width: 1px;
90
+ height: 8px;
91
+ margin: 0 10px;
92
+ background-color: #4e5969;
93
+ }
94
+ }
95
+ }
96
+ .cover-img {
97
+ width: 120px;
98
+ height: 80px;
99
+ margin-left: 24px;
100
+ border-radius: 2px;
101
+ background-repeat: no-repeat;
102
+ background-size: 120px 80px;
103
+ }
104
+
105
+ @media screen and (max-width: 500px) {
106
+ .cover-img {
107
+ width: 100px;
108
+ height: 60px;
109
+ background-size: 100px 60px;
110
+ }
111
+ }
112
+ </style>
@@ -0,0 +1,75 @@
1
+ <template>
2
+ <ul>
3
+ <li v-for="v in currentWikiData" :key="v.route">
4
+ <blog-item
5
+ :route="v.route"
6
+ :title="v.meta.title"
7
+ :description="v.meta.description"
8
+ :date="v.meta.date"
9
+ :tag="v.meta.tag"
10
+ :cover="v.meta.cover"
11
+ :author="v.meta.author || globalAuthor"
12
+ />
13
+ </li>
14
+ </ul>
15
+ <el-pagination
16
+ v-if="wikiList.length >= pageSize"
17
+ small
18
+ background
19
+ v-model:current-page="currentPage"
20
+ :page-size="pageSize"
21
+ :total="filterData.length"
22
+ layout="prev, pager, next, jumper"
23
+ />
24
+ </template>
25
+ <script setup lang="ts">
26
+ import { computed, ref } from 'vue'
27
+ import { ElPagination } from 'element-plus'
28
+ import { useData } from 'vitepress'
29
+ import BlogItem from './BlogItem.vue'
30
+ import {
31
+ useArticles,
32
+ useActiveTag,
33
+ useBlogConfig
34
+ } from '../composables/config/blog'
35
+ import { Theme } from '../composables/config'
36
+
37
+ const { theme, frontmatter } = useData<Theme.Config>()
38
+ const globalAuthor = computed(() => theme.value.blog.author || '')
39
+ const docs = useArticles()
40
+
41
+ const activeTag = useActiveTag()
42
+
43
+ const activeTagLabel = computed(() => activeTag.value.label)
44
+
45
+ const wikiList = computed(() => {
46
+ const data = docs.value
47
+ .filter((v) => v.meta.date && v.meta.title)
48
+ .filter((v) => !v.meta.hidden)
49
+ data.sort((a, b) => +new Date(b.meta.date) - +new Date(a.meta.date))
50
+ return data
51
+ })
52
+
53
+ const filterData = computed(() => {
54
+ if (!activeTagLabel.value) return wikiList.value
55
+ return wikiList.value.filter((v) =>
56
+ v.meta?.tag?.includes(activeTagLabel.value)
57
+ )
58
+ })
59
+
60
+ const { home } = useBlogConfig()
61
+ const pageSize = computed(
62
+ () => frontmatter.value.blog?.pageSize || home?.pageSize || 6
63
+ )
64
+ const currentPage = ref(1)
65
+
66
+ const currentWikiData = computed(() => {
67
+ const startIdx = (currentPage.value - 1) * pageSize.value
68
+ const endIdx = startIdx + pageSize.value
69
+ return filterData.value.slice(startIdx, endIdx)
70
+ })
71
+ </script>
72
+ <style lang="scss" scoped>
73
+ .blog-list {
74
+ }
75
+ </style>