@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.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/node.d.ts +174 -0
- package/node.js +185 -0
- package/package.json +57 -0
- package/src/components/BlogAlert.vue +74 -0
- package/src/components/BlogApp.vue +103 -0
- package/src/components/BlogArticleAnalyze.vue +154 -0
- package/src/components/BlogComment.vue +131 -0
- package/src/components/BlogFriendLink.vue +94 -0
- package/src/components/BlogHomeBanner.vue +105 -0
- package/src/components/BlogHomeInfo.vue +38 -0
- package/src/components/BlogHomeOverview.vue +97 -0
- package/src/components/BlogHomeTags.vue +123 -0
- package/src/components/BlogHotArticle.vue +177 -0
- package/src/components/BlogImagePreview.vue +52 -0
- package/src/components/BlogItem.vue +112 -0
- package/src/components/BlogList.vue +75 -0
- package/src/components/BlogPopover.vue +218 -0
- package/src/components/BlogRecommendArticle.vue +173 -0
- package/src/components/BlogSearch.vue +198 -0
- package/src/components/BlogSidebar.vue +19 -0
- package/src/composables/config/blog.ts +96 -0
- package/src/composables/config/index.ts +159 -0
- package/src/index.ts +18 -0
- package/src/node.ts +175 -0
- package/src/styles/bg.png +0 -0
- package/src/styles/index.scss +95 -0
- package/src/utils/index.ts +84 -0
- package/types/vue-shim.d.ts +6 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="theme-blog-popover" v-show="show">
|
|
3
|
+
<div class="header">
|
|
4
|
+
<div class="title-wrapper">
|
|
5
|
+
<el-icon size="20px"><Flag /></el-icon>
|
|
6
|
+
<span class="title">{{ popoverProps?.title }}</span>
|
|
7
|
+
</div>
|
|
8
|
+
<el-icon @click="show = false" class="close-icon" size="20px"
|
|
9
|
+
><CircleCloseFilled
|
|
10
|
+
/></el-icon>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="body content" v-if="bodyContent.length">
|
|
13
|
+
<PopoverValue v-for="(v, idx) in bodyContent" :key="idx" :item="v">
|
|
14
|
+
{{ v.type !== 'image' ? v.content : '' }}
|
|
15
|
+
</PopoverValue>
|
|
16
|
+
<hr v-if="footerContent.length" />
|
|
17
|
+
</div>
|
|
18
|
+
<div class="footer content">
|
|
19
|
+
<PopoverValue v-for="(v, idx) in footerContent" :key="idx" :item="v">
|
|
20
|
+
{{ v.type !== 'image' ? v.content : '' }}
|
|
21
|
+
</PopoverValue>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div
|
|
25
|
+
class="theme-blog-popover-close"
|
|
26
|
+
v-show="!show && (popoverProps?.reopen ?? true) && popoverProps?.title"
|
|
27
|
+
@click="show = true"
|
|
28
|
+
>
|
|
29
|
+
<el-icon size="20px"><Flag /></el-icon>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script lang="ts" setup>
|
|
34
|
+
import { ElIcon, ElButton } from 'element-plus'
|
|
35
|
+
import { Flag, CircleCloseFilled } from '@element-plus/icons-vue'
|
|
36
|
+
import { computed, onMounted, ref, h } from 'vue'
|
|
37
|
+
import type { BlogPopover } from '@sugarat/theme'
|
|
38
|
+
import { parseStringStyle } from '@vue/shared'
|
|
39
|
+
import { useBlogConfig } from '../composables/config/blog'
|
|
40
|
+
|
|
41
|
+
const { popover: popoverProps } = useBlogConfig()
|
|
42
|
+
|
|
43
|
+
const show = ref(false)
|
|
44
|
+
|
|
45
|
+
const bodyContent = computed(() => {
|
|
46
|
+
return popoverProps?.body || []
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const footerContent = computed(() => {
|
|
50
|
+
return popoverProps?.footer || []
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
onMounted(() => {
|
|
54
|
+
if (!popoverProps?.title) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const storageKey = 'theme-blog-popover'
|
|
59
|
+
// 取旧值
|
|
60
|
+
const oldValue = localStorage.getItem(storageKey)
|
|
61
|
+
const newValue = JSON.stringify(popoverProps)
|
|
62
|
+
localStorage.setItem(storageKey, newValue)
|
|
63
|
+
|
|
64
|
+
// >= 0 每次都展示,区别是否自动消失
|
|
65
|
+
if (Number(popoverProps?.duration ?? '') >= 0) {
|
|
66
|
+
show.value = true
|
|
67
|
+
if (popoverProps?.duration) {
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
show.value = false
|
|
70
|
+
}, popoverProps?.duration)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (oldValue !== newValue && popoverProps?.duration === -1) {
|
|
75
|
+
// 当做新值处理
|
|
76
|
+
show.value = true
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const PopoverValue = (
|
|
81
|
+
props: { key: number; item: BlogPopover.Value },
|
|
82
|
+
{ slots }: any
|
|
83
|
+
) => {
|
|
84
|
+
const { key, item } = props
|
|
85
|
+
if (item.type === 'title') {
|
|
86
|
+
return h(
|
|
87
|
+
'h4',
|
|
88
|
+
{
|
|
89
|
+
style: parseStringStyle(item.style || '')
|
|
90
|
+
},
|
|
91
|
+
item.content
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
if (item.type === 'text') {
|
|
95
|
+
return h(
|
|
96
|
+
'p',
|
|
97
|
+
{
|
|
98
|
+
style: parseStringStyle(item.style || '')
|
|
99
|
+
},
|
|
100
|
+
item.content
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
if (item.type === 'image') {
|
|
104
|
+
return h('img', {
|
|
105
|
+
src: item.src,
|
|
106
|
+
style: parseStringStyle(item.style || '')
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
if (item.type === 'button') {
|
|
110
|
+
return h(
|
|
111
|
+
ElButton,
|
|
112
|
+
{
|
|
113
|
+
type: 'primary',
|
|
114
|
+
onClick: () => {
|
|
115
|
+
window.open(item.link, '_self')
|
|
116
|
+
},
|
|
117
|
+
style: parseStringStyle(item.style || ''),
|
|
118
|
+
...item.props
|
|
119
|
+
},
|
|
120
|
+
slots
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
return h(
|
|
124
|
+
'div',
|
|
125
|
+
{
|
|
126
|
+
key
|
|
127
|
+
},
|
|
128
|
+
''
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<style lang="scss" scoped>
|
|
134
|
+
.theme-blog-popover {
|
|
135
|
+
width: 258px;
|
|
136
|
+
position: fixed;
|
|
137
|
+
top: 80px;
|
|
138
|
+
right: 20px;
|
|
139
|
+
z-index: 19;
|
|
140
|
+
box-sizing: border-box;
|
|
141
|
+
border: 1px solid var(--el-color-primary-light-3);
|
|
142
|
+
border-radius: 6px;
|
|
143
|
+
background-color: rgba(var(--bg-gradient-home));
|
|
144
|
+
box-shadow: var(--box-shadow);
|
|
145
|
+
}
|
|
146
|
+
@media screen and (min-width: 760px) and (max-width: 1140px) {
|
|
147
|
+
.theme-blog-popover {
|
|
148
|
+
top: 200px;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
.header {
|
|
152
|
+
background-color: var(--el-color-primary-light-3);
|
|
153
|
+
color: #fff;
|
|
154
|
+
padding: 6px 4px;
|
|
155
|
+
display: flex;
|
|
156
|
+
justify-content: space-between;
|
|
157
|
+
align-items: center;
|
|
158
|
+
.close-icon {
|
|
159
|
+
cursor: pointer;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.title-wrapper {
|
|
164
|
+
display: flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
.title {
|
|
167
|
+
font-size: 14px;
|
|
168
|
+
padding-left: 6px;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.body {
|
|
173
|
+
box-sizing: border-box;
|
|
174
|
+
padding: 10px 10px 0;
|
|
175
|
+
hr {
|
|
176
|
+
border: none;
|
|
177
|
+
border-bottom: 1px solid #eaecef;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
.footer {
|
|
181
|
+
box-sizing: border-box;
|
|
182
|
+
padding: 10px;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.body.content,
|
|
186
|
+
.footer.content {
|
|
187
|
+
text-align: center;
|
|
188
|
+
h4 {
|
|
189
|
+
text-align: center;
|
|
190
|
+
font-size: 12px;
|
|
191
|
+
}
|
|
192
|
+
p {
|
|
193
|
+
text-align: center;
|
|
194
|
+
padding: 10px 0;
|
|
195
|
+
font-size: 14px;
|
|
196
|
+
}
|
|
197
|
+
img {
|
|
198
|
+
width: 100%;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.theme-blog-popover-close {
|
|
203
|
+
cursor: pointer;
|
|
204
|
+
opacity: 0.5;
|
|
205
|
+
position: fixed;
|
|
206
|
+
z-index: 19;
|
|
207
|
+
top: 80px;
|
|
208
|
+
right: 10px;
|
|
209
|
+
position: fixed;
|
|
210
|
+
background-color: var(--el-color-primary-light-3);
|
|
211
|
+
padding: 8px;
|
|
212
|
+
color: #fff;
|
|
213
|
+
font-size: 12px;
|
|
214
|
+
border-radius: 50%;
|
|
215
|
+
display: flex;
|
|
216
|
+
flex-direction: column;
|
|
217
|
+
}
|
|
218
|
+
</style>
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="card recommend" v-if="recommendList.length || emptyText">
|
|
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>{{ emptyText }}</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 { useRoute } from 'vitepress'
|
|
42
|
+
import { formatShowDate } from '../utils/index'
|
|
43
|
+
import { useArticles, useBlogConfig } from '../composables/config/blog'
|
|
44
|
+
|
|
45
|
+
const { recommend } = useBlogConfig()
|
|
46
|
+
const title = computed(() => recommend?.title || '🔍 相关文章')
|
|
47
|
+
const pageSize = computed(() => recommend?.pageSize || 9)
|
|
48
|
+
const nextText = computed(() => recommend?.nextText || '换一组')
|
|
49
|
+
const emptyText = computed(() => recommend?.empty ?? '暂无推荐文章')
|
|
50
|
+
|
|
51
|
+
const docs = useArticles()
|
|
52
|
+
|
|
53
|
+
const route = useRoute()
|
|
54
|
+
|
|
55
|
+
const recommendList = computed(() => {
|
|
56
|
+
const paths = route.path.split('/')
|
|
57
|
+
const origin = docs.value
|
|
58
|
+
// 过滤出公共路由前缀
|
|
59
|
+
// 限制为同路由前缀
|
|
60
|
+
.filter(
|
|
61
|
+
(v) =>
|
|
62
|
+
v.route.split('/').length === paths.length &&
|
|
63
|
+
v.route.startsWith(paths.slice(0, paths.length - 1).join('/'))
|
|
64
|
+
)
|
|
65
|
+
// 过滤出带标题的
|
|
66
|
+
.filter((v) => !!v.meta.title)
|
|
67
|
+
// 过滤掉自己
|
|
68
|
+
.filter((v) => v.route !== route.path.replace(/.html$/, ''))
|
|
69
|
+
|
|
70
|
+
origin.sort((a, b) => +new Date(b.meta.date) - +new Date(a.meta.date))
|
|
71
|
+
return origin
|
|
72
|
+
})
|
|
73
|
+
const currentPage = ref(1)
|
|
74
|
+
const changePage = () => {
|
|
75
|
+
const newIdx =
|
|
76
|
+
currentPage.value % Math.ceil(recommendList.value.length / pageSize.value)
|
|
77
|
+
currentPage.value = newIdx + 1
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const currentWikiData = computed(() => {
|
|
81
|
+
const startIdx = (currentPage.value - 1) * pageSize.value
|
|
82
|
+
const endIdx = startIdx + pageSize.value
|
|
83
|
+
return recommendList.value.slice(startIdx, endIdx)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const showChangeBtn = computed(() => {
|
|
87
|
+
return recommendList.value.length > pageSize.value
|
|
88
|
+
})
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<style lang="scss" scoped>
|
|
92
|
+
.card {
|
|
93
|
+
position: relative;
|
|
94
|
+
margin: 0 auto 10px;
|
|
95
|
+
padding: 10px;
|
|
96
|
+
width: 100%;
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
border-radius: 0.25rem;
|
|
99
|
+
box-shadow: var(--box-shadow);
|
|
100
|
+
box-sizing: border-box;
|
|
101
|
+
transition: all 0.3s;
|
|
102
|
+
background-color: rgba(var(--bg-gradient));
|
|
103
|
+
display: flex;
|
|
104
|
+
|
|
105
|
+
&:hover {
|
|
106
|
+
box-shadow: var(--box-shadow-hover);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.recommend {
|
|
111
|
+
flex-direction: column;
|
|
112
|
+
padding: 16px 10px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.recommend-container {
|
|
116
|
+
display: flex;
|
|
117
|
+
flex-direction: column;
|
|
118
|
+
list-style: none;
|
|
119
|
+
margin: 0;
|
|
120
|
+
padding: 0 10px 0 0px;
|
|
121
|
+
width: 100%;
|
|
122
|
+
|
|
123
|
+
li {
|
|
124
|
+
display: flex;
|
|
125
|
+
|
|
126
|
+
.num {
|
|
127
|
+
display: block;
|
|
128
|
+
font-size: 14px;
|
|
129
|
+
color: var(--description-font-color);
|
|
130
|
+
font-weight: 600;
|
|
131
|
+
margin: 6px 12px 10px 0;
|
|
132
|
+
width: 18px;
|
|
133
|
+
height: 18px;
|
|
134
|
+
line-height: 18px;
|
|
135
|
+
text-align: center;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.des {
|
|
139
|
+
overflow: hidden;
|
|
140
|
+
text-overflow: ellipsis;
|
|
141
|
+
white-space: nowrap;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.title {
|
|
145
|
+
font-size: 14px;
|
|
146
|
+
color: var(--vp-c-text-1);
|
|
147
|
+
word-break: break-all;
|
|
148
|
+
white-space: break-spaces;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.suffix {
|
|
152
|
+
font-size: 12px;
|
|
153
|
+
color: var(--vp-c-text-2);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.card-header {
|
|
159
|
+
display: flex;
|
|
160
|
+
width: 100%;
|
|
161
|
+
justify-content: space-between;
|
|
162
|
+
align-items: center;
|
|
163
|
+
margin-bottom: 10px;
|
|
164
|
+
.title {
|
|
165
|
+
font-size: 16px;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
.empty-text {
|
|
169
|
+
padding: 6px;
|
|
170
|
+
font-size: 14px;
|
|
171
|
+
text-align: center;
|
|
172
|
+
}
|
|
173
|
+
</style>
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="blog-search" v-if="openSearch">
|
|
3
|
+
<div class="nav-search-btn-wait" @click="searchModal = true">
|
|
4
|
+
<el-icon size="22px">
|
|
5
|
+
<Search />
|
|
6
|
+
</el-icon>
|
|
7
|
+
<span class="search-tip">搜索</span>
|
|
8
|
+
</div>
|
|
9
|
+
<el-dialog
|
|
10
|
+
class="search-dialog"
|
|
11
|
+
:fullscreen="isFullScreen"
|
|
12
|
+
append-to-body
|
|
13
|
+
modal
|
|
14
|
+
v-model="searchModal"
|
|
15
|
+
width="500px"
|
|
16
|
+
align-center
|
|
17
|
+
>
|
|
18
|
+
<template #header>
|
|
19
|
+
<el-input
|
|
20
|
+
ref="searchInput"
|
|
21
|
+
autofocus
|
|
22
|
+
size="large"
|
|
23
|
+
v-model="searchWords"
|
|
24
|
+
class="w-50 m-2"
|
|
25
|
+
placeholder="Search Docs"
|
|
26
|
+
:prefix-icon="Search"
|
|
27
|
+
/>
|
|
28
|
+
</template>
|
|
29
|
+
<el-empty v-if="!searchResult.length" description="No Result" />
|
|
30
|
+
<ul v-else>
|
|
31
|
+
<span>共:{{ searchResult.length }}个搜索结果</span>
|
|
32
|
+
<el-button
|
|
33
|
+
v-if="showNextResult"
|
|
34
|
+
type="primary"
|
|
35
|
+
size="small"
|
|
36
|
+
class="nextPage"
|
|
37
|
+
@click="handleNext"
|
|
38
|
+
>
|
|
39
|
+
换一组</el-button
|
|
40
|
+
>
|
|
41
|
+
<li v-for="item in showSearchResult" :key="item.route">
|
|
42
|
+
<el-card body-style="padding:10px;" shadow="hover">
|
|
43
|
+
<a :href="item.route" @click="searchModal = false">
|
|
44
|
+
<div class="title">
|
|
45
|
+
<span>{{ item.meta.title }}</span>
|
|
46
|
+
<span class="date">
|
|
47
|
+
{{ formatDate(item.meta.date, 'yyyy-MM-dd') }}</span
|
|
48
|
+
>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="des">
|
|
51
|
+
{{ item.meta.description }}
|
|
52
|
+
</div>
|
|
53
|
+
</a>
|
|
54
|
+
</el-card>
|
|
55
|
+
</li>
|
|
56
|
+
</ul>
|
|
57
|
+
</el-dialog>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<script lang="ts" setup>
|
|
62
|
+
import { computed, ref, watch } from 'vue'
|
|
63
|
+
import { Search } from '@element-plus/icons-vue'
|
|
64
|
+
import {
|
|
65
|
+
ElInput,
|
|
66
|
+
ElEmpty,
|
|
67
|
+
ElIcon,
|
|
68
|
+
ElDialog,
|
|
69
|
+
InputInstance,
|
|
70
|
+
ElCard,
|
|
71
|
+
ElButton
|
|
72
|
+
} from 'element-plus'
|
|
73
|
+
import { useWindowSize } from '@vueuse/core'
|
|
74
|
+
import { formatDate } from '../utils'
|
|
75
|
+
import { useArticles, useBlogConfig } from '../composables/config/blog'
|
|
76
|
+
|
|
77
|
+
const { search: openSearch = true } = useBlogConfig()
|
|
78
|
+
|
|
79
|
+
const searchModal = ref(false)
|
|
80
|
+
|
|
81
|
+
const { width } = useWindowSize()
|
|
82
|
+
const isFullScreen = computed(() => width.value < 500)
|
|
83
|
+
|
|
84
|
+
const searchWords = ref('')
|
|
85
|
+
const searchInput = ref<InputInstance>()
|
|
86
|
+
watch(
|
|
87
|
+
() => searchModal.value,
|
|
88
|
+
() => {
|
|
89
|
+
if (searchModal.value) {
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
searchInput.value?.focus()
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const docs = useArticles()
|
|
98
|
+
|
|
99
|
+
const searchResult = computed(() => {
|
|
100
|
+
if (!searchWords.value) return []
|
|
101
|
+
const result = docs.value.filter((v) =>
|
|
102
|
+
`${v.meta.description}${v.meta.title}`.includes(searchWords.value)
|
|
103
|
+
)
|
|
104
|
+
result.sort((a, b) => {
|
|
105
|
+
return +new Date(b.meta.date) - +new Date(a.meta.date)
|
|
106
|
+
})
|
|
107
|
+
return result
|
|
108
|
+
})
|
|
109
|
+
const pageSize = ref(6)
|
|
110
|
+
const currentPage = ref(0)
|
|
111
|
+
const showSearchResult = computed(() => {
|
|
112
|
+
// 合法性处理
|
|
113
|
+
const pageIdx =
|
|
114
|
+
currentPage.value % Math.ceil(searchResult.value.length / pageSize.value)
|
|
115
|
+
const startIdx = pageIdx * pageSize.value
|
|
116
|
+
return searchResult.value.slice(startIdx, startIdx + pageSize.value)
|
|
117
|
+
})
|
|
118
|
+
const showNextResult = computed(
|
|
119
|
+
() => searchResult.value.length > pageSize.value
|
|
120
|
+
)
|
|
121
|
+
const handleNext = () => {
|
|
122
|
+
currentPage.value =
|
|
123
|
+
(currentPage.value + 1) %
|
|
124
|
+
Math.ceil(searchResult.value.length / pageSize.value)
|
|
125
|
+
}
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<style lang="scss" scoped>
|
|
129
|
+
.blog-search {
|
|
130
|
+
flex: 1;
|
|
131
|
+
margin-left: 10px;
|
|
132
|
+
|
|
133
|
+
.nav-search-btn-wait {
|
|
134
|
+
cursor: pointer;
|
|
135
|
+
display: flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
justify-content: center;
|
|
138
|
+
padding: 6px;
|
|
139
|
+
box-sizing: border-box;
|
|
140
|
+
width: 100px;
|
|
141
|
+
|
|
142
|
+
&:hover {
|
|
143
|
+
border: 2px solid #409eff;
|
|
144
|
+
border-radius: 20px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.search-tip {
|
|
148
|
+
color: #909399;
|
|
149
|
+
font-size: 14px;
|
|
150
|
+
padding-left: 10px;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
</style>
|
|
155
|
+
|
|
156
|
+
<style lang="scss">
|
|
157
|
+
.search-dialog {
|
|
158
|
+
.el-empty {
|
|
159
|
+
padding: 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.el-empty__image {
|
|
163
|
+
width: 100px;
|
|
164
|
+
}
|
|
165
|
+
ul {
|
|
166
|
+
position: relative;
|
|
167
|
+
}
|
|
168
|
+
li {
|
|
169
|
+
margin-bottom: 10px;
|
|
170
|
+
font-size: 12px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
li .title {
|
|
174
|
+
display: flex;
|
|
175
|
+
justify-content: space-between;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
li .des {
|
|
179
|
+
text-overflow: ellipsis;
|
|
180
|
+
overflow: hidden;
|
|
181
|
+
word-break: keep-all;
|
|
182
|
+
white-space: nowrap;
|
|
183
|
+
color: var(--el-color-info-dark-2);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
li .date {
|
|
187
|
+
color: var(--el-color-info-light-3);
|
|
188
|
+
min-width: 80px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.nextPage {
|
|
192
|
+
position: absolute;
|
|
193
|
+
left: 50%;
|
|
194
|
+
transform: translateX(-50%);
|
|
195
|
+
top: -30px;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sidebar"><BlogRecommendArticle /></div>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script lang="ts" setup>
|
|
6
|
+
import BlogRecommendArticle from './BlogRecommendArticle.vue'
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<style lang="scss" scoped>
|
|
10
|
+
.sidebar {
|
|
11
|
+
margin-top: 40px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@media screen and (min-width: 960px) and (max-width: 1120px) {
|
|
15
|
+
.sidebar {
|
|
16
|
+
margin-top: 60px;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useData, useRoute } from 'vitepress'
|
|
2
|
+
import {
|
|
3
|
+
Component,
|
|
4
|
+
computed,
|
|
5
|
+
defineComponent,
|
|
6
|
+
h,
|
|
7
|
+
inject,
|
|
8
|
+
InjectionKey,
|
|
9
|
+
provide,
|
|
10
|
+
ref,
|
|
11
|
+
Ref
|
|
12
|
+
} from 'vue'
|
|
13
|
+
import type { Theme } from './index'
|
|
14
|
+
|
|
15
|
+
const configSymbol: InjectionKey<Ref<Theme.Config>> = Symbol('theme-config')
|
|
16
|
+
|
|
17
|
+
const activeTagSymbol: InjectionKey<Ref<Theme.activeTag>> = Symbol('active-tag')
|
|
18
|
+
|
|
19
|
+
const homeConfigSymbol: InjectionKey<Theme.HomeConfig> = Symbol('home-config')
|
|
20
|
+
|
|
21
|
+
export function withConfigProvider(App: Component) {
|
|
22
|
+
return defineComponent({
|
|
23
|
+
name: 'ConfigProvider',
|
|
24
|
+
props: {
|
|
25
|
+
handleChangeSlogan: {
|
|
26
|
+
type: Function,
|
|
27
|
+
required: false
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
setup(props, { slots }) {
|
|
31
|
+
provide(homeConfigSymbol, props as Theme.HomeConfig)
|
|
32
|
+
|
|
33
|
+
const { theme } = useData()
|
|
34
|
+
const config = computed(() => resolveConfig(theme.value))
|
|
35
|
+
provide(configSymbol, config)
|
|
36
|
+
|
|
37
|
+
const activeTag = ref<Theme.activeTag>({
|
|
38
|
+
label: '',
|
|
39
|
+
type: ''
|
|
40
|
+
})
|
|
41
|
+
provide(activeTagSymbol, activeTag)
|
|
42
|
+
return () => h(App, null, slots)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useConfig() {
|
|
48
|
+
return {
|
|
49
|
+
config: inject(configSymbol)!.value
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useBlogConfig() {
|
|
54
|
+
return inject(configSymbol)!.value.blog
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function useHomeConfig() {
|
|
58
|
+
return inject(homeConfigSymbol)!
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function useGiscusConfig() {
|
|
62
|
+
const blogConfig = useConfig()
|
|
63
|
+
return blogConfig.config.blog.comment
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function useArticles() {
|
|
67
|
+
const blogConfig = useConfig()
|
|
68
|
+
const articles = computed(() => blogConfig.config?.blog?.pagesData || [])
|
|
69
|
+
return articles
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useActiveTag() {
|
|
73
|
+
return inject(activeTagSymbol)!
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function useCurrentArticle() {
|
|
77
|
+
const blogConfig = useConfig()
|
|
78
|
+
const route = useRoute()
|
|
79
|
+
|
|
80
|
+
const docs = computed(() => blogConfig.config.blog.pagesData)
|
|
81
|
+
const currentArticle = computed(() =>
|
|
82
|
+
docs.value.find((v) => v.route === route.path.replace(/.html$/, ''))
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return currentArticle
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveConfig(config: Theme.Config): Theme.Config {
|
|
89
|
+
return {
|
|
90
|
+
...config,
|
|
91
|
+
blog: {
|
|
92
|
+
...config?.blog,
|
|
93
|
+
pagesData: config?.blog?.pagesData || []
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|