创建文档
This commit is contained in:
21
theme-vdoing/LICENSE
Normal file
21
theme-vdoing/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019-present gaoyi(Evan) Xu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
7
theme-vdoing/README.md
Normal file
7
theme-vdoing/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# vuepress-theme-vdoing
|
||||
|
||||
vuepress-theme-vdoing for vuepress
|
||||
|
||||
一个基于VuePress的 知识管理兼博客 主题。
|
||||
|
||||
[More](https://github.com/xugaoyi/vuepress-theme-vdoing#readme).
|
||||
165
theme-vdoing/components/AlgoliaSearchBox.vue
Normal file
165
theme-vdoing/components/AlgoliaSearchBox.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<form
|
||||
id="search-form"
|
||||
class="algolia-search-wrapper search-box"
|
||||
role="search"
|
||||
>
|
||||
<input
|
||||
id="algolia-search-input"
|
||||
class="search-query"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['options'],
|
||||
|
||||
data () {
|
||||
return {
|
||||
placeholder: undefined
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.initialize(this.options, this.$lang)
|
||||
this.placeholder = this.$site.themeConfig.searchPlaceholder || ''
|
||||
},
|
||||
|
||||
methods: {
|
||||
initialize (userOptions, lang) {
|
||||
Promise.all([
|
||||
import(/* webpackChunkName: "docsearch" */ 'docsearch.js/dist/cdn/docsearch.min.js'),
|
||||
import(/* webpackChunkName: "docsearch" */ 'docsearch.js/dist/cdn/docsearch.min.css')
|
||||
]).then(([docsearch]) => {
|
||||
docsearch = docsearch.default
|
||||
const { algoliaOptions = {} } = userOptions
|
||||
docsearch(Object.assign(
|
||||
{},
|
||||
userOptions,
|
||||
{
|
||||
inputSelector: '#algolia-search-input',
|
||||
// #697 Make docsearch work well at i18n mode.
|
||||
algoliaOptions: Object.assign({
|
||||
'facetFilters': [`lang:${lang}`].concat(algoliaOptions.facetFilters || [])
|
||||
}, algoliaOptions),
|
||||
handleSelected: (input, event, suggestion) => {
|
||||
const { pathname, hash } = new URL(suggestion.url)
|
||||
const _hash = decodeURIComponent(hash)
|
||||
this.$router.push(`${routepath}${_hash}`)
|
||||
}
|
||||
}
|
||||
))
|
||||
})
|
||||
},
|
||||
|
||||
update (options, lang) {
|
||||
this.$el.innerHTML = '<input id="algolia-search-input" class="search-query">'
|
||||
this.initialize(options, lang)
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
$lang (newValue) {
|
||||
this.update(this.options, newValue)
|
||||
},
|
||||
|
||||
options (newValue) {
|
||||
this.update(newValue, this.$lang)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.algolia-search-wrapper
|
||||
& > span
|
||||
vertical-align middle
|
||||
.algolia-autocomplete
|
||||
line-height normal
|
||||
.ds-dropdown-menu
|
||||
background-color #fff
|
||||
border 1px solid #999
|
||||
border-radius 4px
|
||||
font-size 16px
|
||||
margin 6px 0 0
|
||||
padding 4px
|
||||
text-align left
|
||||
&:before
|
||||
border-color #999
|
||||
[class*=ds-dataset-]
|
||||
border none
|
||||
padding 0
|
||||
.ds-suggestions
|
||||
margin-top 0
|
||||
.ds-suggestion
|
||||
border-bottom 1px solid var(--borderColor)
|
||||
.algolia-docsearch-suggestion--highlight
|
||||
color #2c815b
|
||||
.algolia-docsearch-suggestion
|
||||
border-color var(--borderColor)
|
||||
padding 0
|
||||
.algolia-docsearch-suggestion--category-header
|
||||
padding 5px 10px
|
||||
margin-top 0
|
||||
background $accentColor
|
||||
color #fff
|
||||
font-weight 600
|
||||
.algolia-docsearch-suggestion--highlight
|
||||
background rgba(255, 255, 255, 0.6)
|
||||
.algolia-docsearch-suggestion--wrapper
|
||||
padding 0
|
||||
.algolia-docsearch-suggestion--title
|
||||
font-weight 600
|
||||
margin-bottom 0
|
||||
color var(--textColor)
|
||||
.algolia-docsearch-suggestion--subcategory-column
|
||||
vertical-align top
|
||||
padding 5px 7px 5px 5px
|
||||
border-color var(--borderColor)
|
||||
background #f1f3f5
|
||||
&:after
|
||||
display none
|
||||
.algolia-docsearch-suggestion--subcategory-column-text
|
||||
color #555
|
||||
.algolia-docsearch-footer
|
||||
border-color var(--borderColor)
|
||||
.ds-cursor .algolia-docsearch-suggestion--content
|
||||
background-color #e7edf3 !important
|
||||
color var(--textColor)
|
||||
@media (min-width $MQMobile)
|
||||
.algolia-search-wrapper
|
||||
.algolia-autocomplete
|
||||
.algolia-docsearch-suggestion
|
||||
.algolia-docsearch-suggestion--subcategory-column
|
||||
float none
|
||||
width 150px
|
||||
min-width 150px
|
||||
display table-cell
|
||||
.algolia-docsearch-suggestion--content
|
||||
float none
|
||||
display table-cell
|
||||
width 100%
|
||||
vertical-align top
|
||||
.ds-dropdown-menu
|
||||
min-width 515px !important
|
||||
@media (max-width $MQMobile)
|
||||
.algolia-search-wrapper
|
||||
.ds-dropdown-menu
|
||||
min-width calc(100vw - 4rem) !important
|
||||
max-width calc(100vw - 4rem) !important
|
||||
.algolia-docsearch-suggestion--wrapper
|
||||
padding 5px 7px 5px 5px !important
|
||||
.algolia-docsearch-suggestion--subcategory-column
|
||||
padding 0 !important
|
||||
background white !important
|
||||
.algolia-docsearch-suggestion--subcategory-column-text:after
|
||||
content ' > '
|
||||
font-size 10px
|
||||
line-height 14.4px
|
||||
display inline-block
|
||||
width 5px
|
||||
margin -3px 3px 0
|
||||
vertical-align middle
|
||||
</style>
|
||||
153
theme-vdoing/components/ArchivesPage.vue
Normal file
153
theme-vdoing/components/ArchivesPage.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div class="custom-page archives-page">
|
||||
<div class="theme-vdoing-wrapper">
|
||||
<h1>
|
||||
<img
|
||||
:src="currentBadge"
|
||||
v-if="$themeConfig.titleBadge === false ? false : true"
|
||||
/>
|
||||
{{this.$page.title}}
|
||||
</h1>
|
||||
<ul>
|
||||
<template v-for="(item, index) in postsList">
|
||||
<li
|
||||
class="year"
|
||||
v-if="(year = getYear(index)) !== getYear(index-1)"
|
||||
:key="index+$sortPostsByDate.length"
|
||||
>
|
||||
<h2>{{year}}</h2>
|
||||
</li>
|
||||
<li :key="index">
|
||||
<router-link :to="item.path">
|
||||
<span>{{ getDate(item) }}</span>
|
||||
{{item.title}}
|
||||
</router-link>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import debounce from 'lodash.debounce'
|
||||
import { type } from '../util'
|
||||
import TitleBadgeMixin from '../mixins/titleBadge'
|
||||
|
||||
export default {
|
||||
mixins: [TitleBadgeMixin],
|
||||
data () {
|
||||
return {
|
||||
postsList: [],
|
||||
|
||||
perPage: 80, // 每页长
|
||||
currentPage: 1// 当前页
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.getPageData()
|
||||
},
|
||||
mounted () {
|
||||
|
||||
window.addEventListener('scroll', debounce(() => {
|
||||
if (this.postsList.length < this.$sortPostsByDate.length) {
|
||||
const docEl = document.documentElement
|
||||
const docBody = document.body
|
||||
const scrollTop = docEl.scrollTop || docBody.scrollTop;
|
||||
const clientHeight = docEl.clientHeight || docBody.clientHeight;
|
||||
const scrollHeight = docEl.scrollHeight || docBody.scrollHeight;
|
||||
|
||||
if (scrollHeight > clientHeight && scrollTop + clientHeight >= scrollHeight - 250) {
|
||||
this.loadmore()
|
||||
}
|
||||
}
|
||||
|
||||
}, 200))
|
||||
},
|
||||
methods: {
|
||||
getPageData () {
|
||||
const currentPage = this.currentPage
|
||||
const perPage = this.perPage
|
||||
this.postsList = this.postsList.concat(this.$sortPostsByDate.slice((currentPage - 1) * perPage, currentPage * perPage))
|
||||
},
|
||||
loadmore () {
|
||||
this.currentPage = this.currentPage + 1
|
||||
this.getPageData()
|
||||
},
|
||||
getYear (index) {
|
||||
const item = this.postsList[index]
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
const { frontmatter: { date } } = item
|
||||
if (date && type(date) === 'string') {
|
||||
return date.split(" ")[0].slice(0, 4)
|
||||
}
|
||||
},
|
||||
getDate (item) {
|
||||
const { frontmatter: { date } } = item
|
||||
if (date && type(date) === 'string') {
|
||||
return date.split(" ")[0].slice(5, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='stylus'>
|
||||
@require '../styles/wrapper.styl'
|
||||
|
||||
.archives-page
|
||||
.theme-vdoing-wrapper
|
||||
@extend $vdoing-wrapper
|
||||
position relative
|
||||
@media (min-width $contentWidth + 80)
|
||||
margin-top 1.5rem !important
|
||||
ul, li
|
||||
margin 0
|
||||
padding 0
|
||||
li
|
||||
list-style none
|
||||
&.year
|
||||
position sticky
|
||||
top $navbarHeight
|
||||
background var(--mainBg)
|
||||
z-index 1
|
||||
&.year:not(:first-child)
|
||||
margin-top 3.5rem
|
||||
h2
|
||||
margin-bottom 0.8rem
|
||||
font-weight 400
|
||||
padding 0.5rem 0
|
||||
a
|
||||
display block
|
||||
color var(--textColor)
|
||||
transition padding 0.3s
|
||||
padding 0.5rem 2rem
|
||||
line-height 1.2rem
|
||||
&:hover
|
||||
padding-left 2.5rem
|
||||
color $accentColor
|
||||
background #f9f9f9
|
||||
@media (max-width $contentWidth + 80)
|
||||
padding 0.5rem 1rem
|
||||
font-weight normal
|
||||
&:hover
|
||||
padding-left 1.5rem
|
||||
span
|
||||
opacity 0.6
|
||||
font-size 0.85rem
|
||||
font-weight 400
|
||||
margin-right 0.3rem
|
||||
.loadmore
|
||||
text-align center
|
||||
margin-top 1rem
|
||||
opacity 0.5
|
||||
.theme-mode-dark .archives-page .theme-vdoing-wrapper li a:hover, .theme-mode-read .archives-page .theme-vdoing-wrapper li a:hover
|
||||
background var(--customBlockBg)
|
||||
.hide-navbar
|
||||
.archives-page
|
||||
.theme-vdoing-wrapper
|
||||
li.year
|
||||
top 0
|
||||
</style>
|
||||
206
theme-vdoing/components/ArticleInfo.vue
Normal file
206
theme-vdoing/components/ArticleInfo.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div class="articleInfo-wrap">
|
||||
<div class="articleInfo">
|
||||
<ul
|
||||
class="breadcrumbs"
|
||||
v-if="articleInfo.classify1 && articleInfo.classify1 !== '_posts'"
|
||||
>
|
||||
<li>
|
||||
<router-link
|
||||
to="/"
|
||||
class="iconfont icon-home"
|
||||
title="首页"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
v-if="articleInfo.cataloguePermalink"
|
||||
:to="articleInfo.cataloguePermalink"
|
||||
:title="articleInfo.classify1+'-目录页'"
|
||||
>{{articleInfo.classify1}}</router-link>
|
||||
<router-link
|
||||
v-else-if="$themeConfig.category !== false"
|
||||
:to="`/categories/?category=${encodeURIComponent(articleInfo.classify1)}`"
|
||||
title="分类"
|
||||
>{{articleInfo.classify1}}</router-link>
|
||||
<span v-else>{{ articleInfo.classify1 }}</span>
|
||||
</li>
|
||||
<li v-if="articleInfo.classify2">
|
||||
<router-link
|
||||
v-if="articleInfo.cataloguePermalink"
|
||||
:to="articleInfo.cataloguePermalink + '/#' + articleInfo.classify2"
|
||||
:title="articleInfo.classify1+'#'+articleInfo.classify2"
|
||||
>{{articleInfo.classify2}}</router-link>
|
||||
<router-link
|
||||
v-else-if="$themeConfig.category !== false"
|
||||
:to="`/categories/?category=${encodeURIComponent(articleInfo.classify2)}`"
|
||||
title="分类"
|
||||
>{{articleInfo.classify2}}</router-link>
|
||||
<span v-else>{{articleInfo.classify2}}</span>
|
||||
</li>
|
||||
<li v-if="articleInfo.classify3">
|
||||
<router-link
|
||||
v-if="articleInfo.cataloguePermalink"
|
||||
:to="articleInfo.cataloguePermalink + '/#' + articleInfo.classify3"
|
||||
:title="articleInfo.classify1+'#'+articleInfo.classify3"
|
||||
>{{articleInfo.classify3}}</router-link>
|
||||
<router-link
|
||||
v-else-if="$themeConfig.category !== false"
|
||||
:to="`/categories/?category=${encodeURIComponent(articleInfo.classify3)}`"
|
||||
title="分类"
|
||||
>{{articleInfo.classify3}}</router-link>
|
||||
<span v-else>{{articleInfo.classify3}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="info">
|
||||
<div
|
||||
class="author iconfont icon-touxiang"
|
||||
title="作者"
|
||||
v-if="articleInfo.author"
|
||||
>
|
||||
<a
|
||||
:href="articleInfo.author.href || articleInfo.author.link"
|
||||
v-if="articleInfo.author.href || articleInfo.author.link && typeof(articleInfo.author.link) === 'string'"
|
||||
target="_blank"
|
||||
class="beLink"
|
||||
title="作者"
|
||||
>{{articleInfo.author.name}}</a>
|
||||
<a
|
||||
v-else
|
||||
href="javascript:;"
|
||||
>{{articleInfo.author.name || articleInfo.author}}</a>
|
||||
</div>
|
||||
<div
|
||||
class="date iconfont icon-riqi"
|
||||
title="创建时间"
|
||||
v-if="articleInfo.date"
|
||||
>
|
||||
<a href="javascript:;">{{articleInfo.date}}</a>
|
||||
</div>
|
||||
<div
|
||||
class="date iconfont icon-wenjian"
|
||||
title="分类"
|
||||
v-if="$themeConfig.category !== false && !(articleInfo.classify1 && articleInfo.classify1 !== '_posts') && articleInfo.categories"
|
||||
>
|
||||
<router-link
|
||||
:to="`/categories/?category=${encodeURIComponent(item)}`"
|
||||
v-for="(item, index) in articleInfo.categories"
|
||||
:key="index"
|
||||
>{{item + ' '}}</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
articleInfo: {}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.articleInfo = this.getPageInfo()
|
||||
},
|
||||
watch: {
|
||||
'$route.path' () {
|
||||
this.articleInfo = this.getPageInfo()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getPageInfo () {
|
||||
const pageInfo = this.$page
|
||||
const { relativePath } = pageInfo
|
||||
const { sidebar } = this.$themeConfig
|
||||
|
||||
// 分类采用解析文件夹地址名称的方式
|
||||
const relativePathArr = relativePath.split('/')
|
||||
|
||||
const classifyArr = relativePathArr[0].split('.')
|
||||
const classify1 = classifyArr.length > 1 ? classifyArr[1] : classifyArr[0] // 文章一级分类名称
|
||||
const classify2 = relativePathArr.length > 2 ? relativePathArr[1].split('.')[1] : undefined// 文章二级分类名称
|
||||
const classify3 = relativePathArr.length > 3 ? relativePathArr[2].split('.')[1] : undefined// 文章三级分类名称
|
||||
|
||||
const cataloguePermalink = sidebar && sidebar.catalogue ? sidebar.catalogue[classify1] : undefined// 目录页永久链接
|
||||
|
||||
const author = this.$frontmatter.author || this.$themeConfig.author // 作者
|
||||
let date = (pageInfo.frontmatter.date || '').split(' ')[0] // 文章创建时间
|
||||
|
||||
// 获取页面frontmatter的分类(碎片化文章使用)
|
||||
const { categories } = this.$frontmatter
|
||||
|
||||
return {
|
||||
date,
|
||||
classify1,
|
||||
classify2,
|
||||
classify3,
|
||||
cataloguePermalink,
|
||||
author,
|
||||
categories
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='stylus' scoped>
|
||||
@require '../styles/wrapper.styl'
|
||||
|
||||
.articleInfo-wrap
|
||||
@extend $wrapper
|
||||
position relative
|
||||
z-index 1
|
||||
color #888
|
||||
.articleInfo
|
||||
overflow hidden
|
||||
font-size 0.92rem
|
||||
.breadcrumbs
|
||||
margin 0
|
||||
padding 0
|
||||
overflow hidden
|
||||
display inline-block
|
||||
line-height 2rem
|
||||
@media (max-width 960px)
|
||||
width 100%
|
||||
li
|
||||
list-style-type none
|
||||
float left
|
||||
padding-right 5px
|
||||
&:after
|
||||
content '/'
|
||||
margin-left 5px
|
||||
color #999
|
||||
&:last-child
|
||||
&:after
|
||||
content ''
|
||||
a
|
||||
color #888
|
||||
&:before
|
||||
font-size 0.92rem
|
||||
&:hover
|
||||
color $accentColor
|
||||
.icon-home
|
||||
text-decoration none
|
||||
.info
|
||||
float right
|
||||
line-height 32px
|
||||
@media (max-width 960px)
|
||||
float left
|
||||
div
|
||||
float left
|
||||
margin-left 20px
|
||||
font-size 0.8rem
|
||||
@media (max-width 960px)
|
||||
margin 0 20px 0 0
|
||||
&:before
|
||||
margin-right 3px
|
||||
a
|
||||
color #888
|
||||
&:hover
|
||||
text-decoration none
|
||||
a.beLink
|
||||
&:hover
|
||||
color $accentColor
|
||||
text-decoration underline
|
||||
</style>
|
||||
83
theme-vdoing/components/BloggerBar.vue
Normal file
83
theme-vdoing/components/BloggerBar.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<aside class="blogger-wrapper card-box">
|
||||
<div class="avatar">
|
||||
<img
|
||||
:src="blogger.avatar"
|
||||
alt="头像"
|
||||
title="我好看吗"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="icons"
|
||||
v-if="social && social.icons && social.icons.length"
|
||||
>
|
||||
<a
|
||||
v-for="(item, index) in social.icons"
|
||||
:key="index"
|
||||
:href="item.link"
|
||||
:title="item.title"
|
||||
:class="['iconfont', item.iconClass]"
|
||||
:style="{width: 100/social.icons.length + '%'}"
|
||||
target="_blank"
|
||||
/>
|
||||
</div>
|
||||
<div class="blogger">
|
||||
<span class="name">{{blogger.name}}</span>
|
||||
<span class="slogan">{{blogger.slogan}}</span>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
blogger () {
|
||||
return this.$themeConfig.blogger
|
||||
},
|
||||
social () {
|
||||
return this.$themeConfig.social
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='stylus'>
|
||||
.blogger-wrapper
|
||||
height auto
|
||||
display inline-table
|
||||
padding-top 0!important
|
||||
overflow hidden
|
||||
.avatar
|
||||
width 100%
|
||||
// height 235px
|
||||
overflow hidden
|
||||
@media (max-width 900px)
|
||||
// width 205px
|
||||
// height 205px
|
||||
img
|
||||
width 100%
|
||||
height 100%
|
||||
.icons
|
||||
// border 1px solid var(--borderColor)
|
||||
border-top none
|
||||
height 35px
|
||||
line-height 35px
|
||||
a
|
||||
font-size 20px
|
||||
width 33%
|
||||
color var(--textColor)
|
||||
display block
|
||||
float left
|
||||
text-align center
|
||||
opacity 0.8
|
||||
&:hover
|
||||
color $accentColor
|
||||
.blogger
|
||||
padding 0.3rem 0.95rem 0 0.95rem
|
||||
.name
|
||||
font-size 1.3rem
|
||||
display block
|
||||
margin-bottom 6px
|
||||
.slogan
|
||||
color var(--textColor)
|
||||
</style>
|
||||
53
theme-vdoing/components/BodyBgImg.vue
Normal file
53
theme-vdoing/components/BodyBgImg.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div
|
||||
class="body-bg"
|
||||
:style="`background: url(${bgImg}) center center / cover no-repeat;opacity:${opacity}`"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { type } from '../util'
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
bgImg: '',
|
||||
opacity: 0.5
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
let { bodyBgImg, bodyBgImgOpacity } = this.$themeConfig
|
||||
|
||||
if (type(bodyBgImg) === 'string') {
|
||||
this.bgImg = bodyBgImg
|
||||
} else if (type(bodyBgImg) === 'array') {
|
||||
let count = 0
|
||||
let timer = null
|
||||
|
||||
this.bgImg = bodyBgImg[count]
|
||||
clearInterval(timer)
|
||||
timer = setInterval(() => {
|
||||
if (++count >= bodyBgImg.length) {
|
||||
count = 0
|
||||
}
|
||||
this.bgImg = bodyBgImg[count]
|
||||
}, 15000);
|
||||
}
|
||||
|
||||
if (bodyBgImgOpacity !== undefined) {
|
||||
this.opacity = bodyBgImgOpacity
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='stylus'>
|
||||
.body-bg
|
||||
position fixed
|
||||
left 0
|
||||
top 0
|
||||
z-index -999999
|
||||
height 100vh
|
||||
width 100vw
|
||||
transition background 0.5s
|
||||
</style>
|
||||
262
theme-vdoing/components/Buttons.vue
Normal file
262
theme-vdoing/components/Buttons.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<div class="buttons">
|
||||
<transition name="fade">
|
||||
<div
|
||||
title="返回顶部"
|
||||
class="button blur go-to-top iconfont icon-fanhuidingbu"
|
||||
v-show="showToTop"
|
||||
@click="scrollToTop"
|
||||
/>
|
||||
</transition>
|
||||
<div
|
||||
title="去评论"
|
||||
class="button blur go-to-comment iconfont icon-pinglun"
|
||||
v-show="showCommentBut"
|
||||
@click="scrollToComment"
|
||||
/>
|
||||
<div
|
||||
title="主题模式"
|
||||
class="button blur theme-mode-but iconfont icon-zhuti"
|
||||
@mouseenter="showModeBox = true"
|
||||
@mouseleave="showModeBox = false"
|
||||
@click="showModeBox = true"
|
||||
>
|
||||
<transition name="mode">
|
||||
<ul
|
||||
class="select-box"
|
||||
ref="modeBox"
|
||||
v-show="showModeBox"
|
||||
@click.stop
|
||||
@touchstart.stop
|
||||
>
|
||||
<li
|
||||
v-for="item in modeList"
|
||||
:key="item.KEY"
|
||||
class="iconfont"
|
||||
:class="[item.icon, {active: item.KEY === currentMode}]"
|
||||
@click="toggleMode(item.KEY)"
|
||||
>{{item.name}}</li>
|
||||
</ul>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import debounce from 'lodash.debounce'
|
||||
import storage from 'good-storage' // 本地存储
|
||||
const MOBILE_DESKTOP_BREAKPOINT = 719 // refer to config.styl
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
threshold: 100,
|
||||
scrollTop: null,
|
||||
showCommentBut: false,
|
||||
commentTop: null,
|
||||
currentMode: null,
|
||||
showModeBox: false,
|
||||
modeList: [
|
||||
{
|
||||
name: '跟随系统',
|
||||
icon: 'icon-zidong',
|
||||
KEY: 'auto'
|
||||
},
|
||||
{
|
||||
name: '浅色模式',
|
||||
icon: 'icon-rijianmoshi',
|
||||
KEY: 'light'
|
||||
},
|
||||
{
|
||||
name: '深色模式',
|
||||
icon: 'icon-yejianmoshi',
|
||||
KEY: 'dark'
|
||||
},
|
||||
{
|
||||
name: '阅读模式',
|
||||
icon: 'icon-yuedu',
|
||||
KEY: 'read'
|
||||
}
|
||||
],
|
||||
_scrollTimer: null,
|
||||
_textareaEl: null,
|
||||
_recordScrollTop: null,
|
||||
COMMENT_SELECTOR_1: '#vuepress-plugin-comment', // 评论区元素的选择器1
|
||||
COMMENT_SELECTOR_2: '#valine-vuepress-comment', // 评论区元素的选择器2
|
||||
COMMENT_SELECTOR_3: '.vssue' // 评论区元素的选择器3
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.currentMode = storage.get('mode') || 'auto'
|
||||
|
||||
this.scrollTop = this.getScrollTop()
|
||||
window.addEventListener('scroll', debounce(() => {
|
||||
this.scrollTop = this.getScrollTop()
|
||||
}, 100))
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
this.getCommentTop()
|
||||
})
|
||||
|
||||
// 小屏时选择主题模式后关闭选择框
|
||||
if (document.documentElement.clientWidth < MOBILE_DESKTOP_BREAKPOINT) {
|
||||
const modeBox = this.$refs.modeBox
|
||||
modeBox.onclick = () => {
|
||||
this.showModeBox = false
|
||||
}
|
||||
window.addEventListener('scroll', debounce(() => {
|
||||
if (this.showModeBox) {
|
||||
this.showModeBox = false
|
||||
}
|
||||
}, 100))
|
||||
}
|
||||
|
||||
|
||||
// 移动端对类似:hover效果的处理
|
||||
const buttons = document.querySelectorAll('.buttons .button')
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
const button = buttons[i]
|
||||
button.addEventListener('touchstart', function () {
|
||||
button.classList.add('hover')
|
||||
})
|
||||
button.addEventListener('touchend', function () {
|
||||
setTimeout(() => {
|
||||
button.classList.remove('hover')
|
||||
}, 150)
|
||||
})
|
||||
}
|
||||
|
||||
},
|
||||
computed: {
|
||||
showToTop () {
|
||||
return this.scrollTop > this.threshold
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleMode (key) {
|
||||
this.currentMode = key
|
||||
this.$emit('toggle-theme-mode', key)
|
||||
},
|
||||
getScrollTop () {
|
||||
return window.pageYOffset
|
||||
|| document.documentElement.scrollTop
|
||||
|| document.body.scrollTop || 0
|
||||
},
|
||||
|
||||
scrollToTop () {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
this.scrollTop = 0
|
||||
},
|
||||
|
||||
getCommentTop () {
|
||||
setTimeout(() => {
|
||||
let commentEl = document.querySelector(this.COMMENT_SELECTOR_1) || document.querySelector(this.COMMENT_SELECTOR_2) || document.querySelector(this.COMMENT_SELECTOR_3)
|
||||
if (commentEl) {
|
||||
this.showCommentBut = this.$frontmatter.comment !== false && this.$frontmatter.home !== true
|
||||
this.commentTop = commentEl.offsetTop - 58
|
||||
}
|
||||
}, 500)
|
||||
},
|
||||
|
||||
|
||||
scrollToComment () {
|
||||
window.scrollTo({ top: this.commentTop, behavior: 'smooth' })
|
||||
this._textareaEl = document.querySelector(this.COMMENT_SELECTOR_1 + ' textarea') || document.querySelector(this.COMMENT_SELECTOR_2 + ' input') || document.querySelector(this.COMMENT_SELECTOR_3 + ' textarea')
|
||||
if (this._textareaEl && this.getScrollTop() !== this._recordScrollTop) {
|
||||
document.addEventListener("scroll", this._handleListener)
|
||||
} else if (this._textareaEl && this.getScrollTop() === this._recordScrollTop) {
|
||||
this._handleFocus()
|
||||
}
|
||||
},
|
||||
|
||||
_handleListener () {
|
||||
clearTimeout(this._scrollTimer)
|
||||
this._scrollTimer = setTimeout(() => {
|
||||
document.removeEventListener('scroll', this._handleListener)
|
||||
this._recordScrollTop = this.getScrollTop()
|
||||
this._handleFocus()
|
||||
}, 30)
|
||||
},
|
||||
|
||||
_handleFocus () {
|
||||
this._textareaEl.focus()
|
||||
this._textareaEl.classList.add('yellowBorder')
|
||||
setTimeout(() => {
|
||||
this._textareaEl.classList.remove('yellowBorder')
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.path' () {
|
||||
this.showCommentBut = false
|
||||
this.getCommentTop()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='stylus'>
|
||||
.yellowBorder
|
||||
// border: #FFE089 1px solid!important
|
||||
border-radius 5px
|
||||
box-shadow 0 0 15px #FFE089 !important
|
||||
.buttons
|
||||
position fixed
|
||||
right 2rem
|
||||
bottom 2.5rem
|
||||
z-index 11
|
||||
@media (max-width $MQNarrow)
|
||||
right 1rem
|
||||
bottom 1.5rem
|
||||
.button
|
||||
width 2.2rem
|
||||
height 2.2rem
|
||||
line-height 2.2rem
|
||||
border-radius 50%
|
||||
box-shadow 0 2px 6px rgba(0, 0, 0, 0.15)
|
||||
margin-top 0.9rem
|
||||
text-align center
|
||||
cursor pointer
|
||||
transition all 0.5s
|
||||
background var(--blurBg)
|
||||
&.hover
|
||||
background $accentColor
|
||||
box-shadow 0 0 15px $accentColor
|
||||
&:before
|
||||
color #fff
|
||||
@media (any-hover hover)
|
||||
&:hover
|
||||
background $accentColor
|
||||
box-shadow 0 0 15px $accentColor
|
||||
&:before
|
||||
color #fff
|
||||
.select-box
|
||||
margin 0
|
||||
padding 0.8rem 0
|
||||
position absolute
|
||||
bottom 0rem
|
||||
right 1.5rem
|
||||
background var(--mainBg)
|
||||
border 1px solid var(--borderColor)
|
||||
width 120px
|
||||
border-radius 6px
|
||||
box-shadow 0 0 15px rgba(255, 255, 255, 0.2)
|
||||
li
|
||||
list-style none
|
||||
line-height 2rem
|
||||
font-size 0.95rem
|
||||
&:hover
|
||||
color $accentColor
|
||||
&.active
|
||||
background-color rgba(150, 150, 150, 0.2)
|
||||
color $accentColor
|
||||
.mode-enter-active, .mode-leave-active
|
||||
transition all 0.3s
|
||||
.mode-enter, .mode-leave-to
|
||||
opacity 0
|
||||
transform scale(0.8)
|
||||
.fade-enter-active, .fade-leave-active
|
||||
transition opacity 0.2s
|
||||
.fade-enter, .fade-leave-to
|
||||
opacity 0
|
||||
</style>
|
||||
204
theme-vdoing/components/Catalogue.vue
Normal file
204
theme-vdoing/components/Catalogue.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div class="theme-vdoing-content">
|
||||
<div class="column-wrapper">
|
||||
<img :src="$withBase(pageData.imgUrl)" />
|
||||
<dl class="column-info">
|
||||
<dt class="title">{{pageData.title}}</dt>
|
||||
<dd
|
||||
class="description"
|
||||
v-html="pageData.description"
|
||||
></dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div
|
||||
class="catalogue-wrapper"
|
||||
v-if="isStructuring"
|
||||
>
|
||||
<div class="catalogue-title">目录</div>
|
||||
<div class="catalogue-content">
|
||||
<template v-for="(item, index) in getCatalogueList()">
|
||||
<dl
|
||||
v-if="type(item) === 'array'"
|
||||
:key="index"
|
||||
class="inline"
|
||||
>
|
||||
<dt>
|
||||
<router-link :to="item[2]">{{`${index+1}. ${item[1]}`}}</router-link>
|
||||
</dt>
|
||||
</dl>
|
||||
<dl
|
||||
v-else-if="type(item) === 'object'"
|
||||
:key="index"
|
||||
>
|
||||
<!-- 一级目录 -->
|
||||
<dt :id="anchorText = item.title">
|
||||
<a
|
||||
:href="`#${anchorText}`"
|
||||
class="header-anchor"
|
||||
>#</a>
|
||||
{{`${index+1}. ${item.title}`}}
|
||||
</dt>
|
||||
<dd>
|
||||
<!-- 二级目录 -->
|
||||
<template
|
||||
v-for="(c, i) in item.children"
|
||||
>
|
||||
<template
|
||||
v-if="type(c) === 'array'"
|
||||
>
|
||||
<router-link
|
||||
:to="c[2]"
|
||||
:key="i"
|
||||
>{{`${index+1}-${i+1}. ${c[1]}`}}</router-link>
|
||||
</template>
|
||||
<!-- 三级目录 -->
|
||||
<div
|
||||
v-else-if="type(c) === 'object'"
|
||||
:key="i"
|
||||
class="sub-cat-wrap"
|
||||
>
|
||||
<div :id="anchorText = c.title" class="sub-title">
|
||||
<a
|
||||
:href="`#${anchorText}`"
|
||||
class="header-anchor"
|
||||
>#</a>
|
||||
{{`${index+1}-${i+1}. ${c.title}`}}
|
||||
</div>
|
||||
<router-link
|
||||
v-for="(cc, ii) in c.children"
|
||||
:to="cc[2]"
|
||||
:key="`${index+1}-${i+1}-${ii+1}`"
|
||||
>
|
||||
{{`${index+1}-${i+1}-${ii+1}. ${cc[1]}`}}
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
</dd>
|
||||
</dl>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
pageData: null,
|
||||
isStructuring: true
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.getPageData()
|
||||
|
||||
const sidebar = this.$themeConfig.sidebar
|
||||
if (!sidebar || sidebar === 'auto') {
|
||||
this.isStructuring = false
|
||||
console.error("目录页数据依赖于结构化的侧边栏数据,请在主题设置中将侧边栏字段设置为'structuring',否则无法获取目录数据。")
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getPageData () {
|
||||
const pageComponent = this.$frontmatter.pageComponent
|
||||
if (pageComponent && pageComponent.data) {
|
||||
this.pageData = {
|
||||
...pageComponent.data,
|
||||
title: this.$frontmatter.title
|
||||
}
|
||||
} else {
|
||||
console.error('请在front matter中设置pageComponent和pageComponent.data数据')
|
||||
}
|
||||
},
|
||||
getCatalogueList () {
|
||||
const { sidebar } = this.$site.themeConfig
|
||||
const key = this.$frontmatter.pageComponent.data.key
|
||||
const catalogueList = sidebar[`/${key}/`]
|
||||
|
||||
if (!catalogueList) {
|
||||
console.error('未获取到目录数据,请查看front matter中设置的key是否正确。')
|
||||
}
|
||||
return catalogueList
|
||||
},
|
||||
type (o) { // 数据类型检查
|
||||
return Object.prototype.toString.call(o).match(/\[object (.*?)\]/)[1].toLowerCase()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.path' () {
|
||||
this.getPageData()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus" rel="stylesheet/stylus">
|
||||
dl, dd
|
||||
margin 0
|
||||
.column-wrapper
|
||||
margin-top 1rem
|
||||
display flex
|
||||
padding-bottom 2rem
|
||||
border-bottom 1px solid var(--borderColor)
|
||||
img
|
||||
width 80px
|
||||
height 80px
|
||||
border-radius 2px
|
||||
margin-right 1rem
|
||||
.column-info
|
||||
.title
|
||||
font-size 1.6rem
|
||||
.description
|
||||
color var(--textColor)
|
||||
opacity 0.8
|
||||
margin 0.5rem 0
|
||||
.catalogue-wrapper
|
||||
.catalogue-title
|
||||
font-size 1.45rem
|
||||
margin 2rem 0
|
||||
.catalogue-content
|
||||
dl
|
||||
margin-bottom 1.8rem
|
||||
&.inline
|
||||
display inline-block
|
||||
width 50%
|
||||
margin-bottom 1rem
|
||||
@media (max-width $MQMobileNarrow)
|
||||
width 100%
|
||||
a
|
||||
width 100%
|
||||
&:not(.inline)
|
||||
dt
|
||||
margin-top -($navbarHeight)
|
||||
padding-top $navbarHeight
|
||||
dt
|
||||
font-size 1.1rem
|
||||
&:hover .header-anchor
|
||||
opacity 1
|
||||
dd
|
||||
margin-top 0.7rem
|
||||
margin-left 1rem
|
||||
a:not(.header-anchor)
|
||||
margin-bottom 0.5rem
|
||||
display inline-block
|
||||
width 50%
|
||||
&:hover
|
||||
color $activeColor
|
||||
text-decoration none
|
||||
@media (max-width $MQMobileNarrow)
|
||||
width 100%
|
||||
.sub-cat-wrap
|
||||
margin 5px 0 8px 0
|
||||
font-size .95rem
|
||||
&> a
|
||||
padding-left 1rem
|
||||
box-sizing border-box
|
||||
.sub-title
|
||||
margin-top -($navbarHeight)
|
||||
padding-top $navbarHeight
|
||||
margin-bottom 6px
|
||||
font-size 1rem
|
||||
&:hover
|
||||
.header-anchor
|
||||
opacity 1
|
||||
</style>
|
||||
113
theme-vdoing/components/CategoriesBar.vue
Normal file
113
theme-vdoing/components/CategoriesBar.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="categories-wrapper card-box">
|
||||
<router-link
|
||||
to="/categories/"
|
||||
class="title iconfont icon-wenjianjia"
|
||||
title="全部分类"
|
||||
>{{ length === 'all' ? '全部分类' : '文章分类' }}</router-link>
|
||||
|
||||
<div class="categories">
|
||||
<router-link
|
||||
:to="`/categories/?category=${encodeURIComponent(item.key)}`"
|
||||
v-for="(item, index) in categories"
|
||||
:key="index"
|
||||
:class="{active: item.key === category}"
|
||||
>
|
||||
{{item.key}}
|
||||
<span>{{item.length}}</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/categories/"
|
||||
v-if="length !== 'all' && length < categoriesData.length"
|
||||
class="more"
|
||||
>更多 ...</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
category: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
categoriesData: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
length: {
|
||||
type: [String, Number],
|
||||
default: 'all'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
categories () {
|
||||
if (this.length === 'all') {
|
||||
return this.categoriesData
|
||||
} else {
|
||||
return this.categoriesData.slice(0, this.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='stylus'>
|
||||
.categories-wrapper
|
||||
.title
|
||||
color var(--textColor)
|
||||
opacity 0.9
|
||||
font-size 1.2rem
|
||||
padding 0 .95rem
|
||||
&::before
|
||||
margin-right 0.3rem
|
||||
.categories
|
||||
margin-top 0.6rem
|
||||
a
|
||||
display block
|
||||
padding 8px .95rem 7px .95rem
|
||||
color var(--textColor)
|
||||
opacity 0.8
|
||||
font-size 0.95rem
|
||||
line-height 0.95rem
|
||||
position relative
|
||||
transition all .2s
|
||||
border-left 2px solid transparent
|
||||
margin-top -1px
|
||||
overflow hidden
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
@media (max-width $MQMobile)
|
||||
font-weight 400
|
||||
&.more
|
||||
// color $accentColor
|
||||
&:not(.active):hover
|
||||
color $accentColor
|
||||
background #f8f8f8
|
||||
border-color $accentColor
|
||||
span
|
||||
opacity 0.8
|
||||
span
|
||||
float right
|
||||
background-color var(--textColor)
|
||||
color var(--mainBg)
|
||||
border-radius 8px
|
||||
padding 0 0.13rem
|
||||
min-width 1rem
|
||||
height 1rem
|
||||
line-height 1rem
|
||||
font-size 0.6rem
|
||||
text-align center
|
||||
opacity 0.6
|
||||
transition opacity 0.3s
|
||||
&.active
|
||||
background $accentColor
|
||||
color var(--mainBg)
|
||||
padding-left 0.8rem
|
||||
border-radius 1px
|
||||
border-color transparent
|
||||
.theme-mode-dark .categories-wrapper .categories a:not(.active):hover,
|
||||
.theme-mode-read .categories-wrapper .categories a:not(.active):hover
|
||||
background var(--customBlockBg)
|
||||
</style>
|
||||
131
theme-vdoing/components/CategoriesPage.vue
Normal file
131
theme-vdoing/components/CategoriesPage.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="custom-page categories-page">
|
||||
<MainLayout>
|
||||
<template #mainLeft>
|
||||
<CategoriesBar
|
||||
v-if="$categoriesAndTags.categories.length"
|
||||
:categoriesData="$categoriesAndTags.categories"
|
||||
:category="category"
|
||||
/>
|
||||
<PostList
|
||||
:currentPage="currentPage"
|
||||
:perPage="perPage"
|
||||
:category="category"
|
||||
/>
|
||||
<Pagination
|
||||
:total="total"
|
||||
:perPage="perPage"
|
||||
:currentPage="currentPage"
|
||||
@getCurrentPage="handlePagination"
|
||||
v-show="Math.ceil(total / perPage) > 1"
|
||||
/>
|
||||
</template>
|
||||
<template #mainRight>
|
||||
<CategoriesBar
|
||||
v-if="$categoriesAndTags.categories.length"
|
||||
:categoriesData="$categoriesAndTags.categories"
|
||||
:category="category"
|
||||
/>
|
||||
</template>
|
||||
</MainLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MainLayout from '@theme/components/MainLayout'
|
||||
import PostList from '@theme/components/PostList'
|
||||
import Pagination from '@theme/components/Pagination'
|
||||
import CategoriesBar from '@theme/components/CategoriesBar'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
category: '',
|
||||
total: 0, // 总长
|
||||
perPage: 10, // 每页长
|
||||
currentPage: 1// 当前页
|
||||
}
|
||||
},
|
||||
components: { MainLayout, PostList, Pagination, CategoriesBar },
|
||||
mounted () {
|
||||
const queryCategory = this.$route.query.category
|
||||
if (queryCategory) {
|
||||
this.category = queryCategory
|
||||
this.total = this.$groupPosts.categories[queryCategory].length
|
||||
} else {
|
||||
this.total = this.$sortPosts.length
|
||||
}
|
||||
if (this.$route.query.p) {
|
||||
this.currentPage = Number(this.$route.query.p)
|
||||
}
|
||||
|
||||
// 滚动条定位到当前分类(增强用户体验)
|
||||
const cateEl = document.querySelector('.categories')
|
||||
if (cateEl) {
|
||||
setTimeout(() => {
|
||||
const activeEl = cateEl.querySelector('.active')
|
||||
const topVal = activeEl ? activeEl.offsetTop : 0
|
||||
cateEl.scrollTo({ top: topVal, behavior: 'smooth' })
|
||||
}, 300)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handlePagination (i) { // 分页
|
||||
this.currentPage = i
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.query.category' (category) {
|
||||
this.category = category ? decodeURIComponent(category) : ''
|
||||
if (this.category) {
|
||||
this.total = this.$groupPosts.categories[this.category].length
|
||||
} else {
|
||||
this.total = this.$sortPosts.length
|
||||
}
|
||||
this.currentPage = 1
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='stylus'>
|
||||
.categories-page
|
||||
.categories-wrapper
|
||||
position sticky
|
||||
top ($navbarHeight + 0.9rem)
|
||||
max-height calc(100vh - 10rem)
|
||||
min-height 4.2rem
|
||||
@media (max-width $MQMobile)
|
||||
display none
|
||||
.categories
|
||||
padding-right 0.5rem
|
||||
max-height calc(100vh - 14rem)
|
||||
min-height 2.2rem
|
||||
overflow-y auto
|
||||
transition all 0.2s
|
||||
position relative
|
||||
&::-webkit-scrollbar-track-piece
|
||||
background-color rgba(0, 0, 0, 0.05)
|
||||
&::-webkit-scrollbar-thumb:vertical
|
||||
background-color rgba(0, 0, 0, 0.15)
|
||||
&:hover
|
||||
&::-webkit-scrollbar-track-piece
|
||||
background-color rgba(0, 0, 0, 0.1)
|
||||
&::-webkit-scrollbar-thumb:vertical
|
||||
background-color rgba(0, 0, 0, 0.25)
|
||||
.categories-page
|
||||
.main-left
|
||||
.categories-wrapper
|
||||
position relative
|
||||
top 0
|
||||
padding 0.9rem 1.5rem
|
||||
margin-bottom 0.9rem
|
||||
max-height 15rem
|
||||
border-radius 0
|
||||
display none
|
||||
@media (max-width $MQMobile)
|
||||
display block
|
||||
.categories
|
||||
max-height 12.3rem
|
||||
</style>
|
||||
245
theme-vdoing/components/DropdownLink.vue
Normal file
245
theme-vdoing/components/DropdownLink.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div
|
||||
class="dropdown-wrapper"
|
||||
:class="{ open }"
|
||||
>
|
||||
<button
|
||||
class="dropdown-title"
|
||||
type="button"
|
||||
:aria-label="dropdownAriaLabel"
|
||||
@click="toggle"
|
||||
>
|
||||
<router-link
|
||||
v-if="item.link"
|
||||
:to="item.link"
|
||||
class="link-title"
|
||||
>{{ item.text }}</router-link>
|
||||
<span
|
||||
class="title"
|
||||
v-show="!item.link"
|
||||
>{{ item.text }}</span>
|
||||
<span
|
||||
class="arrow"
|
||||
:class="open ? 'down' : 'right'"
|
||||
></span>
|
||||
</button>
|
||||
|
||||
<DropdownTransition>
|
||||
<ul
|
||||
class="nav-dropdown"
|
||||
v-show="open"
|
||||
>
|
||||
<li
|
||||
class="dropdown-item"
|
||||
:key="subItem.link || index"
|
||||
v-for="(subItem, index) in item.items"
|
||||
>
|
||||
<h4 v-if="subItem.type === 'links'">{{ subItem.text }}</h4>
|
||||
|
||||
<ul
|
||||
class="dropdown-subitem-wrapper"
|
||||
v-if="subItem.type === 'links'"
|
||||
>
|
||||
<li
|
||||
class="dropdown-subitem"
|
||||
:key="childSubItem.link"
|
||||
v-for="childSubItem in subItem.items"
|
||||
>
|
||||
<NavLink
|
||||
@focusout="
|
||||
isLastItemOfArray(childSubItem, subItem.items) &&
|
||||
isLastItemOfArray(subItem, item.items) &&
|
||||
toggle()
|
||||
"
|
||||
:item="childSubItem"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<NavLink
|
||||
v-else
|
||||
@focusout="isLastItemOfArray(subItem, item.items) && toggle()"
|
||||
:item="subItem"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</DropdownTransition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavLink from '@theme/components/NavLink.vue'
|
||||
import DropdownTransition from '@theme/components/DropdownTransition.vue'
|
||||
import last from 'lodash/last'
|
||||
|
||||
export default {
|
||||
components: { NavLink, DropdownTransition },
|
||||
|
||||
data () {
|
||||
return {
|
||||
open: false,
|
||||
isMQMobile: false
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
item: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
dropdownAriaLabel () {
|
||||
return this.item.ariaLabel || this.item.text
|
||||
}
|
||||
},
|
||||
beforeMount () {
|
||||
this.isMQMobile = window.innerWidth < 720 ? true : false;
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.isMQMobile = window.innerWidth < 720 ? true : false;
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
toggle () {
|
||||
if (this.isMQMobile) {
|
||||
this.open = !this.open
|
||||
}
|
||||
},
|
||||
|
||||
isLastItemOfArray (item, array) {
|
||||
return last(array) === item
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
$route () {
|
||||
this.open = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.dropdown-wrapper
|
||||
cursor pointer
|
||||
.dropdown-title
|
||||
display block
|
||||
font-size 0.9rem
|
||||
font-family inherit
|
||||
cursor inherit
|
||||
padding inherit
|
||||
line-height 1.4rem
|
||||
background transparent
|
||||
border none
|
||||
font-weight 500
|
||||
color var(--textColor)
|
||||
&:hover
|
||||
border-color transparent
|
||||
.arrow
|
||||
vertical-align middle
|
||||
margin-top -1px
|
||||
margin-left 0.4rem
|
||||
.nav-dropdown
|
||||
.dropdown-item
|
||||
color inherit
|
||||
line-height 1.7rem
|
||||
h4
|
||||
margin 0.45rem 0 0
|
||||
border-top 1px solid var(--borderColor)
|
||||
padding 0.45rem 1.5rem 0 1.25rem
|
||||
.dropdown-subitem-wrapper
|
||||
padding 0
|
||||
list-style none
|
||||
.dropdown-subitem
|
||||
font-size 0.9em
|
||||
a
|
||||
display block
|
||||
line-height 1.7rem
|
||||
position relative
|
||||
border-bottom none
|
||||
font-weight 400
|
||||
margin-bottom 0
|
||||
padding 0 1.5rem 0 1.25rem
|
||||
&:hover
|
||||
color $accentColor
|
||||
&.router-link-active
|
||||
color $accentColor
|
||||
&::after
|
||||
content ''
|
||||
width 0
|
||||
height 0
|
||||
border-left 5px solid $accentColor
|
||||
border-top 3px solid transparent
|
||||
border-bottom 3px solid transparent
|
||||
position absolute
|
||||
top calc(50% - 2px)
|
||||
left 9px
|
||||
&:first-child h4
|
||||
margin-top 0
|
||||
padding-top 0
|
||||
border-top 0
|
||||
@media (max-width $MQMobile)
|
||||
.dropdown-wrapper
|
||||
&.open .dropdown-title
|
||||
margin-bottom 0.5rem
|
||||
.dropdown-title
|
||||
font-weight 600
|
||||
font-size inherit
|
||||
&:hover
|
||||
color $accentColor
|
||||
.link-title
|
||||
display none
|
||||
.title
|
||||
display inline-block !important
|
||||
.nav-dropdown
|
||||
transition height 0.1s ease-out
|
||||
overflow hidden
|
||||
.dropdown-item
|
||||
h4
|
||||
border-top 0
|
||||
margin-top 0
|
||||
padding-top 0
|
||||
h4, & > a
|
||||
font-size 15px
|
||||
line-height 2rem
|
||||
.dropdown-subitem
|
||||
font-size 14px
|
||||
padding-left 1rem
|
||||
@media (min-width $MQMobile)
|
||||
.dropdown-wrapper
|
||||
height 1.8rem
|
||||
&:hover .nav-dropdown, &.open .nav-dropdown
|
||||
// override the inline style.
|
||||
display block !important
|
||||
&.open:blur
|
||||
display none
|
||||
.dropdown-title .arrow
|
||||
// make the arrow always down at desktop
|
||||
border-left 4px solid transparent
|
||||
border-right 4px solid transparent
|
||||
border-top 6px solid $arrowBgColor
|
||||
border-bottom 0
|
||||
.nav-dropdown
|
||||
display none
|
||||
// Avoid height shaked by clicking
|
||||
height auto !important
|
||||
box-sizing border-box
|
||||
max-height calc(100vh - 2.7rem)
|
||||
overflow-y auto
|
||||
position absolute
|
||||
top 100%
|
||||
right 0
|
||||
background-color var(--mainBg)
|
||||
padding 0.6rem 0
|
||||
border 1px solid var(--borderColor)
|
||||
border-bottom-color var(--borderColor)
|
||||
text-align left
|
||||
border-radius 0.25rem
|
||||
white-space nowrap
|
||||
margin 0
|
||||
.nav-item .dropdown-title a
|
||||
&:hover, &.router-link-active
|
||||
margin-bottom -2px
|
||||
border-bottom 2px solid lighten($accentColor, 8%)
|
||||
</style>
|
||||
32
theme-vdoing/components/DropdownTransition.vue
Normal file
32
theme-vdoing/components/DropdownTransition.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<transition
|
||||
name="dropdown"
|
||||
@enter="setHeight"
|
||||
@after-enter="unsetHeight"
|
||||
@before-leave="setHeight"
|
||||
>
|
||||
<slot />
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DropdownTransition',
|
||||
|
||||
methods: {
|
||||
setHeight (items) {
|
||||
// explicitly set height so that it can be transitioned
|
||||
items.style.height = items.scrollHeight + 'px'
|
||||
},
|
||||
|
||||
unsetHeight (items) {
|
||||
items.style.height = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.dropdown-enter, .dropdown-leave-to
|
||||
height 0 !important
|
||||
</style>
|
||||
74
theme-vdoing/components/Footer.vue
Normal file
74
theme-vdoing/components/Footer.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="footer">
|
||||
<div
|
||||
class="icons"
|
||||
v-if="social && social.icons"
|
||||
>
|
||||
<a
|
||||
:href="item.link"
|
||||
:title="item.title"
|
||||
:class="['iconfont', item.iconClass]"
|
||||
v-for="(item, index) in social.icons"
|
||||
:key="index"
|
||||
target="_blank"
|
||||
></a>
|
||||
</div>
|
||||
|
||||
<!--Vdoing主题遵循MIT协议,完全开源且免费。如果您对主题的修改并不大,希望您保留主题的链接。-->
|
||||
Theme by
|
||||
<a
|
||||
href="https://github.com/xugaoyi/vuepress-theme-vdoing"
|
||||
target="_blank"
|
||||
title="本站主题"
|
||||
>Vdoing</a>
|
||||
<template v-if="footer">
|
||||
| Copyright © {{ footer.createYear }}-{{ new Date().getFullYear() }}
|
||||
<span
|
||||
v-html="footer.copyrightInfo"
|
||||
></span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
social () {
|
||||
return this.$themeConfig.social
|
||||
},
|
||||
footer () {
|
||||
return this.$themeConfig.footer
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='stylus'>
|
||||
// $mobileSidebarWidth = $sidebarWidth * 0.82
|
||||
.footer
|
||||
padding 5rem 1.5rem 2.5rem
|
||||
text-align center
|
||||
color #666
|
||||
box-sizing border-box
|
||||
font-size 0.85rem
|
||||
transition all 0.2s ease
|
||||
.icons
|
||||
margin-bottom 12px
|
||||
.iconfont
|
||||
padding 0 10px
|
||||
font-size 1.3rem
|
||||
a
|
||||
color inherit
|
||||
&:hover
|
||||
color $accentColor
|
||||
@media (min-width ($MQMobile + 1px))
|
||||
.sidebar-open .footer
|
||||
width auto
|
||||
padding-left ($sidebarWidth + 1.5rem)
|
||||
@media (min-width 1520px)
|
||||
.have-rightmenu .footer
|
||||
padding-right ($rightMenuWidth + 1.5rem)
|
||||
.no-sidebar .footer
|
||||
width auto
|
||||
padding-left 1.5rem
|
||||
</style>
|
||||
540
theme-vdoing/components/Home.vue
Normal file
540
theme-vdoing/components/Home.vue
Normal file
@@ -0,0 +1,540 @@
|
||||
<template>
|
||||
<div class="home-wrapper">
|
||||
<!-- banner块 s -->
|
||||
<div
|
||||
class="banner"
|
||||
:class="{ 'hide-banner': !showBanner }"
|
||||
:style="bannerBgStyle"
|
||||
>
|
||||
<div
|
||||
class="banner-conent"
|
||||
:style="
|
||||
!homeData.features && !homeData.heroImage && `padding-top: 7rem`
|
||||
"
|
||||
>
|
||||
<header class="hero">
|
||||
<img
|
||||
v-if="homeData.heroImage"
|
||||
:src="$withBase(homeData.heroImage)"
|
||||
:alt="homeData.heroAlt"
|
||||
/>
|
||||
<h1 v-if="homeData.heroText" id="main-title">
|
||||
{{ homeData.heroText }}
|
||||
</h1>
|
||||
<p v-if="homeData.tagline" class="description">
|
||||
{{ homeData.tagline }}
|
||||
</p>
|
||||
<p class="action" v-if="homeData.actionText && homeData.actionLink">
|
||||
<NavLink class="action-button" :item="actionLink" />
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- PC端features块 s -->
|
||||
<div
|
||||
class="features"
|
||||
v-if="homeData.features && homeData.features.length && !isMQMobile"
|
||||
>
|
||||
<div
|
||||
class="feature"
|
||||
v-for="(feature, index) in homeData.features"
|
||||
:key="index"
|
||||
>
|
||||
<router-link v-if="feature.link" :to="feature.link">
|
||||
<img
|
||||
class="feature-img"
|
||||
v-if="feature.imgUrl"
|
||||
:src="$withBase(feature.imgUrl)"
|
||||
:alt="feature.title"
|
||||
/>
|
||||
<h2>{{ feature.title }}</h2>
|
||||
<p>{{ feature.details }}</p>
|
||||
</router-link>
|
||||
<a v-else href="javascript:;">
|
||||
<img
|
||||
class="feature-img"
|
||||
v-if="feature.imgUrl"
|
||||
:src="$withBase(feature.imgUrl)"
|
||||
:alt="feature.title"
|
||||
/>
|
||||
<h2>{{ feature.title }}</h2>
|
||||
<p>{{ feature.details }}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- PC端features块 e -->
|
||||
</div>
|
||||
|
||||
<!-- 移动端features块 s -->
|
||||
<!-- isMQMobile放到v-if上线后会报错 -->
|
||||
<div
|
||||
class="slide-banner"
|
||||
v-if="homeData.features && homeData.features.length"
|
||||
v-show="isMQMobile"
|
||||
>
|
||||
<div class="banner-wrapper">
|
||||
<div class="slide-banner-scroll" ref="slide">
|
||||
<div class="slide-banner-wrapper">
|
||||
<div
|
||||
class="slide-item"
|
||||
v-for="(feature, index) in homeData.features"
|
||||
:key="index"
|
||||
>
|
||||
<router-link v-if="feature.link" :to="feature.link">
|
||||
<img
|
||||
class="feature-img"
|
||||
v-if="feature.imgUrl"
|
||||
:src="$withBase(feature.imgUrl)"
|
||||
:alt="feature.title"
|
||||
/>
|
||||
<h2>{{ feature.title }}</h2>
|
||||
<p>{{ feature.details }}</p>
|
||||
</router-link>
|
||||
<a v-else href="javascript:;">
|
||||
<img
|
||||
class="feature-img"
|
||||
v-if="feature.imgUrl"
|
||||
:src="$withBase(feature.imgUrl)"
|
||||
:alt="feature.title"
|
||||
/>
|
||||
<h2>{{ feature.title }}</h2>
|
||||
<p>{{ feature.details }}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="docs-wrapper">
|
||||
<span
|
||||
class="doc"
|
||||
v-for="(item, index) in homeData.features.length"
|
||||
:key="index"
|
||||
:class="{ active: currentPageIndex === index }"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 移动端features块 e -->
|
||||
</div>
|
||||
<!-- banner块 e -->
|
||||
|
||||
<MainLayout>
|
||||
<template #mainLeft>
|
||||
<!-- 简约版文章列表 -->
|
||||
<UpdateArticle
|
||||
class="card-box"
|
||||
v-if="homeData.postList === 'simple'"
|
||||
:length="homeData.simplePostListLength || 10"
|
||||
/>
|
||||
|
||||
<!-- 详情版文章列表 -->
|
||||
<template
|
||||
v-else-if="!homeData.postList || homeData.postList === 'detailed'"
|
||||
>
|
||||
<PostList :currentPage="currentPage" :perPage="perPage" />
|
||||
<Pagination
|
||||
:total="total"
|
||||
:perPage="perPage"
|
||||
:currentPage="currentPage"
|
||||
@getCurrentPage="handlePagination"
|
||||
v-show="Math.ceil(total / perPage) > 1"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<Content class="theme-vdoing-content custom card-box" />
|
||||
</template>
|
||||
|
||||
<template #mainRight>
|
||||
<BloggerBar v-if="$themeConfig.blogger" />
|
||||
<CategoriesBar
|
||||
v-if="
|
||||
$themeConfig.category !== false &&
|
||||
$categoriesAndTags.categories.length
|
||||
"
|
||||
:categoriesData="$categoriesAndTags.categories"
|
||||
:length="10"
|
||||
/>
|
||||
<TagsBar
|
||||
v-if="$themeConfig.tag !== false && $categoriesAndTags.tags.length"
|
||||
:tagsData="$categoriesAndTags.tags"
|
||||
:length="30"
|
||||
/>
|
||||
<div
|
||||
class="custom-html-box card-box"
|
||||
v-if="homeSidebarB"
|
||||
v-html="homeSidebarB"
|
||||
></div>
|
||||
</template>
|
||||
</MainLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavLink from "@theme/components/NavLink";
|
||||
import BScroll from "@better-scroll/core"
|
||||
import Slide from "@better-scroll/slide"
|
||||
import MainLayout from '@theme/components/MainLayout'
|
||||
import PostList from '@theme/components/PostList'
|
||||
import UpdateArticle from '@theme/components/UpdateArticle'
|
||||
import Pagination from '@theme/components/Pagination'
|
||||
import BloggerBar from '@theme/components/BloggerBar'
|
||||
import CategoriesBar from '@theme/components/CategoriesBar'
|
||||
import TagsBar from '@theme/components/TagsBar'
|
||||
|
||||
const MOBILE_DESKTOP_BREAKPOINT = 720 // refer to config.styl
|
||||
|
||||
BScroll.use(Slide)
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
isMQMobile: false,
|
||||
|
||||
slide: null,
|
||||
currentPageIndex: 0,
|
||||
playTimer: 0,
|
||||
mark: 0,
|
||||
|
||||
total: 0, // 总长
|
||||
perPage: 10, // 每页长
|
||||
currentPage: 1// 当前页
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
homeSidebarB () {
|
||||
const { htmlModules } = this.$themeConfig
|
||||
return htmlModules ? htmlModules.homeSidebarB : ''
|
||||
},
|
||||
showBanner () { // 当分页不在第一页时隐藏banner栏
|
||||
return this.$route.query.p
|
||||
&& this.$route.query.p != 1
|
||||
&& (!this.homeData.postList || this.homeData.postList === 'detailed')
|
||||
? false : true
|
||||
},
|
||||
bannerBgStyle () {
|
||||
let bannerBg = this.homeData.bannerBg
|
||||
if (!bannerBg || bannerBg === 'auto') { // 默认
|
||||
if (this.$themeConfig.bodyBgImg) { // 当有bodyBgImg时,不显示背景
|
||||
return ''
|
||||
} else { // 网格纹背景
|
||||
return 'background: rgb(40,40,45) url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAYAAAAe2bNZAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABOSURBVFhH7c6xCQAgDAVRR9A6E4hLu4uLiWJ7tSnuQcIvr2TRYsw3/zOGGEOMIcYQY4gxxBhiDDGGGEOMIcYQY4gxxBhiDLkx52W4Gn1tuslCtHJvL54AAAAASUVORK5CYII=)'
|
||||
}
|
||||
} else if (bannerBg === 'none') { // 无背景
|
||||
if (this.$themeConfig.bodyBgImg) {
|
||||
return ''
|
||||
} else {
|
||||
return 'background: var(--mainBg);color: var(--textColor)'
|
||||
}
|
||||
} else if (bannerBg.indexOf('background') > -1) { // 自定义背景样式
|
||||
return bannerBg
|
||||
} else if (bannerBg.indexOf('.') > -1) { // 大图
|
||||
return `background: url(${this.$withBase(bannerBg)}) center center / cover no-repeat`
|
||||
}
|
||||
|
||||
},
|
||||
homeData () {
|
||||
return {
|
||||
...this.$page.frontmatter
|
||||
}
|
||||
},
|
||||
actionLink () {
|
||||
return {
|
||||
link: this.homeData.actionLink,
|
||||
text: this.homeData.actionText
|
||||
};
|
||||
}
|
||||
},
|
||||
components: { NavLink, MainLayout, PostList, UpdateArticle, BloggerBar, CategoriesBar, TagsBar, Pagination },
|
||||
created () {
|
||||
this.total = this.$sortPosts.length
|
||||
},
|
||||
beforeMount () {
|
||||
this.isMQMobile = window.innerWidth < MOBILE_DESKTOP_BREAKPOINT ? true : false; // vupress在打包时不能在beforeCreate(),created()访问浏览器api(如window)
|
||||
},
|
||||
mounted () {
|
||||
if (this.$route.query.p) {
|
||||
this.currentPage = Number(this.$route.query.p)
|
||||
}
|
||||
|
||||
if (this.isMQMobile && (!this.$route.query.p || this.$route.query.p == 1)) {
|
||||
this.init()
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.isMQMobile = window.innerWidth < MOBILE_DESKTOP_BREAKPOINT ? true : false;
|
||||
if (this.isMQMobile && !this.slide && !this.mark) {
|
||||
this.mark++
|
||||
setTimeout(() => {
|
||||
this.init()
|
||||
}, 60)
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
beforeDestroy () {
|
||||
clearTimeout(this.playTimer)
|
||||
this.slide && this.slide.destroy()
|
||||
},
|
||||
watch: {
|
||||
'$route.query.p' () {
|
||||
if (!this.$route.query.p) {
|
||||
this.currentPage = 1
|
||||
} else {
|
||||
this.currentPage = Number(this.$route.query.p)
|
||||
}
|
||||
|
||||
if (this.currentPage === 1 && this.isMQMobile) {
|
||||
setTimeout(() => {
|
||||
this.slide && this.slide.destroy()
|
||||
this.init()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init () {
|
||||
clearTimeout(this.playTimer)
|
||||
this.slide = new BScroll(this.$refs.slide, {
|
||||
scrollX: true, // x轴滚动
|
||||
scrollY: false, // y轴滚动
|
||||
slide: {
|
||||
loop: true,
|
||||
threshold: 100
|
||||
},
|
||||
useTransition: true, // 使用css3 transition动画
|
||||
momentum: false,
|
||||
bounce: false, // 回弹
|
||||
stopPropagation: false, // 是否阻止事件冒泡
|
||||
probeType: 2,
|
||||
preventDefault: false
|
||||
})
|
||||
|
||||
// user touches the slide area
|
||||
this.slide.on('beforeScrollStart', () => {
|
||||
clearTimeout(this.playTimer)
|
||||
})
|
||||
// user touched the slide done
|
||||
this.slide.on('scrollEnd', () => {
|
||||
this.autoGoNext()
|
||||
})
|
||||
this.slide.on('slideWillChange', (page) => {
|
||||
this.currentPageIndex = page.pageX
|
||||
})
|
||||
this.autoGoNext()
|
||||
},
|
||||
autoGoNext () {
|
||||
clearTimeout(this.playTimer)
|
||||
this.playTimer = setTimeout(() => {
|
||||
this.slide.next()
|
||||
}, 4000)
|
||||
},
|
||||
handlePagination (i) { // 分页
|
||||
this.currentPage = i
|
||||
},
|
||||
getScrollTop () {
|
||||
return window.pageYOffset
|
||||
|| document.documentElement.scrollTop
|
||||
|| document.body.scrollTop
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.home-wrapper
|
||||
.banner
|
||||
width 100%
|
||||
min-height 450px
|
||||
margin-top $navbarHeight
|
||||
color $bannerTextColor
|
||||
position relative
|
||||
overflow hidden
|
||||
.banner-conent
|
||||
max-width $homePageWidth
|
||||
margin 0px auto
|
||||
position relative
|
||||
z-index 1
|
||||
overflow hidden
|
||||
.hero
|
||||
text-align center
|
||||
margin-top 3rem
|
||||
img
|
||||
max-width 100%
|
||||
max-height 240px
|
||||
display block
|
||||
margin 2rem auto 1.5rem
|
||||
h1
|
||||
margin 0
|
||||
font-size 3.2rem
|
||||
.description, .action
|
||||
margin 1.5rem auto
|
||||
.description
|
||||
max-width 40rem
|
||||
font-size 1.1rem
|
||||
line-height 1.3
|
||||
opacity 0.9
|
||||
.action-button
|
||||
display inline-block
|
||||
font-size 1.2rem
|
||||
background-color $accentColor
|
||||
padding 0.8rem 1.6rem
|
||||
border-radius 4px
|
||||
transition background-color 0.1s ease
|
||||
box-sizing border-box
|
||||
border-bottom 1px solid darken($accentColor, 10%)
|
||||
color #fff
|
||||
&:hover
|
||||
background-color lighten($accentColor, 10%)
|
||||
// pc端features
|
||||
.features
|
||||
padding 2rem 0
|
||||
margin-top 2.5rem
|
||||
display flex
|
||||
flex-wrap wrap
|
||||
align-items flex-start
|
||||
align-content stretch
|
||||
justify-content space-between
|
||||
.feature
|
||||
flex-grow 1
|
||||
flex-basis 30%
|
||||
max-width 30%
|
||||
text-align center
|
||||
a
|
||||
// color lighten($bannerTextColor,10%)
|
||||
color inherit
|
||||
.feature-img
|
||||
width 10rem
|
||||
height 10rem
|
||||
animation heart 1.2s ease-in-out 0s infinite alternate
|
||||
animation-play-state paused
|
||||
h2
|
||||
font-weight 500
|
||||
font-size 1.3rem
|
||||
border-bottom none
|
||||
padding-bottom 0
|
||||
p
|
||||
opacity 0.8
|
||||
padding 0 0.8rem
|
||||
.feature:hover
|
||||
.feature-img
|
||||
animation-play-state running
|
||||
h2, p
|
||||
color $accentColor
|
||||
// 移动端滑动图标
|
||||
.slide-banner
|
||||
margin-top 2rem
|
||||
.banner-wrapper
|
||||
position relative
|
||||
.slide-banner-scroll
|
||||
min-height 1px
|
||||
overflow hidden
|
||||
.slide-banner-wrapper
|
||||
height 300px
|
||||
.slide-item
|
||||
display inline-block
|
||||
height 300px
|
||||
width 100%
|
||||
text-align center
|
||||
a
|
||||
// color lighten($bannerTextColor,10%)
|
||||
color inherit
|
||||
.feature-img
|
||||
width 10rem
|
||||
height 10rem
|
||||
h2
|
||||
font-size 1.1rem
|
||||
font-weight 500
|
||||
border-bottom none
|
||||
padding-bottom 0
|
||||
p
|
||||
opacity 0.8
|
||||
padding 0 0.8rem
|
||||
.docs-wrapper
|
||||
position absolute
|
||||
bottom 25px
|
||||
left 50%
|
||||
transform translateX(-50%)
|
||||
.doc
|
||||
display inline-block
|
||||
margin 0 4px
|
||||
width 8px
|
||||
height 8px
|
||||
border-radius 50%
|
||||
background var(--textColor)
|
||||
opacity 0.9
|
||||
&.active
|
||||
opacity 0.5
|
||||
// 分页不在第一页时,隐藏banner栏
|
||||
.banner.hide-banner
|
||||
display none
|
||||
& + .main-wrapper
|
||||
margin-top ($navbarHeight + 0.9rem)
|
||||
.main-wrapper
|
||||
margin-top 2rem
|
||||
.main-left
|
||||
.card-box
|
||||
margin-bottom 0.9rem
|
||||
.pagination
|
||||
margin-bottom 4rem
|
||||
.theme-vdoing-content
|
||||
padding 0 2rem
|
||||
overflow hidden
|
||||
&>:first-child
|
||||
padding-top 2rem
|
||||
&>:last-child
|
||||
padding-bottom 2rem
|
||||
.main-right
|
||||
.custom-html-box
|
||||
padding 0
|
||||
overflow hidden
|
||||
@keyframes heart
|
||||
from
|
||||
transform translate(0, 0)
|
||||
to
|
||||
transform translate(0, 8px)
|
||||
// 1025px以下
|
||||
@media (max-width 1025px)
|
||||
.home-wrapper
|
||||
.banner
|
||||
.banner-conent
|
||||
.hero
|
||||
h1
|
||||
font-size 2.5rem
|
||||
.description
|
||||
font-size 1rem
|
||||
.feature
|
||||
a
|
||||
h2
|
||||
font-size 1.1rem
|
||||
.feature-img
|
||||
width 9rem
|
||||
height 9rem
|
||||
// 719px以下
|
||||
@media (max-width $MQMobile)
|
||||
.home-wrapper
|
||||
.banner
|
||||
.banner-conent
|
||||
.features
|
||||
display none !important
|
||||
// 419px以下
|
||||
@media (max-width $MQMobileNarrow)
|
||||
.home-wrapper
|
||||
.banner-conent
|
||||
padding-left 1.5rem
|
||||
padding-right 1.5rem
|
||||
.hero
|
||||
img
|
||||
max-height 210px
|
||||
margin 2rem auto 1.2rem
|
||||
h1
|
||||
font-size 2rem
|
||||
h1, .description, .action
|
||||
margin 1.2rem auto
|
||||
.description
|
||||
font-size 1.2rem
|
||||
.action-button
|
||||
font-size 1rem
|
||||
padding 0.6rem 1.2rem
|
||||
.feature
|
||||
h2
|
||||
font-size 1.25rem
|
||||
</style>
|
||||
59
theme-vdoing/components/MainLayout.vue
Normal file
59
theme-vdoing/components/MainLayout.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="main-wrapper">
|
||||
<div class="main-left">
|
||||
<slot name="mainLeft" />
|
||||
</div>
|
||||
|
||||
<div class="main-right">
|
||||
<slot name="mainRight" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="stylus">
|
||||
.main-wrapper
|
||||
margin 1.5rem auto 0 auto
|
||||
max-width $homePageWidth
|
||||
padding 0 0.9rem
|
||||
box-sizing border-box
|
||||
position relative
|
||||
display flex
|
||||
.main-left
|
||||
flex 1
|
||||
// width 72%
|
||||
.theme-vdoing-content.card-box
|
||||
padding 1rem 1.5rem
|
||||
margin-bottom 0.9rem
|
||||
.home-content
|
||||
padding 1rem 1.5rem 0
|
||||
.main-right
|
||||
>*
|
||||
width 245px
|
||||
box-sizing border-box
|
||||
@media (max-width 900px)
|
||||
width 235px
|
||||
.card-box
|
||||
margin 0 0 0.9rem 0.9rem
|
||||
padding-top 0.95rem
|
||||
padding-bottom 0.95rem
|
||||
// 719px以下
|
||||
@media (max-width $MQMobile)
|
||||
.main-wrapper
|
||||
margin 0.9rem 0
|
||||
padding 0
|
||||
display block
|
||||
.main-left
|
||||
width 100%
|
||||
.post-list
|
||||
margin-bottom 3rem
|
||||
.post
|
||||
border-radius 0
|
||||
.pagination
|
||||
margin-bottom 3rem
|
||||
.main-right
|
||||
.blogger-wrapper
|
||||
display none
|
||||
.card-box
|
||||
margin 0 0 0.9rem 0
|
||||
border-radius 0
|
||||
width 100%
|
||||
</style>
|
||||
54
theme-vdoing/components/NavLink.vue
Normal file
54
theme-vdoing/components/NavLink.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:to="link"
|
||||
@focusout.native="focusoutAction"
|
||||
v-if="!isExternal(link)"
|
||||
:exact="exact"
|
||||
>{{ item.text }}</router-link>
|
||||
<a
|
||||
v-else
|
||||
:href="link"
|
||||
@focusout="focusoutAction"
|
||||
class="nav-link external"
|
||||
:target="isMailto(link) || isTel(link) ? null : '_blank'"
|
||||
:rel="isMailto(link) || isTel(link) ? null : 'noopener noreferrer'"
|
||||
>
|
||||
{{ item.text }}
|
||||
<OutboundLink />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isExternal, isMailto, isTel, ensureExt } from '../util'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
item: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
link () {
|
||||
return ensureExt(this.item.link)
|
||||
},
|
||||
|
||||
exact () {
|
||||
if (this.$site.locales) {
|
||||
return Object.keys(this.$site.locales).some(rootLink => rootLink === this.link)
|
||||
}
|
||||
return this.link === '/'
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
isExternal,
|
||||
isMailto,
|
||||
isTel,
|
||||
focusoutAction () {
|
||||
this.$emit('focusout')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
154
theme-vdoing/components/NavLinks.vue
Normal file
154
theme-vdoing/components/NavLinks.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<nav
|
||||
class="nav-links"
|
||||
v-if="userLinks.length || repoLink"
|
||||
>
|
||||
<!-- user links -->
|
||||
<div
|
||||
class="nav-item"
|
||||
v-for="item in userLinks"
|
||||
:key="item.link"
|
||||
>
|
||||
<DropdownLink
|
||||
v-if="item.type === 'links'"
|
||||
:item="item"
|
||||
/>
|
||||
<NavLink
|
||||
v-else
|
||||
:item="item"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- repo link -->
|
||||
<a
|
||||
v-if="repoLink"
|
||||
:href="repoLink"
|
||||
class="repo-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ repoLabel }}
|
||||
<OutboundLink />
|
||||
</a>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DropdownLink from '@theme/components/DropdownLink.vue'
|
||||
import { resolveNavLinkItem } from '../util'
|
||||
import NavLink from '@theme/components/NavLink.vue'
|
||||
|
||||
export default {
|
||||
components: { NavLink, DropdownLink },
|
||||
|
||||
computed: {
|
||||
userNav () {
|
||||
return this.$themeLocaleConfig.nav || this.$site.themeConfig.nav || []
|
||||
},
|
||||
|
||||
nav () {
|
||||
const { locales } = this.$site
|
||||
if (locales && Object.keys(locales).length > 1) {
|
||||
const currentLink = this.$page.path
|
||||
const routes = this.$router.options.routes
|
||||
const themeLocales = this.$site.themeConfig.locales || {}
|
||||
const languageDropdown = {
|
||||
text: this.$themeLocaleConfig.selectText || 'Languages',
|
||||
ariaLabel: this.$themeLocaleConfig.ariaLabel || 'Select language',
|
||||
items: Object.keys(locales).map(path => {
|
||||
const locale = locales[path]
|
||||
const text = themeLocales[path] && themeLocales[path].label || locale.lang
|
||||
let link
|
||||
// Stay on the current page
|
||||
if (locale.lang === this.$lang) {
|
||||
link = currentLink
|
||||
} else {
|
||||
// Try to stay on the same page
|
||||
link = currentLink.replace(this.$localeConfig.path, path)
|
||||
// fallback to homepage
|
||||
if (!routes.some(route => route.path === link)) {
|
||||
link = path
|
||||
}
|
||||
}
|
||||
return { text, link }
|
||||
})
|
||||
}
|
||||
return [...this.userNav, languageDropdown]
|
||||
}
|
||||
return this.userNav
|
||||
},
|
||||
|
||||
userLinks () {
|
||||
return (this.nav || []).map(link => {
|
||||
return Object.assign(resolveNavLinkItem(link), {
|
||||
items: (link.items || []).map(resolveNavLinkItem)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
repoLink () {
|
||||
const { repo } = this.$site.themeConfig
|
||||
if (repo) {
|
||||
return /^https?:/.test(repo)
|
||||
? repo
|
||||
: `https://github.com/${repo}`
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
repoLabel () {
|
||||
if (!this.repoLink) return
|
||||
if (this.$site.themeConfig.repoLabel) {
|
||||
return this.$site.themeConfig.repoLabel
|
||||
}
|
||||
|
||||
const repoHost = this.repoLink.match(/^https?:\/\/[^/]+/)[0]
|
||||
const platforms = ['GitHub', 'GitLab', 'Bitbucket']
|
||||
for (let i = 0; i < platforms.length; i++) {
|
||||
const platform = platforms[i]
|
||||
if (new RegExp(platform, 'i').test(repoHost)) {
|
||||
return platform
|
||||
}
|
||||
}
|
||||
|
||||
return 'Source'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.nav-links
|
||||
display inline-block
|
||||
a
|
||||
line-height 1.4rem
|
||||
color inherit
|
||||
&:hover, &.router-link-active
|
||||
color $accentColor
|
||||
.nav-item
|
||||
position relative
|
||||
display inline-block
|
||||
margin-left 1.5rem
|
||||
line-height 2rem
|
||||
&:first-child
|
||||
margin-left 0
|
||||
.repo-link
|
||||
margin-left 1.5rem
|
||||
// 959
|
||||
@media (max-width $MQNarrow)
|
||||
.nav-links
|
||||
.nav-item
|
||||
margin-left 1.2rem
|
||||
@media (max-width $MQMobile)
|
||||
.nav-links
|
||||
.nav-item, .repo-link
|
||||
margin-left 0
|
||||
@media (min-width $MQMobile)
|
||||
.nav-links a
|
||||
&:hover, &.router-link-active
|
||||
color var(--textColor)
|
||||
.nav-item > a:not(.external)
|
||||
&:hover, &.router-link-active
|
||||
margin-bottom -2px
|
||||
border-bottom 2px solid lighten($accentColor, 8%)
|
||||
</style>
|
||||
141
theme-vdoing/components/Navbar.vue
Normal file
141
theme-vdoing/components/Navbar.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<header class="navbar blur">
|
||||
<SidebarButton @toggle-sidebar="$emit('toggle-sidebar')" />
|
||||
|
||||
<router-link
|
||||
:to="$localePath"
|
||||
class="home-link"
|
||||
>
|
||||
<img
|
||||
class="logo"
|
||||
v-if="$site.themeConfig.logo"
|
||||
:src="$withBase($site.themeConfig.logo)"
|
||||
:alt="$siteTitle"
|
||||
/>
|
||||
<!-- <span-->
|
||||
<!-- ref="siteName"-->
|
||||
<!-- class="site-name"-->
|
||||
<!-- v-if="$siteTitle"-->
|
||||
<!-- :class="{ 'can-hide': $site.themeConfig.logo }"-->
|
||||
<!-- >{{ $siteTitle }}</span>-->
|
||||
</router-link>
|
||||
|
||||
<div
|
||||
class="links"
|
||||
:style="linksWrapMaxWidth ? {
|
||||
'max-width': linksWrapMaxWidth + 'px'
|
||||
} : {}"
|
||||
>
|
||||
<AlgoliaSearchBox
|
||||
v-if="isAlgoliaSearch"
|
||||
:options="algolia"
|
||||
/>
|
||||
<SearchBox
|
||||
v-else-if="$site.themeConfig.search !== false && $page.frontmatter.search !== false"
|
||||
/>
|
||||
<NavLinks class="can-hide" />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AlgoliaSearchBox from '@AlgoliaSearchBox'
|
||||
import SearchBox from '@SearchBox'
|
||||
import SidebarButton from '@theme/components/SidebarButton.vue'
|
||||
import NavLinks from '@theme/components/NavLinks.vue'
|
||||
|
||||
export default {
|
||||
components: { SidebarButton, NavLinks, SearchBox, AlgoliaSearchBox },
|
||||
|
||||
data () {
|
||||
return {
|
||||
linksWrapMaxWidth: null
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
const MOBILE_DESKTOP_BREAKPOINT = 719 // refer to config.styl
|
||||
const NAVBAR_VERTICAL_PADDING = parseInt(css(this.$el, 'paddingLeft')) + parseInt(css(this.$el, 'paddingRight'))
|
||||
const handleLinksWrapWidth = () => {
|
||||
if (document.documentElement.clientWidth < MOBILE_DESKTOP_BREAKPOINT) {
|
||||
this.linksWrapMaxWidth = null
|
||||
} else {
|
||||
this.linksWrapMaxWidth = this.$el.offsetWidth - NAVBAR_VERTICAL_PADDING
|
||||
- (this.$refs.siteName && this.$refs.siteName.offsetWidth || 0)
|
||||
}
|
||||
}
|
||||
handleLinksWrapWidth()
|
||||
window.addEventListener('resize', handleLinksWrapWidth, false)
|
||||
},
|
||||
|
||||
computed: {
|
||||
algolia () {
|
||||
return this.$themeLocaleConfig.algolia || this.$site.themeConfig.algolia || {}
|
||||
},
|
||||
|
||||
isAlgoliaSearch () {
|
||||
return this.algolia && this.algolia.apiKey && this.algolia.indexName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function css (el, property) {
|
||||
// NOTE: Known bug, will return 'auto' if style value is 'auto'
|
||||
const win = el.ownerDocument.defaultView
|
||||
// null means not to return pseudo styles
|
||||
return win.getComputedStyle(el, null)[property]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
$navbar-vertical-padding = 0.7rem
|
||||
$navbar-horizontal-padding = 1.5rem
|
||||
.navbar
|
||||
padding $navbar-vertical-padding $navbar-horizontal-padding
|
||||
line-height $navbarHeight - 1.4rem
|
||||
transition transform 0.3s
|
||||
a, span, img
|
||||
display inline-block
|
||||
.logo
|
||||
height $navbarHeight - 1.4rem
|
||||
min-width $navbarHeight - 1.4rem
|
||||
margin-right 0.8rem
|
||||
vertical-align top
|
||||
.site-name
|
||||
font-size 1.3rem
|
||||
font-weight 600
|
||||
color var(--textColor)
|
||||
position relative
|
||||
.links
|
||||
padding-left 1.5rem
|
||||
box-sizing border-box
|
||||
white-space nowrap
|
||||
font-size 0.9rem
|
||||
position absolute
|
||||
right $navbar-horizontal-padding
|
||||
top $navbar-vertical-padding
|
||||
display flex
|
||||
.search-box
|
||||
flex 0 0 auto
|
||||
vertical-align top
|
||||
.hide-navbar
|
||||
.navbar
|
||||
transform translateY(-100%)
|
||||
// 959
|
||||
@media (max-width $MQNarrow)
|
||||
.navbar
|
||||
.site-name
|
||||
display none
|
||||
@media (max-width $MQMobile)
|
||||
.navbar
|
||||
padding-left 4rem
|
||||
.can-hide
|
||||
display none
|
||||
.links
|
||||
padding-left 1.5rem
|
||||
.site-name
|
||||
width calc(100vw - 9.4rem)
|
||||
overflow hidden
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
</style>
|
||||
174
theme-vdoing/components/Page.vue
Normal file
174
theme-vdoing/components/Page.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div>
|
||||
<main class="page">
|
||||
|
||||
<div :class="`theme-vdoing-wrapper ${bgStyle}`">
|
||||
<ArticleInfo v-if="isArticle()" />
|
||||
<component
|
||||
class="theme-vdoing-content"
|
||||
v-if="pageComponent"
|
||||
:is="pageComponent"
|
||||
/>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<RightMenu v-if="showRightMenu" />
|
||||
<h1 v-if="showTitle">
|
||||
<img
|
||||
:src="currentBadge"
|
||||
v-if="$themeConfig.titleBadge === false ? false : true"
|
||||
/>
|
||||
{{this.$page.title}}
|
||||
</h1>
|
||||
<slot name="top" v-if="isShowSlotT" />
|
||||
|
||||
<Content class="theme-vdoing-content" />
|
||||
</div>
|
||||
<slot name="bottom" v-if="isShowSlotB" />
|
||||
<PageEdit />
|
||||
|
||||
|
||||
<PageNav v-bind="{ sidebarItems }" />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<UpdateArticle
|
||||
:length="3"
|
||||
:moreArticle="updateBarConfig && updateBarConfig.moreArticle"
|
||||
v-if="isShowUpdateBar"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageEdit from '@theme/components/PageEdit.vue'
|
||||
import PageNav from '@theme/components/PageNav.vue'
|
||||
import ArticleInfo from './ArticleInfo.vue'
|
||||
import Catalogue from './Catalogue.vue'
|
||||
import UpdateArticle from './UpdateArticle.vue'
|
||||
import RightMenu from './RightMenu.vue'
|
||||
|
||||
import TitleBadgeMixin from '../mixins/titleBadge'
|
||||
|
||||
export default {
|
||||
mixins: [TitleBadgeMixin],
|
||||
data () {
|
||||
return {
|
||||
updateBarConfig: null
|
||||
}
|
||||
},
|
||||
props: ['sidebarItems'],
|
||||
components: { PageEdit, PageNav, ArticleInfo, Catalogue, UpdateArticle, RightMenu },
|
||||
created () {
|
||||
this.updateBarConfig = this.$themeConfig.updateBar
|
||||
},
|
||||
computed: {
|
||||
bgStyle () {
|
||||
const { contentBgStyle } = this.$themeConfig
|
||||
return contentBgStyle ? 'bg-style-' + contentBgStyle : ''
|
||||
},
|
||||
isShowUpdateBar () {
|
||||
return this.updateBarConfig && this.updateBarConfig.showToArticle === false ? false : true
|
||||
},
|
||||
showTitle () {
|
||||
return !this.$frontmatter.pageComponent
|
||||
},
|
||||
showRightMenu () {
|
||||
const { $frontmatter, $themeConfig, $page } = this
|
||||
const { sidebar } = $frontmatter
|
||||
return (
|
||||
$themeConfig.rightMenuBar !== false &&
|
||||
$page.headers &&
|
||||
($frontmatter && sidebar && sidebar !== false) !== false
|
||||
)
|
||||
},
|
||||
pageComponent () {
|
||||
return this.$frontmatter.pageComponent ? this.$frontmatter.pageComponent.name : false
|
||||
},
|
||||
isShowSlotT() {
|
||||
return this.getShowStatus('pageTshowMode')
|
||||
},
|
||||
isShowSlotB() {
|
||||
return this.getShowStatus('pageBshowMode')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getShowStatus(prop) {
|
||||
const { htmlModules } = this.$themeConfig
|
||||
if(!htmlModules) return false
|
||||
if (htmlModules[prop] === 'article') { // 仅文章页显示
|
||||
return this.isArticle()
|
||||
} else if (htmlModules[prop] === 'custom'){ // 仅自定义页显示
|
||||
return !(this.isArticle())
|
||||
} else { // 全部显示
|
||||
return true
|
||||
}
|
||||
},
|
||||
isArticle () {
|
||||
return this.$frontmatter.article !== false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@require '../styles/wrapper.styl'
|
||||
|
||||
.page
|
||||
padding-bottom 2rem
|
||||
display block
|
||||
@media (max-width $MQMobile)
|
||||
padding-top $navbarHeight
|
||||
@media (min-width $MQMobile)
|
||||
padding-top ($navbarHeight + 1.5rem)
|
||||
>*
|
||||
@extend $vdoing-wrapper
|
||||
.theme-vdoing-wrapper
|
||||
.content-wrapper
|
||||
position relative
|
||||
h1 img
|
||||
margin-bottom -0.2rem
|
||||
max-width 2.2rem
|
||||
max-height 2.2rem
|
||||
.theme-vdoing-wrapper
|
||||
--linesColor rgba(50, 0, 0, 0.05)
|
||||
&.bg-style-1 // 方格
|
||||
background-image linear-gradient(90deg, var(--linesColor) 3%, transparent 3%), linear-gradient(0deg, var(--linesColor) 3%, transparent 3%)
|
||||
background-position center center
|
||||
background-size 20px 20px
|
||||
&.bg-style-2 // 横线
|
||||
background-image repeating-linear-gradient(0, var(--linesColor) 0, var(--linesColor) 1px, transparent 0, transparent 50%)
|
||||
background-size 30px 30px
|
||||
&.bg-style-3 // 竖线
|
||||
background-image repeating-linear-gradient(90deg, var(--linesColor) 0, var(--linesColor) 1px, transparent 0, transparent 50%)
|
||||
background-size 30px 30px
|
||||
&.bg-style-4 // 左斜线
|
||||
background-image repeating-linear-gradient(-45deg, var(--linesColor) 0, var(--linesColor) 1px, transparent 0, transparent 50%)
|
||||
background-size 20px 20px
|
||||
&.bg-style-5 // 右斜线
|
||||
background-image repeating-linear-gradient(45deg, var(--linesColor) 0, var(--linesColor) 1px, transparent 0, transparent 50%)
|
||||
background-size 20px 20px
|
||||
&.bg-style-6 // 点状
|
||||
background-image radial-gradient(var(--linesColor) 1px, transparent 1px)
|
||||
background-size 10px 10px
|
||||
// 背景纹适应深色模式
|
||||
.theme-mode-dark
|
||||
.theme-vdoing-wrapper
|
||||
--linesColor rgba(125, 125, 125, 0.05)
|
||||
/**
|
||||
* 右侧菜单的自适应
|
||||
*/
|
||||
@media (min-width 720px) and (max-width 1279px)
|
||||
.have-rightmenu
|
||||
.page
|
||||
padding-right 0.8rem !important
|
||||
@media (max-width 1279px)
|
||||
.have-rightmenu
|
||||
.right-menu-wrapper
|
||||
display none
|
||||
@media (min-width 1280px)
|
||||
.have-rightmenu
|
||||
.sidebar .sidebar-sub-headers
|
||||
display none
|
||||
</style>
|
||||
168
theme-vdoing/components/PageEdit.vue
Normal file
168
theme-vdoing/components/PageEdit.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div class="page-edit">
|
||||
<div class="edit-link" v-if="editLink">
|
||||
<a :href="editLink" target="_blank" rel="noopener noreferrer">{{
|
||||
editLinkText
|
||||
}}</a>
|
||||
<OutboundLink />
|
||||
</div>
|
||||
|
||||
<div class="tags" v-if="$themeConfig.tag !== false && tags && tags[0]">
|
||||
<router-link
|
||||
:to="`/tags/?tag=${encodeURIComponent(item)}`"
|
||||
v-for="(item, index) in tags"
|
||||
:key="index"
|
||||
title="标签"
|
||||
>#{{ item }}</router-link
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="last-updated" v-if="lastUpdated">
|
||||
<span class="prefix">{{ lastUpdatedText }}:</span>
|
||||
<span class="time">{{ lastUpdated }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import isNil from 'lodash/isNil'
|
||||
import { endingSlashRE, outboundRE } from '../util'
|
||||
|
||||
export default {
|
||||
name: 'PageEdit',
|
||||
computed: {
|
||||
tags () {
|
||||
return this.$frontmatter.tags
|
||||
},
|
||||
|
||||
lastUpdated () {
|
||||
return this.$page.lastUpdated
|
||||
},
|
||||
|
||||
lastUpdatedText () {
|
||||
if (typeof this.$themeLocaleConfig.lastUpdated === 'string') {
|
||||
return this.$themeLocaleConfig.lastUpdated
|
||||
}
|
||||
if (typeof this.$site.themeConfig.lastUpdated === 'string') {
|
||||
return this.$site.themeConfig.lastUpdated
|
||||
}
|
||||
return 'Last Updated'
|
||||
},
|
||||
|
||||
editLink () {
|
||||
const showEditLink = isNil(this.$page.frontmatter.editLink)
|
||||
? this.$site.themeConfig.editLinks
|
||||
: this.$page.frontmatter.editLink
|
||||
|
||||
const {
|
||||
repo,
|
||||
docsDir = '',
|
||||
docsBranch = 'master',
|
||||
docsRepo = repo
|
||||
} = this.$site.themeConfig
|
||||
|
||||
if (showEditLink && docsRepo && this.$page.relativePath) {
|
||||
return this.createEditLink(
|
||||
repo,
|
||||
docsRepo,
|
||||
docsDir,
|
||||
docsBranch,
|
||||
this.$page.relativePath
|
||||
)
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
editLinkText () {
|
||||
return (
|
||||
this.$themeLocaleConfig.editLinkText
|
||||
|| this.$site.themeConfig.editLinkText
|
||||
|| `Edit this page`
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
createEditLink (repo, docsRepo, docsDir, docsBranch, path) {
|
||||
const bitbucket = /bitbucket.org/
|
||||
if (bitbucket.test(docsRepo)) {
|
||||
const base = docsRepo
|
||||
return (
|
||||
base.replace(endingSlashRE, '')
|
||||
+ `/src`
|
||||
+ `/${docsBranch}/`
|
||||
+ (docsDir ? docsDir.replace(endingSlashRE, '') + '/' : '')
|
||||
+ path
|
||||
+ `?mode=edit&spa=0&at=${docsBranch}&fileviewer=file-view-default`
|
||||
)
|
||||
}
|
||||
|
||||
const gitlab = /gitlab.com/
|
||||
if (gitlab.test(docsRepo)) {
|
||||
const base = docsRepo
|
||||
return (
|
||||
base.replace(endingSlashRE, '')
|
||||
+ `/-/edit`
|
||||
+ `/${docsBranch}/`
|
||||
+ (docsDir ? docsDir.replace(endingSlashRE, '') + '/' : '')
|
||||
+ path
|
||||
)
|
||||
}
|
||||
|
||||
const base = outboundRE.test(docsRepo)
|
||||
? docsRepo
|
||||
: `https://github.com/${docsRepo}`
|
||||
return (
|
||||
base.replace(endingSlashRE, '')
|
||||
+ `/edit`
|
||||
+ `/${docsBranch}/`
|
||||
+ (docsDir ? docsDir.replace(endingSlashRE, '') + '/' : '')
|
||||
+ path
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="stylus">
|
||||
@require '../styles/wrapper.styl'
|
||||
|
||||
.page-edit
|
||||
@extend $wrapper
|
||||
padding-top 1rem
|
||||
padding-bottom 1rem
|
||||
overflow auto
|
||||
.edit-link
|
||||
display inline-block
|
||||
float left
|
||||
margin 0 2rem 0.5rem 0
|
||||
a
|
||||
margin-right 0.25rem
|
||||
.tags
|
||||
float left
|
||||
a
|
||||
margin 0 0.8rem 0.5rem 0
|
||||
display inline-block
|
||||
color var(--textLightenColor)
|
||||
padding 0.2rem 0.7rem
|
||||
font-size 0.9em
|
||||
background-color rgba(128, 128, 128, 0.08)
|
||||
border-radius 3px
|
||||
opacity 0.8
|
||||
.last-updated
|
||||
float right
|
||||
font-size 0.9em
|
||||
.prefix
|
||||
font-weight 500
|
||||
color var(--textColor)
|
||||
opacity 0.8
|
||||
.time
|
||||
font-weight 400
|
||||
color #aaa
|
||||
@media (max-width $MQMobile)
|
||||
.page-edit
|
||||
.edit-link, .tags
|
||||
margin-bottom 0.5rem
|
||||
.last-updated
|
||||
width 100%
|
||||
font-size 0.8em
|
||||
text-align left
|
||||
</style>
|
||||
237
theme-vdoing/components/PageNav.vue
Normal file
237
theme-vdoing/components/PageNav.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div class="page-nav-wapper">
|
||||
<!-- 页面中间左右翻页 -->
|
||||
<div
|
||||
class="page-nav-centre-wrap"
|
||||
v-if="$themeConfig.pageButton !== false && (prev || next)"
|
||||
>
|
||||
<router-link
|
||||
class="page-nav-centre page-nav-centre-prev"
|
||||
v-if="prev"
|
||||
:to="prev.path"
|
||||
@mouseenter.native="showTooltip($event)"
|
||||
@mousemove.native="showTooltip($event)"
|
||||
>
|
||||
<div class="tooltip">{{ prev.title || prev.path }}</div>
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
class="page-nav-centre page-nav-centre-next"
|
||||
v-if="next"
|
||||
:to="next.path"
|
||||
@mouseenter.native="showTooltip($event)"
|
||||
@mousemove.native="showTooltip($event)"
|
||||
>
|
||||
<div class="tooltip">{{ next.title || next.path }}</div>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 底部翻页按钮 -->
|
||||
<div
|
||||
class="page-nav"
|
||||
v-if="prev || next"
|
||||
>
|
||||
<p class="inner">
|
||||
<span
|
||||
v-if="prev"
|
||||
class="prev"
|
||||
>
|
||||
←
|
||||
<router-link
|
||||
v-if="prev"
|
||||
class="prev"
|
||||
:to="prev.path"
|
||||
>{{ prev.title || prev.path }}</router-link>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="next"
|
||||
class="next"
|
||||
>
|
||||
<router-link
|
||||
v-if="next"
|
||||
:to="next.path"
|
||||
>{{ next.title || next.path }}</router-link>→
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { resolvePage } from '../util'
|
||||
import isString from 'lodash/isString'
|
||||
import isNil from 'lodash/isNil'
|
||||
|
||||
export default {
|
||||
name: 'PageNav',
|
||||
props: ['sidebarItems'],
|
||||
computed: {
|
||||
prev () {
|
||||
return resolvePageLink(LINK_TYPES.PREV, this)
|
||||
},
|
||||
|
||||
next () {
|
||||
return resolvePageLink(LINK_TYPES.NEXT, this)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showTooltip (e) {
|
||||
|
||||
const clientW = document.body.clientWidth
|
||||
const X = e.clientX
|
||||
const tooltipEle = e.target.querySelector('.tooltip')
|
||||
if (!tooltipEle) {
|
||||
return
|
||||
}
|
||||
|
||||
const tooltipEleStyle = tooltipEle.style
|
||||
if (X < clientW / 2) {
|
||||
tooltipEleStyle.right = null
|
||||
tooltipEleStyle.left = X + 10 + 'px'
|
||||
} else {
|
||||
tooltipEleStyle.left = null
|
||||
tooltipEleStyle.right = clientW - X + 10 + 'px'
|
||||
}
|
||||
tooltipEleStyle.top = e.clientY + 10 + 'px'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePrev (page, items) {
|
||||
return find(page, items, -1)
|
||||
}
|
||||
|
||||
function resolveNext (page, items) {
|
||||
return find(page, items, 1)
|
||||
}
|
||||
|
||||
const LINK_TYPES = {
|
||||
NEXT: {
|
||||
resolveLink: resolveNext,
|
||||
getThemeLinkConfig: ({ nextLinks }) => nextLinks,
|
||||
getPageLinkConfig: ({ frontmatter }) => frontmatter.next
|
||||
},
|
||||
PREV: {
|
||||
resolveLink: resolvePrev,
|
||||
getThemeLinkConfig: ({ prevLinks }) => prevLinks,
|
||||
getPageLinkConfig: ({ frontmatter }) => frontmatter.prev
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePageLink (
|
||||
linkType,
|
||||
{ $themeConfig, $page, $route, $site, sidebarItems }
|
||||
) {
|
||||
const { resolveLink, getThemeLinkConfig, getPageLinkConfig } = linkType
|
||||
|
||||
// Get link config from theme
|
||||
const themeLinkConfig = getThemeLinkConfig($themeConfig)
|
||||
|
||||
// Get link config from current page
|
||||
const pageLinkConfig = getPageLinkConfig($page)
|
||||
|
||||
// Page link config will overwrite global theme link config if defined
|
||||
const link = isNil(pageLinkConfig) ? themeLinkConfig : pageLinkConfig
|
||||
|
||||
if (link === false) {
|
||||
return
|
||||
} else if (isString(link)) {
|
||||
return resolvePage($site.pages, link, $route.path)
|
||||
} else {
|
||||
return resolveLink($page, sidebarItems)
|
||||
}
|
||||
}
|
||||
|
||||
function find (page, items, offset) {
|
||||
const res = []
|
||||
flatten(items, res)
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
const cur = res[i]
|
||||
if (cur.type === 'page' && cur.path === decodeURIComponent(page.path)) {
|
||||
return res[i + offset]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function flatten (items, res) {
|
||||
for (let i = 0, l = items.length; i < l; i++) {
|
||||
if (items[i].type === 'group') {
|
||||
flatten(items[i].children || [], res)
|
||||
} else {
|
||||
res.push(items[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="stylus">
|
||||
@require '../styles/wrapper.styl'
|
||||
|
||||
.page-nav
|
||||
@extend $wrapper
|
||||
padding-top 1rem
|
||||
padding-bottom 0
|
||||
.inner
|
||||
min-height 2rem
|
||||
margin-top 0
|
||||
border-top 1px solid var(--borderColor)
|
||||
padding-top 1rem
|
||||
overflow auto // clear float
|
||||
.next
|
||||
float right
|
||||
.page-nav-centre-wrap
|
||||
.page-nav-centre
|
||||
position fixed
|
||||
top 50%
|
||||
width 80px
|
||||
height 70px
|
||||
margin-top -35px
|
||||
outline 0
|
||||
transition all 0.2s
|
||||
border-radius 3px
|
||||
opacity 0.55
|
||||
z-index 99
|
||||
@media (max-width 1340px)
|
||||
width 50px
|
||||
@media (max-width 960px)
|
||||
display none
|
||||
&:hover
|
||||
background rgba(153, 153, 153, 0.15)
|
||||
opacity 1
|
||||
.tooltip
|
||||
display block
|
||||
&:before
|
||||
content ''
|
||||
display block
|
||||
width 10px
|
||||
height 10px
|
||||
border-top 2px solid #999
|
||||
border-right 2px solid #999
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
bottom 0
|
||||
left 0
|
||||
margin auto
|
||||
.tooltip
|
||||
display none
|
||||
background rgba(0, 0, 0, 0.5)
|
||||
color #fff
|
||||
padding 4px 8px
|
||||
font-size 13px
|
||||
border-radius 3px
|
||||
position fixed
|
||||
max-width 200px
|
||||
z-index 99
|
||||
.page-nav-centre-prev
|
||||
left 0
|
||||
&:before
|
||||
transform rotate(-135deg)
|
||||
.page-nav-centre-next
|
||||
right 0
|
||||
&:before
|
||||
transform rotate(45deg)
|
||||
.sidebar-open .page-nav-centre-wrap .page-nav-centre-prev
|
||||
left $sidebarWidth
|
||||
.no-sidebar .page-nav-centre-wrap .page-nav-centre-prev
|
||||
left 0
|
||||
</style>
|
||||
233
theme-vdoing/components/Pagination.vue
Normal file
233
theme-vdoing/components/Pagination.vue
Normal file
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<div class="pagination">
|
||||
<span
|
||||
class="card-box prev iconfont icon-jiantou-zuo"
|
||||
:class="{disabled: currentPage === 1}"
|
||||
@click="goPrex()"
|
||||
>
|
||||
<p>上一页</p>
|
||||
</span>
|
||||
|
||||
<!-- 分页在5页及以下时 -->
|
||||
<div
|
||||
class="pagination-list"
|
||||
v-if="pages <= 5"
|
||||
>
|
||||
<span
|
||||
class="card-box"
|
||||
v-for="item in pages"
|
||||
:key="item"
|
||||
:class="{active: currentPage === item}"
|
||||
@click="goIndex(item)"
|
||||
>{{item}}</span>
|
||||
</div>
|
||||
<!-- 分页在5页以上 -->
|
||||
<div
|
||||
class="pagination-list"
|
||||
v-else
|
||||
>
|
||||
<!-- 一号位 -->
|
||||
<span
|
||||
class="card-box"
|
||||
:class="{active: currentPage === 1}"
|
||||
@click="goIndex(1)"
|
||||
>1</span>
|
||||
|
||||
<!-- 二号位 -->
|
||||
<span
|
||||
class="ellipsis ell-two"
|
||||
v-show="currentPage > 3"
|
||||
@click="goIndex(currentPage - 2)"
|
||||
title="上两页"
|
||||
/>
|
||||
<!--这里没有使用v-if的原因是因为部署版本在当前页大于3时刷新页面出现了一些bug-->
|
||||
<span
|
||||
class="card-box"
|
||||
v-show="currentPage <= 3"
|
||||
:class="{active: currentPage === 2}"
|
||||
@click="goIndex(2)"
|
||||
>2</span>
|
||||
|
||||
<!-- 三号位 -->
|
||||
<span
|
||||
class="card-box"
|
||||
:class="{active: currentPage >= 3 && currentPage <= (pages - 2)}"
|
||||
@click="goIndex(threeNum())"
|
||||
>{{ threeNum() }}</span>
|
||||
|
||||
<!-- 四号位 -->
|
||||
<span
|
||||
class="ellipsis ell-four"
|
||||
v-show="currentPage < (pages - 2)"
|
||||
@click="goIndex(currentPage + 2)"
|
||||
title="下两页"
|
||||
/>
|
||||
<span
|
||||
class="card-box"
|
||||
v-show="currentPage >= (pages - 2)"
|
||||
:class="{active: currentPage === pages-1}"
|
||||
@click="goIndex(pages-1)"
|
||||
>{{ pages-1 }}</span>
|
||||
|
||||
<!-- 五号位 -->
|
||||
<span
|
||||
class="card-box"
|
||||
:class="{active: currentPage === pages}"
|
||||
@click="goIndex(pages)"
|
||||
>{{pages}}</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="card-box next iconfont icon-jiantou-you"
|
||||
:class="{disabled: currentPage === pages}"
|
||||
@click="goNext()"
|
||||
>
|
||||
<p>下一页</p>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
total: { // 总长度
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
perPage: { // 每页长
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
currentPage: { // 当前页
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pages () { // 总页数
|
||||
return Math.ceil(this.total / this.perPage)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
threeNum () { // 三号位页码计算
|
||||
let num = 3
|
||||
const currentPage = this.currentPage
|
||||
const pages = this.pages
|
||||
if (currentPage < 3) {
|
||||
num = 3
|
||||
} else if (currentPage > (pages - 3)) {
|
||||
num = pages - 2
|
||||
} else {
|
||||
num = currentPage
|
||||
}
|
||||
return num
|
||||
},
|
||||
goPrex () {
|
||||
let currentPage = this.currentPage
|
||||
if (currentPage > 1) {
|
||||
this.handleEmit(--currentPage)
|
||||
}
|
||||
},
|
||||
goNext () {
|
||||
let currentPage = this.currentPage
|
||||
if (currentPage < this.pages) {
|
||||
this.handleEmit(++currentPage)
|
||||
}
|
||||
},
|
||||
goIndex (i) {
|
||||
if (i !== this.currentPage) {
|
||||
this.handleEmit(i)
|
||||
}
|
||||
},
|
||||
handleEmit (i) {
|
||||
this.$emit('getCurrentPage', i)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='stylus'>
|
||||
.pagination
|
||||
position relative
|
||||
height 60px
|
||||
text-align center
|
||||
span
|
||||
line-height 1rem
|
||||
opacity 0.9
|
||||
cursor pointer
|
||||
&:hover
|
||||
color $accentColor
|
||||
&.ellipsis
|
||||
opacity 0.5
|
||||
&::before
|
||||
content '...'
|
||||
font-size 1.2rem
|
||||
@media (any-hover hover)
|
||||
&.ell-two
|
||||
&:hover
|
||||
&::before
|
||||
content '«'
|
||||
&.ell-four
|
||||
&:hover
|
||||
&::before
|
||||
content '»'
|
||||
> span
|
||||
position absolute
|
||||
top 0
|
||||
padding 1rem 1.2rem
|
||||
font-size 0.95rem
|
||||
&.disabled
|
||||
color rgba(125, 125, 125, 0.5)
|
||||
&.prev
|
||||
left 0
|
||||
border-top-right-radius 32px
|
||||
border-bottom-right-radius 32px
|
||||
&.next
|
||||
right 0
|
||||
border-top-left-radius 32px
|
||||
border-bottom-left-radius 32px
|
||||
&::before
|
||||
float right
|
||||
margin-left 0.3rem
|
||||
p
|
||||
display inline
|
||||
line-height 0.95rem
|
||||
.pagination-list
|
||||
span
|
||||
display inline-block
|
||||
width 2.5rem
|
||||
height 2.5rem
|
||||
line-height 2.5rem
|
||||
margin 0.3rem
|
||||
&.active
|
||||
background $accentColor
|
||||
color var(--mainBg)
|
||||
@media (max-width 800px)
|
||||
.pagination
|
||||
> span
|
||||
padding 1rem 1.5rem
|
||||
p
|
||||
display none
|
||||
// 719px
|
||||
@media (max-width $MQMobile)
|
||||
.pagination
|
||||
> span // 左右按钮
|
||||
padding 0.9rem 1.5rem
|
||||
.pagination-list
|
||||
span
|
||||
width 2.3rem
|
||||
height 2.3rem
|
||||
line-height 2.3rem
|
||||
margin 0.25rem
|
||||
@media (max-width 390px)
|
||||
.pagination
|
||||
> span // 左右按钮
|
||||
padding 0.8rem 1.3rem
|
||||
.pagination-list
|
||||
span
|
||||
width 2rem
|
||||
height 2rem
|
||||
line-height 2rem
|
||||
margin 0.1rem
|
||||
margin-top 0.3rem
|
||||
</style>
|
||||
229
theme-vdoing/components/PostList.vue
Normal file
229
theme-vdoing/components/PostList.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<div
|
||||
class="post-list"
|
||||
ref="postList"
|
||||
>
|
||||
<transition-group
|
||||
tag="div"
|
||||
name="post"
|
||||
>
|
||||
<div
|
||||
class="post card-box"
|
||||
:class="item.frontmatter.sticky && 'iconfont icon-zhiding'"
|
||||
v-for="item in sortPosts"
|
||||
:key="item.key"
|
||||
>
|
||||
<div class="title-wrapper">
|
||||
<h2>
|
||||
<router-link :to="item.path">{{item.title}}</router-link>
|
||||
</h2>
|
||||
<div class="article-info">
|
||||
<a
|
||||
title="作者"
|
||||
class="iconfont icon-touxiang"
|
||||
target="_blank"
|
||||
v-if="item.author && item.author.href"
|
||||
:href="item.author.href"
|
||||
>{{ item.author.name ? item.author.name : item.author }}</a>
|
||||
<span
|
||||
title="作者"
|
||||
class="iconfont icon-touxiang"
|
||||
v-else-if="item.author"
|
||||
>{{ item.author.name ? item.author.name : item.author }}</span>
|
||||
|
||||
<span
|
||||
title="创建时间"
|
||||
class="iconfont icon-riqi"
|
||||
v-if="item.frontmatter.date"
|
||||
>{{ item.frontmatter.date.split(' ')[0]}}</span>
|
||||
<span
|
||||
title="分类"
|
||||
class="iconfont icon-wenjian"
|
||||
v-if="$themeConfig.category !== false && item.frontmatter.categories"
|
||||
>
|
||||
<router-link
|
||||
:to="`/categories/?category=${encodeURIComponent(c)}`"
|
||||
v-for="(c, index) in item.frontmatter.categories"
|
||||
:key="index"
|
||||
>{{c}}</router-link>
|
||||
</span>
|
||||
<span
|
||||
title="标签"
|
||||
class="iconfont icon-biaoqian tags"
|
||||
v-if="$themeConfig.tag !== false && item.frontmatter.tags && item.frontmatter.tags[0]"
|
||||
>
|
||||
<router-link
|
||||
:to="`/tags/?tag=${encodeURIComponent(t)}`"
|
||||
v-for="(t, index) in item.frontmatter.tags"
|
||||
:key="index"
|
||||
>{{t}}</router-link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="excerpt-wrapper"
|
||||
v-if="item.excerpt"
|
||||
>
|
||||
<div
|
||||
class="excerpt"
|
||||
v-html="item.excerpt"
|
||||
></div>
|
||||
<router-link
|
||||
:to="item.path"
|
||||
class="readmore iconfont icon-jiantou-you"
|
||||
>阅读全文</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
category: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
tag: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
perPage: {
|
||||
type: Number,
|
||||
default: 10
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
sortPosts: [],
|
||||
postListOffsetTop: 0
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.setPosts()
|
||||
},
|
||||
mounted () {
|
||||
// this.postListOffsetTop = this.getElementToPageTop(this.$refs.postList) - 240
|
||||
},
|
||||
watch: {
|
||||
currentPage () {
|
||||
if (this.$route.query.p != this.currentPage) { // 此判断防止添加相同的路由信息(如浏览器回退时触发的)
|
||||
this.$router.push({
|
||||
query: {
|
||||
...this.$route.query,
|
||||
p: this.currentPage
|
||||
}
|
||||
})
|
||||
}
|
||||
// setTimeout(() => {
|
||||
// window.scrollTo({ top: this.postListOffsetTop }) // behavior: 'smooth'
|
||||
// },0)
|
||||
this.setPosts()
|
||||
},
|
||||
category () {
|
||||
this.setPosts()
|
||||
},
|
||||
tag () {
|
||||
this.setPosts()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setPosts () {
|
||||
const currentPage = this.currentPage
|
||||
const perPage = this.perPage
|
||||
|
||||
let posts = []
|
||||
if (this.category) {
|
||||
posts = this.$groupPosts.categories[this.category]
|
||||
} else if (this.tag) {
|
||||
posts = this.$groupPosts.tags[this.tag]
|
||||
} else {
|
||||
posts = this.$sortPosts
|
||||
}
|
||||
|
||||
this.sortPosts = posts.slice((currentPage - 1) * perPage, currentPage * perPage)
|
||||
},
|
||||
// getElementToPageTop(el) {
|
||||
// if(el && el.parentElement) {
|
||||
// return this.getElementToPageTop(el.parentElement) + el.offsetTop
|
||||
// }
|
||||
// return el.offsetTop
|
||||
// }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='stylus'>
|
||||
.post-list
|
||||
margin-bottom 4rem
|
||||
.post
|
||||
position relative
|
||||
padding 1rem 1.5rem
|
||||
margin-bottom 0.9rem
|
||||
transition all 0.3s
|
||||
&.post-leave-active
|
||||
display none
|
||||
&.post-enter
|
||||
opacity 0
|
||||
transform translateX(-20px)
|
||||
&::before
|
||||
position absolute
|
||||
top -1px
|
||||
right 0
|
||||
font-size 2.5rem
|
||||
color $activeColor
|
||||
opacity 0.85
|
||||
.title-wrapper
|
||||
a
|
||||
color var(--textColor)
|
||||
&:hover
|
||||
color $accentColor
|
||||
h2
|
||||
margin 0.5rem 0
|
||||
font-size 1.4rem
|
||||
border none
|
||||
a
|
||||
@media (max-width $MQMobile)
|
||||
font-weight 400
|
||||
.article-info
|
||||
> a, > span
|
||||
opacity 0.7
|
||||
font-size 0.8rem
|
||||
margin-right 1rem
|
||||
cursor pointer
|
||||
&::before
|
||||
margin-right 0.3rem
|
||||
a
|
||||
margin 0
|
||||
&:not(:first-child)
|
||||
&::before
|
||||
content '/'
|
||||
.tags a:not(:first-child)::before
|
||||
content '、'
|
||||
.excerpt-wrapper
|
||||
border-top 1px solid var(--borderColor)
|
||||
margin 0.5rem 0
|
||||
overflow hidden
|
||||
.excerpt
|
||||
margin-bottom 0.3rem
|
||||
font-size 0.92rem
|
||||
h1, h2, h3
|
||||
display none
|
||||
img
|
||||
max-height 280px
|
||||
max-width 100% !important
|
||||
margin 0 auto
|
||||
.readmore
|
||||
float right
|
||||
margin-right 1rem
|
||||
line-height 1rem
|
||||
&::before
|
||||
float right
|
||||
font-size 0.8rem
|
||||
margin 0.1rem 0 0 0.2rem
|
||||
</style>
|
||||
95
theme-vdoing/components/RightMenu.vue
Normal file
95
theme-vdoing/components/RightMenu.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="right-menu-wrapper">
|
||||
<div class="right-menu-margin">
|
||||
<div class="right-menu-content">
|
||||
<div
|
||||
:class="['right-menu-item', 'level'+item.level, { active: item.slug === hashText }]"
|
||||
v-for="(item, i) in headers"
|
||||
:key="i"
|
||||
>
|
||||
<a :href="'#'+item.slug">{{item.title}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
headers: [],
|
||||
hashText: ''
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.getHeadersData()
|
||||
this.getHashText()
|
||||
},
|
||||
watch: {
|
||||
$route () {
|
||||
this.headers = this.$page.headers
|
||||
this.getHashText()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getHeadersData () {
|
||||
this.headers = this.$page.headers
|
||||
},
|
||||
getHashText () {
|
||||
this.hashText = decodeURIComponent(window.location.hash.slice(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='stylus'>
|
||||
.right-menu-wrapper
|
||||
width $rightMenuWidth
|
||||
float right
|
||||
margin-right -($rightMenuWidth + 60px)
|
||||
position sticky
|
||||
top 0
|
||||
font-size 0.9rem
|
||||
.right-menu-margin
|
||||
margin-top ($navbarHeight + 1rem)
|
||||
.right-menu-content
|
||||
max-height 80vh
|
||||
position relative
|
||||
overflow hidden
|
||||
&::-webkit-scrollbar-track-piece
|
||||
background none
|
||||
&::-webkit-scrollbar-thumb:vertical
|
||||
background-color hsla(0, 0%, 49%, 0.3)
|
||||
&:hover
|
||||
overflow-y auto
|
||||
.right-menu-item
|
||||
padding 4px 15px
|
||||
border-left 0.13rem solid var(--borderColor)
|
||||
&.level3
|
||||
padding-left 28px
|
||||
&.active
|
||||
border-color $accentColor
|
||||
a
|
||||
color $accentColor
|
||||
opacity 1
|
||||
a
|
||||
color var(--textColor)
|
||||
opacity 0.75
|
||||
display block
|
||||
width ($rightMenuWidth - 30px)
|
||||
&:hover
|
||||
color $accentColor
|
||||
.have-body-img
|
||||
.right-menu-wrapper
|
||||
.right-menu-margin
|
||||
padding 0.3rem 0
|
||||
background var(--sidebarBg)
|
||||
border-radius 5px
|
||||
.right-menu-item
|
||||
border-color transparent
|
||||
&.active
|
||||
border-left 0.2rem solid $accentColor
|
||||
&:hover
|
||||
border-left 0.2rem solid $accentColor
|
||||
</style>
|
||||
121
theme-vdoing/components/Sidebar.vue
Normal file
121
theme-vdoing/components/Sidebar.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<aside class="sidebar">
|
||||
<div
|
||||
class="blogger"
|
||||
v-if="blogger"
|
||||
>
|
||||
<img :src="blogger.avatar" />
|
||||
<div class="blogger-info">
|
||||
<h3>{{blogger.name}}</h3>
|
||||
|
||||
<div
|
||||
class="icons"
|
||||
v-if="blogger.social"
|
||||
>
|
||||
<a
|
||||
:href="item.link"
|
||||
:title="item.title"
|
||||
:class="['iconfont', item.iconClass]"
|
||||
v-for="(item, index) in blogger.social.icons"
|
||||
:key="index"
|
||||
target="_blank"
|
||||
></a>
|
||||
</div>
|
||||
<span v-else>{{blogger.slogan}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端Nav -->
|
||||
<NavLinks />
|
||||
|
||||
<slot name="top" />
|
||||
|
||||
<SidebarLinks
|
||||
:depth="0"
|
||||
:items="items"
|
||||
/>
|
||||
<slot name="bottom" />
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SidebarLinks from '@theme/components/SidebarLinks.vue'
|
||||
import NavLinks from '@theme/components/NavLinks.vue'
|
||||
|
||||
export default {
|
||||
name: 'Sidebar',
|
||||
|
||||
components: { SidebarLinks, NavLinks },
|
||||
|
||||
props: ['items'],
|
||||
|
||||
computed: {
|
||||
blogger () {
|
||||
return this.$themeConfig.blogger
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.sidebar
|
||||
ul
|
||||
padding 0
|
||||
margin 0
|
||||
list-style-type none
|
||||
a
|
||||
display inline-block
|
||||
.nav-links
|
||||
display none
|
||||
border-bottom 1px solid var(--borderColor)
|
||||
padding 0.5rem 0 0.75rem 0
|
||||
a
|
||||
font-weight 600
|
||||
.nav-item, .repo-link
|
||||
display block
|
||||
line-height 1.25rem
|
||||
font-size 1.1em
|
||||
padding 0.5rem 0 0.5rem 1.5rem
|
||||
& > .sidebar-links
|
||||
padding 1.5rem 0
|
||||
& > li > a.sidebar-link
|
||||
font-size 1.1em
|
||||
line-height 1.7
|
||||
font-weight bold
|
||||
& > li:not(:first-child)
|
||||
margin-top 0.75rem
|
||||
.blogger
|
||||
display none
|
||||
border-bottom 1px solid var(--borderColor)
|
||||
img
|
||||
width 60px
|
||||
height 60px
|
||||
border-radius 5px
|
||||
margin 0.75rem 1rem
|
||||
.blogger-info
|
||||
flex 1
|
||||
h3
|
||||
margin 0.95rem 0 0.7rem
|
||||
font-size 1.1rem
|
||||
.icons .iconfont
|
||||
font-size 1.2rem
|
||||
padding-right 0.6rem
|
||||
color #777
|
||||
.sidebar-slot
|
||||
margin-bottom: -.5rem;
|
||||
font-size: .85rem;
|
||||
&.sidebar-slot-top
|
||||
padding: 1.5rem 1.5rem 0;
|
||||
&.sidebar-slot-bottom
|
||||
padding: 0 1.5rem 1.5rem;
|
||||
@media (max-width $MQMobile)
|
||||
.sidebar
|
||||
.blogger
|
||||
display flex
|
||||
.nav-links
|
||||
display block
|
||||
.dropdown-wrapper .nav-dropdown .dropdown-item a.router-link-active::after
|
||||
top calc(1rem - 2px)
|
||||
& > .sidebar-links
|
||||
padding 1rem 0
|
||||
</style>
|
||||
64
theme-vdoing/components/SidebarButton.vue
Normal file
64
theme-vdoing/components/SidebarButton.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div
|
||||
class="sidebar-button"
|
||||
@click="$emit('toggle-sidebar')"
|
||||
title="目录"
|
||||
>
|
||||
<svg
|
||||
class="icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M436 124H12c-6.627 0-12-5.373-12-12V80c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12z"
|
||||
class
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
.sidebar-button
|
||||
cursor pointer
|
||||
display none
|
||||
width 1.25rem
|
||||
height 1.25rem
|
||||
position absolute
|
||||
padding 0.6rem
|
||||
top 0.6rem
|
||||
left 1rem
|
||||
@media (max-width $MQMobile)
|
||||
display block
|
||||
.icon
|
||||
display block
|
||||
width 1.25rem
|
||||
height 1.25rem
|
||||
@media (min-width ($MQMobile + 1px))
|
||||
$mobileSidebarWidth = $sidebarWidth * 0.82
|
||||
.sidebar-button
|
||||
width 40px
|
||||
height 40px
|
||||
display inline-block
|
||||
position fixed
|
||||
left 0
|
||||
top ($navbarHeight + 1rem)
|
||||
text-align center
|
||||
line-height 44px
|
||||
margin 5px 8px
|
||||
color #888
|
||||
border-radius 50%
|
||||
padding 0
|
||||
// transition left 0.2s ease
|
||||
transition all .2s
|
||||
&:hover
|
||||
background $accentColor
|
||||
color #fff
|
||||
box-shadow 0 0 6px $accentColor
|
||||
.icon
|
||||
display inline
|
||||
width 1rem
|
||||
height 1rem
|
||||
</style>
|
||||
128
theme-vdoing/components/SidebarGroup.vue
Normal file
128
theme-vdoing/components/SidebarGroup.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<section
|
||||
class="sidebar-group"
|
||||
:class="[
|
||||
{
|
||||
collapsable,
|
||||
'is-sub-group': depth !== 0
|
||||
},
|
||||
`depth-${depth}`
|
||||
]"
|
||||
>
|
||||
<router-link
|
||||
v-if="item.path"
|
||||
class="sidebar-heading clickable"
|
||||
:class="{
|
||||
open,
|
||||
active: isActive($route, item.path)
|
||||
}"
|
||||
:to="item.path"
|
||||
@click.native="$emit('toggle')"
|
||||
>
|
||||
<span>{{ item.title }}</span>
|
||||
<span
|
||||
class="arrow"
|
||||
v-if="collapsable"
|
||||
:class="open ? 'down' : 'right'"
|
||||
></span>
|
||||
</router-link>
|
||||
|
||||
<p
|
||||
v-else
|
||||
class="sidebar-heading"
|
||||
:class="{ open }"
|
||||
@click="$emit('toggle')"
|
||||
>
|
||||
<span>{{ item.title }}</span>
|
||||
<span
|
||||
class="arrow"
|
||||
v-if="collapsable"
|
||||
:class="open ? 'down' : 'right'"
|
||||
></span>
|
||||
</p>
|
||||
|
||||
<DropdownTransition>
|
||||
<SidebarLinks
|
||||
class="sidebar-group-items"
|
||||
:items="item.children"
|
||||
v-if="open || !collapsable"
|
||||
:sidebar-depth="item.sidebarDepth"
|
||||
:initial-open-group-index="item.initialOpenGroupIndex"
|
||||
:depth="depth + 1"
|
||||
/>
|
||||
</DropdownTransition>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isActive } from '../util'
|
||||
import DropdownTransition from '@theme/components/DropdownTransition.vue'
|
||||
|
||||
export default {
|
||||
name: 'SidebarGroup',
|
||||
props: ['item', 'open', 'collapsable', 'depth'],
|
||||
components: { DropdownTransition },
|
||||
// ref: https://vuejs.org/v2/guide/components-edge-cases.html#Circular-References-Between-Components
|
||||
beforeCreate () {
|
||||
this.$options.components.SidebarLinks = require('./SidebarLinks.vue').default
|
||||
},
|
||||
methods: { isActive }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.sidebar-group
|
||||
.sidebar-group
|
||||
padding-left 0.5em
|
||||
&:not(.collapsable)
|
||||
.sidebar-heading:not(.clickable)
|
||||
cursor auto
|
||||
color inherit
|
||||
// refine styles of nested sidebar groups
|
||||
&.is-sub-group
|
||||
padding-left 0
|
||||
& > .sidebar-heading
|
||||
font-size 1.01em
|
||||
line-height 1.4
|
||||
font-weight bold
|
||||
padding-left 2rem
|
||||
&:not(.clickable)
|
||||
// opacity 0.9
|
||||
& > .sidebar-group-items
|
||||
padding-left 1rem
|
||||
& > li > .sidebar-link
|
||||
font-size 0.98em
|
||||
border-left none
|
||||
&.depth-2
|
||||
& > .sidebar-heading
|
||||
border-left none
|
||||
.sidebar-heading
|
||||
color var(--textColor)
|
||||
transition color 0.15s ease
|
||||
cursor pointer
|
||||
font-size 1.1em
|
||||
font-weight bold
|
||||
// text-transform uppercase
|
||||
padding 0.35rem 1.5rem 0.35rem 1.25rem
|
||||
width 100%
|
||||
box-sizing border-box
|
||||
margin 0
|
||||
border-left 0.25rem solid transparent
|
||||
&.open, &:hover
|
||||
color inherit
|
||||
.arrow
|
||||
position relative
|
||||
top -0.12em
|
||||
left 0.5em
|
||||
&.clickable
|
||||
&.active
|
||||
font-weight 600
|
||||
color $accentColor
|
||||
border-left-color $accentColor
|
||||
&:hover
|
||||
color $accentColor
|
||||
.sidebar-group-items
|
||||
transition height 0.1s ease-out
|
||||
font-size 0.95em
|
||||
overflow hidden
|
||||
</style>
|
||||
124
theme-vdoing/components/SidebarLink.vue
Normal file
124
theme-vdoing/components/SidebarLink.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<script>
|
||||
import { isActive, hashRE, groupHeaders } from '../util'
|
||||
|
||||
export default {
|
||||
functional: true,
|
||||
|
||||
props: ['item', 'sidebarDepth'],
|
||||
|
||||
render (h,
|
||||
{
|
||||
parent: {
|
||||
$page,
|
||||
$site,
|
||||
$route,
|
||||
$themeConfig,
|
||||
$themeLocaleConfig
|
||||
},
|
||||
props: {
|
||||
item,
|
||||
sidebarDepth
|
||||
}
|
||||
}) {
|
||||
// use custom active class matching logic
|
||||
// due to edge case of paths ending with / + hash
|
||||
const selfActive = isActive($route, item.path)
|
||||
// for sidebar: auto pages, a hash link should be active if one of its child
|
||||
// matches
|
||||
const active = item.type === 'auto'
|
||||
? selfActive || item.children.some(c => isActive($route, item.basePath + '#' + c.slug))
|
||||
: selfActive
|
||||
const link = item.type === 'external'
|
||||
? renderExternal(h, item.path, item.title || item.path)
|
||||
: renderLink(h, item.path, item.title || item.path, active)
|
||||
|
||||
const maxDepth = [
|
||||
$page.frontmatter.sidebarDepth,
|
||||
sidebarDepth,
|
||||
$themeLocaleConfig.sidebarDepth,
|
||||
$themeConfig.sidebarDepth,
|
||||
1
|
||||
].find(depth => depth !== undefined)
|
||||
|
||||
const displayAllHeaders = $themeLocaleConfig.displayAllHeaders
|
||||
|| $themeConfig.displayAllHeaders
|
||||
|
||||
if (item.type === 'auto') {
|
||||
return [link, renderChildren(h, item.children, item.basePath, $route, maxDepth)]
|
||||
} else if ((active || displayAllHeaders) && item.headers && !hashRE.test(item.path)) {
|
||||
const children = groupHeaders(item.headers)
|
||||
return [link, renderChildren(h, children, item.path, $route, maxDepth)]
|
||||
} else {
|
||||
return link
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderLink (h, to, text, active) {
|
||||
return h('router-link', {
|
||||
props: {
|
||||
to,
|
||||
activeClass: '',
|
||||
exactActiveClass: ''
|
||||
},
|
||||
class: {
|
||||
active,
|
||||
'sidebar-link': true
|
||||
}
|
||||
}, text)
|
||||
}
|
||||
|
||||
function renderChildren (h, children, path, route, maxDepth, depth = 1) {
|
||||
if (!children || depth > maxDepth) return null
|
||||
return h('ul', { class: 'sidebar-sub-headers' }, children.map(c => {
|
||||
const active = isActive(route, path + '#' + c.slug)
|
||||
return h('li', { class: 'sidebar-sub-header' }, [
|
||||
renderLink(h, path + '#' + c.slug, c.title, active),
|
||||
renderChildren(h, c.children, path, route, maxDepth, depth + 1)
|
||||
])
|
||||
}))
|
||||
}
|
||||
|
||||
function renderExternal (h, to, text) {
|
||||
return h('a', {
|
||||
attrs: {
|
||||
href: to,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer'
|
||||
},
|
||||
class: {
|
||||
'sidebar-link': true
|
||||
}
|
||||
}, [text, h('OutboundLink')])
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.sidebar .sidebar-sub-headers
|
||||
padding-left 1rem
|
||||
font-size 0.95em
|
||||
a.sidebar-link
|
||||
font-size 1em
|
||||
font-weight 400
|
||||
display inline-block
|
||||
color var(--textColor)
|
||||
border-left 0.25rem solid transparent
|
||||
padding 0.35rem 1rem 0.35rem 1.25rem
|
||||
line-height 1.4
|
||||
width 100%
|
||||
box-sizing border-box
|
||||
&:hover
|
||||
color $accentColor
|
||||
&.active
|
||||
font-weight 600
|
||||
color $accentColor
|
||||
border-left-color $accentColor
|
||||
.sidebar-group &
|
||||
padding-left 2rem
|
||||
.sidebar-sub-headers &
|
||||
padding-top 0.25rem
|
||||
padding-bottom 0.25rem
|
||||
border-left none
|
||||
&.active
|
||||
font-weight 500
|
||||
</style>
|
||||
93
theme-vdoing/components/SidebarLinks.vue
Normal file
93
theme-vdoing/components/SidebarLinks.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<ul class="sidebar-links" v-if="items.length">
|
||||
<li v-for="(item, i) in items" :key="i">
|
||||
<SidebarGroup
|
||||
v-if="item.type === 'group'"
|
||||
:item="item"
|
||||
:open="i === openGroupIndex"
|
||||
:collapsable="item.collapsable || item.collapsible"
|
||||
:depth="depth"
|
||||
@toggle="toggleGroup(i)"
|
||||
/>
|
||||
<SidebarLink v-else :sidebarDepth="sidebarDepth" :item="item" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SidebarGroup from '@theme/components/SidebarGroup.vue'
|
||||
import SidebarLink from '@theme/components/SidebarLink.vue'
|
||||
import { isActive } from '../util'
|
||||
|
||||
export default {
|
||||
name: 'SidebarLinks',
|
||||
|
||||
components: { SidebarGroup, SidebarLink },
|
||||
|
||||
props: [
|
||||
'items',
|
||||
'depth', // depth of current sidebar links
|
||||
'sidebarDepth', // depth of headers to be extracted
|
||||
'initialOpenGroupIndex'
|
||||
],
|
||||
|
||||
data () {
|
||||
return {
|
||||
openGroupIndex: this.initialOpenGroupIndex || 0
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.refreshIndex()
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route' () {
|
||||
this.refreshIndex()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
refreshIndex () {
|
||||
const index = resolveOpenGroupIndex(
|
||||
this.$route,
|
||||
this.items
|
||||
)
|
||||
if (index > -1) {
|
||||
this.openGroupIndex = index
|
||||
}
|
||||
},
|
||||
|
||||
toggleGroup (index) {
|
||||
this.openGroupIndex = index === this.openGroupIndex ? -1 : index
|
||||
},
|
||||
|
||||
isActive (page) {
|
||||
return isActive(this.$route, page.regularPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveOpenGroupIndex (route, items) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
if (descendantIsActive(route, item)) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function descendantIsActive (route, item) {
|
||||
if (item.type === 'group') {
|
||||
return item.children.some(child => {
|
||||
if (child.type === 'group') {
|
||||
return descendantIsActive(route, child)
|
||||
} else {
|
||||
return child.type === 'page' && isActive(route, child.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
110
theme-vdoing/components/TagsBar.vue
Normal file
110
theme-vdoing/components/TagsBar.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="tags-wrapper card-box">
|
||||
<router-link
|
||||
to="/tags/"
|
||||
class="title iconfont icon-biaoqian1"
|
||||
title="全部标签"
|
||||
>{{ length === 'all' ? '全部标签' : '热门标签' }}</router-link>
|
||||
<div class="tags">
|
||||
<template v-for="(item, index) in tags">
|
||||
<router-link
|
||||
:to="`/tags/?tag=${encodeURIComponent(item.key)}`"
|
||||
:key="index"
|
||||
:style="tagStyleList[index]"
|
||||
:class="{active: item.key === tag}"
|
||||
>{{item.key}}</router-link>
|
||||
<span :key="index+tags.length" />
|
||||
</template>
|
||||
<router-link
|
||||
to="/tags/"
|
||||
v-if="length !== 'all' && tagsData.length > length"
|
||||
>更多...</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
tag: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
tagsData: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
length: {
|
||||
type: [String, Number],
|
||||
default: 'all'
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
tagBgColor: ['#11a8cd', '#F8B26A', '#67CC86', '#E15B64', '#F47E60', '#849B87'],
|
||||
tagStyleList: []
|
||||
}
|
||||
},
|
||||
created () {
|
||||
for (let i = 0, tagH = this.tags.length; i < tagH; i++) {
|
||||
this.tagStyleList.push(this.getTagStyle())
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tags () {
|
||||
if (this.length === 'all') {
|
||||
return this.tagsData
|
||||
} else {
|
||||
return this.tagsData.slice(0, this.length)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getTagStyle () {
|
||||
const tagBgColor = this.tagBgColor
|
||||
const randomColor = tagBgColor[Math.floor(Math.random() * tagBgColor.length)]
|
||||
return `background: ${randomColor};--randomColor:${randomColor};`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='stylus'>
|
||||
.tags-wrapper
|
||||
padding 0 .95rem
|
||||
.title
|
||||
color var(--textColor)
|
||||
opacity 0.9
|
||||
font-size 1.2rem
|
||||
&::before
|
||||
margin-right 0.3rem
|
||||
.tags
|
||||
text-align justify
|
||||
padding 0.8rem 0.5rem 0.5rem 0.5rem
|
||||
margin 0 -0.5rem -0.5rem -0.5rem
|
||||
a
|
||||
opacity 0.8
|
||||
display inline-block
|
||||
padding 0.2rem 0.4rem
|
||||
transition all 0.4s
|
||||
background-color var(--textColor)
|
||||
color var(--mainBg)
|
||||
border-radius 3px
|
||||
margin 0 0.3rem 0.5rem 0
|
||||
min-width 2rem
|
||||
height 1rem
|
||||
line-height 1rem
|
||||
font-size 0.8rem
|
||||
text-align center
|
||||
@media (max-width $MQMobile)
|
||||
font-weight 400
|
||||
&:hover
|
||||
opacity 1
|
||||
transform scale(1.1)
|
||||
&.active
|
||||
box-shadow 0 5px 10px -5px var(--randomColor, rgba(0, 0, 0, 0.15))
|
||||
transform scale(1.22)
|
||||
opacity 1
|
||||
&:hover
|
||||
text-decoration none
|
||||
</style>
|
||||
122
theme-vdoing/components/TagsPage.vue
Normal file
122
theme-vdoing/components/TagsPage.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="custom-page tags-page">
|
||||
<MainLayout>
|
||||
<template #mainLeft>
|
||||
<TagsBar
|
||||
v-if="$categoriesAndTags.tags.length"
|
||||
:tagsData="$categoriesAndTags.tags"
|
||||
:tag="tag"
|
||||
/>
|
||||
<PostList
|
||||
:currentPage="currentPage"
|
||||
:perPage="perPage"
|
||||
:tag="tag"
|
||||
/>
|
||||
<Pagination
|
||||
:total="total"
|
||||
:perPage="perPage"
|
||||
:currentPage="currentPage"
|
||||
@getCurrentPage="handlePagination"
|
||||
v-show="Math.ceil(total / perPage) > 1"
|
||||
/>
|
||||
</template>
|
||||
<template #mainRight>
|
||||
<TagsBar
|
||||
v-if="$categoriesAndTags.tags.length"
|
||||
:tagsData="$categoriesAndTags.tags"
|
||||
:tag="tag"
|
||||
/>
|
||||
</template>
|
||||
</MainLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MainLayout from '@theme/components/MainLayout'
|
||||
import PostList from '@theme/components/PostList'
|
||||
import Pagination from '@theme/components/Pagination'
|
||||
import TagsBar from '@theme/components/TagsBar'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
tag: '',
|
||||
total: 0, // 总长
|
||||
perPage: 10, // 每页长
|
||||
currentPage: 1// 当前页
|
||||
}
|
||||
},
|
||||
components: { MainLayout, PostList, Pagination, TagsBar },
|
||||
mounted () {
|
||||
const queryTag = this.$route.query.tag
|
||||
|
||||
if (queryTag) {
|
||||
this.tag = queryTag
|
||||
this.total = this.$groupPosts.tags[queryTag].length
|
||||
} else {
|
||||
this.total = this.$sortPosts.length
|
||||
}
|
||||
if (this.$route.query.p) {
|
||||
this.currentPage = Number(this.$route.query.p)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handlePagination (i) { // 分页
|
||||
this.currentPage = i
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.query.tag' (tag) {
|
||||
this.tag = tag ? decodeURIComponent(tag) : ''
|
||||
|
||||
if (this.tag) {
|
||||
this.total = this.$groupPosts.tags[this.tag].length
|
||||
} else {
|
||||
this.total = this.$sortPosts.length
|
||||
}
|
||||
this.currentPage = 1
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='stylus'>
|
||||
.tags-page
|
||||
.tags-wrapper
|
||||
position sticky
|
||||
top ($navbarHeight + 0.9rem)
|
||||
max-height calc(100vh - 10rem)
|
||||
min-height 4.2rem
|
||||
@media (max-width $MQMobile)
|
||||
display none
|
||||
.tags
|
||||
max-height calc(100vh - 14rem)
|
||||
min-height 2.2rem
|
||||
overflow-x hidden
|
||||
overflow-y auto
|
||||
transition all 0.2s
|
||||
&::-webkit-scrollbar-track-piece
|
||||
background-color rgba(0, 0, 0, 0.05)
|
||||
&::-webkit-scrollbar-thumb:vertical
|
||||
background-color rgba(0, 0, 0, 0.15)
|
||||
&:hover
|
||||
&::-webkit-scrollbar-track-piece
|
||||
background-color rgba(0, 0, 0, 0.1)
|
||||
&::-webkit-scrollbar-thumb:vertical
|
||||
background-color rgba(0, 0, 0, 0.25)
|
||||
.tags-page
|
||||
.main-left
|
||||
.tags-wrapper
|
||||
position relative
|
||||
top 0
|
||||
padding 0.9rem 1.5rem
|
||||
margin-bottom 0.9rem
|
||||
max-height 15rem
|
||||
border-radius 0
|
||||
display none
|
||||
@media (max-width $MQMobile)
|
||||
display block
|
||||
.tags
|
||||
max-height 11.5rem
|
||||
</style>
|
||||
151
theme-vdoing/components/UpdateArticle.vue
Normal file
151
theme-vdoing/components/UpdateArticle.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div :class="['article-list',{'no-article-list': isShowArticle}]">
|
||||
<div class="article-title">
|
||||
<router-link
|
||||
:to="moreArticle || '/archives/'"
|
||||
class="iconfont icon-bi"
|
||||
>最近更新</router-link>
|
||||
</div>
|
||||
<div class="article-wrapper">
|
||||
<dl
|
||||
v-for="(item, index) in topPublishPosts"
|
||||
:key="index"
|
||||
>
|
||||
<dd>{{getNum(index)}}</dd>
|
||||
<dt>
|
||||
<router-link :to="item.path">
|
||||
<div>{{item.title}}</div>
|
||||
</router-link>
|
||||
<span>{{getDate(item)}}</span>
|
||||
</dt>
|
||||
</dl>
|
||||
|
||||
<dl>
|
||||
<dd></dd>
|
||||
<dt>
|
||||
<router-link
|
||||
:to="moreArticle || '/archives/'"
|
||||
class="more"
|
||||
>更多文章></router-link>
|
||||
</dt>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'UpdateArticle',
|
||||
props: {
|
||||
length: {
|
||||
type: [String, Number],
|
||||
default: 3
|
||||
},
|
||||
moreArticle: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
posts: [],
|
||||
currentPath: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.posts = this.$site.pages
|
||||
this.currentPath = this.$page.path
|
||||
},
|
||||
computed: {
|
||||
topPublishPosts () {
|
||||
return this.$sortPostsByDate ? this.$sortPostsByDate.filter(post => {
|
||||
const { path } = post
|
||||
return path !== this.currentPath
|
||||
}).slice(0, this.length) : []
|
||||
|
||||
},
|
||||
isShowArticle () {
|
||||
const { frontmatter } = this.$page
|
||||
return !(frontmatter.article !== false)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getNum (index) {
|
||||
return index < 9 ? '0' + (index + 1) : index + 1
|
||||
},
|
||||
getDate (item) {
|
||||
return item.frontmatter.date ? item.frontmatter.date.split(" ")[0].slice(5, 10) : ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route () {
|
||||
this.currentPath = this.$page.path
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='stylus'>
|
||||
// @require '../styles/wrapper.styl'
|
||||
.article-list
|
||||
// @extend $wrapper
|
||||
padding 1rem 2rem
|
||||
@media (max-width $MQNarrow)
|
||||
padding 1rem 1.5rem
|
||||
&.no-article-list
|
||||
display none
|
||||
.article-title
|
||||
border-bottom 1px solid var(--borderColor)
|
||||
font-size 1.3rem
|
||||
padding 1rem
|
||||
a
|
||||
font-size 1.2rem
|
||||
color var(--textColor)
|
||||
opacity 0.9
|
||||
&:before
|
||||
margin-right 0.4rem
|
||||
font-size 1.1rem
|
||||
.article-wrapper
|
||||
overflow hidden
|
||||
dl
|
||||
border-bottom 1px dotted var(--borderColor)
|
||||
float left
|
||||
display flex
|
||||
padding 8px 0
|
||||
margin 0
|
||||
height 45px
|
||||
width 100%
|
||||
dd
|
||||
font-size 1.1rem
|
||||
color #F17229
|
||||
width 50px
|
||||
text-align center
|
||||
margin 0
|
||||
line-height 45px
|
||||
dt
|
||||
flex 1
|
||||
display flex
|
||||
a
|
||||
color var(--textColor)
|
||||
flex 1
|
||||
display flex
|
||||
height 45px
|
||||
align-items center
|
||||
font-weight normal
|
||||
div
|
||||
overflow hidden
|
||||
white-space normal
|
||||
text-overflow ellipsis
|
||||
display -webkit-box
|
||||
-webkit-line-clamp 2
|
||||
-webkit-box-orient vertical
|
||||
&:hover
|
||||
text-decoration underline
|
||||
&.more
|
||||
color $accentColor
|
||||
span
|
||||
width 50px
|
||||
margin-right 15px
|
||||
color #999
|
||||
text-align right
|
||||
font-size 0.9rem
|
||||
line-height 45px
|
||||
</style>
|
||||
47
theme-vdoing/enhanceApp.js
Normal file
47
theme-vdoing/enhanceApp.js
Normal file
@@ -0,0 +1,47 @@
|
||||
// 解决代码选项卡无法加载的问题
|
||||
import Vue from 'vue'
|
||||
import CodeBlock from "@theme/global-components/CodeBlock.vue"
|
||||
import CodeGroup from "@theme/global-components/CodeGroup.vue"
|
||||
// Register the Vue global component
|
||||
Vue.component(CodeBlock)
|
||||
Vue.component(CodeGroup)
|
||||
|
||||
// 注:此文件在浏览器端运行
|
||||
import postsMixin from '@theme/mixins/posts'
|
||||
export default ({
|
||||
Vue, // VuePress 正在使用的 Vue 构造函数
|
||||
options, // 附加到根实例的一些选项
|
||||
router, // 当前应用的路由实例
|
||||
siteData // 站点元数据
|
||||
}) => {
|
||||
// 修复ISO8601时间格式为普通时间格式,以及添加作者信息
|
||||
siteData.pages.map(item => {
|
||||
const { frontmatter: { date, author } } = item
|
||||
if (typeof date === 'string' && date.charAt(date.length - 1) === 'Z') {
|
||||
item.frontmatter.date = repairUTCDate(date)
|
||||
}
|
||||
if (author) {
|
||||
item.author = author
|
||||
} else {
|
||||
if (siteData.themeConfig.author) {
|
||||
item.author = siteData.themeConfig.author
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 将对文章数据的处理结果混入Vue实例
|
||||
Vue.mixin(postsMixin)
|
||||
}
|
||||
|
||||
|
||||
// 修复ISO8601时间格式为普通时间格式
|
||||
function repairUTCDate (date) {
|
||||
if (!(date instanceof Date)) {
|
||||
date = new Date(date)
|
||||
}
|
||||
return `${date.getUTCFullYear()}-${zero(date.getUTCMonth() + 1)}-${zero(date.getUTCDate())} ${zero(date.getUTCHours())}:${zero(date.getUTCMinutes())}:${zero(date.getUTCSeconds())}`;
|
||||
}
|
||||
// 小于10补0
|
||||
function zero (d) {
|
||||
return d.toString().padStart(2, '0')
|
||||
}
|
||||
44
theme-vdoing/global-components/Badge.vue
Normal file
44
theme-vdoing/global-components/Badge.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script>
|
||||
export default {
|
||||
functional: true,
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'tip'
|
||||
},
|
||||
text: String,
|
||||
vertical: {
|
||||
type: String,
|
||||
default: 'top'
|
||||
}
|
||||
},
|
||||
render (h, { props, slots }) {
|
||||
return h('span', {
|
||||
class: ['badge', props.type],
|
||||
style: {
|
||||
verticalAlign: props.vertical
|
||||
}
|
||||
}, props.text || slots().default)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.badge
|
||||
display inline-block
|
||||
font-size 14px
|
||||
height 18px
|
||||
line-height 18px
|
||||
border-radius 3px
|
||||
padding 0 6px
|
||||
color white
|
||||
background-color #42b983
|
||||
&.tip, &.green
|
||||
background-color #42b983
|
||||
&.error
|
||||
background-color #DA5961 // #f66
|
||||
&.warning, &.warn, &.yellow
|
||||
background-color darken(#ffe564, 35%)
|
||||
& + &
|
||||
margin-left 5px
|
||||
</style>
|
||||
38
theme-vdoing/global-components/CodeBlock.vue
Normal file
38
theme-vdoing/global-components/CodeBlock.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="theme-code-block" :class="{ 'theme-code-block__active': active }">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CodeBlock',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.theme-code-block {
|
||||
display: none;
|
||||
}
|
||||
.theme-code-block__active {
|
||||
display: block;
|
||||
}
|
||||
.theme-code-block > pre {
|
||||
background-color: orange;
|
||||
}
|
||||
@media (max-width: 419px) {
|
||||
.theme-code-group div[class*='language-'] {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
99
theme-vdoing/global-components/CodeGroup.vue
Normal file
99
theme-vdoing/global-components/CodeGroup.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div class="theme-code-group">
|
||||
<div class="theme-code-group__nav">
|
||||
<ul class="theme-code-group__ul">
|
||||
<li
|
||||
v-for="(tab, i) in codeTabs"
|
||||
:key="tab.title"
|
||||
class="theme-code-group__li"
|
||||
>
|
||||
<button
|
||||
class="theme-code-group__nav-tab"
|
||||
:class="{
|
||||
'theme-code-group__nav-tab-active': i === activeCodeTabIndex
|
||||
}"
|
||||
@click="changeCodeTab(i)"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<slot />
|
||||
<pre v-if="codeTabs.length < 1" class="pre-blank">
|
||||
// Make sure to add code blocks to your code group</pre
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CodeGroup',
|
||||
data () {
|
||||
return {
|
||||
codeTabs: [],
|
||||
activeCodeTabIndex: -1
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeCodeTabIndex (index) {
|
||||
this.codeTabs.forEach(tab => {
|
||||
tab.elm.classList.remove('theme-code-block__active')
|
||||
})
|
||||
this.codeTabs[index].elm.classList.add('theme-code-block__active')
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.codeTabs = (this.$slots.default || []).filter(slot => Boolean(slot.componentOptions)).map((slot, index) => {
|
||||
if (slot.componentOptions.propsData.active === '') {
|
||||
this.activeCodeTabIndex = index
|
||||
}
|
||||
|
||||
return {
|
||||
title: slot.componentOptions.propsData.title,
|
||||
elm: slot.elm
|
||||
}
|
||||
})
|
||||
|
||||
if (this.activeCodeTabIndex === -1 && this.codeTabs.length > 0) {
|
||||
this.activeCodeTabIndex = 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeCodeTab (index) {
|
||||
this.activeCodeTabIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.theme-code-group, .theme-code-group__nav
|
||||
background-color var(--codeBg)
|
||||
padding-bottom 22px
|
||||
border-radius 6px
|
||||
padding-left 10px
|
||||
padding-top 10px
|
||||
.theme-code-group__nav
|
||||
margin-bottom -35px
|
||||
.theme-code-group__ul
|
||||
margin auto 0
|
||||
padding-left 0
|
||||
display inline-flex
|
||||
list-style none
|
||||
.theme-code-group__li, .theme-code-group__nav-tab
|
||||
border 0
|
||||
padding 5px
|
||||
cursor pointer
|
||||
background-color transparent
|
||||
font-size 0.85em
|
||||
line-height 1.4
|
||||
color var(--codeColor)
|
||||
font-weight 600
|
||||
opacity 0.85
|
||||
.theme-code-group__nav-tab-active
|
||||
border-bottom $accentColor 1px solid
|
||||
opacity 1
|
||||
.pre-blank
|
||||
color $accentColor
|
||||
</style>
|
||||
282
theme-vdoing/index.js
Normal file
282
theme-vdoing/index.js
Normal file
@@ -0,0 +1,282 @@
|
||||
const path = require('path')
|
||||
const setFrontmatter = require('./node_utils/setFrontmatter')
|
||||
const getSidebarData = require('./node_utils/getSidebarData')
|
||||
const { createPage, deletePage } = require('./node_utils/handlePage')
|
||||
const chalk = require('chalk') // 命令行打印美化
|
||||
const yaml = require('js-yaml') // yaml转js
|
||||
const log = console.log
|
||||
|
||||
// md容器名
|
||||
const CARD_LIST = 'cardList'
|
||||
const CARD_IMG_LIST = 'cardImgList'
|
||||
|
||||
// siteConfig base 配置
|
||||
let base = ''
|
||||
|
||||
|
||||
// Theme API.
|
||||
module.exports = (options, ctx) => {
|
||||
const { sourceDir, themeConfig, siteConfig } = ctx
|
||||
|
||||
// base路径
|
||||
base = siteConfig.base || ''
|
||||
|
||||
// 自动设置front matter
|
||||
setFrontmatter(sourceDir, themeConfig)
|
||||
|
||||
// 自动生成结构化侧边栏
|
||||
const sidebar = themeConfig.sidebar
|
||||
if (sidebar === 'structuring' || sidebar && sidebar.mode === 'structuring') {
|
||||
const collapsable = themeConfig.sidebar.collapsable === false ? false : true
|
||||
const sidebarData = getSidebarData(sourceDir, collapsable)
|
||||
if (sidebarData) {
|
||||
themeConfig.sidebar = sidebarData
|
||||
log(chalk.blue('tip ') + chalk.green('add sidebar data. 侧边栏数据成功生成。'))
|
||||
} else {
|
||||
themeConfig.sidebar = 'auto'
|
||||
log(chalk.yellow('warning: fail to add sidebar data, switch to "auto". 未能添加侧边栏数据,将切换为“auto”。'))
|
||||
}
|
||||
}
|
||||
|
||||
// 分类页
|
||||
if (themeConfig.category !== false) {
|
||||
createPage(sourceDir, 'categoriesPage')
|
||||
} else {
|
||||
deletePage(sourceDir, 'categoriesPage')
|
||||
}
|
||||
|
||||
// 标签页
|
||||
if (themeConfig.tag !== false) {
|
||||
createPage(sourceDir, 'tagsPage')
|
||||
} else {
|
||||
deletePage(sourceDir, 'tagsPage')
|
||||
}
|
||||
|
||||
// 归档页
|
||||
if (themeConfig.archive !== false) {
|
||||
createPage(sourceDir, 'archivesPage')
|
||||
} else {
|
||||
deletePage(sourceDir, 'archivesPage')
|
||||
}
|
||||
|
||||
// resolve algolia
|
||||
const isAlgoliaSearch = (
|
||||
themeConfig.algolia
|
||||
|| Object
|
||||
.keys(siteConfig.locales && themeConfig.locales || {})
|
||||
.some(base => themeConfig.locales[base].algolia)
|
||||
)
|
||||
|
||||
const enableSmoothScroll = themeConfig.smoothScroll === true
|
||||
|
||||
return {
|
||||
alias () {
|
||||
return {
|
||||
'@AlgoliaSearchBox': isAlgoliaSearch
|
||||
? path.resolve(__dirname, 'components/AlgoliaSearchBox.vue')
|
||||
: path.resolve(__dirname, 'noopModule.js')
|
||||
}
|
||||
},
|
||||
|
||||
plugins: [
|
||||
['@vuepress/active-header-links', options.activeHeaderLinks],
|
||||
'@vuepress/search',
|
||||
'@vuepress/plugin-nprogress',
|
||||
['smooth-scroll', enableSmoothScroll],
|
||||
|
||||
['container', {
|
||||
type: 'note',
|
||||
defaultTitle: {
|
||||
'/': '笔记',
|
||||
'/en/': 'NOTE'
|
||||
}
|
||||
}],
|
||||
['container', {
|
||||
type: 'tip',
|
||||
defaultTitle: {
|
||||
'/': '提示',
|
||||
'/en/': 'TIP'
|
||||
}
|
||||
}],
|
||||
['container', {
|
||||
type: 'warning',
|
||||
defaultTitle: {
|
||||
'/': '注意',
|
||||
'/en/': 'WARNING'
|
||||
}
|
||||
}],
|
||||
['container', {
|
||||
type: 'danger',
|
||||
defaultTitle: {
|
||||
'/': '警告',
|
||||
'/en/': 'WARNING'
|
||||
}
|
||||
}],
|
||||
['container', {
|
||||
type: 'right',
|
||||
defaultTitle: ''
|
||||
}],
|
||||
['container', {
|
||||
type: 'theorem',
|
||||
before: info => `<div class="custom-block theorem"><p class="title">${info}</p>`,
|
||||
after: '</div>'
|
||||
}],
|
||||
['container', {
|
||||
type: 'details',
|
||||
before: info => `<details class="custom-block details">${info ? `<summary>${info}</summary>` : ''}\n`,
|
||||
after: () => '</details>\n',
|
||||
defaultTitle: {
|
||||
'/': '点击查看',
|
||||
'/en/': 'DETAILS'
|
||||
}
|
||||
}],
|
||||
|
||||
// 内容居中容器
|
||||
['container', {
|
||||
type: 'center',
|
||||
before: info => `<div class="center-container">`,
|
||||
after: () => '</div>'
|
||||
}],
|
||||
|
||||
// 卡片列表
|
||||
[
|
||||
'container',
|
||||
{
|
||||
type: CARD_LIST,
|
||||
render: (tokens, idx) => {
|
||||
// tokens 是整个md文件的虚拟dom结构数组
|
||||
// idx 是tokens中':::' 所在的索引,而且是当前指定type的':::',分别有开始和结束两次的idx
|
||||
// if (tokens[idx].nesting === 1) { // 开头的 ':::' 标记
|
||||
// } else { // 结束的 ':::' 标记
|
||||
// }
|
||||
// 注意:修改这里面的代码后需要在md文件保存一下才会重新执行渲染
|
||||
return renderCardList(tokens, idx, CARD_LIST)
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
// 图文卡片列表
|
||||
[
|
||||
'container',
|
||||
{
|
||||
type: CARD_IMG_LIST,
|
||||
render: (tokens, idx) => {
|
||||
return renderCardList(tokens, idx, CARD_IMG_LIST)
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 渲染md容器的卡片列表
|
||||
function renderCardList (tokens, idx, type) {
|
||||
const END_TYPE = `container_${type}_close`,
|
||||
_tokens$idx = tokens[idx],
|
||||
nesting = _tokens$idx.nesting,
|
||||
info = _tokens$idx.info;
|
||||
|
||||
if (nesting === 1) { // 渲染开头的 ':::' 标记
|
||||
let yamlStr = '';
|
||||
|
||||
for (let i = idx; i < tokens.length; i++) {
|
||||
let _tokens$i = tokens[i],
|
||||
type = _tokens$i.type,
|
||||
content = _tokens$i.content,
|
||||
_info = _tokens$i.info;
|
||||
if (type === END_TYPE) break; // 遇到结束的 ':::' 时
|
||||
if (!content) continue;
|
||||
if (type === 'fence' && _info === 'yaml') { // 是代码块类型,并且是yaml代码
|
||||
yamlStr = content
|
||||
}
|
||||
}
|
||||
|
||||
if (yamlStr) { // 正确解析出yaml字符串后
|
||||
const dataObj = yaml.safeLoad(yamlStr) // 将yaml字符串解析成js对象
|
||||
let dataList = []
|
||||
|
||||
if (dataObj) { // 正确解析出数据对象
|
||||
dataList = Array.isArray(dataObj) ? dataObj : dataObj.list
|
||||
}
|
||||
|
||||
if (dataList && dataList.length) { // 有列表数据
|
||||
|
||||
// 每行显示几个
|
||||
let row = Number(info.split(' ').pop())
|
||||
if (!row || row > 4 || row < 1) {
|
||||
row = 3 // 默认 3
|
||||
}
|
||||
|
||||
let listDOM = ''
|
||||
if (type === CARD_LIST) { // 普通卡片列表
|
||||
listDOM = getCardListDOM(dataList, row)
|
||||
} else if (type === CARD_IMG_LIST) { // 卡片图片列表
|
||||
listDOM = getCardImgListDOM(dataList, row)
|
||||
}
|
||||
|
||||
return `<div class="${type}Container"><div class="card-list">${listDOM}</div>`
|
||||
}
|
||||
}
|
||||
} else { // 渲染':::' 结尾
|
||||
return '</div>'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 将数据解析成DOM结构 - 普通卡片列表
|
||||
function getCardListDOM (dataList, row) {
|
||||
let listDOM = ''
|
||||
dataList.forEach(item => {
|
||||
listDOM += `
|
||||
<${item.link ? 'a href="' + item.link + '" target="_blank"' : 'span'} class="card-item ${row ? 'row-' + row : ''}"
|
||||
style="${item.bgColor ? 'background-color:' + item.bgColor + ';--randomColor:' + item.bgColor + ';' : '--randomColor: var(--bodyBg);'}${item.textColor ? 'color:' + item.textColor + ';' : ''}"
|
||||
>
|
||||
${item.avatar ? '<img src="' + withBase(item.avatar) + '" class="no-zoom">' : ''}
|
||||
<div>
|
||||
<p class="name">${item.name}</p>
|
||||
<p class="desc">${item.desc}</p>
|
||||
</div>
|
||||
</${item.link ? 'a' : 'span'}>
|
||||
`
|
||||
})
|
||||
return listDOM
|
||||
}
|
||||
|
||||
|
||||
// 将数据解析成DOM结构 - 图文卡片列表
|
||||
function getCardImgListDOM (dataList, row) {
|
||||
let listDOM = ''
|
||||
dataList.forEach(item => {
|
||||
listDOM += `
|
||||
<div class="card-item ${row ? 'row-' + row : ''}" >
|
||||
<a href="${item.link}" target="_blank">
|
||||
<div class="box-img">
|
||||
<img src="${withBase(item.img)}" class="no-zoom">
|
||||
</div>
|
||||
<div class="box-info">
|
||||
<p class="name">${item.name}</p>
|
||||
${item.desc ? `<p class="desc">${item.desc}</p>` : ''}
|
||||
</div>
|
||||
|
||||
${item.avatar || item.author ? `<div class="box-footer">
|
||||
${item.avatar ? `<img src="${withBase(item.avatar)}" class="no-zoom">` : ''}
|
||||
${item.author ? `<span>${item.author}</span>` : ''}
|
||||
</div>`: ''}
|
||||
</a>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
return listDOM
|
||||
}
|
||||
|
||||
// 添加base路径
|
||||
function withBase (path) {
|
||||
if (base && path.charAt(0) === '/') {
|
||||
return base + path.slice(1);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
35
theme-vdoing/layouts/404.vue
Normal file
35
theme-vdoing/layouts/404.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="theme-container">
|
||||
<div class="theme-vdoing-content">
|
||||
<span>404</span>
|
||||
<blockquote>{{ getMsg() }}</blockquote>
|
||||
<router-link to="/">返回首页</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const msgs = [
|
||||
`这里什么都没有。`,
|
||||
`我是谁?我在哪?`,
|
||||
`这是一个Four-Oh-Four.`,
|
||||
`看来我们的链接坏掉了~`
|
||||
]
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
getMsg () {
|
||||
return msgs[Math.floor(Math.random() * msgs.length)]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.theme-vdoing-content
|
||||
margin 3rem auto
|
||||
padding 1.5rem
|
||||
span
|
||||
font-size 6rem
|
||||
color $accentColor
|
||||
</style>
|
||||
382
theme-vdoing/layouts/Layout.vue
Normal file
382
theme-vdoing/layouts/Layout.vue
Normal file
@@ -0,0 +1,382 @@
|
||||
<template>
|
||||
<div
|
||||
class="theme-container"
|
||||
:class="pageClasses"
|
||||
@touchstart="onTouchStart"
|
||||
@touchend="onTouchEnd"
|
||||
>
|
||||
<Navbar
|
||||
v-if="shouldShowNavbar"
|
||||
@toggle-sidebar="toggleSidebar"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="sidebar-mask"
|
||||
@click="toggleSidebar(false)"
|
||||
></div>
|
||||
|
||||
<Sidebar
|
||||
:items="sidebarItems"
|
||||
@toggle-sidebar="toggleSidebar"
|
||||
v-show="showSidebar"
|
||||
>
|
||||
<template
|
||||
#top
|
||||
v-if="sidebarSlotTop"
|
||||
>
|
||||
<div class="sidebar-slot sidebar-slot-top" v-html="sidebarSlotTop"></div>
|
||||
</template>
|
||||
<template
|
||||
#bottom
|
||||
v-if="sidebarSlotBottom"
|
||||
>
|
||||
<div class="sidebar-slot sidebar-slot-bottom" v-html="sidebarSlotBottom"></div>
|
||||
</template>
|
||||
<!-- <slot name="sidebar-top" #top />
|
||||
<slot name="sidebar-bottom" #bottom /> -->
|
||||
</Sidebar>
|
||||
|
||||
<!-- 首页 -->
|
||||
<Home v-if="$page.frontmatter.home" />
|
||||
|
||||
<!-- 分类页 -->
|
||||
<CategoriesPage v-else-if="$page.frontmatter.categoriesPage" />
|
||||
|
||||
<!-- 标签页 -->
|
||||
<TagsPage v-else-if="$page.frontmatter.tagsPage" />
|
||||
|
||||
<!-- 归档页 -->
|
||||
<ArchivesPage v-else-if="$page.frontmatter.archivesPage" />
|
||||
|
||||
<!-- 文章页或其他页 -->
|
||||
<Page
|
||||
v-else
|
||||
:sidebar-items="sidebarItems"
|
||||
>
|
||||
<template
|
||||
#top
|
||||
v-if="pageSlotTop"
|
||||
>
|
||||
<div class="page-slot page-slot-top" v-html="pageSlotTop"></div>
|
||||
</template>
|
||||
<template
|
||||
#bottom
|
||||
v-if="pageSlotBottom"
|
||||
>
|
||||
<div class="page-slot page-slot-bottom" v-html="pageSlotBottom"></div>
|
||||
</template>
|
||||
<!-- <slot
|
||||
name="page-top"
|
||||
#top
|
||||
/>
|
||||
<slot
|
||||
name="page-bottom"
|
||||
#bottom
|
||||
/> -->
|
||||
</Page>
|
||||
|
||||
<Footer />
|
||||
|
||||
<Buttons
|
||||
ref="buttons"
|
||||
@toggle-theme-mode="toggleThemeMode"
|
||||
/>
|
||||
|
||||
<BodyBgImg v-if="$themeConfig.bodyBgImg" />
|
||||
|
||||
<!-- 自定义html插入左右下角的小窗口 -->
|
||||
<div class="custom-html-window custom-html-window-lb" v-if="windowLB" v-show="showWindowLB">
|
||||
<div class="custom-wrapper">
|
||||
<i class="close-but" @click="showWindowLB = false">×</i>
|
||||
<div v-html="windowLB"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom-html-window custom-html-window-rb" v-if="windowRB" v-show="showWindowRB">
|
||||
<div class="custom-wrapper">
|
||||
<i class="close-but" @click="showWindowRB = false">×</i>
|
||||
<div v-html="windowRB"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Home from '@theme/components/Home.vue'
|
||||
import Navbar from '@theme/components/Navbar.vue'
|
||||
import Page from '@theme/components/Page.vue'
|
||||
import CategoriesPage from '@theme/components/CategoriesPage.vue'
|
||||
import TagsPage from '@theme/components/TagsPage.vue'
|
||||
import ArchivesPage from '@theme/components/ArchivesPage.vue'
|
||||
import Sidebar from '@theme/components/Sidebar.vue'
|
||||
import Buttons from '@theme/components/Buttons.vue'
|
||||
import Footer from '@theme/components/Footer'
|
||||
import BodyBgImg from '@theme/components/BodyBgImg'
|
||||
import { resolveSidebarItems } from '../util'
|
||||
import storage from 'good-storage' // 本地存储
|
||||
import _ from 'lodash'
|
||||
|
||||
const MOBILE_DESKTOP_BREAKPOINT = 719 // refer to config.styl
|
||||
const NAVBAR_HEIGHT = 58 // 导航栏高度
|
||||
|
||||
export default {
|
||||
components: { Home, Navbar, Page, CategoriesPage, TagsPage, ArchivesPage, Sidebar, Footer, Buttons, BodyBgImg },
|
||||
|
||||
data () {
|
||||
return {
|
||||
hideNavbar: false,
|
||||
isSidebarOpen: true,
|
||||
showSidebar: false,
|
||||
themeMode: 'light',
|
||||
showWindowLB: true,
|
||||
showWindowRB: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sidebarSlotTop() {
|
||||
return this.getHtmlStr('sidebarT')
|
||||
},
|
||||
sidebarSlotBottom() {
|
||||
return this.getHtmlStr('sidebarB')
|
||||
},
|
||||
pageSlotTop() {
|
||||
return this.getHtmlStr('pageT')
|
||||
},
|
||||
pageSlotBottom() {
|
||||
return this.getHtmlStr('pageB')
|
||||
},
|
||||
windowLB() {
|
||||
return this.getHtmlStr('windowLB')
|
||||
},
|
||||
windowRB() {
|
||||
return this.getHtmlStr('windowRB')
|
||||
},
|
||||
showRightMenu () {
|
||||
const { headers } = this.$page
|
||||
return (
|
||||
!this.$frontmatter.home
|
||||
&& this.$themeConfig.rightMenuBar !== false
|
||||
&& headers
|
||||
&& headers.length
|
||||
&& this.$frontmatter.sidebar !== false
|
||||
)
|
||||
},
|
||||
shouldShowNavbar () {
|
||||
const { themeConfig } = this.$site
|
||||
const { frontmatter } = this.$page
|
||||
if (
|
||||
frontmatter.navbar === false
|
||||
|| themeConfig.navbar === false) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
this.$title
|
||||
|| themeConfig.logo
|
||||
|| themeConfig.repo
|
||||
|| themeConfig.nav
|
||||
|| this.$themeLocaleConfig.nav
|
||||
)
|
||||
},
|
||||
|
||||
shouldShowSidebar () {
|
||||
const { frontmatter } = this.$page
|
||||
return (
|
||||
!frontmatter.home
|
||||
&& frontmatter.sidebar !== false
|
||||
&& this.sidebarItems.length
|
||||
)
|
||||
},
|
||||
|
||||
sidebarItems () {
|
||||
return resolveSidebarItems(
|
||||
this.$page,
|
||||
this.$page.regularPath,
|
||||
this.$site,
|
||||
this.$localePath
|
||||
)
|
||||
},
|
||||
|
||||
pageClasses () {
|
||||
const userPageClass = this.$page.frontmatter.pageClass
|
||||
return [
|
||||
{
|
||||
'no-navbar': !this.shouldShowNavbar,
|
||||
'hide-navbar': this.hideNavbar, // 向下滚动隐藏导航栏
|
||||
'sidebar-open': this.isSidebarOpen,
|
||||
'no-sidebar': !this.shouldShowSidebar,
|
||||
'have-rightmenu': this.showRightMenu,
|
||||
'have-body-img': this.$themeConfig.bodyBgImg
|
||||
},
|
||||
// 'theme-mode-' + this.themeMode,
|
||||
userPageClass
|
||||
]
|
||||
}
|
||||
},
|
||||
created () {
|
||||
const sidebarOpen = this.$themeConfig.sidebarOpen
|
||||
if (sidebarOpen === false) {
|
||||
this.isSidebarOpen = sidebarOpen
|
||||
}
|
||||
},
|
||||
beforeMount () {
|
||||
this.isSidebarOpenOfclientWidth()
|
||||
const mode = storage.get('mode') // 不放在created是因为vuepress不能在created访问浏览器api,如window
|
||||
if (!mode || mode === 'auto') { // 当未切换过模式,或模式处于'跟随系统'时
|
||||
this._autoMode()
|
||||
} else {
|
||||
this.themeMode = mode
|
||||
}
|
||||
this.setBodyClass()
|
||||
|
||||
// 引入图标库
|
||||
const social = this.$themeConfig.social
|
||||
if (social && social.iconfontCssFile) {
|
||||
let linkElm = document.createElement("link")
|
||||
linkElm.setAttribute('rel', 'stylesheet');
|
||||
linkElm.setAttribute("type", "text/css")
|
||||
linkElm.setAttribute("href", social.iconfontCssFile)
|
||||
document.head.appendChild(linkElm)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// 初始化页面时链接锚点无法跳转到指定id的解决方案
|
||||
const hash = document.location.hash;
|
||||
if (hash.length > 1) {
|
||||
const id = decodeURIComponent(hash.substring(1))
|
||||
const element = document.getElementById(id)
|
||||
if (element) element.scrollIntoView()
|
||||
}
|
||||
|
||||
// 解决移动端初始化页面时侧边栏闪现的问题
|
||||
this.showSidebar = true
|
||||
this.$router.afterEach(() => {
|
||||
this.isSidebarOpenOfclientWidth()
|
||||
})
|
||||
|
||||
// 向下滚动收起导航栏
|
||||
let p = 0, t = 0;
|
||||
window.addEventListener('scroll', _.throttle(() => {
|
||||
if (!this.isSidebarOpen) { // 侧边栏关闭时
|
||||
p = this.getScrollTop()
|
||||
if (t < p && p > NAVBAR_HEIGHT) { // 向下滚动
|
||||
this.hideNavbar = true
|
||||
} else { // 向上
|
||||
this.hideNavbar = false
|
||||
}
|
||||
setTimeout(() => { t = p }, 0)
|
||||
}
|
||||
}, 300))
|
||||
|
||||
},
|
||||
watch: {
|
||||
isSidebarOpen () {
|
||||
if (this.isSidebarOpen) { // 侧边栏打开时,恢复导航栏显示
|
||||
this.hideNavbar = false
|
||||
}
|
||||
},
|
||||
themeMode () {
|
||||
this.setBodyClass()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getHtmlStr(module) {
|
||||
const { htmlModules } = this.$themeConfig
|
||||
return htmlModules ? htmlModules[module] : ''
|
||||
},
|
||||
setBodyClass () {
|
||||
document.body.className = 'theme-mode-' + this.themeMode
|
||||
},
|
||||
getScrollTop () {
|
||||
return window.pageYOffset
|
||||
|| document.documentElement.scrollTop
|
||||
|| document.body.scrollTop || 0
|
||||
},
|
||||
isSidebarOpenOfclientWidth () {
|
||||
if (document.documentElement.clientWidth < MOBILE_DESKTOP_BREAKPOINT) {
|
||||
this.isSidebarOpen = false
|
||||
}
|
||||
},
|
||||
toggleSidebar (to) {
|
||||
this.isSidebarOpen = typeof to === 'boolean' ? to : !this.isSidebarOpen
|
||||
this.$emit('toggle-sidebar', this.isSidebarOpen)
|
||||
},
|
||||
_autoMode () {
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) { // 系统处于深色模式
|
||||
this.themeMode = 'dark'
|
||||
} else {
|
||||
this.themeMode = 'light'
|
||||
}
|
||||
},
|
||||
toggleThemeMode (key) {
|
||||
if (key === 'auto') {
|
||||
this._autoMode()
|
||||
} else {
|
||||
this.themeMode = key
|
||||
}
|
||||
storage.set('mode', key)
|
||||
},
|
||||
|
||||
// side swipe
|
||||
onTouchStart (e) {
|
||||
this.touchStart = {
|
||||
x: e.changedTouches[0].clientX,
|
||||
y: e.changedTouches[0].clientY
|
||||
}
|
||||
},
|
||||
|
||||
onTouchEnd (e) {
|
||||
const dx = e.changedTouches[0].clientX - this.touchStart.x
|
||||
const dy = e.changedTouches[0].clientY - this.touchStart.y
|
||||
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 40) {
|
||||
if (dx > 0 && this.touchStart.x <= 80) {
|
||||
this.toggleSidebar(true)
|
||||
} else {
|
||||
this.toggleSidebar(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.custom-html-window
|
||||
position fixed
|
||||
bottom 0
|
||||
display flex
|
||||
overflow hidden
|
||||
font-weight 350
|
||||
@media (max-width 960px)
|
||||
display none
|
||||
.custom-wrapper
|
||||
position relative
|
||||
max-width 200px
|
||||
max-height 200px
|
||||
.close-but
|
||||
cursor pointer
|
||||
position: absolute
|
||||
right: 0
|
||||
top: 0
|
||||
font-size 2rem
|
||||
line-height 1.5rem
|
||||
width 1.5rem
|
||||
height 1.5rem
|
||||
opacity 0
|
||||
transition all .2s
|
||||
&:hover
|
||||
opacity .9
|
||||
&:hover
|
||||
.close-but
|
||||
opacity .7
|
||||
&.custom-html-window-lb
|
||||
left 0
|
||||
z-index 99
|
||||
&>*
|
||||
align-self: flex-end;
|
||||
&.custom-html-window-rb
|
||||
right 80px
|
||||
z-index 10
|
||||
justify-content: flex-end
|
||||
&>*
|
||||
align-self: flex-end;
|
||||
</style>
|
||||
21
theme-vdoing/mixins/posts.js
Normal file
21
theme-vdoing/mixins/posts.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { filterPosts, sortPosts, sortPostsByDate, groupPosts, categoriesAndTags } from '../util/postData'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
$filterPosts () { // 过滤非文章页和首页的文章数据
|
||||
return filterPosts(this.$site.pages)
|
||||
},
|
||||
$sortPosts () { // 按置顶和时间排序的文章数据
|
||||
return sortPosts(this.$filterPosts)
|
||||
},
|
||||
$sortPostsByDate () { // 仅按时间排序的文章数据
|
||||
return sortPostsByDate(this.$filterPosts)
|
||||
},
|
||||
$groupPosts () { // 按分类和标签分组的文章数据
|
||||
return groupPosts(this.$sortPosts)
|
||||
},
|
||||
$categoriesAndTags () { // 所有分类和标签数据
|
||||
return categoriesAndTags(this.$groupPosts)
|
||||
}
|
||||
}
|
||||
}
|
||||
28
theme-vdoing/mixins/titleBadge.js
Normal file
28
theme-vdoing/mixins/titleBadge.js
Normal file
@@ -0,0 +1,28 @@
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
badges: [
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAAXNSR0IArs4c6QAABGpJREFUSA3tVVtoXFUU3fvOI53UlmCaKIFmwEhsE7QK0ipFEdHEKpXaZGrp15SINsXUWvBDpBgQRKi0+KKoFeJHfZA+ED9KKoIU2gYD9UejTW4rVIzm0VSTziPzuNu1z507dibTTjL4U/DAzLn3nL3X2o91ziX6f9wMFdh6Jvbm9nNSV0msViVO6tN1Rm7NMu2OpeJ9lWBUTDxrJbYTS0hInuwciu9eLHlFxCLCZEk3MegsJmZ5K/JD6t7FkFdEvGUo1g7qJoG3MHImqRIn8/nzY1K9UPKKiJmtnUqHVE3Gbuay6vJE/N2FEmuxFjW2nUuE0yQXRRxLiTUAzs36zhZvOXJPdX850EVnnLZkB8prodQoM5JGj7Xk2mvC7JB8tG04Ef5PiXtG0UtxupRQSfTnBoCy554x18yJHI6I+G5Eru4LHmPJZEQsrvPUbMiA8G/WgMK7w7I+ez7++o2ANfbrjvaOl1tFMs+htG3IrZH9/hDX1Pr8Tc0UvH8tcX29KzAgIGcEkINyW5BF9x891hw6VYqgJHEk0huccS7vh3C6gTiODL+26huuBtbct8eZnqLML8PkxGYpuPZBqtqwkSjgc4mB5gbgig5i+y0UDK35LMxXisn9xQtK+nd26gTIHsHe/oblK/b29fUmN/8Y+9jAQrnBp56m1LcDlDp9irKTExSKduXJVWSqdBMA08pEJnEIOB3FPPMybu/oeV8zFeYN3xx576Q6RH+VmplE4ncQV5v+5rzSoyOU7PuEAg8g803PwBJ0CExno/jcMbN8tONYeOmHiuUNryvm3fRUy4tMPVLdAGkUhNWuggGrJcXPv+ouCjz0MKUHz1J2/E8IC9nqTabcxgaBYM0hPhD5Y65FsbxRQKxCQrDjDctW7PUM3HuZunFyifSAqEfuzCp48Il24luWUWZoyJCaPR82jE0+kFA643wRFVni4RYSq3ohJO2pZ7B5dO4xkDWbEpossJPLSrPjYID8rS2UHTlvyNxqIGsg674XJJ7vnh5L7PNwC4hh2sjCI96mzszOTpxLF0T7l88Yz7lAuK6OnL8gXLOnTvpzSb22YG8W7us3jSebFHeeqnXRG1vt+MoUM84LQIBmMsCTAcOauTh0T0l0neQK7m2bLMt2mGxU3HYssS0J2cdv5wljlPsrIuZLAG/2DOZIXgCYT8uMGZN+e2kSirfxZOPCsC0f24nTZzspnVn9VePS1Z5vubmAGGXG8ZFno9Hel0yfA5ZPhF7Dh972BQJ2qCpgH67lmWtBYbvk6sz02wjky2vXyz0XErP/kFB619js1BtwfOV4OPRqOQBjy3Qbk18vigUPPSD5ceHnwck7W9bhAqZdd7SuG7w4/P2F/GaJh8c7e9qgow+Q7cGBo+98WsLkuktFqiZabtXuQTu/Y5ETbR0v7tNSFnvrmu6pjdoan2KjMu8q/Hmj1EfCO2ZGfEIbIXKUlw8qaX9/b2oeSJmFksSeT/Fn0V3nSypChh4Gjh74ybO9aeZ/AN2dwciu2/MhAAAAAElFTkSuQmCC',
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAAXNSR0IArs4c6QAABH1JREFUSA3tVl1oHFUUPmdmd2ltklqbpJDiNnXFmgbFktho7YMPNiJSSZM0+CAYSkUELVhM6YuwIPpgoOKDqOBDC0XE2CQoNtQXBUFTTcCi+Wlh1V2TQExsUzcltd3M9Tt3ZjZzZ2fT+OJTL8yeM+eee757fmeJbq//KQL8X3DUSFOcfr7cRsRtxNQMWueeVzOkaITIGqQHNg5y8+jNW9ldM7A6nTpAjuolUikAwq7CE3WcM2RRDz+XGVgN3FptU/aUSlvq9Pa3iZ1+sgAqJyyAFqkipd9dqiwHF3P65YycLWc/6sqGrvoEoIp6DOFaX5h6+dnfjkWprwqsPk0dUGq5vySwDImC10KxFHgGL1SWoc92O3eVht09qdXNH11I2SsTsJYqMWzihqGMi+A+Garf3BAuuLI5oGlULyNfyB/HYNujwktOfRrMr5t77NmevqaUopx0grnKAyvVpmwUDB4x6FPXuGvYLTDwWsejwgtgkYKPqRJg8SV6xaiZ3ZTppGneS4yfH5/66fZSDHv+QZci/+h5c5UHtpy67JUqGppM0sh0Nc1dW6/N1W5Yoqat8/TU/VnadmdeW2PLLSyh0cvxBs3KbqTmwYPpxN4do/mzE8nEpvX/UMu2Wbp74zUAK5q6WkHns7V0eWkdPbPzd3rxkTGybadYySumVzhcaJFbs5UrEkQ/+CK8gF5dnh/6ciIZ73gwQ927L1IitoxKLXYP3SjYdOrHHfTZhRRlFyrorafPk20B3HPD1y2G3qKZME5Jcf3t/HUC13/8tSd++vqFveMUTwAUxSUFI1QekR1+bIze3D9MF2aq6cPvG72CgnldWCFqyRw3lwH8ZMerjTD9ElRO7Gv44wNpC90aASqGfVlz/Rx17srQ57/UU26hkhQqUB7dBR71WmzQhHUnblGmVOEw0jhbV1n9OlXUDCIRGaNV5Jp43N516fN7JmnTHdfp7Hgy0luO4aMhtkLL8Bi3bUWYvzh5Mn1dTxrL6QmGuRhGL/TiTTxRoEdTszSaq9GR0NGA3KdkOz3hqSV3MIDhQ5IVX/Ivx3umBti2es2h4eZby7x8br1rkf7Mo90AqC8aQ3sJeNzqFRu+vSANAQe3PL7l0HGOAdwDCeZYvNKeoZp1Qfs6Aipndh86HmFRi0LAnEO47wsqM6cdfjh3jBPUzhZy7nvlUfFsamED1VQt6aISHVymXZ/B2aCtIG8AI8xfobj2d3en1wWVhOeHELKmLQ1s211s88comkv4UCwWyF787mJdYXtNfhKAXVqnKTq8QZvGAGGOfaTo5pGZ/PwbUCr5+DPr/1J92JNHr9aOl/F3iI5+O1nfybsGxoimvZ3ViWSluDITw3P37mypheDIPY0tw7+O/5ApbkYw+zpfaUVu32Pi98+defdUhEpZkRFq0aqyNh9FuL9hpYbEm6iwi0z2REd09ZmyENEbuhjDWzKvZXTqKYaBIr3tt5kuPtQBZFvEUwHt60vfCNu41XsksH9Ij1BMMz1Y0OOunHNShFIP5868g5zeXmuLwL9T4b6Q2+KejgAAAABJRU5ErkJggg==',
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAAXNSR0IArs4c6QAABKFJREFUSA3tVl1oFVcQnrMbrak3QUgkya1akpJYcrUtIqW1JvFBE9LiQ5v6JmJpolbMg32rVrhgoYK0QiMY6i9Y6EMaW5D+xFJaTYItIuK2Kr3+BJNwkxBj05sQY3b3nM6cs2dv9t7NT/vQJw/sndk5M/PNzJkzewGerP+pAmy+ON8lLzUJgA8ZYxYIYZmGYRnctDaWvJJAmTtfP1pvXsBCCPP8QFcCaRkZYACgDZFO4stNIcBCajEOlmmC9XpJ9bAGCaPaPmzPl32dvLSVu3BWCTQs0XQQ6g0DYgwLIoAZbBCdW/i+781o1VVlm/410mw4h06Y7bIPHNyWDyL4FHkX03Q8SrzNhZTZriieckWt7cL6MM85YcLpsi/7O9/iXFT6MswI0DmmpkSaJ0qLxFIm3+i1THHB3zmBH3PYx9CcykcLOeQVVa7QtdxTgQgEleX2AjHYfwA+2ddV77ruGoJUbhGDI09YSNXyMpUt5ylOzxgbUmtOp7NmbNt8v3arjTBfYELmLUV+M+nSawNNAUqpT3ClJWg5I3BLT+cGW/DXNGCa6tx1aakCGEigArTn4TDIPdrXXYKCZNrHLMCOEPvHBlLQ99s9eHB7EB6NTki73CVPQ2F5MSx/uRQixfmq7rK0wYD8w8E905bnPDfwoWs/rfv93NWN/ZfvwsLIU7A09gxECyISeGJkHAau98L97tuw7NXnoPyNF8FcYGLGKsOs0mN3OEyec9esGW/ZEl945dTP34wlR2FZVQWU1q0Cw8Tr7p+hgLLNL0FPxx/Q35mA8aEUrH6nCgwEl0tn7wUiZYJnNRh6DK4UH/k0lfyrsBKdPVv/AriGIQcEDQZ65LBAGe2Rzui9Ybjz7XUppz1/uKBbyVPGkN3ZAeC6hr0x7Nr38N5+EqkoOm17xpoqR9ohQF55ERSvr4Dkr3chNfC3DMzGJlNBElW8w9nsGQvhNGIzDkXzCg8cLK951xHsFBlTJspJNi3ZFIMF2AeDV3q8DNOB+YHi6QTrChDIWDBRi5U5f+ZMfJLu3ccrqxtdxk4SKH336LFxSmkqefwU5T8fhdSdQf9IVKD6aNiwI/hnmcAZ91isYMJIaCUCx9W098+LgruikeTqzqqxKPUwqJyCPJiyemVVZBOijDGjD38Os0jOiSPL1z3SPjXNANbiNPXAdzTfukjjuknNBbyz3nwgTd3AVFqUJ5hpHlq9MveLnWwttUfoygBmvVjuikxND3znrhsELnZk7k+OjIGxeNEkomyLVta0xxn+HZhjBc4YZ/AFjHjz9u3xRZl2BN4aq9nFwWh16IrQ1aHHEd3j1+4/dB9OtH4e29A2H1DyHQRmOSfQZ1Fy7MHBTGB6J/Djq6p3OxyO2cB+4Car7v/o3GXgfAkj23+x9ID1Teoamo/SXcbvSf2PX7Vc8DdCmE1vN9di+32P9/5YR3vLnhCVGUWBjEkr3yh4H8v9CzmsbdhzOKzsJKM90iFdaTMjRPhGVsakRvOaRidljo6H6G7j+ctrJpsP+4COhDIl0La2+FS4+5mlocBaXY5QnGZysIBYoeSsl5qQzrSj/cgNrfuEzlWBfwA+EjrZyWUvpAAAAABJRU5ErkJggg=='
|
||||
],
|
||||
currentBadge: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.$themeConfig.titleBadgeIcons) {
|
||||
this.badges = this.$themeConfig.titleBadgeIcons
|
||||
}
|
||||
this.currentBadge = this.getBadge()
|
||||
},
|
||||
watch: {
|
||||
'$route.path' () {
|
||||
this.currentBadge = this.getBadge()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getBadge () {
|
||||
return this.badges[Math.floor(Math.random() * this.badges.length)]
|
||||
}
|
||||
}
|
||||
}
|
||||
158
theme-vdoing/node_utils/getSidebarData.js
Normal file
158
theme-vdoing/node_utils/getSidebarData.js
Normal file
@@ -0,0 +1,158 @@
|
||||
const fs = require('fs'); // 文件模块
|
||||
const path = require('path'); // 路径模块
|
||||
const chalk = require('chalk') // 命令行打印美化
|
||||
const matter = require('gray-matter'); // front matter解析器
|
||||
const log = console.log
|
||||
|
||||
let catalogueData = {}; // 目录页数据
|
||||
|
||||
/**
|
||||
* 生成侧边栏数据
|
||||
* @param {String} sourceDir .md文件所在源目录(一般是docs目录)
|
||||
* @param {Boolean} collapsable 是否可折叠
|
||||
*/
|
||||
function createSidebarData (sourceDir, collapsable) {
|
||||
const sidebarData = {};
|
||||
const tocs = readTocs(sourceDir);
|
||||
tocs.forEach(toc => { // toc是每个目录的绝对路径
|
||||
|
||||
const tocArr = toc.split('\\')
|
||||
if (tocArr[tocArr.length - 1] === '_posts') { // 碎片化文章
|
||||
|
||||
// 注释说明:碎片化文章不需要生成结构化侧边栏 2020.05.01
|
||||
// const sidebarArr = mapTocToPostSidebar(toc);
|
||||
// sidebarData[`/${path.basename(toc)}/`] = sidebarArr
|
||||
|
||||
} else {
|
||||
const sidebarObj = mapTocToSidebar(toc, collapsable);
|
||||
if (!sidebarObj.sidebar.length) {
|
||||
log(chalk.yellow(`warning: 该目录 "${toc}" 内部没有任何文件或文件序号出错,将忽略生成对应侧边栏`))
|
||||
return;
|
||||
}
|
||||
sidebarData[`/${path.basename(toc)}/`] = sidebarObj.sidebar
|
||||
sidebarData.catalogue = sidebarObj.catalogueData
|
||||
}
|
||||
})
|
||||
|
||||
return sidebarData
|
||||
}
|
||||
|
||||
module.exports = createSidebarData;
|
||||
|
||||
|
||||
/**
|
||||
* 读取指定目录下的文件绝对路径
|
||||
* @param {String} root 指定的目录
|
||||
*/
|
||||
function readTocs (root) {
|
||||
const result = [];
|
||||
const files = fs.readdirSync(root); // 读取目录,返回数组,成员是root底下所有的目录名 (包含文件夹和文件)
|
||||
files.forEach(name => {
|
||||
const file = path.resolve(root, name); // 将路径或路径片段的序列解析为绝对路径
|
||||
if (fs.statSync(file).isDirectory() && name !== '.vuepress' && name !== '@pages') { // 是否为文件夹目录,并排除.vuepress文件夹
|
||||
result.push(file);
|
||||
}
|
||||
})
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将碎片化文章目录(_posts)映射为对应的侧边栏配置数据
|
||||
* @param {String} root
|
||||
*/
|
||||
function mapTocToPostSidebar (root) {
|
||||
let postSidebar = [] // 碎片化文章数据
|
||||
const files = fs.readdirSync(root); // 读取目录(文件和文件夹),返回数组
|
||||
|
||||
files.forEach(filename => {
|
||||
const file = path.resolve(root, filename); // 方法:将路径或路径片段的序列解析为绝对路径
|
||||
const stat = fs.statSync(file); // 文件信息
|
||||
|
||||
const fileNameArr = filename.split('.');
|
||||
if (fileNameArr.length > 2) {
|
||||
log(chalk.yellow(`warning: 该文件 "${file}" 在_posts文件夹中,不应有序号,且文件名中间不应有'.'`))
|
||||
return
|
||||
}
|
||||
if (stat.isDirectory()) { // 是文件夹目录
|
||||
// log(chalk.yellow(`warning: 该目录 "${file}" 内文件无法生成侧边栏,_posts文件夹里面不能有二级目录。`))
|
||||
return
|
||||
}
|
||||
|
||||
let [title, type] = filename.split('.');
|
||||
if (type !== 'md') {
|
||||
log(chalk.yellow(`warning: 该文件 "${file}" 非.md格式文件,不支持该文件类型`))
|
||||
return;
|
||||
}
|
||||
|
||||
const contentStr = fs.readFileSync(file, 'utf8') // 读取md文件内容,返回字符串
|
||||
const { data } = matter(contentStr) // 解析出front matter数据
|
||||
const permalink = data.permalink || ''
|
||||
if (data.title) {
|
||||
title = data.title
|
||||
}
|
||||
postSidebar.push([filename, title, permalink]); // [<路径>, <标题>, <永久链接>]
|
||||
})
|
||||
|
||||
return postSidebar
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将目录映射为对应的侧边栏配置数据
|
||||
* @param {String} root
|
||||
* @param {Boolean} collapsable
|
||||
* @param {String} prefix
|
||||
*/
|
||||
|
||||
function mapTocToSidebar (root, collapsable, prefix = '') {
|
||||
let sidebar = []; // 结构化文章侧边栏数据
|
||||
const files = fs.readdirSync(root); // 读取目录(文件和文件夹),返回数组
|
||||
|
||||
files.forEach(filename => {
|
||||
const file = path.resolve(root, filename); // 方法:将路径或路径片段的序列解析为绝对路径
|
||||
const stat = fs.statSync(file); // 文件信息
|
||||
let [order, title, type] = filename.split('.');
|
||||
order = parseInt(order, 10);
|
||||
if (isNaN(order) || order < 0) {
|
||||
log(chalk.yellow(`warning: 该文件 "${file}" 序号出错,请填写正确的序号`))
|
||||
return;
|
||||
}
|
||||
if (sidebar[order]) { // 判断序号是否已经存在
|
||||
log(chalk.yellow(`warning: 该文件 "${file}" 的序号在同一级别中重复出现,将会被覆盖`))
|
||||
}
|
||||
if (stat.isDirectory()) { // 是文件夹目录
|
||||
sidebar[order] = {
|
||||
title,
|
||||
collapsable, // 是否可折叠,默认true
|
||||
children: mapTocToSidebar(file, collapsable, prefix + filename + '/').sidebar // 子栏路径添加前缀
|
||||
}
|
||||
} else { // 是文件
|
||||
if (type !== 'md') {
|
||||
log(chalk.yellow(`warning: 该文件 "${file}" 非.md格式文件,不支持该文件类型`))
|
||||
return;
|
||||
}
|
||||
const contentStr = fs.readFileSync(file, 'utf8') // 读取md文件内容,返回字符串
|
||||
const { data } = matter(contentStr) // 解析出front matter数据
|
||||
const permalink = data.permalink || ''
|
||||
|
||||
// 目录页对应的永久链接,用于给面包屑提供链接
|
||||
const { pageComponent } = data
|
||||
if (pageComponent && pageComponent.name === "Catalogue") {
|
||||
catalogueData[title] = permalink
|
||||
}
|
||||
|
||||
if (data.title) {
|
||||
title = data.title
|
||||
}
|
||||
sidebar[order] = [prefix + filename, title, permalink]; // [<路径>, <标题>, <永久链接>]
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
sidebar = sidebar.filter(item => item !== null && item !== undefined);
|
||||
return {
|
||||
sidebar,
|
||||
catalogueData
|
||||
};
|
||||
}
|
||||
82
theme-vdoing/node_utils/handlePage.js
Normal file
82
theme-vdoing/node_utils/handlePage.js
Normal file
@@ -0,0 +1,82 @@
|
||||
// 生成或删除页面(分类页、标签页、归档页...)
|
||||
|
||||
const fs = require('fs'); // 文件模块
|
||||
const path = require('path'); // 路径模块
|
||||
const chalk = require('chalk') // 命令行打印美化
|
||||
const { type } = require('./modules/fn');
|
||||
const log = console.log
|
||||
|
||||
function createPage (sourceDir, page) {
|
||||
const dirPath = path.join(sourceDir, '@pages') // 生成的文件夹路径
|
||||
|
||||
// 文件夹不存在时
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath) // 创建文件夹
|
||||
}
|
||||
|
||||
const pagePath = path.join(dirPath, `${page}.md`) // 生成的文件路径
|
||||
|
||||
// 文件已经存在时跳出
|
||||
if (fs.existsSync(pagePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 注意:反引号字符串的格式会映射到文件
|
||||
let content = ''
|
||||
if (page.indexOf('categories') > -1) {
|
||||
content = `---
|
||||
categoriesPage: true
|
||||
title: 分类
|
||||
permalink: /categories/
|
||||
article: false
|
||||
---`
|
||||
} else if (page.indexOf('tags') > -1) {
|
||||
content = `---
|
||||
tagsPage: true
|
||||
title: 标签
|
||||
permalink: /tags/
|
||||
article: false
|
||||
---`
|
||||
} else if (page.indexOf('archives') > -1) {
|
||||
content = `---
|
||||
archivesPage: true
|
||||
title: 归档
|
||||
permalink: /archives/
|
||||
article: false
|
||||
---`
|
||||
}
|
||||
|
||||
if (content) {
|
||||
fs.writeFileSync(pagePath, content)
|
||||
log(chalk.blue('tip ') + chalk.green(`create page(生成页面): ${pagePath}`))
|
||||
}
|
||||
}
|
||||
|
||||
// 删除页面文件
|
||||
function deletePage (sourceDir, page) {
|
||||
const dirPath = path.join(sourceDir, '@pages') // 文件夹路径
|
||||
const pagePath = path.join(dirPath, `${page}.md`) // 文件路径
|
||||
|
||||
// 文件是否存在
|
||||
if (fs.existsSync(pagePath)) {
|
||||
fs.unlinkSync(pagePath)
|
||||
log(chalk.blue('tip ') + chalk.green(`delete page(删除页面): ${pagePath}`))
|
||||
}
|
||||
deleteDir(dirPath)
|
||||
}
|
||||
|
||||
// 删除文件夹
|
||||
function deleteDir (dirPath) {
|
||||
if (fs.existsSync(dirPath)) {
|
||||
const files = fs.readdirSync(dirPath)
|
||||
if (type(files) === 'array' && files.length === 0) {
|
||||
fs.rmdirSync(dirPath)
|
||||
log(chalk.blue('tip ') + chalk.green(`delete dir(删除目录): ${dirPath}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createPage,
|
||||
deletePage
|
||||
}
|
||||
21
theme-vdoing/node_utils/modules/fn.js
Normal file
21
theme-vdoing/node_utils/modules/fn.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// 类型判断
|
||||
exports.type = function (o) {
|
||||
var s = Object.prototype.toString.call(o)
|
||||
return s.match(/\[object (.*?)\]/)[1].toLowerCase()
|
||||
}
|
||||
|
||||
// 修复date时区格式的问题
|
||||
exports.repairDate = function (date) {
|
||||
date = new Date(date);
|
||||
return `${date.getUTCFullYear()}-${zero(date.getUTCMonth() + 1)}-${zero(date.getUTCDate())} ${zero(date.getUTCHours())}:${zero(date.getUTCMinutes())}:${zero(date.getUTCSeconds())}`;
|
||||
}
|
||||
|
||||
// 日期的格式
|
||||
exports.dateFormat = function (date) {
|
||||
return `${date.getFullYear()}-${zero(date.getMonth() + 1)}-${zero(date.getDate())} ${zero(date.getHours())}:${zero(date.getMinutes())}:${zero(date.getSeconds())}`
|
||||
}
|
||||
|
||||
// 小于10补0
|
||||
function zero (d) {
|
||||
return d.toString().padStart(2, '0')
|
||||
}
|
||||
43
theme-vdoing/node_utils/modules/readFileList.js
Normal file
43
theme-vdoing/node_utils/modules/readFileList.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 读取所有md文件数据
|
||||
*/
|
||||
const fs = require('fs'); // 文件模块
|
||||
const path = require('path'); // 路径模块
|
||||
const chalk = require('chalk') // 命令行打印美化
|
||||
const log = console.log
|
||||
|
||||
function readFileList (dir, filesList = []) {
|
||||
const files = fs.readdirSync(dir);
|
||||
files.forEach((item, index) => {
|
||||
let filePath = path.join(dir, item);
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.isDirectory() && item !== '.vuepress' && item !== '@pages') {
|
||||
readFileList(path.join(dir, item), filesList); //递归读取文件
|
||||
} else {
|
||||
if (path.basename(dir) !== 'docs') { // 过滤docs目录级下的文件
|
||||
|
||||
const fileNameArr = path.basename(filePath).split('.')
|
||||
let name = null, type = null;
|
||||
if (fileNameArr.length === 2) { // 没有序号的文件
|
||||
name = fileNameArr[0]
|
||||
type = fileNameArr[1]
|
||||
} else if (fileNameArr.length === 3) { // 有序号的文件
|
||||
name = fileNameArr[1]
|
||||
type = fileNameArr[2]
|
||||
} else { // 超过两个‘.’的
|
||||
log(chalk.yellow(`warning: 该文件 "${filePath}" 没有按照约定命名,将忽略生成相应数据。`))
|
||||
return
|
||||
}
|
||||
if (type === 'md') { // 过滤非md文件
|
||||
filesList.push({
|
||||
name,
|
||||
filePath
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return filesList;
|
||||
}
|
||||
|
||||
module.exports = readFileList;
|
||||
153
theme-vdoing/node_utils/setFrontmatter.js
Normal file
153
theme-vdoing/node_utils/setFrontmatter.js
Normal file
@@ -0,0 +1,153 @@
|
||||
const fs = require('fs'); // 文件模块
|
||||
const matter = require('gray-matter'); // FrontMatter解析器 https://github.com/jonschlinkert/gray-matter
|
||||
const jsonToYaml = require('json2yaml')
|
||||
const chalk = require('chalk') // 命令行打印美化
|
||||
// const arg = process.argv.splice(2)[0]; // 获取命令行传入的参数
|
||||
const readFileList = require('./modules/readFileList');
|
||||
const { type, repairDate, dateFormat } = require('./modules/fn');
|
||||
const log = console.log
|
||||
const path = require('path');
|
||||
|
||||
const PREFIX = '/pages/'
|
||||
|
||||
|
||||
/**
|
||||
* 给.md文件设置frontmatter(标题、日期、永久链接等数据)
|
||||
*/
|
||||
function setFrontmatter (sourceDir, themeConfig) {
|
||||
|
||||
const isCategory = themeConfig.category
|
||||
const isTag = themeConfig.tag
|
||||
const categoryText = themeConfig.categoryText || '随笔'
|
||||
|
||||
const files = readFileList(sourceDir); // 读取所有md文件数据
|
||||
|
||||
files.forEach(file => {
|
||||
let dataStr = fs.readFileSync(file.filePath, 'utf8');// 读取每个md文件内容
|
||||
|
||||
// fileMatterObj => {content:'剔除frontmatter后的文件内容字符串', data:{<frontmatter对象>}, ...}
|
||||
const fileMatterObj = matter(dataStr);
|
||||
|
||||
if (Object.keys(fileMatterObj.data).length === 0) { // 未定义FrontMatter数据
|
||||
const stat = fs.statSync(file.filePath);
|
||||
const dateStr = dateFormat(
|
||||
getBirthtime(stat)
|
||||
); // 文件的创建时间
|
||||
const categories = getCategories(
|
||||
file,
|
||||
categoryText
|
||||
);
|
||||
|
||||
let cateLabelStr = '';
|
||||
categories.forEach(item => {
|
||||
cateLabelStr += '\r\n - ' + item
|
||||
});
|
||||
|
||||
let cateStr = '';
|
||||
if (!(isCategory === false)) {
|
||||
cateStr = '\r\ncategories:' + cateLabelStr
|
||||
};
|
||||
|
||||
// 注意下面这些反引号字符串的格式会映射到文件
|
||||
// const cateStr = isCategory === false ? '' : `
|
||||
// categories:
|
||||
// - ${categories[0]}${categories[1] ? '\r\n - ' + categories[1] : ''}`;
|
||||
|
||||
const tagsStr = isTag === false ? '' : `
|
||||
tags:
|
||||
- `;
|
||||
|
||||
const fmData = `---
|
||||
title: ${file.name}
|
||||
date: ${dateStr}
|
||||
permalink: ${getPermalink()}${file.filePath.indexOf('_posts') > -1 ? '\r\nsidebar: auto' : ''}${cateStr}${tagsStr}
|
||||
---`;
|
||||
|
||||
fs.writeFileSync(file.filePath, `${fmData}\r\n${fileMatterObj.content}`); // 写入
|
||||
log(chalk.blue('tip ') + chalk.green(`write frontmatter(写入frontmatter):${file.filePath} `))
|
||||
|
||||
} else { // 已有FrontMatter
|
||||
const matterData = fileMatterObj.data;
|
||||
let mark = false;
|
||||
|
||||
// 已有FrontMatter,但是没有title、date、permalink、categories、tags数据的
|
||||
if (!matterData.hasOwnProperty('title')) { // 标题
|
||||
matterData.title = file.name;
|
||||
mark = true;
|
||||
}
|
||||
|
||||
if (!matterData.hasOwnProperty('date')) { // 日期
|
||||
const stat = fs.statSync(file.filePath);
|
||||
matterData.date = dateFormat(getBirthtime(stat));
|
||||
mark = true;
|
||||
}
|
||||
|
||||
if (!matterData.hasOwnProperty('permalink')) { // 永久链接
|
||||
matterData.permalink = getPermalink();
|
||||
mark = true;
|
||||
}
|
||||
|
||||
if (file.filePath.indexOf('_posts') > -1 && !matterData.hasOwnProperty('sidebar')) { // auto侧边栏,_posts文件夹特有
|
||||
matterData.sidebar = "auto";
|
||||
mark = true;
|
||||
}
|
||||
|
||||
if (!matterData.hasOwnProperty('pageComponent') && matterData.article !== false) { // 是文章页才添加分类和标签
|
||||
if (isCategory !== false && !matterData.hasOwnProperty('categories')) { // 分类
|
||||
matterData.categories = getCategories(file, categoryText)
|
||||
mark = true;
|
||||
}
|
||||
|
||||
if (isTag !== false && !matterData.hasOwnProperty('tags')) { // 标签
|
||||
matterData.tags = [''];
|
||||
mark = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (mark) {
|
||||
if (matterData.date && type(matterData.date) === 'date') {
|
||||
matterData.date = repairDate(matterData.date) // 修复时间格式
|
||||
}
|
||||
const newData = jsonToYaml.stringify(matterData).replace(/\n\s{2}/g, "\n").replace(/"/g, "") + '---\r\n' + fileMatterObj.content;
|
||||
fs.writeFileSync(file.filePath, newData); // 写入
|
||||
log(chalk.blue('tip ') + chalk.green(`write frontmatter(写入frontmatter):${file.filePath} `))
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取分类数据
|
||||
function getCategories (file, categoryText) {
|
||||
let categories = []
|
||||
|
||||
if (file.filePath.indexOf('_posts') === -1) {
|
||||
// 不在_posts文件夹
|
||||
let filePathArr = file.filePath.split(path.sep) // path.sep用于兼容不同系统下的路径斜杠
|
||||
filePathArr.pop()
|
||||
|
||||
let ind = filePathArr.indexOf('docs')
|
||||
if (ind !== -1) {
|
||||
while (filePathArr[++ind] !== undefined) {
|
||||
categories.push(filePathArr[ind].split('.').pop()) // 获取分类
|
||||
}
|
||||
}
|
||||
} else {
|
||||
categories.push(categoryText)
|
||||
}
|
||||
return categories
|
||||
}
|
||||
|
||||
// 获取文件创建时间
|
||||
function getBirthtime (stat) {
|
||||
// 在一些系统下无法获取birthtime属性的正确时间,使用atime代替
|
||||
return stat.birthtime.getFullYear() != 1970 ? stat.birthtime : stat.atime
|
||||
}
|
||||
|
||||
// 定义永久链接数据
|
||||
function getPermalink () {
|
||||
return `${PREFIX + (Math.random() + Math.random()).toString(16).slice(2, 8)}/`
|
||||
}
|
||||
|
||||
|
||||
module.exports = setFrontmatter;
|
||||
1
theme-vdoing/noopModule.js
Normal file
1
theme-vdoing/noopModule.js
Normal file
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
47
theme-vdoing/package.json
Normal file
47
theme-vdoing/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "vuepress-theme-vdoing",
|
||||
"version": "1.8.1",
|
||||
"description": "Vdoing theme for VuePress. 一个基于VuePress的知识管理兼博客主题。",
|
||||
"author": {
|
||||
"name": "gaoyi(Evan) Xu"
|
||||
},
|
||||
"homepage": "https://github.com/xugaoyi/vuepress-theme-vdoing#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/xugaoyi/vuepress-theme-vdoing/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/xugaoyi/vuepress-theme-vdoing.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@better-scroll/core": "^2.0.0-beta.6",
|
||||
"@better-scroll/slide": "^2.0.0-beta.6",
|
||||
"@vuepress/plugin-active-header-links": "^1.2.0",
|
||||
"@vuepress/plugin-nprogress": "^1.2.0",
|
||||
"@vuepress/plugin-search": "^1.2.0",
|
||||
"chalk": "^4.0.0",
|
||||
"json2yaml": "^1.1.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"docsearch.js": "^2.5.2",
|
||||
"good-storage": "^1.1.1",
|
||||
"lodash": "^4.17.15",
|
||||
"stylus": "^0.54.5",
|
||||
"stylus-loader": "^3.0.2",
|
||||
"vuepress-plugin-container": "^2.0.2",
|
||||
"vuepress-plugin-smooth-scroll": "^0.0.3"
|
||||
},
|
||||
"keywords": [
|
||||
"documentation",
|
||||
"vue",
|
||||
"vuepress",
|
||||
"generator",
|
||||
"theme",
|
||||
"blog"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"maintainers": [{
|
||||
"name": "Evan xu",
|
||||
"email": "894072666@qq.com"
|
||||
}]
|
||||
}
|
||||
22
theme-vdoing/styles/arrow.styl
Normal file
22
theme-vdoing/styles/arrow.styl
Normal file
@@ -0,0 +1,22 @@
|
||||
@require './config'
|
||||
|
||||
.arrow
|
||||
display inline-block
|
||||
width 0
|
||||
height 0
|
||||
&.up
|
||||
border-left 4px solid transparent
|
||||
border-right 4px solid transparent
|
||||
border-bottom 6px solid $arrowBgColor
|
||||
&.down
|
||||
border-left 4px solid transparent
|
||||
border-right 4px solid transparent
|
||||
border-top 6px solid $arrowBgColor
|
||||
&.right
|
||||
border-top 4px solid transparent
|
||||
border-bottom 4px solid transparent
|
||||
border-left 6px solid $arrowBgColor
|
||||
&.left
|
||||
border-top 4px solid transparent
|
||||
border-bottom 4px solid transparent
|
||||
border-right 6px solid $arrowBgColor
|
||||
274
theme-vdoing/styles/code-theme.styl
Normal file
274
theme-vdoing/styles/code-theme.styl
Normal file
@@ -0,0 +1,274 @@
|
||||
// 适合浅色背景 //@import '~prismjs/themes/prism.css'
|
||||
codeThemeLight()
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: black;
|
||||
background: none;
|
||||
text-shadow: 0 1px white;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
|
||||
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
|
||||
code[class*="language-"]::selection, code[class*="language-"] ::selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
@media print {
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #f5f2f0;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: slategray;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #905;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #690;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #9a6e3a;
|
||||
background: hsla(0, 0%, 100%, .5);
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #07a;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #DD4A68;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
// 行高亮颜色
|
||||
div[class*="language-"]
|
||||
.highlight-lines
|
||||
.highlighted
|
||||
background-color rgba(200, 200, 200, 40%)
|
||||
&.line-numbers-mode
|
||||
.highlight-lines .highlighted
|
||||
&:before
|
||||
background-color rgba(200, 200, 200, 40%)
|
||||
|
||||
|
||||
// 适合深色背景 // @import '~prismjs/themes/prism-tomorrow.css'
|
||||
codeThemeDark()
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #ccc;
|
||||
background: none;
|
||||
text-shadow: none;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.block-comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.token.tag,
|
||||
.token.attr-name,
|
||||
.token.namespace,
|
||||
.token.deleted {
|
||||
color: #e2777a;
|
||||
}
|
||||
|
||||
.token.function-name {
|
||||
color: #6196cc;
|
||||
}
|
||||
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.function {
|
||||
color: #f08d49;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.class-name,
|
||||
.token.constant,
|
||||
.token.symbol {
|
||||
color: #f8c555;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.important,
|
||||
.token.atrule,
|
||||
.token.keyword,
|
||||
.token.builtin {
|
||||
color: #cc99cd;
|
||||
}
|
||||
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.attr-value,
|
||||
.token.regex,
|
||||
.token.variable {
|
||||
color: #7ec699;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url {
|
||||
color: #67cdcc;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.token.inserted {
|
||||
color: green;
|
||||
}
|
||||
142
theme-vdoing/styles/code.styl
Normal file
142
theme-vdoing/styles/code.styl
Normal file
@@ -0,0 +1,142 @@
|
||||
body {$contentClass}
|
||||
code
|
||||
color var(--textLightenColor)
|
||||
padding 0.25rem 0.5rem
|
||||
margin 0
|
||||
font-size 0.9em
|
||||
// background-color rgba(27,31,35,0.05)
|
||||
background-color rgba(100,100,100,0.08)
|
||||
border-radius 3px
|
||||
.token
|
||||
&.deleted
|
||||
color #EC5975
|
||||
&.inserted
|
||||
color $accentColor
|
||||
|
||||
body {$contentClass}
|
||||
pre, pre[class*="language-"]
|
||||
line-height 1.4
|
||||
padding 1.25rem 1.5rem
|
||||
margin 0.85rem 0
|
||||
background-color $codeBgColor
|
||||
border-radius 6px
|
||||
overflow auto
|
||||
code
|
||||
// color #fff
|
||||
color var(--codeColor)
|
||||
padding 0
|
||||
background-color transparent
|
||||
border-radius 0
|
||||
|
||||
div[class*="language-"]
|
||||
position relative
|
||||
// background-color $codeBgColor
|
||||
background-color var(--codeBg)
|
||||
border-radius 6px
|
||||
.highlight-lines
|
||||
user-select none
|
||||
padding-top 1.3rem
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width 100%
|
||||
line-height 1.4
|
||||
.highlighted
|
||||
background-color rgba(0, 0, 0, 30%)
|
||||
pre, pre[class*="language-"]
|
||||
background transparent
|
||||
position relative !important
|
||||
z-index 1
|
||||
&::before
|
||||
position absolute
|
||||
z-index 3
|
||||
top 0.8em
|
||||
right 1em
|
||||
font-size 0.8rem
|
||||
color rgba(150,150,150,.7)
|
||||
// color rgba(255, 255, 255, 0.4)
|
||||
&:not(.line-numbers-mode)
|
||||
.line-numbers-wrapper
|
||||
display none
|
||||
&.line-numbers-mode
|
||||
.highlight-lines .highlighted
|
||||
position relative
|
||||
&:before
|
||||
content ' '
|
||||
position absolute
|
||||
z-index 3
|
||||
left 0
|
||||
top 0
|
||||
display block
|
||||
width $lineNumbersWrapperWidth
|
||||
height 100%
|
||||
background-color rgba(0, 0, 0, 30%)
|
||||
pre
|
||||
padding-left $lineNumbersWrapperWidth + 1 rem
|
||||
vertical-align middle
|
||||
.line-numbers-wrapper
|
||||
position absolute
|
||||
top 0
|
||||
width $lineNumbersWrapperWidth
|
||||
text-align center
|
||||
// color rgba(255, 255, 255, 0.3)
|
||||
color rgba(127, 127, 127, .5)
|
||||
padding 1.25rem 0
|
||||
line-height 1.4
|
||||
br
|
||||
user-select none
|
||||
.line-number
|
||||
position relative
|
||||
z-index 4
|
||||
user-select none
|
||||
font-size 0.85em
|
||||
&::after
|
||||
content ''
|
||||
position absolute
|
||||
z-index 2
|
||||
top 0
|
||||
left 0
|
||||
width $lineNumbersWrapperWidth
|
||||
height 100%
|
||||
border-radius 6px 0 0 6px
|
||||
// border-right 1px solid rgba(0, 0, 0, 66%)
|
||||
// background-color $codeBgColor
|
||||
border-right 1px solid var(--borderColor)
|
||||
background-color var(--codeBg)
|
||||
|
||||
|
||||
for lang in $codeLang
|
||||
div{'[class~="language-' + lang + '"]'}
|
||||
&:before
|
||||
content ('' + lang)
|
||||
|
||||
div[class~="language-javascript"]
|
||||
&:before
|
||||
content "js"
|
||||
|
||||
div[class~="language-typescript"]
|
||||
&:before
|
||||
content "ts"
|
||||
|
||||
div[class~="language-markup"]
|
||||
&:before
|
||||
content "html"
|
||||
|
||||
div[class~="language-markdown"]
|
||||
&:before
|
||||
content "md"
|
||||
|
||||
div[class~="language-json"]:before
|
||||
content "json"
|
||||
|
||||
div[class~="language-ruby"]:before
|
||||
content "rb"
|
||||
|
||||
div[class~="language-python"]:before
|
||||
content "py"
|
||||
|
||||
div[class~="language-bash"]:before
|
||||
content "sh"
|
||||
|
||||
div[class~="language-php"]:before
|
||||
content "php"
|
||||
1
theme-vdoing/styles/config.styl
Normal file
1
theme-vdoing/styles/config.styl
Normal file
@@ -0,0 +1 @@
|
||||
$contentClass = '.theme-vdoing-content'
|
||||
87
theme-vdoing/styles/custom-blocks.styl
Normal file
87
theme-vdoing/styles/custom-blocks.styl
Normal file
@@ -0,0 +1,87 @@
|
||||
.custom-block
|
||||
.custom-block-title
|
||||
font-weight 600
|
||||
margin-bottom .2rem
|
||||
p
|
||||
margin 0
|
||||
&.tip, &.warning, &.danger, &.note
|
||||
padding .5rem 1.5rem
|
||||
border-left-width .5rem
|
||||
border-left-style solid
|
||||
margin 1rem 0
|
||||
&.tip
|
||||
background-color #f3f5f7
|
||||
border-color #42b983
|
||||
color darken(#42b983, 50%)
|
||||
&.warning
|
||||
background-color #FFF7D0
|
||||
border-color darken(#ffe564, 35%)
|
||||
color darken(#ffe564, 70%)
|
||||
.custom-block-title
|
||||
color darken(#ffe564, 50%)
|
||||
a
|
||||
color var(--textColor)
|
||||
&.danger
|
||||
background-color #ffe6e6
|
||||
border-color darken(red, 20%)
|
||||
color darken(red, 70%)
|
||||
.custom-block-title
|
||||
color darken(red, 40%)
|
||||
a
|
||||
color var(--textColor)
|
||||
&.note
|
||||
background-color #E8F5FA
|
||||
border-color #157BAE
|
||||
color darken(#157BAE, 40%)
|
||||
&.right
|
||||
color var(--textColor)
|
||||
font-size 0.9rem
|
||||
text-align right
|
||||
&.theorem
|
||||
margin 1rem 0
|
||||
padding .8rem 1.5rem
|
||||
border-radius 2px
|
||||
background-color var(--customBlockBg)
|
||||
.title
|
||||
font-weight bold
|
||||
margin .5rem 0
|
||||
&.details
|
||||
display block
|
||||
position relative
|
||||
border-radius 2px
|
||||
margin 1em 0
|
||||
padding 1.6em
|
||||
background-color var(--customBlockBg)
|
||||
p
|
||||
margin .8rem 0
|
||||
h4
|
||||
margin-top 0
|
||||
figure, p
|
||||
&:last-child
|
||||
margin-bottom 0
|
||||
padding-bottom 0
|
||||
summary
|
||||
outline none
|
||||
cursor pointer
|
||||
&:hover
|
||||
color $accentColor
|
||||
|
||||
// 深色模式适配
|
||||
.theme-mode-dark
|
||||
.custom-block
|
||||
&.warning
|
||||
background-color rgba(255, 247, 208, .2)
|
||||
color darken(#ffe564, 35%)
|
||||
.custom-block-title
|
||||
color darken(#ffe564, 15%)
|
||||
&.tip
|
||||
background-color rgba(243, 245, 247, .2)
|
||||
color darken(#42b983, 0%)
|
||||
&.danger
|
||||
background-color rgba(255, 230, 230, .4)
|
||||
color darken(red, 50%)
|
||||
a
|
||||
color $accentColor
|
||||
&.note
|
||||
background-color rgba(243, 245, 247, .2)
|
||||
color darken(#157BAE, 0%)
|
||||
286
theme-vdoing/styles/index.styl
Normal file
286
theme-vdoing/styles/index.styl
Normal file
@@ -0,0 +1,286 @@
|
||||
// 引入字体图标
|
||||
@import '//at.alicdn.com/t/font_1678482_kdcbbwxa6v.css'
|
||||
|
||||
@require './config'
|
||||
@require './code'
|
||||
@require './custom-blocks'
|
||||
@require './arrow'
|
||||
@require './wrapper'
|
||||
@require './toc'
|
||||
@require './markdown-container'
|
||||
|
||||
|
||||
html, body
|
||||
padding 0
|
||||
margin 0
|
||||
|
||||
body
|
||||
font-family -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif
|
||||
-webkit-font-smoothing antialiased
|
||||
-moz-osx-font-smoothing grayscale
|
||||
-webkit-tap-highlight-color transparent
|
||||
font-size 16px
|
||||
color $textColor
|
||||
background var(--bodyBg)
|
||||
|
||||
// 去掉黄色边框
|
||||
a,input,button
|
||||
outline: none; -webkit-tap-highlight-color: rgba(255, 255, 255, 0); -webkit-focus-ring-color: rgba(0, 0, 0, 0);
|
||||
// 滚动条样式
|
||||
@media (min-width: $MQMobile)
|
||||
::-webkit-scrollbar
|
||||
width:6px;
|
||||
height:5px;
|
||||
::-webkit-scrollbar-track-piece
|
||||
background-color:rgba(0,0,0,.15)
|
||||
-webkit-border-radius:3px
|
||||
::-webkit-scrollbar-thumb:vertical
|
||||
height:5px;
|
||||
background-color:rgba(0,0,0,.28)
|
||||
-webkit-border-radius:3px
|
||||
::-webkit-scrollbar-thumb:horizontal
|
||||
width:5px;
|
||||
background-color:rgba(0,0,0,.28)
|
||||
-webkit-border-radius:3px
|
||||
|
||||
|
||||
.card-box // 卡片
|
||||
border-radius 5px
|
||||
background var(--mainBg)
|
||||
box-shadow 0 1px 2px 0 rgba(0,0,0,.1)
|
||||
transition box-shadow .5s
|
||||
&:hover
|
||||
box-shadow 0 1px 15px 0 rgba(0,0,0,.1)
|
||||
|
||||
.blur // 模糊滤镜
|
||||
backdrop-filter saturate(200%) blur(20px)
|
||||
|
||||
.custom-page // 自定义页面
|
||||
min-height calc(100vh - 3.6rem)
|
||||
padding-top $navbarHeight
|
||||
padding-bottom .9rem
|
||||
.theme-vdoing-wrapper
|
||||
margin 0 auto
|
||||
|
||||
// 默认的搜索框样式重置
|
||||
body .search-box
|
||||
input
|
||||
background-color transparent
|
||||
color var(--textColor)
|
||||
border 1px solid var(--borderColor, #ccc)
|
||||
@media (max-width: $MQNarrow)
|
||||
border-color transparent
|
||||
|
||||
|
||||
|
||||
.page
|
||||
transition padding .2s ease
|
||||
padding-left .8rem
|
||||
|
||||
.navbar
|
||||
position fixed
|
||||
z-index 20
|
||||
top 0
|
||||
left 0
|
||||
right 0
|
||||
height $navbarHeight
|
||||
background-color var(--blurBg)
|
||||
box-sizing border-box
|
||||
box-shadow 0 2px 5px rgba(0,0,0,.06)
|
||||
|
||||
.sidebar-mask
|
||||
position fixed
|
||||
z-index 12
|
||||
top 0
|
||||
left 0
|
||||
width 100vw
|
||||
height 100vh
|
||||
display none
|
||||
|
||||
.sidebar
|
||||
font-size 16px
|
||||
background-color var(--sidebarBg)
|
||||
width $sidebarWidth
|
||||
position fixed
|
||||
z-index 13
|
||||
margin 0
|
||||
top $navbarHeight
|
||||
left 0
|
||||
bottom 0
|
||||
box-sizing border-box
|
||||
border-right 1px solid var(--borderColor)
|
||||
overflow-y auto
|
||||
transform translateX(-100%)
|
||||
transition transform .2s
|
||||
@media (max-width: $MQMobile)
|
||||
background-color var(--mainBg)
|
||||
|
||||
|
||||
{$contentClass}:not(.custom)
|
||||
word-wrap break-word
|
||||
@extend $wrapper
|
||||
> *:first-child
|
||||
// margin-top $navbarHeight // 内容第一个元素的top距离
|
||||
|
||||
a:hover
|
||||
text-decoration underline
|
||||
|
||||
p.demo
|
||||
padding 1rem 1.5rem
|
||||
border 1px solid #ddd
|
||||
border-radius 4px
|
||||
|
||||
img
|
||||
max-width 100%
|
||||
|
||||
{$contentClass}.custom
|
||||
padding 0
|
||||
margin 0
|
||||
|
||||
img
|
||||
max-width 100%
|
||||
|
||||
a
|
||||
font-weight 500
|
||||
color $accentColor
|
||||
text-decoration none
|
||||
|
||||
p a code
|
||||
font-weight 400
|
||||
color $accentColor
|
||||
|
||||
kbd
|
||||
background #eee
|
||||
border solid 0.15rem #ddd
|
||||
border-bottom solid 0.25rem #ddd
|
||||
border-radius 0.15rem
|
||||
padding 0 0.15em
|
||||
|
||||
blockquote
|
||||
font-size 1rem
|
||||
opacity .75
|
||||
border-left .2rem solid rgba(100,100,100,.3)
|
||||
margin 1rem 0
|
||||
padding .25rem 0 .25rem 1rem
|
||||
|
||||
& > p
|
||||
margin 0
|
||||
|
||||
ul, ol
|
||||
padding-left 1.2em
|
||||
|
||||
strong
|
||||
font-weight 600
|
||||
|
||||
h1, h2, h3, h4, h5, h6
|
||||
font-weight 600
|
||||
line-height 1.25
|
||||
|
||||
{$contentClass}:not(.custom) > &
|
||||
margin-top (0.5rem - $navbarHeight)
|
||||
padding-top ($navbarHeight + 1rem)
|
||||
margin-bottom 0
|
||||
|
||||
&:first-child
|
||||
// margin-top -1.5rem
|
||||
margin-bottom 1rem
|
||||
|
||||
+ p, + pre, + .custom-block
|
||||
margin-top 2rem
|
||||
|
||||
&:focus .header-anchor,
|
||||
&:hover .header-anchor
|
||||
opacity: 1
|
||||
|
||||
|
||||
// 没有h1标签时
|
||||
p,pre,.custom-block
|
||||
{$contentClass}:not(.custom) > &
|
||||
&:first-child
|
||||
margin-top 2rem
|
||||
|
||||
|
||||
h1
|
||||
font-size 1.8rem
|
||||
{$contentClass}:not(.custom) > & // 页面内容的首个h1标签隐藏
|
||||
&:first-child
|
||||
display none
|
||||
|
||||
h2
|
||||
font-size 1.5rem
|
||||
padding-bottom .3rem
|
||||
border-bottom 1px solid var(--borderColor)
|
||||
|
||||
h3
|
||||
font-size 1.35rem
|
||||
|
||||
a.header-anchor
|
||||
font-size 0.85em
|
||||
float left
|
||||
margin-left -0.87em
|
||||
padding-right 0.23em
|
||||
margin-top 0.125em
|
||||
opacity 0
|
||||
|
||||
&:focus,
|
||||
&:hover
|
||||
text-decoration none
|
||||
|
||||
code, kbd, .line-number
|
||||
font-family source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace
|
||||
|
||||
p, ul, ol
|
||||
line-height 1.7
|
||||
|
||||
hr
|
||||
border 0
|
||||
border-top 1px solid var(--borderColor)
|
||||
|
||||
table
|
||||
border-collapse collapse
|
||||
margin 1rem 0
|
||||
overflow-x: auto
|
||||
width 100%
|
||||
display inline-table
|
||||
@media (max-width: $MQMobile)
|
||||
display block
|
||||
tr
|
||||
border-top 1px solid var(--borderColor)
|
||||
|
||||
&:nth-child(2n)
|
||||
background-color rgba(150,150,150,0.1)
|
||||
|
||||
th, td
|
||||
border 1px solid var(--borderColor)
|
||||
padding .6em 1em
|
||||
@media (max-width: $MQMobile)
|
||||
padding .3em .5em
|
||||
a
|
||||
word-break break-all
|
||||
|
||||
.theme-container
|
||||
// background var(--mainBg)
|
||||
color var(--textColor)
|
||||
min-height 100vh
|
||||
&.sidebar-open
|
||||
.sidebar-mask
|
||||
display: block
|
||||
|
||||
&.no-navbar
|
||||
{$contentClass}:not(.custom) > h1, h2, h3, h4, h5, h6
|
||||
margin-top 1.5rem
|
||||
padding-top 0
|
||||
|
||||
.sidebar
|
||||
top 0
|
||||
|
||||
|
||||
@media (min-width: ($MQMobile + 1px))
|
||||
.theme-container.no-sidebar
|
||||
.sidebar
|
||||
display none
|
||||
|
||||
.page
|
||||
padding-left 0
|
||||
|
||||
@require 'mobile.styl'
|
||||
206
theme-vdoing/styles/markdown-container.styl
Normal file
206
theme-vdoing/styles/markdown-container.styl
Normal file
@@ -0,0 +1,206 @@
|
||||
// markdown容器样式
|
||||
|
||||
// 居中容器
|
||||
.center-container
|
||||
text-align: center
|
||||
|
||||
h1, h2, h3, h4, h5, h6
|
||||
.center-container > &
|
||||
margin-top (0.5rem - $navbarHeight)
|
||||
padding-top ($navbarHeight + 1rem)
|
||||
margin-bottom 0
|
||||
a.header-anchor
|
||||
float none
|
||||
padding-right: 0
|
||||
margin-left: -.9rem
|
||||
|
||||
|
||||
// 普通卡片列表
|
||||
.cardListContainer
|
||||
margin .7rem 0
|
||||
&>:not(.card-list)
|
||||
display none
|
||||
.card-list
|
||||
margin -0.35rem
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
.card-item
|
||||
width calc(100%/3 - .7rem)
|
||||
margin .35rem
|
||||
background var(--bodyBg)
|
||||
border-radius 3px
|
||||
color var(--textColor)
|
||||
display flex
|
||||
box-shadow 1px 1px 2px 0 rgba(0,0,0,.06)
|
||||
transition all .4s
|
||||
&:hover
|
||||
text-decoration none
|
||||
box-shadow: 0 10px 20px -10px var(--randomColor, rgba(0,0,0,0.15));
|
||||
transform: translateY(-3px) scale(1.01, 1.01)
|
||||
img
|
||||
// transform rotate(8deg) scale(1.1, 1.1)
|
||||
box-shadow 3px 2px 7px rgba(0, 0, 0, 0.15)
|
||||
div p
|
||||
text-shadow 3px 2px 5px rgba(0, 0, 0, 0.15)
|
||||
img
|
||||
width 60px
|
||||
height 60px
|
||||
border-radius 50%
|
||||
border 2px solid #fff
|
||||
margin 1rem
|
||||
margin-right 0
|
||||
box-shadow 3px 2px 5px rgba(0, 0, 0, 0.08)
|
||||
transition all .4s
|
||||
div
|
||||
flex 1
|
||||
display inline-block
|
||||
float right
|
||||
padding 1rem 0
|
||||
p
|
||||
margin 0
|
||||
padding 0 1rem
|
||||
transition text-shadow .4s
|
||||
text-align center
|
||||
.name
|
||||
margin .2rem 0 .3rem 0
|
||||
.desc
|
||||
font-size .8rem
|
||||
line-height 1.1rem
|
||||
opacity .8
|
||||
margin-bottom .2rem
|
||||
.card-item.row-1
|
||||
width calc(100% - .7rem)
|
||||
img
|
||||
margin-left 2rem
|
||||
.card-item.row-2
|
||||
width calc(100%/2 - .7rem)
|
||||
img
|
||||
margin-left 1.5rem
|
||||
.card-item.row-3
|
||||
width calc(100%/3 - .7rem)
|
||||
.card-item.row-4
|
||||
width calc(100%/4 - .7rem)
|
||||
|
||||
// 图文卡片列表
|
||||
.cardImgListContainer
|
||||
margin 1rem 0
|
||||
&>:not(.card-list)
|
||||
display none
|
||||
.card-list
|
||||
margin -0.5rem
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
.card-item
|
||||
width calc(100%/3 - 1rem)
|
||||
margin .5rem
|
||||
background var(--mainBg)
|
||||
border 1px solid rgba(0,0,0,0.1)
|
||||
box-sizing: border-box
|
||||
border-radius 3px
|
||||
overflow hidden
|
||||
color var(--textColor)
|
||||
box-shadow 2px 2px 10px rgba(0,0,0,.04)
|
||||
display flex
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
align-content: stretch;
|
||||
transition: all .4s
|
||||
&:hover
|
||||
box-shadow 1px 1px 20px rgba(0,0,0,.1)
|
||||
transform: translateY(-3px)
|
||||
.box-img
|
||||
overflow hidden
|
||||
position relative
|
||||
background #000
|
||||
img
|
||||
display block
|
||||
width 100%
|
||||
height auto
|
||||
transition: all .3s
|
||||
// &:hover
|
||||
// img
|
||||
// transform: scale(1.1, 1.1)
|
||||
// opacity .75
|
||||
a
|
||||
color var(--textColor)
|
||||
transition: color .3s
|
||||
&:hover
|
||||
// color $accentColor
|
||||
text-decoration none
|
||||
.box-info
|
||||
padding: .8rem 1rem
|
||||
p
|
||||
margin 0
|
||||
.desc
|
||||
margin-top: .3rem
|
||||
opacity .8
|
||||
font-size: .9rem
|
||||
line-height: 1.1rem
|
||||
.box-footer
|
||||
overflow hidden
|
||||
padding: .8rem 1rem
|
||||
border-top: 1px solid rgba(0,0,0,0.1)
|
||||
img
|
||||
width 1.8rem
|
||||
height 1.8rem
|
||||
border-radius 50%
|
||||
float left
|
||||
span
|
||||
line-height 1.8rem
|
||||
float left
|
||||
margin-left: .6rem
|
||||
font-size: .8rem
|
||||
.card-item.row-1
|
||||
width calc(100% - 1rem)
|
||||
.card-item.row-2
|
||||
width calc(100%/2 - 1rem)
|
||||
.card-item.row-3
|
||||
width calc(100%/3 - 1rem)
|
||||
.card-item.row-4
|
||||
width calc(100%/4 - 1rem)
|
||||
|
||||
.theme-mode-dark
|
||||
.cardImgListContainer
|
||||
.card-list
|
||||
.card-item
|
||||
border-color: var(--borderColor)
|
||||
.box-footer
|
||||
border-color: var(--borderColor)
|
||||
|
||||
// 卡片列表的响应
|
||||
@media (max-width: 900px)
|
||||
.cardListContainer
|
||||
.card-list
|
||||
.card-item.row-4
|
||||
width calc(100%/3 - .7rem)
|
||||
.cardImgListContainer
|
||||
.card-list
|
||||
.card-item.row-4
|
||||
width calc(100%/3 - 1rem)
|
||||
|
||||
@media (max-width: 720px)
|
||||
.cardListContainer
|
||||
.card-list
|
||||
.card-item.row-3, .card-item.row-4
|
||||
width calc(100%/2 - .7rem)
|
||||
img
|
||||
margin-left 1.5rem
|
||||
.cardImgListContainer
|
||||
.card-list
|
||||
.card-item.row-3, .card-item.row-4
|
||||
width calc(100%/2 - 1rem)
|
||||
|
||||
@media (max-width: 500px)
|
||||
.cardListContainer
|
||||
.card-list
|
||||
.card-item.row-1, .card-item.row-2, .card-item.row-3, .card-item.row-4
|
||||
width calc(100% - .7rem)
|
||||
img
|
||||
margin-left 1.5rem
|
||||
.cardImgListContainer
|
||||
.card-list
|
||||
.card-item.row-1, .card-item.row-2, .card-item.row-3, .card-item.row-4
|
||||
width calc(100% - 1rem)
|
||||
78
theme-vdoing/styles/mobile.styl
Normal file
78
theme-vdoing/styles/mobile.styl
Normal file
@@ -0,0 +1,78 @@
|
||||
@require './config'
|
||||
|
||||
$mobileSidebarWidth = $sidebarWidth * 0.9
|
||||
|
||||
// narrow desktop / iPad
|
||||
@media (max-width: $MQNarrow)
|
||||
.sidebar
|
||||
font-size 15px
|
||||
|
||||
@media (max-width: $MQMobile)
|
||||
.sidebar
|
||||
width $sidebarWidth * 0.95
|
||||
|
||||
@media (min-width: ($MQMobile + 1px)) and (max-width: $MQNarrow)
|
||||
.sidebar
|
||||
width $mobileSidebarWidth
|
||||
.theme-container
|
||||
&.sidebar-open
|
||||
.page
|
||||
padding-left ($mobileSidebarWidth + .8rem)!important
|
||||
|
||||
// wide mobile
|
||||
@media (max-width: $MQMobile)
|
||||
.sidebar
|
||||
top 0
|
||||
height 100vh
|
||||
padding-top $navbarHeight
|
||||
transform translateX(-100%)
|
||||
transition transform .2s ease
|
||||
.page
|
||||
padding-left 0
|
||||
.theme-container
|
||||
&.sidebar-open
|
||||
.sidebar
|
||||
transform translateX(0)
|
||||
.sidebar-mask // 蒙版在小屏中才能显示
|
||||
display block
|
||||
&.no-navbar
|
||||
.sidebar
|
||||
padding-top: 0
|
||||
|
||||
// narrow mobile
|
||||
@media (max-width: $MQMobileNarrow)
|
||||
h1
|
||||
font-size 1.9rem
|
||||
{$contentClass}
|
||||
div[class*="language-"]
|
||||
margin 0.85rem -1.5rem
|
||||
border-radius 0
|
||||
|
||||
// 侧边栏显示隐藏的适配
|
||||
@media (min-width: ($MQMobile + 1px)) // 720px
|
||||
.theme-container
|
||||
&.sidebar-open
|
||||
.sidebar-mask
|
||||
display: none
|
||||
.sidebar
|
||||
transform translateX(0)
|
||||
.sidebar-button
|
||||
left $sidebarWidth
|
||||
.page
|
||||
padding-left ($sidebarWidth + .8rem)
|
||||
padding-right .8rem
|
||||
&.have-rightmenu
|
||||
.page
|
||||
padding-right ($rightMenuWidth + .8rem)
|
||||
&.no-sidebar
|
||||
.page
|
||||
padding-left 0!important
|
||||
@media (max-width: $MQNarrow)
|
||||
.theme-container
|
||||
&.sidebar-open:not(.on-sidebar)
|
||||
.sidebar-button
|
||||
$mobileSidebarWidth = $sidebarWidth * 0.7
|
||||
left $mobileSidebarWidth
|
||||
.theme-container.no-sidebar
|
||||
.sidebar-button
|
||||
display none
|
||||
86
theme-vdoing/styles/palette.styl
Normal file
86
theme-vdoing/styles/palette.styl
Normal file
@@ -0,0 +1,86 @@
|
||||
// 主题调色板
|
||||
|
||||
// 原主题预设变量
|
||||
// 颜色
|
||||
// $accentColor = #3eaf7c
|
||||
// $textColor = #2c3e50
|
||||
// $borderColor = #eaecef
|
||||
// $codeBgColor = #282c34
|
||||
// $arrowBgColor = #ccc
|
||||
// $badgeTipColor = #42b983
|
||||
// $badgeWarningColor = darken(#ffe564, 35%)
|
||||
// $badgeErrorColor = #DA5961
|
||||
// 布局
|
||||
// $navbarHeight = 3.6rem
|
||||
// $sidebarWidth = 20rem
|
||||
// $contentWidth = 740px
|
||||
// $homePageWidth = 960px
|
||||
// 代码块
|
||||
// $lineNumbersWrapperWidth = 3.5rem
|
||||
|
||||
|
||||
@require './code-theme'
|
||||
|
||||
//***vdoing主题-变量***//
|
||||
|
||||
// 颜色
|
||||
|
||||
$bannerTextColor = #fff // 首页banner区(博客标题)文本颜色
|
||||
$accentColor = #11A8CD
|
||||
$activeColor = #ff5722
|
||||
$arrowBgColor = #ccc
|
||||
$badgeTipColor = #42b983
|
||||
$badgeWarningColor = darken(#ffe564, 35%)
|
||||
$badgeErrorColor = #DA5961
|
||||
|
||||
// 布局
|
||||
$navbarHeight = 3.6rem
|
||||
$sidebarWidth = 18rem
|
||||
$contentWidth = 860px
|
||||
$homePageWidth = 1100px
|
||||
$rightMenuWidth = 230px // 右侧菜单
|
||||
|
||||
// 代码块
|
||||
$lineNumbersWrapperWidth = 2.5rem
|
||||
|
||||
// 浅色模式
|
||||
.theme-mode-light
|
||||
--bodyBg: #f4f4f4
|
||||
--mainBg: rgba(255,255,255,1)
|
||||
--sidebarBg: rgba(255,255,255,.8)
|
||||
--blurBg: rgba(255,255,255,.9)
|
||||
--customBlockBg: #f1f1f1
|
||||
--textColor: #00323c
|
||||
--textLightenColor: #0085AD
|
||||
--borderColor: rgba(0,0,0,.15)
|
||||
--codeBg: #f6f6f6
|
||||
--codeColor: #525252
|
||||
codeThemeLight()
|
||||
|
||||
// 深色模式
|
||||
.theme-mode-dark
|
||||
--bodyBg: rgb(39,39,43)
|
||||
--mainBg: rgba(30,30,34,1)
|
||||
--sidebarBg: rgba(30,30,34,.8)
|
||||
--blurBg: rgba(30,30,34,.8)
|
||||
--customBlockBg: rgb(39,39,43)
|
||||
--textColor: rgb(155,155,170)
|
||||
--textLightenColor: #0085AD
|
||||
--borderColor: #2C2C3A
|
||||
--codeBg: #252526
|
||||
--codeColor: #fff
|
||||
codeThemeDark()
|
||||
|
||||
// 阅读模式
|
||||
.theme-mode-read
|
||||
--bodyBg: rgb(236,236,204)
|
||||
--mainBg: rgba(245,245,213,1)
|
||||
--sidebarBg: rgba(245,245,213,.8)
|
||||
--blurBg: rgba(245,245,213,.9)
|
||||
--customBlockBg: rgb(236,236,204)
|
||||
--textColor: #704214
|
||||
--textLightenColor: #996633
|
||||
--borderColor: rgba(0,0,0,.15)
|
||||
--codeBg: #282c34
|
||||
--codeColor: #fff
|
||||
codeThemeDark()
|
||||
3
theme-vdoing/styles/toc.styl
Normal file
3
theme-vdoing/styles/toc.styl
Normal file
@@ -0,0 +1,3 @@
|
||||
.table-of-contents
|
||||
.badge
|
||||
vertical-align middle
|
||||
16
theme-vdoing/styles/wrapper.styl
Normal file
16
theme-vdoing/styles/wrapper.styl
Normal file
@@ -0,0 +1,16 @@
|
||||
$vdoing-wrapper
|
||||
max-width $contentWidth
|
||||
margin 0 auto
|
||||
padding 1rem 2.5rem 2rem 2.5rem
|
||||
&:not(.footer)
|
||||
background var(--mainBg)
|
||||
box-shadow 0 1px 2px 0 rgba(0,0,0,.1)
|
||||
margin-bottom 1rem
|
||||
@media (min-width $contentWidth + 80)
|
||||
border-radius 2px
|
||||
@media (max-width: $MQNarrow)
|
||||
padding 1rem 2rem
|
||||
@media (max-width: $MQMobileNarrow)
|
||||
padding 1rem 1.5rem
|
||||
$wrapper
|
||||
max-width $contentWidth
|
||||
11
theme-vdoing/templates/dev.html
Normal file
11
theme-vdoing/templates/dev.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title></title>
|
||||
</head>
|
||||
<body class="theme-mode-light">
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
17
theme-vdoing/templates/ssr.html
Normal file
17
theme-vdoing/templates/ssr.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ lang }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>{{ title }}</title>
|
||||
<meta name="generator" content="VuePress {{ version }}">
|
||||
{{{ userHeadTags }}}
|
||||
{{{ pageMeta }}}
|
||||
{{{ renderResourceHints() }}}
|
||||
{{{ renderStyles() }}}
|
||||
</head>
|
||||
<body class="theme-mode-light">
|
||||
<!--vue-ssr-outlet-->
|
||||
{{{ renderScripts() }}}
|
||||
</body>
|
||||
</html>
|
||||
291
theme-vdoing/util/index.js
Normal file
291
theme-vdoing/util/index.js
Normal file
@@ -0,0 +1,291 @@
|
||||
export const hashRE = /#.*$/
|
||||
export const extRE = /\.(md|html)$/
|
||||
export const endingSlashRE = /\/$/
|
||||
export const outboundRE = /^[a-z]+:/i
|
||||
|
||||
export function normalize (path) {
|
||||
return decodeURI(path)
|
||||
.replace(hashRE, '')
|
||||
.replace(extRE, '')
|
||||
}
|
||||
|
||||
export function getHash (path) {
|
||||
const match = path.match(hashRE)
|
||||
if (match) {
|
||||
return match[0]
|
||||
}
|
||||
}
|
||||
|
||||
export function isExternal (path) {
|
||||
return outboundRE.test(path)
|
||||
}
|
||||
|
||||
export function isMailto (path) {
|
||||
return /^mailto:/.test(path)
|
||||
}
|
||||
|
||||
export function isTel (path) {
|
||||
return /^tel:/.test(path)
|
||||
}
|
||||
|
||||
export function ensureExt (path) {
|
||||
if (isExternal(path)) {
|
||||
return path
|
||||
}
|
||||
const hashMatch = path.match(hashRE)
|
||||
const hash = hashMatch ? hashMatch[0] : ''
|
||||
const normalized = normalize(path)
|
||||
|
||||
if (endingSlashRE.test(normalized)) {
|
||||
return path
|
||||
}
|
||||
return normalized + '.html' + hash
|
||||
}
|
||||
|
||||
export function isActive (route, path) {
|
||||
const routeHash = route.hash
|
||||
const linkHash = getHash(path)
|
||||
if (linkHash && routeHash !== linkHash) {
|
||||
return false
|
||||
}
|
||||
const routePath = normalize(route.path)
|
||||
const pagePath = normalize(path)
|
||||
return routePath === pagePath
|
||||
}
|
||||
|
||||
export function resolvePage (pages, rawPath, base) {
|
||||
if (isExternal(rawPath)) {
|
||||
return {
|
||||
type: 'external',
|
||||
path: rawPath
|
||||
}
|
||||
}
|
||||
if (base) {
|
||||
rawPath = resolvePath(rawPath, base)
|
||||
}
|
||||
const path = normalize(rawPath)
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
if (normalize(pages[i].regularPath) === path) {
|
||||
return Object.assign({}, pages[i], {
|
||||
type: 'page',
|
||||
path: ensureExt(pages[i].path)
|
||||
})
|
||||
}
|
||||
}
|
||||
console.error(`[vuepress] No matching page found for sidebar item "${rawPath}"`)
|
||||
return {}
|
||||
}
|
||||
|
||||
function resolvePath (relative, base, append) {
|
||||
const firstChar = relative.charAt(0)
|
||||
if (firstChar === '/') {
|
||||
return relative
|
||||
}
|
||||
|
||||
if (firstChar === '?' || firstChar === '#') {
|
||||
return base + relative
|
||||
}
|
||||
|
||||
const stack = base.split('/')
|
||||
|
||||
// remove trailing segment if:
|
||||
// - not appending
|
||||
// - appending to trailing slash (last segment is empty)
|
||||
if (!append || !stack[stack.length - 1]) {
|
||||
stack.pop()
|
||||
}
|
||||
|
||||
// resolve relative path
|
||||
const segments = relative.replace(/^\//, '').split('/')
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const segment = segments[i]
|
||||
if (segment === '..') {
|
||||
stack.pop()
|
||||
} else if (segment !== '.') {
|
||||
stack.push(segment)
|
||||
}
|
||||
}
|
||||
|
||||
// ensure leading slash
|
||||
if (stack[0] !== '') {
|
||||
stack.unshift('')
|
||||
}
|
||||
|
||||
return stack.join('/')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { Page } page
|
||||
* @param { string } regularPath
|
||||
* @param { SiteData } site
|
||||
* @param { string } localePath
|
||||
* @returns { SidebarGroup }
|
||||
*/
|
||||
export function resolveSidebarItems (page, regularPath, site, localePath) {
|
||||
const { pages, themeConfig } = site
|
||||
|
||||
const localeConfig = localePath && themeConfig.locales
|
||||
? themeConfig.locales[localePath] || themeConfig
|
||||
: themeConfig
|
||||
|
||||
const pageSidebarConfig = page.frontmatter.sidebar || localeConfig.sidebar || themeConfig.sidebar
|
||||
if (pageSidebarConfig === 'auto') {
|
||||
return resolveHeaders(page)
|
||||
}
|
||||
|
||||
const sidebarConfig = localeConfig.sidebar || themeConfig.sidebar
|
||||
if (!sidebarConfig) {
|
||||
return []
|
||||
} else {
|
||||
const { base, config } = resolveMatchingConfig(regularPath, sidebarConfig)
|
||||
if (config === 'auto') {
|
||||
return resolveHeaders(page)
|
||||
}
|
||||
return config
|
||||
? config.map(item => resolveItem(item, pages, base))
|
||||
: []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { Page } page
|
||||
* @returns { SidebarGroup }
|
||||
*/
|
||||
function resolveHeaders (page) {
|
||||
const headers = groupHeaders(page.headers || [])
|
||||
return [{
|
||||
type: 'group',
|
||||
collapsable: false,
|
||||
title: page.title,
|
||||
path: null,
|
||||
children: headers.map(h => ({
|
||||
type: 'auto',
|
||||
title: h.title,
|
||||
basePath: page.path,
|
||||
path: page.path + '#' + h.slug,
|
||||
children: h.children || []
|
||||
}))
|
||||
}]
|
||||
}
|
||||
|
||||
export function groupHeaders (headers) {
|
||||
// group h3s under h2
|
||||
headers = headers.map(h => Object.assign({}, h))
|
||||
let lastH2
|
||||
headers.forEach(h => {
|
||||
if (h.level === 2) {
|
||||
lastH2 = h
|
||||
} else if (lastH2) {
|
||||
(lastH2.children || (lastH2.children = [])).push(h)
|
||||
}
|
||||
})
|
||||
return headers.filter(h => h.level === 2)
|
||||
}
|
||||
|
||||
export function resolveNavLinkItem (linkItem) {
|
||||
return Object.assign(linkItem, {
|
||||
type: linkItem.items && linkItem.items.length ? 'links' : 'link'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { Route } route
|
||||
* @param { Array<string|string[]> | Array<SidebarGroup> | [link: string]: SidebarConfig } config
|
||||
* @returns { base: string, config: SidebarConfig }
|
||||
*/
|
||||
export function resolveMatchingConfig (regularPath, config) {
|
||||
if (Array.isArray(config)) {
|
||||
return {
|
||||
base: '/',
|
||||
config: config
|
||||
}
|
||||
}
|
||||
for (const base in config) {
|
||||
if (ensureEndingSlash(regularPath).indexOf(encodeURI(base)) === 0) {
|
||||
return {
|
||||
base,
|
||||
config: config[base]
|
||||
}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
function ensureEndingSlash (path) {
|
||||
return /(\.html|\/)$/.test(path)
|
||||
? path
|
||||
: path + '/'
|
||||
}
|
||||
|
||||
function resolveItem (item, pages, base, groupDepth = 1) {
|
||||
if (typeof item === 'string') {
|
||||
return resolvePage(pages, item, base)
|
||||
} else if (Array.isArray(item)) {
|
||||
return Object.assign(resolvePage(pages, item[0], base), {
|
||||
title: item[1]
|
||||
})
|
||||
} else {
|
||||
if (groupDepth > 3) {
|
||||
console.error(
|
||||
'[vuepress] detected a too deep nested sidebar group.'
|
||||
)
|
||||
}
|
||||
const children = item.children || []
|
||||
if (children.length === 0 && item.path) {
|
||||
return Object.assign(resolvePage(pages, item.path, base), {
|
||||
title: item.title
|
||||
})
|
||||
}
|
||||
return {
|
||||
type: 'group',
|
||||
path: item.path,
|
||||
title: item.title,
|
||||
sidebarDepth: item.sidebarDepth,
|
||||
initialOpenGroupIndex: item.initialOpenGroupIndex,
|
||||
children: children.map(child => resolveItem(child, pages, base, groupDepth + 1)),
|
||||
collapsable: item.collapsable !== false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 类型判断
|
||||
export function type (o) {
|
||||
const s = Object.prototype.toString.call(o)
|
||||
return s.match(/\[object (.*?)\]/)[1].toLowerCase()
|
||||
}
|
||||
|
||||
// 日期格式化(只获取年月日)
|
||||
export function dateFormat (date) {
|
||||
if (!(date instanceof Date)) {
|
||||
date = new Date(date)
|
||||
}
|
||||
return `${date.getUTCFullYear()}-${zero(date.getUTCMonth() + 1)}-${zero(date.getUTCDate())}`
|
||||
}
|
||||
|
||||
// 小于10补0
|
||||
export function zero (d) {
|
||||
return d.toString().padStart(2, '0')
|
||||
}
|
||||
|
||||
// 获取时间的时间戳
|
||||
export function getTimeNum (post) {
|
||||
let dateStr = post.frontmatter.date || post.lastUpdated
|
||||
let date = new Date(dateStr)
|
||||
if (date == "Invalid Date") { // 修复new Date()在Safari下出现Invalid Date的问题
|
||||
date = new Date(dateStr.replace(/-/g, '/'))
|
||||
}
|
||||
return date.getTime()
|
||||
}
|
||||
|
||||
// 比对时间
|
||||
export function compareDate (a, b) {
|
||||
return getTimeNum(b) - getTimeNum(a)
|
||||
}
|
||||
|
||||
// 将特殊符号编码(应用于url)
|
||||
export function encodeUrl (str) {
|
||||
str = str + ''
|
||||
str = str.replace(/ |((?=[\x21-\x7e]+)[^A-Za-z0-9])/g, '-')
|
||||
return str
|
||||
}
|
||||
108
theme-vdoing/util/postData.js
Normal file
108
theme-vdoing/util/postData.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import { type, compareDate } from './index'
|
||||
|
||||
/**
|
||||
* 过滤非文章页
|
||||
* @param {Array} posts 所有文章数据
|
||||
*/
|
||||
export function filterPosts (posts) {
|
||||
posts = posts.filter(item => {
|
||||
const { frontmatter: { pageComponent, article, home } } = item
|
||||
return !(pageComponent || article === false || home === true) // 存在页面组件、article字段为false,以及首页
|
||||
})
|
||||
return posts
|
||||
}
|
||||
|
||||
/**
|
||||
* 按置顶和时间排序
|
||||
* @param {Array} posts 过滤非文章页之后的文章数据
|
||||
*/
|
||||
export function sortPosts (posts) {
|
||||
posts.sort((prev, next) => {
|
||||
const prevSticky = prev.frontmatter.sticky
|
||||
const nextSticky = next.frontmatter.sticky
|
||||
if (prevSticky && nextSticky) {
|
||||
return prevSticky == nextSticky ? compareDate(prev, next) : (prevSticky - nextSticky)
|
||||
} else if (prevSticky && !nextSticky) {
|
||||
return -1
|
||||
} else if (!prevSticky && nextSticky) {
|
||||
return 1
|
||||
}
|
||||
return compareDate(prev, next)
|
||||
})
|
||||
return posts
|
||||
}
|
||||
|
||||
/**
|
||||
* 按时间排序
|
||||
* @param {Array} posts 过滤非文章页之后的文章数据
|
||||
*/
|
||||
export function sortPostsByDate (posts) {
|
||||
posts.sort((prev, next) => {
|
||||
return compareDate(prev, next)
|
||||
})
|
||||
return posts
|
||||
}
|
||||
|
||||
/**
|
||||
* 按分类和标签分组
|
||||
* @param {Array} posts 按时间排序之后的文章数据
|
||||
*/
|
||||
export function groupPosts (posts) {
|
||||
const categoriesObj = {}
|
||||
const tagsObj = {}
|
||||
|
||||
for (let i = 0, postsL = posts.length; i < postsL; i++) {
|
||||
const { frontmatter: { categories, tags } } = posts[i]
|
||||
if (type(categories) === 'array') {
|
||||
categories.forEach(item => {
|
||||
if (item) { // 分类值是有效的
|
||||
if (!categoriesObj[item]) {
|
||||
categoriesObj[item] = []
|
||||
}
|
||||
categoriesObj[item].push(posts[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
if (type(tags) === 'array') {
|
||||
tags.forEach(item => {
|
||||
if (item) { // 标签值是有效的
|
||||
if (!tagsObj[item]) {
|
||||
tagsObj[item] = []
|
||||
}
|
||||
tagsObj[item].push(posts[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
categories: categoriesObj,
|
||||
tags: tagsObj
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有分类和标签
|
||||
* @param {Object} groupPosts 按分类和标签分组之后的文章数据
|
||||
*/
|
||||
export function categoriesAndTags (groupPosts) {
|
||||
const categoriesArr = []
|
||||
const tagsArr = []
|
||||
|
||||
for (let key in groupPosts.categories) {
|
||||
categoriesArr.push({
|
||||
key,
|
||||
length: groupPosts.categories[key].length
|
||||
})
|
||||
}
|
||||
|
||||
for (let key in groupPosts.tags) {
|
||||
tagsArr.push({
|
||||
key,
|
||||
length: groupPosts.tags[key].length
|
||||
})
|
||||
}
|
||||
return {
|
||||
categories: categoriesArr,
|
||||
tags: tagsArr
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user