创建文档

This commit is contained in:
lhc
2021-02-03 17:01:33 +08:00
commit 6f2985e47a
139 changed files with 13727 additions and 0 deletions

21
theme-vdoing/LICENSE Normal file
View 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
View File

@@ -0,0 +1,7 @@
# vuepress-theme-vdoing
vuepress-theme-vdoing for vuepress
一个基于VuePress的 知识管理兼博客 主题。
[More](https://github.com/xugaoyi/vuepress-theme-vdoing#readme).

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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')
}

View 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>

View 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>

View 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
View 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;
}
}

View 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>

View 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>

View 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)
}
}
}

View 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)]
}
}
}

View 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
};
}

View 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
}

View 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')
}

View 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;

View 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;

View File

@@ -0,0 +1 @@
export default {}

47
theme-vdoing/package.json Normal file
View 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"
}]
}

View 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

View 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;
}

View 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"

View File

@@ -0,0 +1 @@
$contentClass = '.theme-vdoing-content'

View 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%)

View 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'

View 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)

View 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

View 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()

View File

@@ -0,0 +1,3 @@
.table-of-contents
.badge
vertical-align middle

View 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

View 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>

View 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
View 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
}

View 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
}
}