mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
feat(theme): 新增 加密 功能
This commit is contained in:
parent
ba899ec7ca
commit
254eb7a9ea
21
theme/LICENSE
Normal file
21
theme/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (C) 2021 - PRESENT by pengzhanbo
|
||||||
|
|
||||||
|
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.
|
||||||
@ -1,8 +1,20 @@
|
|||||||
# `vuepress-theme-plume`
|
# vuepress-theme-plume
|
||||||
|
|
||||||
一款基于 VuePress@2 的博客皮肤。
|
<p align="center">
|
||||||
|
<img src="https://github.com/pengzhanbo/vuepress-theme-plume/raw/main/docs/plume.svg" width="200px" />
|
||||||
|
</p>
|
||||||
|
|
||||||
[查看使用文档](https://pengzhanbo.cn/note/vuepress-theme-plume)
|
[](https://www.npmjs.com/package/vuepress-theme-plume)
|
||||||
|
[](https://www.npmjs.com/package/vuepress-theme-plume)
|
||||||
|

|
||||||
|
|
||||||
|
一个简约的,干净的,容易上手的 vuepress 主题,适用于博客和文档。
|
||||||
|
|
||||||
|
开箱即用,仅需少量配置即可使用,让您更专注于 内容的创作,更好的表达你的想法,形成你的知识笔记。
|
||||||
|
|
||||||
|
内置了丰富的强大的功能,旨在让内容更具有表现力。
|
||||||
|
|
||||||
|
### [查看文档](https://pengzhanbo.cn/note/vuepress-theme-plume)
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@ -28,3 +40,13 @@ export default defineUserConfig({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `plumeTheme(options)`
|
||||||
|
|
||||||
|
__options__ : `PlumeThemeOptions`
|
||||||
|
|
||||||
|
[查看 options 详细说明](https://pengzhanbo.cn/note/vuepress-theme-plume/theme-config/)
|
||||||
|
|
||||||
|
## LICENSE
|
||||||
|
|
||||||
|
[MIT](https://github.com/pengzhanbo/vuepress-theme-plume/blob/main/LICENSE)
|
||||||
|
|||||||
192
theme/src/client/components/EncryptGlobal.vue
Normal file
192
theme/src/client/components/EncryptGlobal.vue
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useSiteLocaleData } from 'vuepress/client'
|
||||||
|
import { useThemeLocaleData } from '../composables/index.js'
|
||||||
|
import { useGlobalEncrypt } from '../composables/encrypt.js'
|
||||||
|
import IconLock from './icons/IconLock.vue'
|
||||||
|
import VFooter from './VFooter.vue'
|
||||||
|
|
||||||
|
const theme = useThemeLocaleData()
|
||||||
|
const siteData = useSiteLocaleData()
|
||||||
|
const { compareGlobal } = useGlobalEncrypt()
|
||||||
|
|
||||||
|
const avatar = computed(() => theme.value.avatar)
|
||||||
|
const title = computed(() => avatar.value?.name || siteData.value.title)
|
||||||
|
|
||||||
|
const password = ref('')
|
||||||
|
const errorCode = ref(0) // 0: no error, 1: wrong password
|
||||||
|
|
||||||
|
function compare() {
|
||||||
|
const result = compareGlobal(password.value)
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
errorCode.value = 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
errorCode.value = 0
|
||||||
|
password.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="global-encrypt-wrapper">
|
||||||
|
<div class="global-encrypt-container">
|
||||||
|
<div v-if="avatar || title" class="profile">
|
||||||
|
<p v-if="avatar" class="avatar" :class="{ circle: avatar.circle }">
|
||||||
|
<img :src="avatar.url" :alt="avatar.name">
|
||||||
|
</p>
|
||||||
|
<h3 v-if="title">
|
||||||
|
{{ title }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="encrypt">
|
||||||
|
<p class="encrypt-text" v-html="theme.encryptGlobalText ?? 'Only Password can access this site'" />
|
||||||
|
<p class="encrypt-input-wrapper">
|
||||||
|
<IconLock class="icon icon-lock" />
|
||||||
|
<input
|
||||||
|
v-model="password"
|
||||||
|
class="encrypt-input"
|
||||||
|
:class="{ error: errorCode === 1 }"
|
||||||
|
type="password"
|
||||||
|
:placeholder="theme.encryptPlaceholder ?? 'Enter Password'"
|
||||||
|
@keyup.enter="compare"
|
||||||
|
@input="password && (errorCode = 0)"
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<button class="encrypt-button" @click="compare">
|
||||||
|
{{ theme.encryptButtonText ?? 'Confirm' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<VFooter />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.global-encrypt-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: var(--vp-c-bg);
|
||||||
|
transition: background-color var(--t-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.global-encrypt-wrapper {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--vp-c-bg-soft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-encrypt-container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 20px;
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
border-bottom: solid 1px var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.global-encrypt-container {
|
||||||
|
width: 400px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: -40px;
|
||||||
|
background-color: var(--vp-c-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: var(--vp-shadow-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar img {
|
||||||
|
width: 120px;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar.circle img {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile h3 {
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
text-align: center;
|
||||||
|
transition: color var(--t-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt-text {
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-lock {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
color: var(--vp-c-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px 8px 32px;
|
||||||
|
background-color: var(--vp-c-bg);
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color var(--t-color), background-color var(--t-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt-input:focus {
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt-input.error {
|
||||||
|
border-color: var(--vp-c-danger-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vp-c-white);
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--vp-c-brand-1);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: none;
|
||||||
|
transition: background-color var(--t-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt-button:hover {
|
||||||
|
background-color: var(--vp-c-brand-2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
131
theme/src/client/components/EncryptPage.vue
Normal file
131
theme/src/client/components/EncryptPage.vue
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { usePageEncrypt } from '../composables/encrypt.js'
|
||||||
|
import { useThemeLocaleData } from '../composables/index.js'
|
||||||
|
import IconLock from './icons/IconLock.vue'
|
||||||
|
|
||||||
|
const theme = useThemeLocaleData()
|
||||||
|
const { comparePage } = usePageEncrypt()
|
||||||
|
|
||||||
|
const password = ref('')
|
||||||
|
const errorCode = ref(0) // 0: no error, 1: wrong password
|
||||||
|
|
||||||
|
function compare() {
|
||||||
|
const result = comparePage(password.value)
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
errorCode.value = 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
errorCode.value = 0
|
||||||
|
password.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-encrypt-wrapper">
|
||||||
|
<div class="logo">
|
||||||
|
<IconLock class="icon icon-lock-head" />
|
||||||
|
</div>
|
||||||
|
<div class="encrypt">
|
||||||
|
<p class="encrypt-text" v-html="theme.encryptPageText ?? 'Only Password can access this page'" />
|
||||||
|
<p class="encrypt-input-wrapper">
|
||||||
|
<IconLock class="icon icon-lock" />
|
||||||
|
<input
|
||||||
|
v-model="password"
|
||||||
|
class="encrypt-input"
|
||||||
|
:class="{ error: errorCode === 1 }"
|
||||||
|
type="password"
|
||||||
|
:placeholder="theme.encryptPlaceholder ?? 'Enter Password'"
|
||||||
|
@keyup.enter="compare"
|
||||||
|
@input="password && (errorCode = 0)"
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<button class="encrypt-button" @click="compare">
|
||||||
|
{{ theme.encryptButtonText ?? 'Confirm' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.icon-lock-head {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.page-encrypt-wrapper {
|
||||||
|
width: 400px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 40px auto 0;
|
||||||
|
background-color: var(--vp-c-bg-alt);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: var(--vp-shadow-2);
|
||||||
|
transition: var(--t-color);
|
||||||
|
transition-property: box-shadow, background-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt-text {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
text-align: center;
|
||||||
|
transition: color var(--t-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-lock {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
color: var(--vp-c-border);
|
||||||
|
transition: color var(--t-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px 8px 32px;
|
||||||
|
background-color: var(--vp-c-bg-soft);
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color var(--t-color), background-color var(--t-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt-input:focus {
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt-input.error {
|
||||||
|
border-color: var(--vp-c-danger-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vp-c-white);
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--vp-c-brand-1);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: none;
|
||||||
|
transition: background-color var(--t-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt-button:hover {
|
||||||
|
background-color: var(--vp-c-brand-2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -5,20 +5,23 @@ import { useMediumZoom } from '@vuepress/plugin-medium-zoom/client'
|
|||||||
import { onContentUpdated } from '@vuepress-plume/plugin-content-update/client'
|
import { onContentUpdated } from '@vuepress-plume/plugin-content-update/client'
|
||||||
import type { PlumeThemePageData } from '../../shared/index.js'
|
import type { PlumeThemePageData } from '../../shared/index.js'
|
||||||
import { useDarkMode, useSidebar } from '../composables/index.js'
|
import { useDarkMode, useSidebar } from '../composables/index.js'
|
||||||
|
import { usePageEncrypt } from '../composables/encrypt.js'
|
||||||
import PageAside from './PageAside.vue'
|
import PageAside from './PageAside.vue'
|
||||||
import PageFooter from './PageFooter.vue'
|
import PageFooter from './PageFooter.vue'
|
||||||
import PageMeta from './PageMeta.vue'
|
import PageMeta from './PageMeta.vue'
|
||||||
|
import EncryptPage from './EncryptPage.vue'
|
||||||
|
|
||||||
const { hasSidebar, hasAside } = useSidebar()
|
const { hasSidebar, hasAside } = useSidebar()
|
||||||
const isDark = useDarkMode()
|
const isDark = useDarkMode()
|
||||||
const page = usePageData<PlumeThemePageData>()
|
const page = usePageData<PlumeThemePageData>()
|
||||||
|
|
||||||
|
const { isPageDecrypted } = usePageEncrypt()
|
||||||
|
|
||||||
const hasComments = computed(() => {
|
const hasComments = computed(() => {
|
||||||
return page.value.frontmatter.comments !== false
|
return page.value.frontmatter.comments !== false
|
||||||
})
|
})
|
||||||
|
|
||||||
const zoom = useMediumZoom()
|
const zoom = useMediumZoom()
|
||||||
|
|
||||||
onContentUpdated(() => zoom?.refresh())
|
onContentUpdated(() => zoom?.refresh())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -29,10 +32,11 @@ onContentUpdated(() => zoom?.refresh())
|
|||||||
'has-sidebar': hasSidebar,
|
'has-sidebar': hasSidebar,
|
||||||
'has-aside': hasAside,
|
'has-aside': hasAside,
|
||||||
'is-blog': page.isBlogPost,
|
'is-blog': page.isBlogPost,
|
||||||
|
'with-encrypt': !isPageDecrypted,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div v-if="hasAside" class="aside">
|
<div v-if="hasAside && isPageDecrypted" class="aside">
|
||||||
<div class="aside-container">
|
<div class="aside-container">
|
||||||
<div class="aside-content">
|
<div class="aside-content">
|
||||||
<PageAside />
|
<PageAside />
|
||||||
@ -43,9 +47,12 @@ onContentUpdated(() => zoom?.refresh())
|
|||||||
<div class="content-container">
|
<div class="content-container">
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<PageMeta />
|
<PageMeta />
|
||||||
|
<EncryptPage v-if="!isPageDecrypted" />
|
||||||
|
<template v-else>
|
||||||
<Content class="plume-content" />
|
<Content class="plume-content" />
|
||||||
<PageFooter />
|
<PageFooter />
|
||||||
<PageComment v-if="hasComments" :darkmode="isDark" />
|
<PageComment v-if="hasComments" :darkmode="isDark" />
|
||||||
|
</template>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -61,9 +68,14 @@ onContentUpdated(() => zoom?.refresh())
|
|||||||
|
|
||||||
.plume-page {
|
.plume-page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-height: calc(100vh - var(--vp-nav-height) - var(--vp-footer-height, 0px) - 49px);
|
||||||
padding: 32px 24px 96px;
|
padding: 32px 24px 96px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plume-page.with-encrypt {
|
||||||
|
padding: 32px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@ -82,7 +94,8 @@ onContentUpdated(() => zoom?.refresh())
|
|||||||
.aside-container {
|
.aside-container {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100vh;
|
min-height: calc(100vh - var(--vp-footer-height, 0px));
|
||||||
|
max-height: 100vh;
|
||||||
padding-top:
|
padding-top:
|
||||||
calc(
|
calc(
|
||||||
var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 32px
|
var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 32px
|
||||||
@ -105,7 +118,7 @@ onContentUpdated(() => zoom?.refresh())
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height:
|
min-height:
|
||||||
calc(
|
calc(
|
||||||
100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 32px)
|
100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 32px + var(--vp-footer-height, 0px))
|
||||||
);
|
);
|
||||||
padding-bottom: 32px;
|
padding-bottom: 32px;
|
||||||
}
|
}
|
||||||
@ -135,6 +148,10 @@ onContentUpdated(() => zoom?.refresh())
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 960px) {
|
@media (min-width: 960px) {
|
||||||
|
.plume-page {
|
||||||
|
min-height: calc(100vh - var(--vp-nav-height) - var(--vp-footer-height, 0px));
|
||||||
|
}
|
||||||
|
|
||||||
.plume-page,
|
.plume-page,
|
||||||
.plume-page.is-blog {
|
.plume-page.is-blog {
|
||||||
padding: 32px 32px 0;
|
padding: 32px 32px 0;
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import AutoLink from './AutoLink.vue'
|
|||||||
import IconClock from './icons/IconClock.vue'
|
import IconClock from './icons/IconClock.vue'
|
||||||
import IconFolder from './icons/IconFolder.vue'
|
import IconFolder from './icons/IconFolder.vue'
|
||||||
import IconTag from './icons/IconTag.vue'
|
import IconTag from './icons/IconTag.vue'
|
||||||
|
import IconLock from './icons/IconLock.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
post: PlumeThemeBlogPostItem
|
post: PlumeThemeBlogPostItem
|
||||||
@ -40,6 +41,7 @@ const createTime = computed(() =>
|
|||||||
>
|
>
|
||||||
TOP
|
TOP
|
||||||
</div>
|
</div>
|
||||||
|
<IconLock v-if="post.encrypt" class="icon-lock" />
|
||||||
<AutoLink :href="post.path">
|
<AutoLink :href="post.path">
|
||||||
{{ post.title }}
|
{{ post.title }}
|
||||||
</AutoLink>
|
</AutoLink>
|
||||||
@ -96,6 +98,16 @@ const createTime = computed(() =>
|
|||||||
transition-property: color, background-color;
|
transition-property: color, background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-item .icon-lock {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-left: 3px;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
transition: var(--t-color);
|
||||||
|
transition-property: color;
|
||||||
|
}
|
||||||
|
|
||||||
.post-item h3 {
|
.post-item h3 {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -205,5 +217,6 @@ const createTime = computed(() =>
|
|||||||
|
|
||||||
.plume-content :deep(p strong) {
|
.plume-content :deep(p strong) {
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
|
transition: color var(--t-color);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
8
theme/src/client/components/icons/IconLock.vue
Normal file
8
theme/src/client/components/icons/IconLock.vue
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M18 8h-1V7c0-2.757-2.243-5-5-5S7 4.243 7 7v1H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2M9 7c0-1.654 1.346-3 3-3s3 1.346 3 3v1H9zm4 8.723V18h-2v-2.277c-.595-.346-1-.984-1-1.723a2 2 0 1 1 4 0c0 .738-.405 1.376-1 1.723"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
176
theme/src/client/composables/encrypt.ts
Normal file
176
theme/src/client/composables/encrypt.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import { compareSync, genSaltSync } from 'bcrypt-ts/browser'
|
||||||
|
import { type Ref, computed } from 'vue'
|
||||||
|
import { hasOwn, useSessionStorage } from '@vueuse/core'
|
||||||
|
import { usePageData, useRoute } from 'vuepress/client'
|
||||||
|
import type { PlumeThemePageData } from '../../shared/index.js'
|
||||||
|
|
||||||
|
declare const __PLUME_ENCRYPT_GLOBAL__: boolean
|
||||||
|
declare const __PLUME_ENCRYPT_SEPARATOR__: string
|
||||||
|
declare const __PLUME_ENCRYPT_ADMIN__: string
|
||||||
|
declare const __PLUME_ENCRYPT_KEYS__: string[]
|
||||||
|
declare const __PLUME_ENCRYPT_RULES__: Record<string, string>
|
||||||
|
|
||||||
|
const global = __PLUME_ENCRYPT_GLOBAL__
|
||||||
|
const separator = __PLUME_ENCRYPT_SEPARATOR__
|
||||||
|
const admin = __PLUME_ENCRYPT_ADMIN__
|
||||||
|
const matches = __PLUME_ENCRYPT_KEYS__
|
||||||
|
const rules = __PLUME_ENCRYPT_RULES__
|
||||||
|
|
||||||
|
const admins = admin.split(separator)
|
||||||
|
|
||||||
|
const ruleList = Object.keys(rules).map(key => ({
|
||||||
|
key,
|
||||||
|
match: matches[key] as string,
|
||||||
|
rules: rules[key].split(separator),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const storage = useSessionStorage('2a0a3d6afb2fdf1f', () => ({
|
||||||
|
s: [genSaltSync(10), genSaltSync(10)] as const,
|
||||||
|
g: '' as string,
|
||||||
|
p: {} as Record<string, string>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
function mergeHash(hash: string) {
|
||||||
|
const [left, right] = storage.value.s
|
||||||
|
return left + hash + right
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitHash(hash: string) {
|
||||||
|
const [left, right] = storage.value.s
|
||||||
|
if (!hash.startsWith(left) || !hash.endsWith(right))
|
||||||
|
return ''
|
||||||
|
|
||||||
|
return hash.slice(left.length, hash.length - right.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = new Map<string, boolean>()
|
||||||
|
function compare(content: string, hash: string) {
|
||||||
|
const key = [content, hash].join(separator)
|
||||||
|
if (cache.has(key))
|
||||||
|
return cache.get(key)
|
||||||
|
|
||||||
|
const result = compareSync(content, hash)
|
||||||
|
cache.set(key, result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGlobalEncrypt(): {
|
||||||
|
isGlobalDecrypted: Ref<boolean>
|
||||||
|
compareGlobal: (password: string) => boolean
|
||||||
|
} {
|
||||||
|
const isGlobalDecrypted = computed(() => {
|
||||||
|
if (!global)
|
||||||
|
return true
|
||||||
|
|
||||||
|
const hash = splitHash(storage.value.g)
|
||||||
|
|
||||||
|
return !!hash && admins.includes(hash)
|
||||||
|
})
|
||||||
|
|
||||||
|
function compareGlobal(password: string) {
|
||||||
|
if (!password)
|
||||||
|
return false
|
||||||
|
|
||||||
|
for (const admin of admins) {
|
||||||
|
if (compare(password, admin)) {
|
||||||
|
storage.value.g = mergeHash(admin)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isGlobalDecrypted,
|
||||||
|
compareGlobal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePageEncrypt() {
|
||||||
|
const page = usePageData<PlumeThemePageData>()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const hasPageEncrypt = computed(() => ruleList.length ? matches.some(toMatch) : false)
|
||||||
|
|
||||||
|
const hashList = computed(() => ruleList.length
|
||||||
|
? ruleList
|
||||||
|
.filter(item => toMatch(item.match))
|
||||||
|
: [])
|
||||||
|
|
||||||
|
const isPageDecrypted = computed(() => {
|
||||||
|
if (!hasPageEncrypt.value)
|
||||||
|
return true
|
||||||
|
|
||||||
|
const hash = splitHash(storage.value.p.__GLOBAL__ || '')
|
||||||
|
if (hash && admins.includes(hash))
|
||||||
|
return true
|
||||||
|
|
||||||
|
for (const { key, rules } of hashList.value) {
|
||||||
|
if (hasOwn(storage.value.p, key)) {
|
||||||
|
const hash = splitHash(storage.value.p[key])
|
||||||
|
if (hash && rules.includes(hash))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
function toMatch(match: string) {
|
||||||
|
const relativePath = page.value.filePathRelative || ''
|
||||||
|
if (match[0] === '^') {
|
||||||
|
const regex = new RegExp(match)
|
||||||
|
return regex.test(route.path) || (relativePath && regex.test(relativePath))
|
||||||
|
}
|
||||||
|
if (match.endsWith('.md'))
|
||||||
|
return relativePath && relativePath.endsWith(match)
|
||||||
|
|
||||||
|
return route.path.startsWith(match) || relativePath.startsWith(match)
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparePage(password: string) {
|
||||||
|
if (!password)
|
||||||
|
return false
|
||||||
|
|
||||||
|
let decrypted = false
|
||||||
|
|
||||||
|
// check global
|
||||||
|
for (const admin of admins) {
|
||||||
|
if (compare(password, admin)) {
|
||||||
|
decrypted = true
|
||||||
|
storage.value.p = {
|
||||||
|
...storage.value.p,
|
||||||
|
__GLOBAL__: mergeHash(admin),
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check page
|
||||||
|
if (!decrypted) {
|
||||||
|
for (const { match, key, rules } of hashList.value) {
|
||||||
|
if (toMatch(match)) {
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (compare(password, rule)) {
|
||||||
|
decrypted = true
|
||||||
|
storage.value.p = {
|
||||||
|
...storage.value.p,
|
||||||
|
[key]: mergeHash(rule),
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (decrypted)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isPageDecrypted,
|
||||||
|
comparePage,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,10 +14,12 @@ import Sidebar from '../components/Sidebar.vue'
|
|||||||
import SkipLink from '../components/SkipLink.vue'
|
import SkipLink from '../components/SkipLink.vue'
|
||||||
import VFooter from '../components/VFooter.vue'
|
import VFooter from '../components/VFooter.vue'
|
||||||
import BackToTop from '../components/BackToTop.vue'
|
import BackToTop from '../components/BackToTop.vue'
|
||||||
|
import EncryptGlobal from '../components/EncryptGlobal.vue'
|
||||||
import {
|
import {
|
||||||
useCloseSidebarOnEscape,
|
useCloseSidebarOnEscape,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from '../composables/index.js'
|
} from '../composables/index.js'
|
||||||
|
import { useGlobalEncrypt, usePageEncrypt } from '../composables/encrypt.js'
|
||||||
|
|
||||||
const page = usePageData<PlumeThemePageData>()
|
const page = usePageData<PlumeThemePageData>()
|
||||||
|
|
||||||
@ -27,6 +29,9 @@ const {
|
|||||||
close: closeSidebar,
|
close: closeSidebar,
|
||||||
} = useSidebar()
|
} = useSidebar()
|
||||||
|
|
||||||
|
const { isGlobalDecrypted } = useGlobalEncrypt()
|
||||||
|
const { isPageDecrypted } = usePageEncrypt()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
watch(() => route.path, closeSidebar)
|
watch(() => route.path, closeSidebar)
|
||||||
|
|
||||||
@ -46,10 +51,16 @@ provide('is-sidebar-open', isSidebarOpen)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="theme-plume">
|
<div class="theme-plume">
|
||||||
|
<EncryptGlobal v-if="!isGlobalDecrypted" />
|
||||||
|
<template v-else>
|
||||||
<SkipLink />
|
<SkipLink />
|
||||||
<Backdrop :show="isSidebarOpen" @click="closeSidebar" />
|
<Backdrop :show="isSidebarOpen" @click="closeSidebar" />
|
||||||
<Nav />
|
<Nav />
|
||||||
<LocalNav :open="isSidebarOpen" @open-menu="openSidebar" />
|
<LocalNav
|
||||||
|
:open="isSidebarOpen"
|
||||||
|
:show-outline="isPageDecrypted"
|
||||||
|
@open-menu="openSidebar"
|
||||||
|
/>
|
||||||
<Sidebar :open="isSidebarOpen" />
|
<Sidebar :open="isSidebarOpen" />
|
||||||
<LayoutContent>
|
<LayoutContent>
|
||||||
<Home v-if="page.frontmatter.home" />
|
<Home v-if="page.frontmatter.home" />
|
||||||
@ -59,6 +70,7 @@ provide('is-sidebar-open', isSidebarOpen)
|
|||||||
<BackToTop />
|
<BackToTop />
|
||||||
<VFooter />
|
<VFooter />
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,11 @@ const defaultLocales: NonNullable<PlumeThemeLocaleOptions['locales']> = {
|
|||||||
lastUpdatedText: 'Last Updated',
|
lastUpdatedText: 'Last Updated',
|
||||||
contributorsText: 'Contributors',
|
contributorsText: 'Contributors',
|
||||||
appearanceText: 'Appearance',
|
appearanceText: 'Appearance',
|
||||||
|
|
||||||
|
encryptButtonText: 'Confirm',
|
||||||
|
encryptPlaceholder: 'Enter password',
|
||||||
|
encryptGlobalText: 'Only password can access this site',
|
||||||
|
encryptPageText: 'Only password can access this page',
|
||||||
},
|
},
|
||||||
'zh-CN': {
|
'zh-CN': {
|
||||||
selectLanguageName: '简体中文',
|
selectLanguageName: '简体中文',
|
||||||
@ -34,6 +39,11 @@ const defaultLocales: NonNullable<PlumeThemeLocaleOptions['locales']> = {
|
|||||||
quote: '但是,如果你不改变方向,并且一直寻找,最终可能会到达你要去的地方。',
|
quote: '但是,如果你不改变方向,并且一直寻找,最终可能会到达你要去的地方。',
|
||||||
linkText: '返回首页',
|
linkText: '返回首页',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
encryptButtonText: '确认',
|
||||||
|
encryptPlaceholder: '请输入密码',
|
||||||
|
encryptGlobalText: '本站只允许密码访问',
|
||||||
|
encryptPageText: '本页面只允许密码访问',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import { sitemapPlugin } from '@vuepress/plugin-sitemap'
|
|||||||
import { contentUpdatePlugin } from '@vuepress-plume/plugin-content-update'
|
import { contentUpdatePlugin } from '@vuepress-plume/plugin-content-update'
|
||||||
import { searchPlugin } from '@vuepress-plume/plugin-search'
|
import { searchPlugin } from '@vuepress-plume/plugin-search'
|
||||||
import type {
|
import type {
|
||||||
|
PlumeThemeEncrypt,
|
||||||
PlumeThemeLocaleOptions,
|
PlumeThemeLocaleOptions,
|
||||||
PlumeThemePluginOptions,
|
PlumeThemePluginOptions,
|
||||||
} from '../shared/index.js'
|
} from '../shared/index.js'
|
||||||
@ -33,11 +34,13 @@ import { resolveNotesList } from './resolveNotesList.js'
|
|||||||
import { resolvedDocsearchOption, resolvedSearchOptions } from './searchPluginOptions.js'
|
import { resolvedDocsearchOption, resolvedSearchOptions } from './searchPluginOptions.js'
|
||||||
import { customContainers } from './container.js'
|
import { customContainers } from './container.js'
|
||||||
import { BLOG_TAGS_COLORS_PRESET, generateBlogTagsColors } from './blogTags.js'
|
import { BLOG_TAGS_COLORS_PRESET, generateBlogTagsColors } from './blogTags.js'
|
||||||
|
import { isEncryptPage } from './resolveEncrypt.js'
|
||||||
|
|
||||||
export function setupPlugins(
|
export function setupPlugins(
|
||||||
app: App,
|
app: App,
|
||||||
options: PlumeThemePluginOptions,
|
options: PlumeThemePluginOptions,
|
||||||
localeOptions: PlumeThemeLocaleOptions,
|
localeOptions: PlumeThemeLocaleOptions,
|
||||||
|
encrypt?: PlumeThemeEncrypt,
|
||||||
): PluginConfig {
|
): PluginConfig {
|
||||||
const isProd = !app.env.isDev
|
const isProd = !app.env.isDev
|
||||||
|
|
||||||
@ -76,13 +79,15 @@ export function setupPlugins(
|
|||||||
extendBlogData: (page: any, extra) => {
|
extendBlogData: (page: any, extra) => {
|
||||||
const tags = page.frontmatter.tags
|
const tags = page.frontmatter.tags
|
||||||
generateBlogTagsColors(extra.tagsColors, tags)
|
generateBlogTagsColors(extra.tagsColors, tags)
|
||||||
return {
|
const data: Record<string, any> = {
|
||||||
categoryList: page.data.categoryList,
|
categoryList: page.data.categoryList,
|
||||||
tags,
|
tags,
|
||||||
sticky: page.frontmatter.sticky,
|
sticky: page.frontmatter.sticky,
|
||||||
createTime: page.data.frontmatter.createTime,
|
createTime: page.data.frontmatter.createTime,
|
||||||
lang: page.lang,
|
lang: page.lang,
|
||||||
}
|
}
|
||||||
|
isEncryptPage(page, encrypt) && (data.encrypt = true)
|
||||||
|
return data
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
59
theme/src/node/resolveEncrypt.ts
Normal file
59
theme/src/node/resolveEncrypt.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { genSaltSync, hashSync } from 'bcrypt-ts'
|
||||||
|
import { isNumber, isString, random, toArray } from '@pengzhanbo/utils'
|
||||||
|
import type { Page } from 'vuepress/core'
|
||||||
|
import type { PlumeThemeEncrypt, PlumeThemePageData } from '../shared/index.js'
|
||||||
|
|
||||||
|
const isStringLike = (value: unknown): boolean => isString(value) || isNumber(value)
|
||||||
|
const separator = ':'
|
||||||
|
|
||||||
|
export function resolveEncrypt(encrypt?: PlumeThemeEncrypt) {
|
||||||
|
const salt = () => genSaltSync(random(8, 16))
|
||||||
|
|
||||||
|
const admin = encrypt?.admin
|
||||||
|
? toArray(encrypt.admin)
|
||||||
|
.filter(isStringLike)
|
||||||
|
.map(item => hashSync(String(item), salt()))
|
||||||
|
.join(separator)
|
||||||
|
: ''
|
||||||
|
|
||||||
|
const rules: Record<string, string> = {}
|
||||||
|
const keys = Object.keys(encrypt?.rules ?? {})
|
||||||
|
|
||||||
|
if (encrypt?.rules) {
|
||||||
|
Object.keys(encrypt.rules).forEach((key) => {
|
||||||
|
const index = keys.indexOf(key)
|
||||||
|
|
||||||
|
rules[String(index)] = toArray(encrypt.rules![key])
|
||||||
|
.filter(isStringLike)
|
||||||
|
.map(item => hashSync(String(item), salt()))
|
||||||
|
.join(separator)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
__PLUME_ENCRYPT_GLOBAL__: encrypt?.global ?? false,
|
||||||
|
__PLUME_ENCRYPT_SEPARATOR__: separator,
|
||||||
|
__PLUME_ENCRYPT_ADMIN__: admin,
|
||||||
|
__PLUME_ENCRYPT_KEYS__: keys,
|
||||||
|
__PLUME_ENCRYPT_RULES__: rules,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEncryptPage(page: Page<PlumeThemePageData>, encrypt?: PlumeThemeEncrypt) {
|
||||||
|
if (!encrypt)
|
||||||
|
return false
|
||||||
|
|
||||||
|
const rules = encrypt.rules ?? {}
|
||||||
|
|
||||||
|
return Object.keys(rules).some((match) => {
|
||||||
|
const relativePath = page.data.filePathRelative || ''
|
||||||
|
if (match[0] === '^') {
|
||||||
|
const regex = new RegExp(match)
|
||||||
|
return regex.test(page.path) || (relativePath && regex.test(relativePath))
|
||||||
|
}
|
||||||
|
if (match.endsWith('.md'))
|
||||||
|
return relativePath && relativePath.endsWith(match)
|
||||||
|
|
||||||
|
return page.path.startsWith(match) || relativePath.startsWith(match)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,28 +1,39 @@
|
|||||||
import type { Page, Theme } from 'vuepress/core'
|
import type { Page, Theme } from 'vuepress/core'
|
||||||
import { templateRenderer } from 'vuepress/utils'
|
import { logger, templateRenderer } from 'vuepress/utils'
|
||||||
import type { PlumeThemeOptions, PlumeThemePageData } from '../shared/index.js'
|
import type { PlumeThemeOptions, PlumeThemePageData } from '../shared/index.js'
|
||||||
import { mergeLocaleOptions } from './defaultOptions.js'
|
import { mergeLocaleOptions } from './defaultOptions.js'
|
||||||
import { setupPlugins } from './plugins.js'
|
import { setupPlugins } from './plugins.js'
|
||||||
import { extendsPageData, setupPage } from './setupPages.js'
|
import { extendsPageData, setupPage } from './setupPages.js'
|
||||||
import { getThemePackage, resolve, templates } from './utils.js'
|
import { getThemePackage, resolve, templates } from './utils.js'
|
||||||
|
import { resolveEncrypt } from './resolveEncrypt.js'
|
||||||
|
|
||||||
const THEME_NAME = 'vuepress-theme-plume'
|
const THEME_NAME = 'vuepress-theme-plume'
|
||||||
|
|
||||||
export function plumeTheme({
|
export function plumeTheme({
|
||||||
themePlugins,
|
themePlugins,
|
||||||
plugins,
|
plugins,
|
||||||
|
encrypt,
|
||||||
...localeOptions
|
...localeOptions
|
||||||
}: PlumeThemeOptions = {}): Theme {
|
}: PlumeThemeOptions = {}): Theme {
|
||||||
const pluginsOptions = plugins ?? themePlugins ?? {}
|
const pluginsOptions = plugins ?? themePlugins ?? {}
|
||||||
const pkg = getThemePackage()
|
const pkg = getThemePackage()
|
||||||
|
|
||||||
|
if (themePlugins) {
|
||||||
|
logger.warn(
|
||||||
|
`The 'themePlugins' option is deprecated. Please use 'plugins' instead.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (app) => {
|
return (app) => {
|
||||||
localeOptions = mergeLocaleOptions(app, localeOptions)
|
localeOptions = mergeLocaleOptions(app, localeOptions)
|
||||||
return {
|
return {
|
||||||
name: THEME_NAME,
|
name: THEME_NAME,
|
||||||
|
define: {
|
||||||
|
...resolveEncrypt(encrypt),
|
||||||
|
},
|
||||||
templateBuild: templates('build.html'),
|
templateBuild: templates('build.html'),
|
||||||
clientConfigFile: resolve('client/config.js'),
|
clientConfigFile: resolve('client/config.js'),
|
||||||
plugins: setupPlugins(app, pluginsOptions, localeOptions),
|
plugins: setupPlugins(app, pluginsOptions, localeOptions, encrypt),
|
||||||
onInitialized: app => setupPage(app, localeOptions),
|
onInitialized: app => setupPage(app, localeOptions),
|
||||||
extendsPage: (page) => {
|
extendsPage: (page) => {
|
||||||
extendsPageData(app, page as Page<PlumeThemePageData>, localeOptions)
|
extendsPageData(app, page as Page<PlumeThemePageData>, localeOptions)
|
||||||
@ -44,7 +55,7 @@ export function plumeTheme({
|
|||||||
if (um === 'dark' || (um !== 'light' && sm)) {
|
if (um === 'dark' || (um !== 'light' && sm)) {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
}
|
}
|
||||||
})();`,
|
})();`.replace(/^\s+|\s+$/gm, '').replace(/\n/g, ''),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,8 @@ export interface PlumeThemeBlogPostItem extends BlogPostDataItem {
|
|||||||
sticky: boolean
|
sticky: boolean
|
||||||
categoryLost: PageCategoryData[]
|
categoryLost: PageCategoryData[]
|
||||||
createTime: string
|
createTime: string
|
||||||
|
lang: string
|
||||||
|
encrypt?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlumeThemeBlogPostData = PlumeThemeBlogPostItem[]
|
export type PlumeThemeBlogPostData = PlumeThemeBlogPostItem[]
|
||||||
|
|||||||
@ -68,6 +68,9 @@ export interface PlumeThemeBlog {
|
|||||||
*/
|
*/
|
||||||
exclude?: string[]
|
exclude?: string[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页
|
||||||
|
*/
|
||||||
pagination?: false | {
|
pagination?: false | {
|
||||||
/**
|
/**
|
||||||
* 每页显示的文章数量
|
* 每页显示的文章数量
|
||||||
@ -98,6 +101,37 @@ export interface PlumeThemeBlog {
|
|||||||
archives?: boolean
|
archives?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PlumeThemeEncrypt {
|
||||||
|
/**
|
||||||
|
* 是否启用全站加密
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
global?: boolean
|
||||||
|
/**
|
||||||
|
* 超级权限密码, 该密码可以解密全站,以及任意加密的文章
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
admin?: string | string[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章密码, 可以通过 文章的 markdown 文件相对路径、页面访问路径、
|
||||||
|
* 目录路径 等,对 单个文章 或者 整个目录 进行 加密。
|
||||||
|
* 如果是以 `^` 开头,则被认为是类似于正则表达式进行匹配。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "前端/基础/html.md": "123",
|
||||||
|
* "/article/23c44c/": ["456", "789"],
|
||||||
|
* "^/note/(note1|note2)/": "123"
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
rules?: {
|
||||||
|
[key: string]: string | string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface PlumeThemeLocaleData extends LocaleData {
|
export interface PlumeThemeLocaleData extends LocaleData {
|
||||||
/**
|
/**
|
||||||
* 网站站点首页
|
* 网站站点首页
|
||||||
@ -296,4 +330,28 @@ export interface PlumeThemeLocaleData extends LocaleData {
|
|||||||
linkLabel?: string
|
linkLabel?: string
|
||||||
linkText?: string
|
linkText?: string
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 加密
|
||||||
|
*/
|
||||||
|
encrypt?: PlumeThemeEncrypt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全站加密时的提示
|
||||||
|
*/
|
||||||
|
encryptGlobalText?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章加密时的提示
|
||||||
|
*/
|
||||||
|
encryptPageText?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密确认按钮文本
|
||||||
|
*/
|
||||||
|
encryptButtonText?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密时输入框的 placeholder
|
||||||
|
*/
|
||||||
|
encryptPlaceholder?: string
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user