mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
feat(theme): optimize page encrypt (#750)
This commit is contained in:
parent
87cda0c824
commit
a5dfef7202
@ -293,7 +293,7 @@ watch(
|
|||||||
|
|
||||||
@media (min-width: 960px) {
|
@media (min-width: 960px) {
|
||||||
.content {
|
.content {
|
||||||
padding: 0 32px 128px;
|
padding: 0 32px 88px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,10 @@ const { global, info } = defineProps<{
|
|||||||
info?: string
|
info?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'validate', isValidate: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const { theme } = useData()
|
const { theme } = useData()
|
||||||
const { compareGlobal, comparePage } = useEncryptCompare()
|
const { compareGlobal, comparePage } = useEncryptCompare()
|
||||||
|
|
||||||
@ -29,6 +33,7 @@ async function onSubmit() {
|
|||||||
errorCode.value = 0
|
errorCode.value = 0
|
||||||
password.value = ''
|
password.value = ''
|
||||||
}
|
}
|
||||||
|
emit('validate', errorCode.value === 0)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -44,8 +49,10 @@ async function onSubmit() {
|
|||||||
class="encrypt-input"
|
class="encrypt-input"
|
||||||
:class="{ error: errorCode === 1 }"
|
:class="{ error: errorCode === 1 }"
|
||||||
type="password"
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
:placeholder="theme.encryptPlaceholder ?? 'Enter Password'"
|
:placeholder="theme.encryptPlaceholder ?? 'Enter Password'"
|
||||||
@keyup.enter="onSubmit"
|
@keyup.enter="onSubmit"
|
||||||
|
@focus="!password && (errorCode = 0)"
|
||||||
@input="password && (errorCode = 0)"
|
@input="password && (errorCode = 0)"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
@ -83,9 +90,9 @@ async function onSubmit() {
|
|||||||
.encrypt-input {
|
.encrypt-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px 12px 8px 32px;
|
padding: 8px 12px 8px 32px;
|
||||||
background-color: transparent;
|
background-color: var(--vp-c-bg-soft);
|
||||||
border: 1px solid var(--vp-c-border);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 4px;
|
border-radius: 21px;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color var(--vp-t-color), background-color var(--vp-t-color);
|
transition: border-color var(--vp-t-color), background-color var(--vp-t-color);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import VPEncryptForm from '@theme/VPEncryptForm.vue'
|
import VPEncryptForm from '@theme/VPEncryptForm.vue'
|
||||||
|
import { useTemplateRef } from 'vue'
|
||||||
import { useData } from '../composables/index.js'
|
import { useData } from '../composables/index.js'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@ -7,20 +8,42 @@ defineOptions({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { theme, frontmatter } = useData<'post'>()
|
const { theme, frontmatter } = useData<'post'>()
|
||||||
|
|
||||||
|
const el = useTemplateRef<HTMLElement>('el')
|
||||||
|
function onValidate(isValidate: boolean) {
|
||||||
|
if (!isValidate) {
|
||||||
|
el.value?.classList.add('animation')
|
||||||
|
setTimeout(() => {
|
||||||
|
el.value?.classList.remove('animation')
|
||||||
|
}, 800)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<div class="vp-page-encrypt" v-bind="$attrs">
|
<div ref="el" class="vp-page-encrypt" v-bind="$attrs">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<span class="vpi-lock icon-lock-head" />
|
<span class="vpi-lock icon-lock-head" />
|
||||||
</div>
|
</div>
|
||||||
<VPEncryptForm :info="frontmatter.passwordHint || theme.encryptPageText" />
|
<VPEncryptForm :info="frontmatter.passwordHint || theme.encryptPageText" @validate="onValidate" />
|
||||||
</div>
|
</div>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.vp-page-encrypt {
|
||||||
|
transition: var(--vp-t-color);
|
||||||
|
transition-property: box-shadow, border-color, transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-page-encrypt.animation {
|
||||||
|
animation-name: encrypt-error;
|
||||||
|
animation-duration: 0.15s;
|
||||||
|
animation-timing-function: ease-in-out;
|
||||||
|
animation-iteration-count: 4;
|
||||||
|
}
|
||||||
|
|
||||||
.vp-page-encrypt .logo {
|
.vp-page-encrypt .logo {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@ -37,15 +60,26 @@ const { theme, frontmatter } = useData<'post'>()
|
|||||||
width: 400px;
|
width: 400px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin: 40px auto 0;
|
margin: 40px auto 0;
|
||||||
border: solid 1px var(--vp-c-divider);
|
background: var(--vp-c-bg-soft);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: var(--vp-shadow-1);
|
}
|
||||||
transition: var(--vp-t-color);
|
}
|
||||||
transition-property: box-shadow, border-color;
|
|
||||||
|
@keyframes encrypt-error {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vp-page-encrypt:hover {
|
33% {
|
||||||
box-shadow: var(--vp-shadow-2);
|
transform: translateX(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
67% {
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,7 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import VPEncryptPage from '@theme/VPEncryptPage.vue'
|
||||||
|
import { useEncrypt } from '../composables/index.js'
|
||||||
|
|
||||||
|
const { isPageDecrypted } = useEncrypt()
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="vp-page">
|
<div class="vp-page">
|
||||||
<slot name="page-top" />
|
<VPEncryptPage v-if="!isPageDecrypted" />
|
||||||
<Content class="vp-doc plume-content" vp-content />
|
<template v-else>
|
||||||
<slot name="page-bottom" />
|
<slot name="page-top" />
|
||||||
|
<Content class="vp-doc plume-content" vp-content />
|
||||||
|
<slot name="page-bottom" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { encrypt as rawEncrypt } from '@internal/encrypt'
|
import { encrypt as rawEncrypt } from '@internal/encrypt'
|
||||||
|
import { decodeData } from '@vuepress/helper/client'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
export type EncryptConfig = readonly [
|
export type EncryptConfig = readonly [
|
||||||
@ -35,14 +36,15 @@ export function useEncryptData(): EncryptRef {
|
|||||||
function resolveEncryptData(
|
function resolveEncryptData(
|
||||||
[global, separator, admin, matches, rules]: EncryptConfig,
|
[global, separator, admin, matches, rules]: EncryptConfig,
|
||||||
): EncryptData {
|
): EncryptData {
|
||||||
|
const keys = matches.map(match => decodeData(match))
|
||||||
return {
|
return {
|
||||||
global,
|
global,
|
||||||
separator,
|
separator,
|
||||||
matches,
|
matches: keys,
|
||||||
admins: admin.split(separator),
|
admins: admin.split(separator),
|
||||||
ruleList: Object.keys(rules).map(key => ({
|
ruleList: Object.keys(rules).map(key => ({
|
||||||
key,
|
key,
|
||||||
match: matches[key] as string,
|
match: keys[key] as string,
|
||||||
rules: rules[key].split(separator),
|
rules: rules[key].split(separator),
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { hasOwn, useSessionStorage } from '@vueuse/core'
|
|||||||
import { compare, genSaltSync } from 'bcrypt-ts/browser'
|
import { compare, genSaltSync } from 'bcrypt-ts/browser'
|
||||||
import { computed, inject, provide } from 'vue'
|
import { computed, inject, provide } from 'vue'
|
||||||
import { useRoute } from 'vuepress/client'
|
import { useRoute } from 'vuepress/client'
|
||||||
|
import { removeLeadingSlash } from 'vuepress/shared'
|
||||||
import { useData } from './data.js'
|
import { useData } from './data.js'
|
||||||
import { useEncryptData } from './encrypt-data.js'
|
import { useEncryptData } from './encrypt-data.js'
|
||||||
|
|
||||||
@ -73,12 +74,12 @@ function toMatch(match: string, pagePath: string, filePathRelative: string | nul
|
|||||||
const relativePath = filePathRelative || ''
|
const relativePath = filePathRelative || ''
|
||||||
if (match[0] === '^') {
|
if (match[0] === '^') {
|
||||||
const regex = createMatchRegex(match)
|
const regex = createMatchRegex(match)
|
||||||
return regex.test(pagePath) || (relativePath && regex.test(relativePath))
|
return regex.test(pagePath) || regex.test(relativePath)
|
||||||
}
|
}
|
||||||
if (match.endsWith('.md'))
|
if (match.endsWith('.md'))
|
||||||
return relativePath && relativePath.endsWith(match)
|
return relativePath && relativePath.endsWith(match)
|
||||||
|
|
||||||
return pagePath.startsWith(match) || relativePath.startsWith(match)
|
return pagePath.startsWith(match) || relativePath.startsWith(removeLeadingSlash(match))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupEncrypt(): void {
|
export function setupEncrypt(): void {
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import type { App } from 'vuepress'
|
|||||||
import type { Page } from 'vuepress/core'
|
import type { Page } from 'vuepress/core'
|
||||||
import type { EncryptOptions, ThemePageData } from '../../shared/index.js'
|
import type { EncryptOptions, ThemePageData } from '../../shared/index.js'
|
||||||
import type { FsCache } from '../utils/index.js'
|
import type { FsCache } from '../utils/index.js'
|
||||||
import { isNumber, isString, toArray } from '@pengzhanbo/utils'
|
import { isEmptyObject, isNumber, isString, toArray } from '@pengzhanbo/utils'
|
||||||
|
import { encodeData, removeLeadingSlash } from '@vuepress/helper'
|
||||||
import { getThemeConfig } from '../loadConfig/index.js'
|
import { getThemeConfig } from '../loadConfig/index.js'
|
||||||
import { createFsCache, genEncrypt, hash, perf, resolveContent, writeTemp } from '../utils/index.js'
|
import { createFsCache, genEncrypt, hash, perf, resolveContent, writeTemp } from '../utils/index.js'
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ export type EncryptConfig = readonly [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const isStringLike = (value: unknown): boolean => isString(value) || isNumber(value)
|
const isStringLike = (value: unknown): boolean => isString(value) || isNumber(value)
|
||||||
|
|
||||||
const separator = ':'
|
const separator = ':'
|
||||||
let contentHash = ''
|
let contentHash = ''
|
||||||
let fsCache: FsCache<[string, EncryptConfig]> | null = null
|
let fsCache: FsCache<[string, EncryptConfig]> | null = null
|
||||||
@ -52,14 +54,19 @@ function resolveEncrypt(encrypt?: EncryptOptions): EncryptConfig {
|
|||||||
.join(separator)
|
.join(separator)
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
const rules: Record<string, string> = {}
|
const encryptRules = Object.keys(encrypt?.rules ?? {}).reduce((acc, key) => {
|
||||||
const keys = Object.keys(encrypt?.rules ?? {})
|
acc[encodeData(key)] = encrypt!.rules![key]
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, string | string[]>)
|
||||||
|
|
||||||
if (encrypt?.rules) {
|
const rules: Record<string, string> = {}
|
||||||
Object.keys(encrypt.rules).forEach((key) => {
|
const keys = Object.keys(encryptRules)
|
||||||
|
|
||||||
|
if (!isEmptyObject(encryptRules)) {
|
||||||
|
Object.keys(encryptRules).forEach((key) => {
|
||||||
const index = keys.indexOf(key)
|
const index = keys.indexOf(key)
|
||||||
|
|
||||||
rules[String(index)] = toArray(encrypt.rules![key])
|
rules[String(index)] = toArray(encryptRules[key])
|
||||||
.filter(isStringLike)
|
.filter(isStringLike)
|
||||||
.map(item => genEncrypt(item))
|
.map(item => genEncrypt(item))
|
||||||
.join(separator)
|
.join(separator)
|
||||||
@ -82,11 +89,11 @@ export function isEncryptPage(page: Page<ThemePageData>, encrypt?: EncryptOption
|
|||||||
const relativePath = page.data.filePathRelative || ''
|
const relativePath = page.data.filePathRelative || ''
|
||||||
if (match[0] === '^') {
|
if (match[0] === '^') {
|
||||||
const regex = new RegExp(match)
|
const regex = new RegExp(match)
|
||||||
return regex.test(page.path) || (relativePath && regex.test(relativePath))
|
return regex.test(page.path) || regex.test(relativePath)
|
||||||
}
|
}
|
||||||
if (match.endsWith('.md'))
|
if (match.endsWith('.md'))
|
||||||
return relativePath && relativePath.endsWith(match)
|
return relativePath.endsWith(match)
|
||||||
|
|
||||||
return page.path.startsWith(match) || relativePath.startsWith(match)
|
return page.path.startsWith(match) || relativePath.startsWith(removeLeadingSlash(match))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user