feat(theme): optimize page encrypt (#750)

This commit is contained in:
pengzhanbo 2025-11-09 13:48:15 +08:00 committed by GitHub
parent 87cda0c824
commit a5dfef7202
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 89 additions and 28 deletions

View File

@ -293,7 +293,7 @@ watch(
@media (min-width: 960px) {
.content {
padding: 0 32px 128px;
padding: 0 32px 88px;
}
}

View File

@ -7,6 +7,10 @@ const { global, info } = defineProps<{
info?: string
}>()
const emit = defineEmits<{
(e: 'validate', isValidate: boolean): void
}>()
const { theme } = useData()
const { compareGlobal, comparePage } = useEncryptCompare()
@ -29,6 +33,7 @@ async function onSubmit() {
errorCode.value = 0
password.value = ''
}
emit('validate', errorCode.value === 0)
}
</script>
@ -44,8 +49,10 @@ async function onSubmit() {
class="encrypt-input"
:class="{ error: errorCode === 1 }"
type="password"
autocomplete="off"
:placeholder="theme.encryptPlaceholder ?? 'Enter Password'"
@keyup.enter="onSubmit"
@focus="!password && (errorCode = 0)"
@input="password && (errorCode = 0)"
>
</label>
@ -83,9 +90,9 @@ async function onSubmit() {
.encrypt-input {
width: 100%;
padding: 8px 12px 8px 32px;
background-color: transparent;
border: 1px solid var(--vp-c-border);
border-radius: 4px;
background-color: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 21px;
outline: none;
transition: border-color var(--vp-t-color), background-color var(--vp-t-color);
}

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import VPEncryptForm from '@theme/VPEncryptForm.vue'
import { useTemplateRef } from 'vue'
import { useData } from '../composables/index.js'
defineOptions({
@ -7,20 +8,42 @@ defineOptions({
})
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>
<template>
<ClientOnly>
<div class="vp-page-encrypt" v-bind="$attrs">
<div ref="el" class="vp-page-encrypt" v-bind="$attrs">
<div class="logo">
<span class="vpi-lock icon-lock-head" />
</div>
<VPEncryptForm :info="frontmatter.passwordHint || theme.encryptPageText" />
<VPEncryptForm :info="frontmatter.passwordHint || theme.encryptPageText" @validate="onValidate" />
</div>
</ClientOnly>
</template>
<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 {
text-align: center;
}
@ -37,15 +60,26 @@ const { theme, frontmatter } = useData<'post'>()
width: 400px;
padding: 20px;
margin: 40px auto 0;
border: solid 1px var(--vp-c-divider);
background: var(--vp-c-bg-soft);
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 {
box-shadow: var(--vp-shadow-2);
33% {
transform: translateX(-4px);
}
67% {
transform: translateX(4px);
}
100% {
transform: translateX(0);
}
}
</style>

View File

@ -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>
<div class="vp-page">
<slot name="page-top" />
<Content class="vp-doc plume-content" vp-content />
<slot name="page-bottom" />
<VPEncryptPage v-if="!isPageDecrypted" />
<template v-else>
<slot name="page-top" />
<Content class="vp-doc plume-content" vp-content />
<slot name="page-bottom" />
</template>
</div>
</template>

View File

@ -1,5 +1,6 @@
import type { Ref } from 'vue'
import { encrypt as rawEncrypt } from '@internal/encrypt'
import { decodeData } from '@vuepress/helper/client'
import { ref } from 'vue'
export type EncryptConfig = readonly [
@ -35,14 +36,15 @@ export function useEncryptData(): EncryptRef {
function resolveEncryptData(
[global, separator, admin, matches, rules]: EncryptConfig,
): EncryptData {
const keys = matches.map(match => decodeData(match))
return {
global,
separator,
matches,
matches: keys,
admins: admin.split(separator),
ruleList: Object.keys(rules).map(key => ({
key,
match: matches[key] as string,
match: keys[key] as string,
rules: rules[key].split(separator),
})),
}

View File

@ -4,6 +4,7 @@ import { hasOwn, useSessionStorage } from '@vueuse/core'
import { compare, genSaltSync } from 'bcrypt-ts/browser'
import { computed, inject, provide } from 'vue'
import { useRoute } from 'vuepress/client'
import { removeLeadingSlash } from 'vuepress/shared'
import { useData } from './data.js'
import { useEncryptData } from './encrypt-data.js'
@ -73,12 +74,12 @@ function toMatch(match: string, pagePath: string, filePathRelative: string | nul
const relativePath = filePathRelative || ''
if (match[0] === '^') {
const regex = createMatchRegex(match)
return regex.test(pagePath) || (relativePath && regex.test(relativePath))
return regex.test(pagePath) || regex.test(relativePath)
}
if (match.endsWith('.md'))
return relativePath && relativePath.endsWith(match)
return pagePath.startsWith(match) || relativePath.startsWith(match)
return pagePath.startsWith(match) || relativePath.startsWith(removeLeadingSlash(match))
}
export function setupEncrypt(): void {

View File

@ -2,7 +2,8 @@ import type { App } from 'vuepress'
import type { Page } from 'vuepress/core'
import type { EncryptOptions, ThemePageData } from '../../shared/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 { 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 separator = ':'
let contentHash = ''
let fsCache: FsCache<[string, EncryptConfig]> | null = null
@ -52,14 +54,19 @@ function resolveEncrypt(encrypt?: EncryptOptions): EncryptConfig {
.join(separator)
: ''
const rules: Record<string, string> = {}
const keys = Object.keys(encrypt?.rules ?? {})
const encryptRules = Object.keys(encrypt?.rules ?? {}).reduce((acc, key) => {
acc[encodeData(key)] = encrypt!.rules![key]
return acc
}, {} as Record<string, string | string[]>)
if (encrypt?.rules) {
Object.keys(encrypt.rules).forEach((key) => {
const rules: Record<string, string> = {}
const keys = Object.keys(encryptRules)
if (!isEmptyObject(encryptRules)) {
Object.keys(encryptRules).forEach((key) => {
const index = keys.indexOf(key)
rules[String(index)] = toArray(encrypt.rules![key])
rules[String(index)] = toArray(encryptRules[key])
.filter(isStringLike)
.map(item => genEncrypt(item))
.join(separator)
@ -82,11 +89,11 @@ export function isEncryptPage(page: Page<ThemePageData>, encrypt?: EncryptOption
const relativePath = page.data.filePathRelative || ''
if (match[0] === '^') {
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'))
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))
})
}