<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>AnKode</title><description>只因你太美</description><link>https://blog.anxko.cn/</link><language>zh_CN</language><item><title>Astro&amp;Fuwari添加支持嵌套与局部的文章密码保护加密块</title><link>https://blog.anxko.cn/posts/4c5fc27ed86f2b27/</link><guid isPermaLink="true">https://blog.anxko.cn/posts/4c5fc27ed86f2b27/</guid><description>WTF？纯静态也能加密文章？！不仅能，还能加密中加密的嵌套玩法</description><pubDate>Sat, 25 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;div data-encrypt-hidden=&quot;password-hint&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;密码提示：计算下面定积分表达式的值，其中常数为35，计算结果即为密码。&lt;/p&gt;
&lt;p&gt;$$
\frac{1}{2 \displaystyle\int_0^1 t ,(1-t)^{35} , dt} = ?
$$&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;&amp;lt;div fuwari-encrypt-password=&quot;666&quot; fuwari-encrypt-tip=&quot;检测到疑似入机，请先答题...（嘟嘟嘟）&quot; fuwari-encrypt-hidden=&quot;password-hint&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;div data-encrypt-hidden=&quot;mysterious-revival&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;我叫杨间，当你看到这句话的时候，我已经...（Sorry，走错片场了(´◓ｑ◔｀)）&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;&amp;lt;div fuwari-encrypt-password=&quot;test&quot; fuwari-encrypt-tip=&quot;密码：test&quot; fuwari-encrypt-hidden=&quot;mysterious-revival&quot;&amp;gt;&lt;/p&gt;
&lt;h1&gt;实现方法梳理&lt;/h1&gt;
&lt;h2&gt;如何较为安全的加密与解密&lt;/h2&gt;
&lt;p&gt;我们都知道，浏览器环境下纯粹的web前端加密与解密完全就是掩耳盗铃，基本上稍微学过的人都能直接F12分析出来加解密逻辑并且还能拿到密钥，所以我们不能将密钥放在代码里，而是要“抛出去”，让密钥由访问者提供。&lt;/p&gt;
&lt;p&gt;而Fuwari是基于Astro这个框架的，Node JS环境下，我们可以在编译构建时（服务端），先将文章内容加密，然后再将密文放到代码里（客户端），然后显示一个解密框输入密码进行解密。&lt;/p&gt;
&lt;h2&gt;如何支持嵌套和局部加密&lt;/h2&gt;
&lt;p&gt;看网上有关给Fuwari添加文章加密的方法，都是直接添加一个文章元数据用于控制是否加密，然后直接将整个文章加密，这种方法确实省力，但是却不够灵活。&lt;/p&gt;
&lt;p&gt;由于Fuwari是用Markdown文档写文章的，所以我的想法是直接自定义一个Markdown块，然后内容加密并展示一个解密框即可，这样无论是嵌套还是局部都能迎刃而解。&lt;/p&gt;
&lt;p&gt;不过要用代码处理Markdown起来比较复杂，所以可以直接选择解析渲染后的HTML值，从最内层标记需要加密的DOM开始一层层加密，解密时反过来一层层解密即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 浏览器中实际DOM原文 --&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;div&amp;gt;
        我是元素内容
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;


&amp;lt;!--
    嵌套第一层加密后
    使用自定义属性data-encrypt-tag来标记解密框对应的密文DOM
--&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;div data-encrypt-content=&quot;元素内容加密后的内容&quot; data-encrypt-tag=&quot;1&quot;&amp;gt;
        &amp;lt;div data-encrypt-tag=&quot;1&quot;&amp;gt;对应的解密弹窗&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;


&amp;lt;!-- 嵌套第二层加密后 --&amp;gt;
&amp;lt;div data-encrypt-content=&quot;元素内容加密后的内容&quot; data-encrypt-tag=&quot;2&quot;&amp;gt;
    &amp;lt;div data-encrypt-tag=&quot;2&quot;&amp;gt;对应的解密弹窗&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;&amp;lt;div fuwari-encrypt-password=&quot;123456&quot; fuwari-encrypt-tip=&quot;密码：123456&quot;&amp;gt;&lt;/p&gt;
&lt;h1&gt;代码实现&lt;/h1&gt;
&lt;h2&gt;CSS样式&lt;/h2&gt;
&lt;p&gt;在CSS文件 &lt;code&gt;src/styles/markdown.css&lt;/code&gt; 中，添加代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@keyframes shake {
    0% {
        transform: translate(0)
    }

    20% {
        transform: translate(-5px)
    }

    40% {
        transform: translate(5px)
    }

    60% {
        transform: translate(-3px)
    }

    80% {
        transform: translate(3px)
    }

    to {
        transform: translate(0)
    }
}

.animate-shake {
    animation: shake .5s cubic-bezier(.36,.07,.19,.97) both
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装依赖&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pnpm install node-forge
pnpm install @types/node-forge
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;加解密工具类&lt;/h2&gt;
&lt;p&gt;在目录 &lt;code&gt;src/utils&lt;/code&gt; 下创建名为 &lt;code&gt;encrypt-utils.ts&lt;/code&gt; 的工具函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import forge from &quot;node-forge&quot;

/**
 * 派生密钥
 */
export function deriveKey(keyStr: string): string {
	if (keyStr.trim() === &quot;&quot;) {
		throw new Error(&quot;The password entered is empty.&quot;)
	}
	const md = forge.md.sha256.create()
	md.update(keyStr)
	const keyBytes = md.digest().getBytes() // 32 字节
	return forge.util.encode64(keyBytes)
}

/**
 * 加密
 */
export function encode(keyBase: string, aad: string, data: string): string {
	if (!keyBase.trim() || !aad.trim()) {
		throw new Error(&quot;The input parameter is empty.&quot;)
	}
	const key = forge.util.decode64(keyBase)
	const iv = forge.random.getBytesSync(12)
	const aadBytes = forge.util.encodeUtf8(aad)
	const plaintext = forge.util.encodeUtf8(data)
	const cipher = forge.cipher.createCipher(&quot;AES-GCM&quot;, key)
	cipher.start({
		iv: iv,
		additionalData: aadBytes,
		tagLength: 128, // 128 位 = 16 字节标签
	})

	cipher.update(forge.util.createBuffer(plaintext))
	if (!cipher.finish()) {
		throw new Error(&quot;Encryption failed.&quot;)
	}

	const ciphertext = cipher.output.getBytes()
	const tag = cipher.mode.tag.getBytes()

	const combined = iv + ciphertext + tag
	return forge.util.encode64(combined)
}

/**
 * 解密
 */
export function decode(keyBase: string, aad: string, data: string): string {
	if (keyBase.trim() === &quot;&quot; || aad.trim() === &quot;&quot; || data.trim() === &quot;&quot;) {
		throw new Error(&quot;The input parameter is empty.&quot;)
	}
	const key = forge.util.decode64(keyBase)
	const combined = forge.util.decode64(data)
	const iv = combined.slice(0, 12)
	const tag = combined.slice(-16)
	const ciphertext = combined.slice(12, -16)

	const aadBytes = forge.util.encodeUtf8(aad)
	const decipher = forge.cipher.createDecipher(&quot;AES-GCM&quot;, key)
	decipher.start({
		iv: iv,
		additionalData: aadBytes,
		tagLength: 128,
		tag: forge.util.createBuffer(tag),
	})

	decipher.update(forge.util.createBuffer(ciphertext))

	if (!decipher.finish()) {
		throw new Error(&quot;Decryption failed.&quot;)
	}

	const plaintext = decipher.output.getBytes()
	return forge.util.decodeUtf8(plaintext)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;核心组件&lt;/h2&gt;
&lt;p&gt;在目录 &lt;code&gt;src/components/md/&lt;/code&gt; 下创建名为 &lt;code&gt;EnctyprCard.astro&lt;/code&gt; 的组件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
import crypto from &quot;node:crypto&quot;
import { deriveKey, encode } from &quot;@utils/encrypt-utils&quot;
import { HTMLElement, parse } from &quot;node-html-parser&quot;

const componentHtml = await Astro.slots.render(&quot;default&quot;)
const rootHtmlNode = parse(componentHtml)

interface EncryptNode {
	node: HTMLElement
	depth: number
}

// 获取全部标签节点
let allNodes: EncryptNode[] = collectNodes(rootHtmlNode, 0)

// 按深度降序排列（最深在前）
allNodes.sort((a, b) =&amp;gt; b.depth - a.depth)

// 设置最大的处理深度小于等于3层（递归处理，尽量别设置太大）
const MAX_DEPTH = 3
const nodesToProcess: EncryptNode[] = allNodes.filter(
	(n) =&amp;gt; n.depth &amp;lt;= MAX_DEPTH,
)

// 最后处理（深度降序已保证最深优先）
for (const { node } of nodesToProcess) {
	// 获取预设的密码（属性不存在则不处理）
	const password = node.getAttribute(&quot;fuwari-encrypt-password&quot;)
	if (password) {
		// 获取预设的提示
		const tip = node.getAttribute(&quot;fuwari-encrypt-tip&quot;) || &quot;此内容受密码保护&quot;

		const routeSplit = Astro.url.pathname.split(&quot;/&quot;)
		// 将数据加密处理
		const encryptData = encode(
			deriveKey(password),
			routeSplit[routeSplit.length - 2],
			node.innerHTML,
		)

		// 生成标记（disableEntropyCache禁用缓存）
		const sign = crypto.randomUUID({ disableEntropyCache: true })

		// 添加存储密文的自定义属性和用于DOM操作时获得当前节点的标记
		node.setAttributes({
			&quot;data-encrypt-content&quot;: encryptData,
			&quot;data-encrypt-sign&quot;: sign,
		})

		const siblingDom = node.nextElementSibling
		if (siblingDom?.hasAttribute(&quot;fuwari-encrypt-password&quot;)) {
			node.setAttribute(&quot;style&quot;, &quot;margin-bottom: 1.25em;&quot;)
		}

		// 添加卡片样式html
		node.innerHTML = `
			&amp;lt;div class=&quot;password-protect-container&quot;&amp;gt;
				&amp;lt;div class=&quot;password-input-wrapper flex flex-col items-center justify-center min-h-[300px] p-8 rounded-xl bg-[var(--card-bg)] border border-[var(--line-divider)] shadow-sm&quot;&amp;gt;
					&amp;lt;div class=&quot;mb-8 text-center&quot;&amp;gt;
						&amp;lt;div class=&quot;text-4xl font-bold mb-3 text-[var(--primary)] transform transition-all&quot;&amp;gt;
							&amp;lt;svg width=&quot;1em&quot; height=&quot;1em&quot; class=&quot;inline-block hover:rotate-12 text-4xl&quot; data-icon=&quot;material-symbols:lock-outline&quot;&amp;gt;
								&amp;lt;symbol id=&quot;ai:material-symbols:lock-outline&quot; viewBox=&quot;0 0 24 24&quot;&amp;gt;
									&amp;lt;path fill=&quot;currentColor&quot; d=&quot;M6 22q-.825 0-1.412-.587T4 20V10q0-.825.588-1.412T6 8h1V6q0-2.075 1.463-3.537T12 1t3.538 1.463T17 6v2h1q.825 0 1.413.588T20 10v10q0 .825-.587 1.413T18 22zm0-2h12V10H6zm6-3q.825 0 1.413-.587T14 15t-.587-1.412T12 13t-1.412.588T10 15t.588 1.413T12 17M9 8h6V6q0-1.25-.875-2.125T12 3t-2.125.875T9 6zM6 20V10z&quot;&amp;gt;&amp;lt;/path&amp;gt;
								&amp;lt;/symbol&amp;gt;
								&amp;lt;use href=&quot;#ai:material-symbols:lock-outline&quot;&amp;gt;&amp;lt;/use&amp;gt;
							&amp;lt;/svg&amp;gt;
						&amp;lt;/div&amp;gt;
						&amp;lt;h3 class=&quot;text-xl font-semibold mb-2&quot;&amp;gt;${tip}&amp;lt;/h3&amp;gt;
						&amp;lt;p class=&quot;text-sm text-[var(--meta-divider)] mb-4&quot;&amp;gt;请输入密码以查看完整内容&amp;lt;/p&amp;gt;
						&amp;lt;div class=&quot;flex flex-col sm:flex-row gap-3 w-full max-w-md relative group&quot;&amp;gt;
							&amp;lt;input data-encrypt-sign=&quot;${sign}&quot; class=&quot;flex-1 rounded-lg border-2 border-[var(--line-divider)] bg-[var(--card-bg)] p-3 text-[var(--primary)] placeholder:italic placeholder:text-[var(--meta-divider)] focus:border-[var(--primary)] focus:outline-none transition-all duration-300 shadow-sm focus:shadow-md&quot; placeholder=&quot;请输入密码...&quot; type=&quot;password&quot; autocomplete=&quot;off&quot; autofocus=&quot;&quot;&amp;gt;
							&amp;lt;button data-encrypt-sign=&quot;${sign}&quot; class=&quot;px-6 py-3 bg-[var(--primary)] text-white rounded-lg hover:bg-[var(--primary)]/90 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]/50 transition-all duration-300 active:scale-95 shadow-sm hover:shadow-md sm:w-auto w-full&quot;&amp;gt; 解锁 &amp;lt;/button&amp;gt;
						&amp;lt;/div&amp;gt;
						&amp;lt;div data-encrypt-sign=&quot;${sign}&quot; class=&quot;password-error hidden mt-4 px-4 py-2 text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-md&quot;&amp;gt;
							&amp;lt;span class=&quot;flex items-center gap-1&quot;&amp;gt;
								&amp;lt;svg xmlns=&quot;https://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; class=&quot;inline&quot;&amp;gt;
									&amp;lt;circle cx=&quot;12&quot; cy=&quot;12&quot; r=&quot;10&quot;&amp;gt;&amp;lt;/circle&amp;gt;
									&amp;lt;line x1=&quot;12&quot; y1=&quot;8&quot; x2=&quot;12&quot; y2=&quot;12&quot;&amp;gt;&amp;lt;/line&amp;gt;
									&amp;lt;line x1=&quot;12&quot; y1=&quot;16&quot; x2=&quot;12.01&quot; y2=&quot;16&quot;&amp;gt;&amp;lt;/line&amp;gt;
								&amp;lt;/svg&amp;gt;
								密码错误，请重试
							&amp;lt;/span&amp;gt;
						&amp;lt;/div&amp;gt;
					&amp;lt;/div&amp;gt;
				&amp;lt;/div&amp;gt;
			&amp;lt;/div&amp;gt;
        `
	}
	// 不管处不处理都要移除掉这个属性
	node.removeAttribute(&quot;fuwari-encrypt-password&quot;)
	node.removeAttribute(&quot;fuwari-encrypt-tip&quot;)
}

const newHtmlString = rootHtmlNode.toString()

/**
 * 收集所有指定元素节点
 *
 * @param _element 根元素
 * @param _currentDepth 深度层级
 */
function collectNodes(_element: HTMLElement, _currentDepth: number) {
	let nodes: EncryptNode[] = []
	let depth = _currentDepth

	// 如果当前元素本身有自定义的属性，则深度+1
	if (_element.hasAttribute?.(&quot;fuwari-encrypt-password&quot;)) {
		depth = _currentDepth + 1
		nodes.push({ node: _element, depth: depth })
	} else {
		depth = _currentDepth
	}

	// 递归处理子节点（只处理元素节点）
	const children = _element.childNodes || []
	for (const child of children) {
		if (child.nodeType === 1) {
			// 元素节点
			nodes.push(...collectNodes(child as HTMLElement, depth))
		}
	}
	return nodes
}
---

&amp;lt;div id=&quot;encrypt-block&quot; set:html={newHtmlString}&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script&amp;gt;
import { deriveKey, decode } from &quot;@/utils/encrypt-utils&quot;

function getAad(){
	const routeSplit = window.location.pathname.split(&quot;/&quot;)
	return routeSplit[routeSplit.length - 2]
}

function savelocalCache(sign: string, password: string){
	try{
		if(!sign || !password){
			return
		}
		const getCache = window.localStorage.getItem(window.location.pathname) || &apos;&apos;
		const parseCache = JSON.parse(getCache)
		const dataArray = parseCache.data

		const signExist = new Set(dataArray.map((item: {key: string}) =&amp;gt; item.key)).has(sign)
		if(!signExist){
			dataArray.push({
				key: sign,
				value: password
			})

			parseCache.time = Date.now() + 24 * 60 * 60 * 1000
			window.localStorage.setItem(window.location.pathname, JSON.stringify(parseCache))
		}
	}catch(err){
		window.localStorage.setItem(window.location.pathname, JSON.stringify({
			data: [
				{
					key: sign,
					value: password,
				}
			],
			time: Date.now() + 24 * 60 * 60 * 1000
		}))
	}
}

function loadLocalCahce(){
	try{
		const getCache = window.localStorage.getItem(window.location.pathname) || &quot;&quot;
		const parseCache = JSON.parse(getCache)
		const dataArray = parseCache.data
		const time = parseCache.time

		const getValueFun = (arr: Array&amp;lt;{key: string, value: string}&amp;gt;, key: string) =&amp;gt; {
			return arr.filter(obj =&amp;gt; obj.key === key).map(obj =&amp;gt; obj.value)[0]
		}

		const recursion = (el: Element) =&amp;gt; {
			const allEncryptDivDom = el?.querySelectorAll(&quot;div[data-encrypt-content][data-encrypt-sign]&quot;)
			allEncryptDivDom?.forEach((el: Element) =&amp;gt; {
				const sign = el.getAttribute(&quot;data-encrypt-sign&quot;) || &quot;&quot;
				const signExist = new Set(dataArray.map((item: {key: string}) =&amp;gt; item.key)).has(sign)
				if(signExist){
					const password = getValueFun(dataArray, sign)
					try{
						const decodeContent = decode(password, getAad(), el.getAttribute(&quot;data-encrypt-content&quot;) || &quot;&quot;)
						el.innerHTML = decodeContent
					}catch(e){
						// 主动捕获异常，避免输出异常信息
						console.warn(`解密失败 route:[${window.location.pathname}], sign:[${sign}], password:[${password}]`, e)
					}
					setTimeout(()=&amp;gt;{
						recursion(el)
					}, 10)
				}
			})
		}

		if(Date.now() &amp;gt;= time){
			window.localStorage.removeItem(window.location.pathname)
			return
		}

		recursion(document.getElementById(&quot;encrypt-block&quot;) as HTMLElement)
	}catch(err){
		console.error(err)
	}
}

function bindEvent(){
	// 创建按钮加载动画
	const createLoadingSpinner = (el: HTMLButtonElement) =&amp;gt; {
		const spinnerHTML = `
				&amp;lt;svg class=&quot;animate-spin h-5 w-5 text-white&quot; xmlns=&quot;https://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot;&amp;gt;
					&amp;lt;circle class=&quot;opacity-25&quot; cx=&quot;12&quot; cy=&quot;12&quot; r=&quot;10&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;4&quot;&amp;gt;&amp;lt;/circle&amp;gt;
					&amp;lt;path class=&quot;opacity-75&quot; fill=&quot;currentColor&quot; d=&quot;M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z&quot;&amp;gt;&amp;lt;/path&amp;gt;
				&amp;lt;/svg&amp;gt;
			`
		const spinnerContainer = document.createElement(&quot;div&quot;)
		spinnerContainer.innerHTML = spinnerHTML
		el.innerHTML = spinnerContainer.innerHTML
	}

	// 移除按钮加载动画
	const removeLoadingSpinner = (el: HTMLButtonElement, text: string) =&amp;gt; {
		el.innerHTML = text
	}

	const handleSubmit = () =&amp;gt; {
		const encryptBlockDom = document.getElementById(&quot;encrypt-block&quot;) as HTMLElement
		const encryptContentDom = encryptBlockDom.querySelectorAll&amp;lt;HTMLElement&amp;gt;(&quot;div[data-encrypt-content][data-encrypt-sign]&quot;)
		encryptContentDom.forEach((el: HTMLElement) =&amp;gt; {
			const sign = el.getAttribute(&quot;data-encrypt-sign&quot;) || &quot;&quot;
			const encryptContent = el.getAttribute(&quot;data-encrypt-content&quot;) || &quot;&quot;
			const input = el.querySelector&amp;lt;HTMLInputElement&amp;gt;(`input[data-encrypt-sign=&quot;${sign}&quot;]`)
			const button = el.querySelector&amp;lt;HTMLButtonElement&amp;gt;(`button[data-encrypt-sign=&quot;${sign}&quot;]`)
			const message = el.querySelector&amp;lt;HTMLDivElement&amp;gt;(`div[data-encrypt-sign=&quot;${sign}&quot;][class*=&quot;password-error&quot;]`)
			if(!input || !button || !message) {
				console.warn(&quot;The specified element cannot be operated on because it does not exist.&quot;)
				return
			}

			// 添加输入框动画效果
			if (input) {
				input.addEventListener(&quot;focus&quot;, () =&amp;gt; {
					input.classList.add(&quot;border-[var(--primary)]&quot;, &quot;shadow-md&quot;)
				})

				input.addEventListener(&quot;blur&quot;, () =&amp;gt; {
					if (!input.value.trim()) {
						input.classList.remove(&quot;border-[var(--primary)]&quot;, &quot;shadow-md&quot;)
					}
				})
			}

			const submitEvent  = async () =&amp;gt; {
				const password = input.value.trim()
				if(!password){
					// 如果没有输入密码，添加一个轻微抖动效果
					input.classList.add(&quot;animate-shake&quot;)
					setTimeout(() =&amp;gt; input.classList.remove(&quot;animate-shake&quot;), 500)
					return
				}

				// 禁用，阻止可再点击
				input.disabled = true
				button.disabled = true
				
				// 输入框样式变化、按钮加载动画、提示内容
				input.classList.add(&quot;opacity-50&quot;)
				createLoadingSpinner(button)
				message.classList.add(&quot;hidden&quot;)

				// 添加一个短暂的延迟，提供更好的视觉反馈
				await new Promise((resolve) =&amp;gt; setTimeout(resolve, 500))

				try{
					const derivedKeyValue = deriveKey(password)
					const html = decode(derivedKeyValue, getAad(), encryptContent)
					removeLoadingSpinner(button, &quot;√成功&quot;)
					button.classList.remove(&quot;bg-[var(--primary)]&quot;)
					button.classList.add(&quot;bg-green-500&quot;)

					savelocalCache(sign, derivedKeyValue)
					input.value = &quot;&quot;

					// 延迟执行添加一个透明度过渡效果
					setTimeout(() =&amp;gt; {
						el.style.opacity = &quot;0&quot;

						setTimeout(() =&amp;gt; {
							el.innerHTML = html
							el.style.opacity = &quot;1&quot;
							el.style.transition = &quot;opacity 0.3s ease-in-out&quot;
						}, 300)
					}, 500)
				}catch(er){
					message.classList.add(&quot;animate-shake&quot;)
					message.classList.remove(&quot;hidden&quot;), 20
					setTimeout(() =&amp;gt; message.classList.remove(&quot;animate-shake&quot;), 500)

					button.innerHTML = &quot;重试&quot;

					input.classList.remove(&quot;opacity-50&quot;)

					input.disabled = false
					button.disabled = false

					input.focus()
				}
			}

			// 绑定按钮事件
			button.addEventListener(&quot;click&quot;, submitEvent)
			// 绑定回车按下事件
			input.addEventListener(&quot;keypress&quot;, (e) =&amp;gt; {
				if (e.key === &quot;Enter&quot; &amp;amp;&amp;amp; document.activeElement === input) {
					submitEvent()
				}
			})

			// 页面切换时，清理输入框内容（内容为空则能阻止浏览器跳出保存密码的弹窗）
			document.addEventListener(&quot;astro:before-swap&quot;, () =&amp;gt; {
				if(!window.location.pathname.startsWith(&quot;/posts/&quot;)){
					input.value = &quot;&quot;
				}
			})
		})
	}

	const observer = new MutationObserver((mutationsList, observer) =&amp;gt; {
		mutationsList.forEach(mutation =&amp;gt; {
			if(mutation.type === &apos;childList&apos;){
				handleSubmit()
			}
		});
	})
	observer.observe(document.getElementById(&quot;encrypt-block&quot;) as HTMLElement, {
		childList: true, // 监听子节点的添加或删除（true/false）
		attributes: true, // 监听属性变化（true/false）
		attributeFilter: [&quot;data-encrypt-content&quot;, &quot;data-encrypt-sign&quot;], // 指定监听的属性名数组（如 [&apos;class&apos;, &apos;id&apos;]），不指定则监听所有属性
		attributeOldValue: false, // 记录属性旧值（需 attributes: true）
		characterData: true, // 监听文本节点内容变化（true/false）
		characterDataOldValue: false, // 记录文本旧值（需 characterData: true）
		subtree: true // 监听所有后代节点的变化（true/false）
	})
	handleSubmit()

	// 当页面切换时，手动关闭dom监听服务
	document.addEventListener(&quot;astro:before-swap&quot;, () =&amp;gt; {
		if(!window.location.pathname.startsWith(&quot;/posts/&quot;)){
			observer?.disconnect()
		}
	})
}

// 浏览器直接访问的情况下直接初始化
loadLocalCahce()
bindEvent()

// 由于Astro的View Transitions机制的问题，所以我们需要监听这个事件，才能让页面切换时初始化
document.addEventListener(&quot;astro:page-load&quot;, () =&amp;gt; {
	if(window.location.pathname.startsWith(&quot;/posts/&quot;)){
		loadLocalCahce()
		bindEvent()
	}
})

&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用组件&lt;/h2&gt;
&lt;p&gt;在文件 &lt;code&gt;src/pages/posts/[...slug].astro&lt;/code&gt; 文件修改代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
// 忽略其他代码
import EnctyprCard from &quot;@/components/md/EnctyprCard.astro&quot;
---

&amp;lt;!-- 
    忽略其他代码
    原本的代码大概在文件第106行左右
 --&amp;gt;
&amp;lt;Markdown class=&quot;mb-6 markdown-content onload-animation&quot;&amp;gt;
    &amp;lt;EnctyprCard&amp;gt;
        &amp;lt;Content /&amp;gt;
    &amp;lt;/EnctyprCard&amp;gt;
&amp;lt;/Markdown&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;文章中使用&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div fuwari-encrypt-password=&quot;密码（必须）&quot; fuwari-encrypt-tip=&quot;提示内容（可为空）&quot;&amp;gt;
加密内容，注意可以不用写 `&amp;lt;/div&amp;gt;` 这样的闭合标签
除非你需要下一个加密块与这个同级，如下：

&amp;lt;/div&amp;gt;&amp;lt;div fuwari-encrypt-password=&quot;密码（必须）&quot; fuwari-encrypt-tip=&quot;提示内容（可为空）&quot;&amp;gt;
同级加密块，在前面加个闭合标签即可
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Astro&amp;Fuwari可控制封面图在文章内显示与隐藏</title><link>https://blog.anxko.cn/posts/dac47d9d85a1047e/</link><guid isPermaLink="true">https://blog.anxko.cn/posts/dac47d9d85a1047e/</guid><description>高情商：优化文章内容显示。低情商：净整些没用的。</description><pubDate>Thu, 23 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;文章内添加元数据&lt;/h1&gt;
&lt;p&gt;在文章头部元数据设置中添加自定义的，如：imageShow，用于控制是否显示封面图&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
imageShow: true
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/dac47d9d85a1047e/f1d069be-d9a0-4d56-a2b6-3fbc5c0e1dc1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;新增元数据类型&lt;/h1&gt;
&lt;p&gt;在 &lt;code&gt;src/content/config.ts&lt;/code&gt; 文件中添加一行对应的元数据类型，并设置为boolean属性且默认为true，意思就是默认为显示图片&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const postsCollection = defineCollection({
	schema: z.object({
		// ...忽略其他多余代码...
		imageShow: z.boolean().optional().default(true), // 添加这个自定义属性
	}),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;修改判断条件&lt;/h1&gt;
&lt;p&gt;在 &lt;code&gt;src/pages/posts/[...slug].astro&lt;/code&gt; 文件中，大概100行找到这行代码，并添加一个判断image是否也为true的条件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- entry.data.imageShow &amp;amp;&amp;amp; --&amp;gt;
{entry.data.image &amp;amp;&amp;amp; entry.data.imageShow &amp;amp;&amp;amp;
	&amp;lt;ImageWrapper id=&quot;post-cover&quot; src={entry.data.image} basePath={path.join(&quot;content/posts/&quot;, getDir(entry.id))} class=&quot;mb-8 rounded-xl banner-container onload-animation&quot;/&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Windows10/11永久禁止系统自动更新</title><link>https://blog.anxko.cn/posts/606de2dcd15551ab/</link><guid isPermaLink="true">https://blog.anxko.cn/posts/606de2dcd15551ab/</guid><description>Windows总是自动更新？快快阻止它！(锁喉中...)</description><pubDate>Fri, 17 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;只需键盘按下 &lt;code&gt;win + R&lt;/code&gt; ，在弹出的界面输入框中输入 &lt;code&gt;regedit&lt;/code&gt; 后确定
&lt;img src=&quot;/images/606de2dcd15551ab/20ba89e4-8ebd-42f2-9e36-6d5806c0cbc5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在左侧目录依次打开，找到 &lt;code&gt;计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsUpdate\UX&lt;/code&gt; 中的 &lt;code&gt;Settings&lt;/code&gt; 文件夹，在其中鼠标右键指针放在 &lt;code&gt;创建&lt;/code&gt; 上，选择 &lt;code&gt;DWORD（32位值）&lt;/code&gt; 的选项
&lt;img src=&quot;/images/606de2dcd15551ab/d98a0f75-55b6-4d70-8dfd-5dff04f8e014.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;鼠标右键刚才创建的新值，然后重命名为 &lt;code&gt;FlightSettingsMaxPauseDays&lt;/code&gt;
&lt;img src=&quot;/images/606de2dcd15551ab/5a5f9323-dff9-4aca-8a3f-63c308d257d7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;鼠标右键 &lt;code&gt;FlightSettingsMaxPauseDays&lt;/code&gt; ，选择 &lt;code&gt;修改&lt;/code&gt;
&lt;img src=&quot;/images/606de2dcd15551ab/66f9cc5d-be1b-4417-994b-fe043b0adee9.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在弹窗中先选择基数为 &lt;code&gt;十进制&lt;/code&gt; ，然后再输入想要停止更新的天数，如：4500天（注意不要设置过大，不然下一步骤会很累）
&lt;img src=&quot;/images/606de2dcd15551ab/5825da4a-188b-457d-addd-ef4f6146a37a.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后在设置中找到windows更新，里面选择暂停更新，然后一直往下拉，找到最长时间的周选项即可
&lt;img src=&quot;/images/606de2dcd15551ab/aea035cb-1342-4acf-ac6d-725ec026b8d9.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>AndroidStudio通用汉化教程</title><link>https://blog.anxko.cn/posts/dd89de07da151af3/</link><guid isPermaLink="true">https://blog.anxko.cn/posts/dd89de07da151af3/</guid><description>使用Jetbrains中文插件解决AndroidStudio的汉化问题</description><pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;首先去Jetbrains插件官网下载中文插件包：
&amp;lt;a href=&quot;https://plugins.jetbrains.com/plugin/13710-chinese-simplified-language-pack----&quot; target=&quot;_blank&quot;&amp;gt;下载中文插件包&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/dd89de07da151af3/01454fb5-e824-4d95-b0b7-1e2c8cb62d67.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;将下载下来的压缩包解压，在其中找到叫lib的文件夹里的.jar文件，因为jar包本身就是一种压缩包，所以直接将这个jar包解压出来即可
&lt;img src=&quot;/images/dd89de07da151af3/47455d4e-d6e7-4151-9e7b-7f7ded2c7b06.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在解压出的jar包文件中找到并编辑 &lt;code&gt;.\META-INF\plugin.xml&lt;/code&gt; 文件
&lt;img src=&quot;/images/dd89de07da151af3/5e6dd9c5-760b-400d-bd49-a71b2b6948b7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打开AndroidStudio，点击左下角的齿轮-&amp;gt;About查看版本信息，复制其中的AI-xxx.xxx.xxx
&lt;img src=&quot;/images/dd89de07da151af3/84e0e2b4-4377-4827-9bef-a99eb6387a54.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;粘贴到plugin.xml文件中三个位置后保存，将解压出来的jar包文件全部重新压缩成zip，修改文件后缀名为.jar
&lt;img src=&quot;/images/dd89de07da151af3/ef537da1-4d5c-461a-822e-47b32481f643.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;/images/dd89de07da151af3/cd3e3f7d-5aef-4661-a963-b0c096fe32da.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在AndroidStudio中点击 &lt;code&gt;Plugins&lt;/code&gt;-&amp;gt; &lt;code&gt;Installed&lt;/code&gt; -&amp;gt; &lt;code&gt;Install Plugin from Disk&lt;/code&gt; ，选择刚才修改过重新压缩的jar包进行安装
&lt;img src=&quot;/images/dd89de07da151af3/1bcdb8e9-6df0-4197-93bf-0e6ace0017e1.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;/images/dd89de07da151af3/ad8a2991-3dc4-4fc3-b0dc-73f42ebaf4d0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在AndroidStudio主页点击 &lt;code&gt;Customize&lt;/code&gt; -&amp;gt; &lt;code&gt;Get More Languages&lt;/code&gt;
&lt;img src=&quot;/images/dd89de07da151af3/a6a0e8c3-2ae8-4931-b2c5-0baa05b0679d.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;默认选第一个就行，可以看到右下角的OK和Apply按钮是亮的，点击OK后，就会发现可以选择Chinese了
&lt;img src=&quot;/images/dd89de07da151af3/62dfdbcc-bdfb-4c72-8a31-fb38e631a745.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;/images/dd89de07da151af3/bd73f8ec-7b04-47d0-9522-43d18ad13885.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后重启即可
&lt;img src=&quot;/images/dd89de07da151af3/57afb4d3-c7df-42ae-8032-1b3f30d80dba.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>欢迎来到我的船新博客！</title><link>https://blog.anxko.cn/posts/helloworld/</link><guid isPermaLink="true">https://blog.anxko.cn/posts/helloworld/</guid><description>欢迎光临我的互联网新小窝！这里装满我的热爱、思考与碎碎念，常来坐坐~</description><pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;以前都是使用由PHP语言编写的Typecho或者WordPress搭建的，后来发现出了个由Java语言编写的新博客程序Halo。因其现代化的UI设计，以及使用的新时代技术，所以选择了使用它，但是其本质还是动态网站，需要依赖于后端程序，又因其技术架构原因需要使用到服务器，只为了博客网站这成本属实是有点不值得。
好在近年来国内厂商也是学上CF整上免费安全CDN以及Pages部署服务了（以前我不使用CF，很大原因是因为最开始玩上这些时，我就直接选择了国内云服务，而CF会导致国内访问速度大打折扣！），不过现在有了国内厂商的Pages部署，那就很棒了！&lt;/p&gt;
</content:encoded></item></channel></rss>