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) { @media (min-width: 960px) {
.content { .content {
padding: 0 32px 128px; padding: 0 32px 88px;
} }
} }

View File

@ -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);
} }

View File

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

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

View File

@ -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),
})), })),
} }

View File

@ -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 {

View File

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