<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss/feed.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>薰逸の猫窝</title><description>埋骨何须桑梓地，人生无处不青山。</description><link>https://xunyi.cloud</link><item><title>不到 100 行代码实现人体动作识别：Python + MediaPipe 实时骨骼动画</title><link>https://xunyi.cloud/post/note/ai/mediapipe-pose-detection</link><guid isPermaLink="false">note/ai/mediapipe-pose-detection</guid><description>曾经想过让电脑像镜子一样实时模仿你的动作吗？或者创建一个 AI 健身教练来纠正你的姿势？今天，我们将一起动手实现这个酷炫的想法！本文将带你从零开始，使用 Python 和 Google 的 MediaPipe 框架，构建一个能够实时识别人体动作并绘制出骨骼动画的程序。</description><pubDate>Sat, 29 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;曾经想过让电脑像镜子一样实时模仿你的动作吗？或者创建一个 AI 健身教练来纠正你的姿势？今天，我们将一起动手实现这个酷炫的想法！本文将带你从零开始，使用 Python 和 Google 的 MediaPipe 框架，构建一个能够实时识别人体动作并绘制出骨骼动画的程序。&lt;/p&gt;
&lt;p&gt;最棒的是？整个过程非常简单，代码不到 100 行，对初学者也极其友好！&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;/img/posts/note/ai/mediapipe-pose-detection/9c4c6404a8cf9f6f9736ce61a73670f1.webp&quot; alt=&quot;在这里插入图片描述&quot; loading=&quot;lazy&quot; /&gt;&lt;figcaption&gt;在这里插入图片描述&lt;/figcaption&gt;&lt;/figure&gt;
&lt;blockquote&gt;
&lt;p&gt;演示图片来自&lt;a href=&quot;https://www.bilibili.com/video/BV13x4y1G7xe?buvid=XU1204A183FF9E3162FF9331389097176F711&amp;amp;from_spmid=main.my-history.0.0&amp;amp;is_story_h5=false&amp;amp;mid=BMlrQLPXyp0dXnwSBBFjcA%3D%3D&amp;amp;plat_id=116&amp;amp;share_from=ugc&amp;amp;share_medium=android&amp;amp;share_plat=android&amp;amp;share_session_id=6f8669a8-7881-4ec2-b018-404b3c9833ee&amp;amp;share_source=WEIXIN&amp;amp;share_tag=s_i&amp;amp;spmid=united.player-video-detail.0.0&amp;amp;timestamp=1760633959&amp;amp;unique_k=y2o5Ol2&amp;amp;up_id=1813797500&amp;amp;vd_source=669d3673e44303b48f278f22b39659db&quot;&gt;【电工版】ラヴィット ◯ Loveit 兮白版编舞翻跳，擦边不存在的！！&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;🧐 这是什么技术？&lt;a href=&quot;#-这是什么技术&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;我们即将实现的功能，在计算机视觉领域被称为“&lt;strong&gt;人体姿态估计 (Human Pose Estimation)&lt;/strong&gt;”。&lt;/p&gt;
&lt;p&gt;它的核心任务是：从一张图片或一帧视频中，识别出人体的关键节点（如肩膀、手肘、膝盖、手腕等），并把它们连接起来，形成一个“骨骼”框架。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MediaPipe 是由 Google Research 开发并开源的一个功能强大的、跨平台的机器学习解决方案框架。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;它的设计目标是让开发者能够轻松地构建和部署包含多种模式（如视频、音频、时序数据）的复杂机器学习应用管道（Pipelines）。&lt;/p&gt;
&lt;p&gt;你可以把它想象成一个“&lt;strong&gt;AI 积木盒&lt;/strong&gt;”。Google 已经帮你把许多复杂的 AI 功能（比如人脸检测、手势识别、我们这次用的姿态估计）封装成了一个个即插即用的“积木块”（官方称为 &lt;strong&gt;Solutions&lt;/strong&gt;）。我们开发者所要做的，就是像搭积木一样，简单地调用这些功能，快速搭建出自己的应用，而无需从头训练复杂的深度学习模型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么选择 MediaPipe？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;除了我们在前面提到的几点，MediaPipe 的核心优势可以归纳为以下几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;🚀 现成的解决方案 (Ready-to-use Solutions)&lt;/strong&gt; MediaPipe 不仅仅能做姿态估计。它提供了一整套丰富的预构建解决方案，涵盖了&lt;strong&gt;人脸检测、面部网格（Face Mesh）、手势追踪、物体检测、图像分割、头发分割&lt;/strong&gt;等众多领域。这些方案都经过了高度优化，开箱即用，让你的创意可以快速落地。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;⚡️ 极致的性能 (Extreme Performance)&lt;/strong&gt; 性能是 MediaPipe 的核心优势之一。它的处理管道专为低延迟和高吞吐量设计，即使在资源有限的设备（如中端智能手机）或普通个人电脑的 CPU 上也能实现令人惊叹的实时处理效果。这也是为什么我们的骨骼追踪程序如此流畅的原因。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;🌍 一次编写，随处部署 (Write Once, Deploy Anywhere)&lt;/strong&gt; MediaPipe 的架构设计使其具有极高的可移植性。你用 Python 开发的应用原型，其核心逻辑可以非常轻松地迁移到 &lt;strong&gt;Android (Java/C++)&lt;/strong&gt;, &lt;strong&gt;iOS (Objective-C/C++)&lt;/strong&gt;, &lt;strong&gt;C++ 桌面应用&lt;/strong&gt;，甚至是 &lt;strong&gt;Web (JavaScript/Wasm)&lt;/strong&gt;。这为应用的跨平台部署节省了大量的时间和精力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;🛠️ 可定制与可扩展 (Customizable and Extensible)&lt;/strong&gt; 虽然 MediaPipe 提供了现成的解决方案，但它本质上是一个非常灵活的框架。对于高级用户，你可以深入其内部，自定义处理流程图（Graph），替换或组合不同的计算模块（Calculator），或者将自己的 TensorFlow Lite 模型集成进去，构建独一无二的 AI 应用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;想要深入探索 MediaPipe 的所有功能,请访问它的官方网站：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://ai.google.dev/edge/mediapipe/solutions/guide?hl=zh-cn&quot;&gt;MediaPipe 官方网站&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/google/mediapipe&quot;&gt;GitHub 官方仓库&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;🚀 第一步：准备你的开发环境&lt;a href=&quot;#-第一步准备你的开发环境&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;在敲代码之前，我们需要安装两个核心的 Python 库：&lt;code&gt;opencv-python&lt;/code&gt; 和 &lt;code&gt;mediapipe&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OpenCV&lt;/strong&gt;: 一个强大的计算机视觉库，我们用它来捕获摄像头画面、处理图像和显示结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MediaPipe&lt;/strong&gt;: 我们项目的“大脑”，负责所有姿态估计的 AI 计算。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;打开你的终端（Terminal、CMD 或 PowerShell），然后运行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;pip&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; opencv-python&lt;/span&gt;&lt;span&gt; mediapipe&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确保你的电脑连接了一个摄像头，然后我们就可以开始编写代码了！&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;👨‍💻 第二步：编写核心代码（逐行详解）&lt;a href=&quot;#-第二步编写核心代码逐行详解&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;创建一个名为 &lt;code&gt;dance_tracker.py&lt;/code&gt; 的 Python 文件，然后跟着下面的步骤，把代码一块一块地放进去。&lt;/p&gt;
&lt;h4&gt;1. 导入工具并初始化&lt;a href=&quot;#1-导入工具并初始化&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;首先，我们需要导入 &lt;code&gt;cv2&lt;/code&gt; (OpenCV) 和 &lt;code&gt;mediapipe&lt;/code&gt;，并初始化姿态估计模型和绘图工具。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; cv2&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; mediapipe &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; mp&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 初始化 MediaPipe Pose 模型&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;mp_pose &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; mp.solutions.pose&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;pose &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; mp_pose.Pose(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    min_detection_confidence&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;0.5&lt;/span&gt;&lt;span&gt;,  &lt;/span&gt;&lt;span&gt;# 检测置信度阈值&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    min_tracking_confidence&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;0.5&lt;/span&gt;&lt;span&gt;    # 追踪置信度阈值&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 初始化 MediaPipe 的绘图工具&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;mp_drawing &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; mp.solutions.drawing_utils&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mp.solutions.pose&lt;/code&gt;: 这是 MediaPipe 提供的姿态估计解决方案。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mp_pose.Pose()&lt;/code&gt;: 我们在这里创建了一个姿态估计器的实例。两个&lt;code&gt;confidence&lt;/code&gt;参数决定了模型的敏感度，0.5 是一个很好的默认值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mp.solutions.drawing_utils&lt;/code&gt;: 这是一套方便的工具，可以帮我们轻松地将识别出的骨骼绘制到图像上。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 打开摄像头并开始主循环&lt;a href=&quot;#2-打开摄像头并开始主循环&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;接下来，我们需要访问电脑的摄像头，并创建一个循环来不断地读取画面。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# 打开默认摄像头&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;cap &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cv2.VideoCapture(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; not&lt;/span&gt;&lt;span&gt; cap.isOpened():&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;错误：无法打开摄像头。&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    exit&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 实时处理摄像头的每一帧&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;while&lt;/span&gt;&lt;span&gt; cap.isOpened():&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    success, image &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cap.read()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; not&lt;/span&gt;&lt;span&gt; success:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;忽略了一个空帧。&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        continue&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # ... 后续处理代码将放在这里 ...&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 循环结束后，释放资源&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;cap.release()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;cv2.destroyAllWindows()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cv2.VideoCapture(0)&lt;/code&gt;: &lt;code&gt;0&lt;/code&gt; 通常代表你电脑的默认摄像头。如果你有多个摄像头，可以尝试 &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt; 等。如果你想识别视频，就输入’视频路径’，例如&lt;code&gt;cap = cv2.VideoCapture(&apos;miku.mov&apos;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;while cap.isOpened()&lt;/code&gt;: 这是一个经典的循环结构，只要摄像头开着，就一直执行循环内的代码。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cap.read()&lt;/code&gt;: 这个函数会返回两个值：&lt;code&gt;success&lt;/code&gt; (一个布尔值，表示是否成功读取) 和 &lt;code&gt;image&lt;/code&gt; (读取到的那一帧画面)。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 图像处理与姿态估计&lt;a href=&quot;#3-图像处理与姿态估计&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;这是最核心的部分。在循环内部，我们将对每一帧图像进行处理。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    # 为了提高性能，将图像标记为不可写&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    image.flags.writeable &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; False&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # MediaPipe 需要 RGB 格式，但 OpenCV 是 BGR，所以需要转换&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    image_rgb &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cv2.cvtColor(image, cv2.&lt;/span&gt;&lt;span&gt;COLOR_BGR2RGB&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # 使用 MediaPipe 模型处理图像，得到姿态结果&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    results &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; pose.process(image_rgb)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # 处理完后，再把图像标记为可写，以便绘制&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    image.flags.writeable &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; True&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;颜色转换&lt;/strong&gt;: 这是一个非常重要的细节！OpenCV 默认使用 BGR 色彩空间，而几乎所有其他的库（包括 MediaPipe）都使用 RGB。如果不转换，模型将无法正确识别颜色，导致性能下降。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;pose.process(image_rgb)&lt;/code&gt;&lt;/strong&gt;: ✨ &lt;strong&gt;魔法发生的地方！&lt;/strong&gt; 就是这一行代码，调用了复杂的 AI 模型，对输入的 RGB 图像进行分析，然后返回一个包含所有姿态信息的 &lt;code&gt;results&lt;/code&gt; 对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;image.flags.writeable&lt;/code&gt;: 这是 MediaPipe 官方推荐的一个性能优化技巧。通过临时将图像设为“只读”，可以避免不必要的数据拷贝，提高处理速度。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 绘制骨骼并显示结果&lt;a href=&quot;#4-绘制骨骼并显示结果&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;模型已经给了我们结果，现在需要把它可视化地画出来。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    # 在图像上绘制骨骼&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; results.pose_landmarks:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        mp_drawing.draw_landmarks(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            image,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            results.pose_landmarks,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            mp_pose.&lt;/span&gt;&lt;span&gt;POSE_CONNECTIONS&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            # 自定义关节点和连接线的样式&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            landmark_drawing_spec&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;mp_drawing.DrawingSpec(&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;thickness&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;circle_radius&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            connection_drawing_spec&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;mp_drawing.DrawingSpec(&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;thickness&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;circle_radius&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # 将图像水平翻转，产生镜像效果&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    flipped_image &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cv2.flip(image, &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # 创建窗口并显示图像&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    cv2.imshow(&lt;/span&gt;&lt;span&gt;&apos;Real-time Dance Skeleton by MediaPipe&apos;&lt;/span&gt;&lt;span&gt;, flipped_image)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # 如果按下 &apos;q&apos; 键或 ESC 键，则退出循环&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; cv2.waitKey(&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt; 0x&lt;/span&gt;&lt;span&gt;FF&lt;/span&gt;&lt;span&gt; in&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;ord&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;q&apos;&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;27&lt;/span&gt;&lt;span&gt;]:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        break&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;results.pose_landmarks&lt;/code&gt;: 如果模型在画面中检测到了人，这个属性就会包含所有 33 个身体关节点的位置信息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mp_drawing.draw_landmarks()&lt;/code&gt;: 这个函数帮我们完成了所有繁琐的绘图工作。我们只需要把原始图像、关节点信息和预设的连接方式 (&lt;code&gt;mp_pose.POSE_CONNECTIONS&lt;/code&gt;) 传给它就行了。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cv2.flip(image, 1)&lt;/code&gt;: 水平翻转图像，这样屏幕里的动作就会像照镜子一样，与你的左右方向一致，体验更好。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cv2.imshow()&lt;/code&gt; 和 &lt;code&gt;cv2.waitKey()&lt;/code&gt;: 这两行代码负责显示窗口，并检测键盘输入，以便我们可以通过按 &lt;code&gt;q&lt;/code&gt; 键来优雅地关闭程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;✅ 第三步：整合代码并运行！&lt;a href=&quot;#-第三步整合代码并运行&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;将上面所有的代码片段整合到你的 &lt;code&gt;dance_tracker.py&lt;/code&gt; 文件中。完整的代码如下：&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# dance_tracker.py&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 一个使用 MediaPipe 和 OpenCV 的实时人体姿态估计程序&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; cv2&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; mediapipe &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; mp&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 初始化 MediaPipe Pose 模型&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;mp_pose &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; mp.solutions.pose&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;pose &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; mp_pose.Pose(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    min_detection_confidence&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;0.5&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    min_tracking_confidence&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;0.5&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 初始化 MediaPipe 的绘图工具&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;mp_drawing &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; mp.solutions.drawing_utils&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 打开默认摄像头&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;cap &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cv2.VideoCapture(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; not&lt;/span&gt;&lt;span&gt; cap.isOpened():&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;错误：无法打开摄像头。&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    exit&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 实时处理摄像头的每一帧&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;while&lt;/span&gt;&lt;span&gt; cap.isOpened():&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    success, image &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cap.read()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; not&lt;/span&gt;&lt;span&gt; success:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;忽略了一个空帧。&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        continue&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # 提高性能：将图像标记为不可写&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    image.flags.writeable &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; False&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # 转换色彩空间 BGR -&amp;gt; RGB&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    image_rgb &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cv2.cvtColor(image, cv2.&lt;/span&gt;&lt;span&gt;COLOR_BGR2RGB&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # 运行姿态估计&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    results &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; pose.process(image_rgb)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # 将图像标记为可写，以便绘制&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    image.flags.writeable &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; True&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # 在图像上绘制骨骼&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; results.pose_landmarks:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        mp_drawing.draw_landmarks(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            image,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            results.pose_landmarks,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            mp_pose.&lt;/span&gt;&lt;span&gt;POSE_CONNECTIONS&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            landmark_drawing_spec&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;mp_drawing.DrawingSpec(&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;thickness&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;circle_radius&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            connection_drawing_spec&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;mp_drawing.DrawingSpec(&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;thickness&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;circle_radius&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # 产生镜像效果&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    flipped_image &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cv2.flip(image, &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # 显示结果&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    cv2.imshow(&lt;/span&gt;&lt;span&gt;&apos;Real-time Dance Skeleton by MediaPipe&apos;&lt;/span&gt;&lt;span&gt;, flipped_image)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # 按 &apos;q&apos; 或 ESC 退出&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; cv2.waitKey(&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt; 0x&lt;/span&gt;&lt;span&gt;FF&lt;/span&gt;&lt;span&gt; in&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;ord&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;q&apos;&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;27&lt;/span&gt;&lt;span&gt;]:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        break&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 释放资源&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;cap.release()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;cv2.destroyAllWindows()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;现在，打开终端，进入文件所在的目录，然后运行它！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;python&lt;/span&gt;&lt;span&gt; dance_tracker.py&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个窗口应该会弹出，显示你的摄像头画面。当你出现在镜头前时，一个由绿色节点和蓝色线条组成的骨骼动画就会神奇地叠加在你的身上，并实时跟随你的每一个动作！&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;💡 常见问题与展望&lt;a href=&quot;#-常见问题与展望&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;h4&gt;1: 程序报错“无法打开摄像头”怎么办？&lt;a href=&quot;#1-程序报错无法打开摄像头怎么办&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;这通常是权限问题，特别是在 macOS 或 Windows 上。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;macOS&lt;/strong&gt;: 请进入 &lt;code&gt;系统设置 &amp;gt; 隐私与安全 &amp;gt; 摄像头&lt;/code&gt;，确保你用来运行脚本的程序（如“终端”或你的代码编辑器 VS Code）的权限开关是打开的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Windows&lt;/strong&gt;: 请进入 &lt;code&gt;设置 &amp;gt; 隐私和安全性 &amp;gt; 相机&lt;/code&gt;，确保“允许应用访问你的相机”是开启的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2&lt;div&gt;&lt;/div&gt;&lt;a href=&quot;#2&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;当你成功运行了基础版本后，还可以尝试做一些更有趣的事情：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;处理视频文件&lt;/strong&gt;： 将 &lt;code&gt;cap = cv2.VideoCapture(0)&lt;/code&gt; 改为 &lt;code&gt;cap = cv2.VideoCapture(&apos;你的视频文件.mp4&apos;)&lt;/code&gt;，就可以处理本地的舞蹈视频了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;获取特定关节点坐标&lt;/strong&gt;： &lt;code&gt;results.pose_landmarks&lt;/code&gt; 对象中包含了所有 33 个关节点的坐标（x, y, z, visibility）。你可以访问它们并进行计算，例如：
&lt;ul&gt;
&lt;li&gt;计算手臂弯曲的角度。&lt;/li&gt;
&lt;li&gt;判断你是否做出了某个特定的姿势（比如“大”字形）。&lt;/li&gt;
&lt;li&gt;将骨骼数据发送到 Unity 或 Unreal Engine，驱动一个 3D 模型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;美化视觉效果&lt;/strong&gt;： 你可以完全自定义绘制的骨骼样式，比如改变颜色、粗细，或者用更酷炫的图形元素替换简单的线条和圆点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能&lt;/strong&gt;： MediaPipe 已经非常快了。如果你的电脑配置较高（有不错的独立显卡），它的表现会更好。如果你觉得卡顿，可以尝试降低摄像头的分辨率。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;总结&lt;a href=&quot;#总结&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;恭喜你！你已经成功构建了一个属于自己的 AI 实时动作捕捉应用。通过这个项目，我们不仅学会了如何使用 OpenCV 和 MediaPipe，更打开了通往计算机视觉世界的大门。希望这篇文章能激发你的创造力，去探索更多 AI 与现实世界交互的可能性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Happy coding!&lt;/strong&gt;&lt;/p&gt;</content:encoded><category>category:笔记</category></item><item><title>OpenCV 环境搭建：安装必要的开发工具与库</title><link>https://xunyi.cloud/post/note/algorithm/opencv-setup-tools</link><guid isPermaLink="false">note/algorithm/opencv-setup-tools</guid><description>第 1 步：安装必要的开发工具和库
我们需要 C++ 编译器、项目构建工具 (CMake) 和核心的图像处理库 (OpenCV)。
在终端中输入以下命令：
sudo pacman -S base-devel cmake opencv

base-devel: 这是一个软件包组，包含了 gcc</description><pubDate>Sat, 29 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;strong&gt;第 1 步：安装必要的开发工具和库&lt;/strong&gt;&lt;a href=&quot;#第-1-步安装必要的开发工具和库&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;我们需要 C++ 编译器、项目构建工具 (CMake) 和核心的图像处理库 (OpenCV)。&lt;/p&gt;
&lt;p&gt;在终端中输入以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo pacman -S base-devel cmake opencv&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;base-devel&lt;/code&gt;: 这是一个软件包组，包含了 &lt;code&gt;gcc&lt;/code&gt; (C++ 编译器), &lt;code&gt;make&lt;/code&gt; (编译工具) 等一系列最基础的开发工具。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cmake&lt;/code&gt;: 一个跨平台的项目构建工具，它能读取我们后续编写的配置文件 (&lt;code&gt;CMakeLists.txt&lt;/code&gt;) 并生成 &lt;code&gt;make&lt;/code&gt; 所需的 &lt;code&gt;Makefile&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;opencv&lt;/code&gt;: 我们项目最核心的依赖库，提供了所有图像处理和人脸识别的功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;第 2 步：创建项目文件夹结构&lt;/strong&gt;&lt;a href=&quot;#第-2-步创建项目文件夹结构&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;现在，我们需要为项目创建一个干净整洁的工作空间。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建一个主文件夹&lt;/strong&gt;。我们把它放在您的主目录 (&lt;code&gt;~&lt;/code&gt;) 下，并命名为 &lt;code&gt;FaceRecProject&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Bash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# cd ~ 会确保您当前在主目录下&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;cd ~&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;mkdir FaceRecProject&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进入这个主文件夹&lt;/strong&gt;。之后的所有操作都在这里进行。&lt;/p&gt;
&lt;p&gt;Bash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cd FaceRecProject&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建项目所需的子文件夹&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Bash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;mkdir build&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;mkdir data&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;mkdir cascades&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;build&lt;/code&gt;: 用于存放所有编译过程中产生的临时文件和最终生成的可执行程序，让主目录保持干净。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data&lt;/code&gt;: 用于存放我们提供给程序学习的人脸照片。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cascades&lt;/code&gt;: 用于存放 OpenCV 官方提供的人脸检测模型文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;第 3 步：下载人脸检测模型&lt;/strong&gt;&lt;a href=&quot;#第-3-步下载人脸检测模型&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;我们需要一个预训练好的模型来告诉程序“人脸长什么样”。&lt;/p&gt;
&lt;p&gt;在终端中（确保您仍在 &lt;code&gt;FaceRecProject&lt;/code&gt; 文件夹下），运行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;wget -P ./cascades https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_alt.xml&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wget&lt;/code&gt;: 一个从网络上下载文件的命令行工具。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-P ./cascades&lt;/code&gt;: &lt;code&gt;-P&lt;/code&gt; 参数告诉 &lt;code&gt;wget&lt;/code&gt; 把下载的文件放到指定的文件夹里，也就是我们刚创建的 &lt;code&gt;cascades&lt;/code&gt; 文件夹。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://...&lt;/code&gt;: 这是模型文件的下载地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;第 4 步：编写项目代码&lt;/strong&gt;&lt;a href=&quot;#第-4-步编写项目代码&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;现在我们要创建三个核心的文本文件：构建脚本和两个 C++ 源代码文件。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建构建脚本 &lt;code&gt;CMakeLists.txt&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Bash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# 使用 nano 文本编辑器创建一个新文件&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;nano CMakeLists.txt&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将下面的代码&lt;strong&gt;完整地复制&lt;/strong&gt;并粘贴到 &lt;code&gt;nano&lt;/code&gt; 编辑器窗口中：&lt;/p&gt;
&lt;p&gt;CMake&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cmake_minimum_required(VERSION 3.10)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;project(FaceRecognition)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;set(CMAKE_CXX_STANDARD 17)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;set(CMAKE_CXX_STANDARD_REQUIRED ON)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;find_package(OpenCV REQUIRED COMPONENTS core highgui imgproc objdetect videoio face)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;include_directories(${OpenCV_INCLUDE_DIRS})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;add_executable(train_model train_model.cpp)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;target_link_libraries(train_model ${OpenCV_LIBS} stdc++fs)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;add_executable(recognize_face recognize_face.cpp)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;target_link_libraries(recognize_face ${OpenCV_LIBS})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;粘贴完成后，按 &lt;code&gt;Ctrl+X&lt;/code&gt;，然后按 &lt;code&gt;Y&lt;/code&gt;，最后按 &lt;code&gt;回车&lt;/code&gt; 来保存并退出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建训练程序 &lt;code&gt;train_model.cpp&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Bash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;nano train_model.cpp&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将下面的代码&lt;strong&gt;完整地复制&lt;/strong&gt;并粘贴进去：&lt;/p&gt;
&lt;p&gt;C++&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;#include &amp;lt;iostream&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;fstream&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;sstream&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;vector&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;filesystem&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;opencv2/core.hpp&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;opencv2/imgcodecs.hpp&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;opencv2/imgproc.hpp&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;opencv2/face.hpp&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;using namespace cv;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;using namespace cv::face;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;using namespace std;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;namespace fs = std::filesystem;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;void create_dataset_csv(const string&amp;amp; data_path, const string&amp;amp; output_file) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ofstream csv_file(output_file);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    int label = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    for (const auto&amp;amp; entry : fs::directory_iterator(data_path)) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        if (entry.is_directory()) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            for (const auto&amp;amp; img_entry : fs::directory_iterator(entry.path())) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                if (img_entry.is_regular_file()) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    csv_file &amp;lt;&amp;lt; img_entry.path().string() &amp;lt;&amp;lt; &quot;;&quot; &amp;lt;&amp;lt; label &amp;lt;&amp;lt; endl;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            label++;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    csv_file.close();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    cout &amp;lt;&amp;lt; &quot;Dataset CSV file created at &quot; &amp;lt;&amp;lt; output_file &amp;lt;&amp;lt; endl;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;static void read_csv(const string&amp;amp; filename, vector&amp;lt;Mat&amp;gt;&amp;amp; images, vector&amp;lt;int&amp;gt;&amp;amp; labels, char separator = &apos;;&apos;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ifstream file(filename.c_str(), ifstream::in);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    string line, path, classlabel;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    while (getline(file, line)) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        stringstream liness(line);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        getline(liness, path, separator);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        getline(liness, classlabel);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        if(!path.empty() &amp;amp;&amp;amp; !classlabel.empty()) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            images.push_back(imread(path, IMREAD_GRAYSCALE));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            labels.push_back(atoi(classlabel.c_str()));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;int main() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    string data_dir = &quot;./data&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    string csv_file_path = &quot;./data.csv&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    create_dataset_csv(data_dir, csv_file_path);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    vector&amp;lt;Mat&amp;gt; images;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    vector&amp;lt;int&amp;gt; labels;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    try {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        read_csv(csv_file_path, images, labels);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    } catch (const cv::Exception&amp;amp; e) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        cerr &amp;lt;&amp;lt; &quot;Error opening file \&quot;&quot; &amp;lt;&amp;lt; csv_file_path &amp;lt;&amp;lt; &quot;\&quot;. Reason: &quot; &amp;lt;&amp;lt; e.msg &amp;lt;&amp;lt; endl;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        exit(1);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if(images.size() &amp;lt;= 1) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        cerr &amp;lt;&amp;lt; &quot;Error: This program needs at least 2 images to train. Please add more images to your data directory.&quot; &amp;lt;&amp;lt; endl;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        exit(1);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    Ptr&amp;lt;LBPHFaceRecognizer&amp;gt; model = LBPHFaceRecognizer::create();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    cout &amp;lt;&amp;lt; &quot;Training the model...&quot; &amp;lt;&amp;lt; endl;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    model-&amp;gt;train(images, labels);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    string model_filename = &quot;face_model.yml&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    model-&amp;gt;save(model_filename);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    cout &amp;lt;&amp;lt; &quot;Model trained and saved to &quot; &amp;lt;&amp;lt; model_filename &amp;lt;&amp;lt; endl;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return 0;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;同样按 &lt;code&gt;Ctrl+X&lt;/code&gt;, &lt;code&gt;Y&lt;/code&gt;, &lt;code&gt;回车&lt;/code&gt; 保存退出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建识别程序 &lt;code&gt;recognize_face.cpp&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Bash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;nano recognize_face.cpp&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将下面的代码&lt;strong&gt;完整地复制&lt;/strong&gt;并粘贴进去：&lt;/p&gt;
&lt;p&gt;C++&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;#include &amp;lt;iostream&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;map&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;filesystem&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;chrono&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;iomanip&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;sstream&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;opencv2/core.hpp&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;opencv2/highgui.hpp&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;opencv2/imgproc.hpp&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;opencv2/objdetect.hpp&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;#include &amp;lt;opencv2/face.hpp&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;using namespace std;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;using namespace cv;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;using namespace cv::face;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;namespace fs = std::filesystem;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;int main() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    string cascade_path = &quot;./cascades/haarcascade_frontalface_alt.xml&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    string model_path = &quot;face_model.yml&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    CascadeClassifier face_cascade;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if (!face_cascade.load(cascade_path)) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        cerr &amp;lt;&amp;lt; &quot;Error: Could not load face cascade.&quot; &amp;lt;&amp;lt; endl;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return -1;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    Ptr&amp;lt;FaceRecognizer&amp;gt; model = LBPHFaceRecognizer::create();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    try {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        model-&amp;gt;read(model_path);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    } catch (const cv::Exception&amp;amp; e) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        cerr &amp;lt;&amp;lt; &quot;Error: Could not load trained model. Have you trained the model yet?&quot; &amp;lt;&amp;lt; endl;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return -1;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    map&amp;lt;int, string&amp;gt; label_to_name;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    label_to_name[0] = &quot;Person 1&quot;; // 可在此处修改识别出的名字&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    label_to_name[1] = &quot;Person 2&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    string strangers_dir = &quot;strangers&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if (!fs::exists(strangers_dir)) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        fs::create_directory(strangers_dir);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    auto last_stranger_save_time = chrono::steady_clock::now();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const auto save_cooldown = chrono::seconds(5);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    VideoCapture cap(0);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if (!cap.isOpened()) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        cerr &amp;lt;&amp;lt; &quot;Error: Could not open camera.&quot; &amp;lt;&amp;lt; endl;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return -1;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    Mat frame;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    while (true) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        cap &amp;gt;&amp;gt; frame;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        if (frame.empty()) break;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        Mat gray;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        cvtColor(frame, gray, COLOR_BGR2GRAY);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        equalizeHist(gray, gray);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        vector&amp;lt;Rect&amp;gt; faces;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        face_cascade.detectMultiScale(gray, faces, 1.1, 4, 0|CASCADE_SCALE_IMAGE, Size(30, 30));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        for (const auto&amp;amp; face_rect : faces) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            Mat face_roi = gray(face_rect);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            Mat resized_face;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            resize(face_roi, resized_face, Size(92, 112), 1.0, 1.0, INTER_CUBIC);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            int predicted_label = -1;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            double confidence = 0.0;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            model-&amp;gt;predict(resized_face, predicted_label, confidence);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            rectangle(frame, face_rect, Scalar(0, 255, 0), 2);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            string text;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            if (predicted_label != -1 &amp;amp;&amp;amp; confidence &amp;lt; 85.0) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                 string name = label_to_name.count(predicted_label) ? label_to_name[predicted_label] : &quot;Known Person&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                 text = format(&quot;%s (Conf: %.2f)&quot;, name.c_str(), confidence);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            } else {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                text = &quot;Stranger&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                auto now = chrono::steady_clock::now();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                if (chrono::duration_cast&amp;lt;chrono::seconds&amp;gt;(now - last_stranger_save_time) &amp;gt; save_cooldown) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    auto time_now_t = chrono::system_clock::to_time_t(chrono::system_clock::now());&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    stringstream ss;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    ss &amp;lt;&amp;lt; put_time(localtime(&amp;amp;time_now_t), &quot;%Y%m%d_%H%M%S&quot;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    string filename = strangers_dir + &quot;/stranger_&quot; + ss.str() + &quot;.jpg&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    imwrite(filename, frame(face_rect));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    cout &amp;lt;&amp;lt; &quot;Stranger detected! Image saved to &quot; &amp;lt;&amp;lt; filename &amp;lt;&amp;lt; endl;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    last_stranger_save_time = now;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            Point text_pos(face_rect.x, face_rect.y - 10);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            putText(frame, text, text_pos, FONT_HERSHEY_SIMPLEX, 0.6, Scalar(0, 255, 0), 2);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        imshow(&quot;Face Recognition&quot;, frame);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        if (waitKey(10) == &apos;q&apos;) break;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return 0;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;按 &lt;code&gt;Ctrl+X&lt;/code&gt;, &lt;code&gt;Y&lt;/code&gt;, &lt;code&gt;回车&lt;/code&gt; 保存退出。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;第 5 步：准备训练数据&lt;/strong&gt;&lt;a href=&quot;#第-5-步准备训练数据&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;这是教程序认识人脸的关键。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;data&lt;/code&gt; 文件夹里，&lt;strong&gt;为每一个人创建一个单独的子文件夹&lt;/strong&gt;。文件夹的名字最好是英文，比如 &lt;code&gt;zhangsan&lt;/code&gt;, &lt;code&gt;lisi&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Bash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# 例如，创建两个人的文件夹&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;mkdir data/zhangsan&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;mkdir data/lisi&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将每个人的照片（JPG 或 PNG 格式）放入对应的文件夹。每个人的照片建议至少 10 张，越多越好，最好包含不同角度、不同表情、不同光线下的照片。&lt;/p&gt;
&lt;p&gt;您可以用文件管理器来复制粘贴，或者用 cp 命令。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;第 6 步：编译项目&lt;/strong&gt;&lt;a href=&quot;#第-6-步编译项目&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;现在，所有代码和数据都准备好了，让我们来编译它。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进入 &lt;code&gt;build&lt;/code&gt; 文件夹&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Bash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cd build&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行 CMake&lt;/strong&gt;。它会读取上一级目录的 &lt;code&gt;CMakeLists.txt&lt;/code&gt; 文件并生成配置。&lt;/p&gt;
&lt;p&gt;Bash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cmake ..&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;..&lt;/code&gt; 代表上一级目录。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行 make&lt;/strong&gt;。它会根据 CMake 生成的配置，调用 C++ 编译器来编译源代码。&lt;/p&gt;
&lt;p&gt;Bash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;make&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等待编译完成。如果一切顺利，您不会看到任何红色的错误信息。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;第 7 步：运行程序&lt;/strong&gt;&lt;a href=&quot;#第-7-步运行程序&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;编译成功后，&lt;code&gt;build&lt;/code&gt; 文件夹里会生成两个可执行文件：&lt;code&gt;train_model&lt;/code&gt; 和 &lt;code&gt;recognize_face&lt;/code&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;首先，训练模型&lt;/strong&gt;。我们需要回到项目主目录来运行，这样程序才能找到 &lt;code&gt;data&lt;/code&gt; 文件夹。&lt;/p&gt;
&lt;p&gt;Bash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# 回到主目录 FaceRecProject&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;cd ..&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 运行训练程序&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;./build/train_model&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序会读取 &lt;code&gt;data&lt;/code&gt; 文件夹里的照片，训练模型，然后在主目录生成一个 &lt;code&gt;face_model.yml&lt;/code&gt; 文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;最后，运行实时识别程序&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;Bash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# 确保仍在主目录&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;./build/recognize_face&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序会请求打开您的摄像头。授权后，一个名为 “Face Recognition” 的窗口就会弹出。它会实时地检测和识别人脸，并将陌生人的头像保存在 &lt;code&gt;strangers&lt;/code&gt; 文件夹。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;按 &lt;code&gt;q&lt;/code&gt; 键可以关闭窗口并退出程序。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;恭喜！你已经从零开始，成功地在 Arch Linux 上构建并运行了一个属于您自己的 C++ 人脸识别应用。&lt;/p&gt;</content:encoded><category>category:笔记</category></item><item><title>无依赖全栈评论系统：Go (Gin) + Web Components 快速搭建</title><link>https://xunyi.cloud/post/note/back-end/go-webcomponents-comment-system</link><guid isPermaLink="false">note/back-end/go-webcomponents-comment-system</guid><description>想要给网站加个评论功能，接入第三方服务怕数据不安全，引入庞大的前端框架又觉得“杀鸡焉用牛刀”？
其实，利用浏览器现代标准 Web Components 和编译极其方便的 Go</description><pubDate>Sat, 29 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;想要给网站加个评论功能，接入第三方服务怕数据不安全，引入庞大的前端框架又觉得“杀鸡焉用牛刀”？&lt;/p&gt;
&lt;p&gt;其实，利用浏览器现代标准 &lt;strong&gt;Web Components&lt;/strong&gt; 和编译极其方便的 &lt;strong&gt;Go 语言&lt;/strong&gt;，我们可以快速构建一个&lt;strong&gt;无依赖、高性能&lt;/strong&gt;的全栈评论系统。它不需要复杂的构建流程，部署时只需一个二进制文件，非常适合个人开发者快速上手。&lt;/p&gt;
&lt;h2&gt;技术栈一览&lt;a href=&quot;#技术栈一览&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Backend (后端):&lt;/strong&gt; Go (Gin 框架) + Gorm + SQLite
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;理由：部署极其简单，编译后只是一个二进制文件，自带数据库，无需额外安装 MySQL。&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Frontend (前端):&lt;/strong&gt; 原生 JavaScript + Web Components
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;理由：无依赖，无构建流程，标准 HTML 标签，真正的组件化。&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;第一部分：后端 —— 极速搭建 API&lt;a href=&quot;#第一部分后端--极速搭建-api&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;我们将使用 Go 语言最流行的 Gin 框架，配合 Gorm 进行数据库操作。整个后端代码不到 60 行。&lt;/p&gt;
&lt;h3&gt;1. 初始化项目&lt;a href=&quot;#1-初始化项目&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;首先，在终端中创建一个文件夹并初始化 Go 模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; go-comments&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; go-comments&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; mod&lt;/span&gt;&lt;span&gt; init&lt;/span&gt;&lt;span&gt; go-comments&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装必要的依赖包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Web 框架&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; get&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt; github.com/gin-gonic/gin&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# ORM 和 数据库驱动&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; get&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt; gorm.io/gorm&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; get&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt; gorm.io/driver/sqlite&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 跨域处理中间件&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; get&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt; github.com/gin-contrib/cors&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 编写 &lt;code&gt;main.go&lt;/code&gt;&lt;a href=&quot;#2-编写-maingo&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;新建 &lt;code&gt;main.go&lt;/code&gt;，这是我们要实现的全部逻辑：&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; main&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	&quot;&lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	&quot;&lt;/span&gt;&lt;span&gt;github.com/gin-contrib/cors&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	&quot;&lt;/span&gt;&lt;span&gt;github.com/gin-gonic/gin&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	&quot;&lt;/span&gt;&lt;span&gt;gorm.io/driver/sqlite&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	&quot;&lt;/span&gt;&lt;span&gt;gorm.io/gorm&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// 定义评论模型，Gorm 会自动把它转换成数据库表&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; Comment&lt;/span&gt;&lt;span&gt; struct&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	ID        &lt;/span&gt;&lt;span&gt;uint&lt;/span&gt;&lt;span&gt;      `gorm:&quot;primaryKey&quot; json:&quot;id&quot;`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	Content   &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;    `json:&quot;content&quot;`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	CreatedAt &lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Time&lt;/span&gt;&lt;span&gt; `json:&quot;created_at&quot;`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; main&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	// 1. 连接 SQLite 数据库 (自动创建 comments.db)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	db, err &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; gorm.&lt;/span&gt;&lt;span&gt;Open&lt;/span&gt;&lt;span&gt;(sqlite.&lt;/span&gt;&lt;span&gt;Open&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;comments.db&quot;&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;gorm&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Config&lt;/span&gt;&lt;span&gt;{})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	if&lt;/span&gt;&lt;span&gt; err &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; nil&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		panic&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;数据库连接失败&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	// 2. 自动迁移：保证数据库表结构与 Struct 同步&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	db.&lt;/span&gt;&lt;span&gt;AutoMigrate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;Comment&lt;/span&gt;&lt;span&gt;{})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	// 3. 初始化 Gin 引擎&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	r &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; gin.&lt;/span&gt;&lt;span&gt;Default&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	// 4. 配置 CORS (允许跨域)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	// 因为前端可能跑在不同端口，必须允许跨域请求&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	r.&lt;/span&gt;&lt;span&gt;Use&lt;/span&gt;&lt;span&gt;(cors.&lt;/span&gt;&lt;span&gt;Default&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	// 5. 路由定义&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	// 获取评论列表&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	r.&lt;/span&gt;&lt;span&gt;GET&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/comments&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt;gin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		var&lt;/span&gt;&lt;span&gt; comments []&lt;/span&gt;&lt;span&gt;Comment&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		// 按时间倒序查询&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		db.&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;created_at desc&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;Find&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;comments)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		c.&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;, comments)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	// 提交新评论&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	r.&lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/comments&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt;gin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		var&lt;/span&gt;&lt;span&gt; jsonInput &lt;/span&gt;&lt;span&gt;Comment&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		// 绑定 JSON 数据&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		if&lt;/span&gt;&lt;span&gt; err &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; c.&lt;/span&gt;&lt;span&gt;ShouldBindJSON&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;jsonInput); err &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; nil&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			c.&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;gin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;H&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;error&quot;&lt;/span&gt;&lt;span&gt;: err.&lt;/span&gt;&lt;span&gt;Error&lt;/span&gt;&lt;span&gt;()})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			return&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		// 写入数据库&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		db.&lt;/span&gt;&lt;span&gt;Create&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;jsonInput)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		c.&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;, jsonInput)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	// 6. 在 8080 端口启动服务&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	r.&lt;/span&gt;&lt;span&gt;Run&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;:8080&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;运行后端非常简单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span&gt; main.go&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到 &lt;code&gt;Listening and serving HTTP on :8080&lt;/code&gt; 即表示成功。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;第二部分：前端 —— 封装 Web Components&lt;a href=&quot;#第二部分前端--封装-web-components&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;我们要创建一个自定义标签 &lt;code&gt;&amp;lt;my-comments&amp;gt;&lt;/code&gt;，无论把它放在哪个 HTML 页面里，它都能自动渲染出评论区并拥有交互功能。&lt;/p&gt;
&lt;h3&gt;编写 &lt;code&gt;index.html&lt;/code&gt;&lt;a href=&quot;#编写-indexhtml&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;为了演示方便，我们将 JS 直接写在 HTML 里（实际开发中建议分离为 &lt;code&gt;.js&lt;/code&gt; 文件）。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;!&lt;/span&gt;&lt;span&gt;DOCTYPE&lt;/span&gt;&lt;span&gt; html&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt; lang&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;zh-CN&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;head&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt; charset&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;UTF-8&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;&amp;gt;Go + Web Components Demo&amp;lt;/&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;head&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;my-comments&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;my-comments&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 自定义组件类&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        class&lt;/span&gt;&lt;span&gt; MyComments&lt;/span&gt;&lt;span&gt; extends&lt;/span&gt;&lt;span&gt; HTMLElement&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            constructor&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                super&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                // 开启 Shadow DOM，让样式与外部隔离，互不影响&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;attachShadow&lt;/span&gt;&lt;span&gt;({ mode: &lt;/span&gt;&lt;span&gt;&apos;open&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                this&lt;/span&gt;&lt;span&gt;.apiUrl &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &apos;http://localhost:8080/comments&apos;&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            connectedCallback&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;render&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fetchComments&lt;/span&gt;&lt;span&gt;(); &lt;/span&gt;&lt;span&gt;// 组件加载时拉取数据&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            render&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                this&lt;/span&gt;&lt;span&gt;.shadowRoot.innerHTML &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; `&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                &amp;lt;style&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    :host { display: block; font-family: sans-serif; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    .container { &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        max-width: 500px; margin: 20px auto; &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        box-shadow: 0 2px 10px rgba(0,0,0,0.05);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    h3 { margin-top: 0; color: #333; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    ul { list-style: none; padding: 0; max-height: 300px; overflow-y: auto; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    li { border-bottom: 1px solid #f0f0f0; padding: 10px 0; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    .content { font-size: 16px; color: #444; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    .time { font-size: 12px; color: #999; margin-top: 4px; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    .input-group { display: flex; gap: 10px; margin-top: 20px; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    input { flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    button { &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        padding: 8px 16px; background: #007bff; color: white; &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        border: none; border-radius: 4px; cursor: pointer; &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    button:hover { background: #0056b3; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                &amp;lt;/style&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                &amp;lt;div class=&quot;container&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    &amp;lt;h3&amp;gt;💬 评论区&amp;lt;/h3&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    &amp;lt;ul id=&quot;list&quot;&amp;gt;加载中...&amp;lt;/ul&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    &amp;lt;div class=&quot;input-group&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        &amp;lt;input type=&quot;text&quot; id=&quot;input&quot; placeholder=&quot;写下你的想法...&quot; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        &amp;lt;button id=&quot;btn&quot;&amp;gt;发送&amp;lt;/button&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    &amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                &amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                `&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                // 绑定点击事件&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                this&lt;/span&gt;&lt;span&gt;.shadowRoot.&lt;/span&gt;&lt;span&gt;getElementById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;btn&apos;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;onclick&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; () &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;postComment&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            // 获取评论 (GET)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            async&lt;/span&gt;&lt;span&gt; fetchComments&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    const&lt;/span&gt;&lt;span&gt; res&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.apiUrl);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    const&lt;/span&gt;&lt;span&gt; data&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; res.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    const&lt;/span&gt;&lt;span&gt; list&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; this&lt;/span&gt;&lt;span&gt;.shadowRoot.&lt;/span&gt;&lt;span&gt;getElementById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;list&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    if&lt;/span&gt;&lt;span&gt; (data.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt; ===&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        list.innerHTML &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &apos;&amp;lt;li style=&quot;text-align:center;color:#999&quot;&amp;gt;暂无评论，快来抢沙发！&amp;lt;/li&amp;gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    list.innerHTML &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; data.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; `&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        &amp;lt;li&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            &amp;lt;div class=&quot;content&quot;&amp;gt;${&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;escapeHtml&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;content&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            &amp;lt;div class=&quot;time&quot;&amp;gt;${&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; Date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;created_at&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;toLocaleString&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;}&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        &amp;lt;/li&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    `&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                } &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (err) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    console.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;API Error:&quot;&lt;/span&gt;&lt;span&gt;, err);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    this&lt;/span&gt;&lt;span&gt;.shadowRoot.&lt;/span&gt;&lt;span&gt;getElementById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;list&apos;&lt;/span&gt;&lt;span&gt;).innerHTML &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &apos;加载失败，请检查后端服务。&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            // 提交评论 (POST)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            async&lt;/span&gt;&lt;span&gt; postComment&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                const&lt;/span&gt;&lt;span&gt; input&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; this&lt;/span&gt;&lt;span&gt;.shadowRoot.&lt;/span&gt;&lt;span&gt;getElementById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;input&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                const&lt;/span&gt;&lt;span&gt; content&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; input.value.&lt;/span&gt;&lt;span&gt;trim&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;content) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    await&lt;/span&gt;&lt;span&gt; fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.apiUrl, {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        method: &lt;/span&gt;&lt;span&gt;&apos;POST&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        headers: { &lt;/span&gt;&lt;span&gt;&apos;Content-Type&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;application/json&apos;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        body: &lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;({ content: content })&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    input.value &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &apos;&apos;&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;// 清空输入框&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fetchComments&lt;/span&gt;&lt;span&gt;(); &lt;/span&gt;&lt;span&gt;// 刷新列表&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                } &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (err) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    alert&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;发送失败！&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            // 简单的 XSS 防御&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            escapeHtml&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                return&lt;/span&gt;&lt;span&gt; text&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    .&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&amp;amp;amp;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    .&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&amp;amp;lt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    .&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&amp;amp;gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    .&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&amp;amp;quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    .&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&amp;amp;#039;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 注册组件&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        customElements.&lt;/span&gt;&lt;span&gt;define&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;my-comments&apos;&lt;/span&gt;&lt;span&gt;, MyComments);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;/&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;运行效果&lt;a href=&quot;#运行效果&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;保持 &lt;code&gt;go run main.go&lt;/code&gt; 在终端运行。&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;/img/posts/note/back-end/go-webcomponents-comment-system/7715125d05acb0297662daca4a369095.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;双击打开 &lt;code&gt;index.html&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输入评论并发送，你会发现评论立刻保存到了 SQLite 数据库中，刷新页面数据依然存在。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;&lt;img src=&quot;/img/posts/note/back-end/go-webcomponents-comment-system/9fe331792d0f75d4096b042f9eb7dd6b.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;&lt;/figure&gt;
&lt;h2&gt;总结&lt;a href=&quot;#总结&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;通过这个小 Demo，我们发现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Go 做后端真的很爽&lt;/strong&gt;：不用配置复杂的 Webpack/Vite，不用担心数据库安装，一个二进制文件走天下。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web Components 很强大&lt;/strong&gt;：它让我们回归 HTML 的本质。想象一下，你可以在你的个人博客、公司的老旧项目，甚至是别人的网站（通过油猴脚本）里，只用一行 &lt;code&gt;&amp;lt;my-comments&amp;gt;&amp;lt;/my-comments&amp;gt;&lt;/code&gt; 就插入这个功能，是不是很酷？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;希望这篇教程能给你带来灵感！Happy Coding! ✨&lt;/p&gt;</content:encoded><category>category:笔记</category></item><item><title>手搓登录鉴权系统：用 Go (Gin) 与原生 JavaScript 从零实现完整登录流程</title><link>https://xunyi.cloud/post/note/back-end/handcrafted-auth-with-gin</link><guid isPermaLink="false">note/back-end/handcrafted-auth-with-gin</guid><description>
在现在的 Web 开发中，我们太习惯于依赖现成的库了：前端用 Auth0，后端用 Passport.js。但如果剥去这些层层封装，“登录”这件事的本质究竟是什么？
今天，我将剥离所有复杂的第三方库，带大家用最原生的 Go (Gin) 和 JavaScript，手搓一个包含后端中间件拦截、前端</description><pubDate>Sat, 29 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;在现在的 Web 开发中，我们太习惯于依赖现成的库了：前端用 Auth0，后端用 Passport.js。但如果剥去这些层层封装，&lt;strong&gt;“登录”这件事的本质究竟是什么？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;今天，我将剥离所有复杂的第三方库，带大家用最原生的 Go (Gin) 和 JavaScript，手搓一个包含&lt;strong&gt;后端中间件拦截&lt;/strong&gt;、&lt;strong&gt;前端 Token 管理&lt;/strong&gt;、&lt;strong&gt;交互式登录弹窗&lt;/strong&gt;的完整鉴权系统。我们将不仅实现功能，更要探讨其背后的架构思维。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;第一章：顶层架构设计&lt;a href=&quot;#第一章顶层架构设计&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;在写代码之前，我们先梳理一下逻辑。对于个人博客系统（Mikuweb）而言，我们的需求非常明确：&lt;strong&gt;单管理员模式&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们需要构建一个“闭环”：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;前端&lt;/strong&gt;：拿着密码去换通行证（Token）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前端&lt;/strong&gt;：把通行证缝在衣服上（LocalStorage），每次办事（发请求）都亮出来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后端&lt;/strong&gt;：设置一道安检门（Middleware），有证的放行，没证的拦截。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;数据流向图&lt;a href=&quot;#数据流向图&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;pre&gt;sequenceDiagram
    participant U as 👤 用户 (Browser)
    participant A as 🚪 登录接口 (/login)
    participant M as 🛡️ 鉴权中间件 (Middleware)
    participant H as ⚙️ 业务逻辑 (/admin/posts)

    Note over U, A: 阶段一：获取凭证
    U-&amp;gt;&amp;gt;A: 1. 提交密码 (POST /login)
    alt 密码正确
        A--&amp;gt;&amp;gt;U: 2. 返回 Token (凭证)
        Note over U: 将 Token 存入 LocalStorage
    else 密码错误
        A--&amp;gt;&amp;gt;U: 401 Unauthorized
    end

    Note over U, H: 阶段二：使用凭证
    U-&amp;gt;&amp;gt;M: 3. 发起敏感操作 (Header携带 Token)
    M-&amp;gt;&amp;gt;M: 4. 检查 Token 是否有效
    alt 验证通过
        M-&amp;gt;&amp;gt;H: 5. next() 放行
        H--&amp;gt;&amp;gt;U: 6. 操作成功
    else 验证失败
        M--&amp;gt;&amp;gt;U: 401 拦截请求
    end&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;第二章：后端实现 —— 守门员的艺术 (Go + Gin)&lt;a href=&quot;#第二章后端实现--守门员的艺术-go--gin&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;后端的灵魂不在于那个 &lt;code&gt;login&lt;/code&gt; 接口，而在于&lt;strong&gt;中间件 (Middleware)&lt;/strong&gt; 的设计。&lt;/p&gt;
&lt;h3&gt;1. 定义“上帝密码”与配置&lt;a href=&quot;#1-定义上帝密码与配置&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;为了演示最纯粹的逻辑，我们这里使用“硬编码”的单密码模式（生产环境请务必使用环境变量）。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;// main.go&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 这是唯一的通关秘籍，实际开发中建议读取 os.Getenv(&quot;APP_PASSWORD&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ADMIN_PASSWORD&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; &quot;miku_is_cute&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 管理员公开信息&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ADMIN_NICKNAME&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; &quot;awkker&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ADMIN_AVATAR&lt;/span&gt;&lt;span&gt;   =&lt;/span&gt;&lt;span&gt; &quot;/static/xunyi.png&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2. 核心：编写鉴权中间件 (The Gatekeeper)&lt;a href=&quot;#2-核心编写鉴权中间件-the-gatekeeper&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;这是很多初学者容易卡住的地方。&lt;strong&gt;中间件本质上是一个拦截器&lt;/strong&gt;。在 Gin 中，它控制着请求的生命周期。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;// AuthMiddleware 就像机场安检，不通过安检（Token错误），永远进不了候机厅（业务逻辑）&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; AuthMiddleware&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;gin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;HandlerFunc&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return&lt;/span&gt;&lt;span&gt; func&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt;gin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // Step 1: 检查旅客有没有带“通行证”&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 约定：前端必须在 HTTP Header 的 &quot;Authorization&quot; 字段中携带 Token&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        token &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; c.&lt;/span&gt;&lt;span&gt;GetHeader&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Authorization&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // Step 2: 验证通行证的真伪&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 这里我们做简单的字符串比对，实际场景中通常是校验 JWT 的签名&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        if&lt;/span&gt;&lt;span&gt; token &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; ADMIN_PASSWORD {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            // 🛑 核心操作：Abort&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            // Abort() 会阻止挂在当前路由下的后续 Handler 执行&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            c.&lt;/span&gt;&lt;span&gt;AbortWithStatusJSON&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;gin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;H&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                &quot;error&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;权限不足：请先登录喵！(&amp;gt;_&amp;lt;)&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            })&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            return&lt;/span&gt;&lt;span&gt; // 必须 return，否则函数会继续向下跑&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // ✅ 核心操作：Next&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 验证通过，放行给下一个处理函数（比如发布文章的 Controller）&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        c.&lt;/span&gt;&lt;span&gt;Next&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3. 路由分组：声明式权限管理&lt;a href=&quot;#3-路由分组声明式权限管理&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;有了中间件，我们不需要在每个接口里写 &lt;code&gt;if password == ...&lt;/code&gt;。我们利用 &lt;strong&gt;路由组 (Group)&lt;/strong&gt; 来圈定保护范围。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; main&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    r &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; gin.&lt;/span&gt;&lt;span&gt;Default&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // === 公共区域 (Public) ===&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 任何人都可以看文章，不需要 Token&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    r.&lt;/span&gt;&lt;span&gt;GET&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/posts&quot;&lt;/span&gt;&lt;span&gt;, postController.GetList)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    r.&lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/login&quot;&lt;/span&gt;&lt;span&gt;, authController.Login) &lt;/span&gt;&lt;span&gt;// 登录接口本身必须是公开的&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // === 禁区 (Private/Admin) ===&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 使用 Use() 挂载我们刚才写的中间件&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    admin &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; r.&lt;/span&gt;&lt;span&gt;Group&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/admin&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    admin.&lt;/span&gt;&lt;span&gt;Use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;AuthMiddleware&lt;/span&gt;&lt;span&gt;()) &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 只有携带正确 Token 的请求才能到达这里&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        admin.&lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/posts&quot;&lt;/span&gt;&lt;span&gt;, postController.Create)   &lt;/span&gt;&lt;span&gt;// 写文章&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        admin.&lt;/span&gt;&lt;span&gt;DELETE&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/posts/:id&quot;&lt;/span&gt;&lt;span&gt;, postController.Delete) &lt;/span&gt;&lt;span&gt;// 删文章&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    r.&lt;/span&gt;&lt;span&gt;Run&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;:8080&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;第三章：前端实现 —— 状态管理与微交互 (Native JS)&lt;a href=&quot;#第三章前端实现--状态管理与微交互-native-js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;前端的难点在于：&lt;strong&gt;HTTP 是无状态的，浏览器怎么“记住”我登录了？&lt;/strong&gt; 以及，如何通过微小的动画提升用户体验？&lt;/p&gt;
&lt;h3&gt;1. 状态持久化：LocalStorage&lt;a href=&quot;#1-状态持久化localstorage&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;我们使用 &lt;code&gt;localStorage&lt;/code&gt; 而不是 &lt;code&gt;sessionStorage&lt;/code&gt;，这样即使用户关闭浏览器再打开，登录状态依然存在。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;// static/js/user.js&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// 封装一个 User 模块，负责管理身份&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; UserModule&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 检查是否登录：也就是看看兜里有没有 Token&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    isLoggedIn&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return&lt;/span&gt;&lt;span&gt; !!&lt;/span&gt;&lt;span&gt;localStorage.&lt;/span&gt;&lt;span&gt;getItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;auth_token&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 登录成功后的处理&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    loginSuccess&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;token&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;userInfo&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 🗝️ 核心：把 Token 存起来！&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        localStorage.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;auth_token&apos;&lt;/span&gt;&lt;span&gt;, token);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        localStorage.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;user_info&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(userInfo));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 刷新页面，让 UI 根据新状态重新渲染&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        location.&lt;/span&gt;&lt;span&gt;reload&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 退出登录&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    logout&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 销毁 Token&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        localStorage.&lt;/span&gt;&lt;span&gt;removeItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;auth_token&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        location.&lt;/span&gt;&lt;span&gt;reload&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2. 发起带权的请求 (Fetch Wrapper)&lt;a href=&quot;#2-发起带权的请求-fetch-wrapper&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;这是最关键的一步。当我们调用后端的 &lt;code&gt;/admin&lt;/code&gt; 接口时，必须&lt;strong&gt;手动&lt;/strong&gt;把 Token 塞进 Header 里。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; deleteArticle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; token&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; localStorage.&lt;/span&gt;&lt;span&gt;getItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;auth_token&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 如果没有 Token，直接在这里拦截，省去一次网络请求&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;token) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        alert&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;请先登录！&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; response&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`/admin/posts/${&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        method: &lt;/span&gt;&lt;span&gt;&apos;DELETE&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        headers: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &apos;Content-Type&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;application/json&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            // 🗝️ 核心：出示通行证！名字要和后端 GetHeader 里的保持一致&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &apos;Authorization&apos;&lt;/span&gt;&lt;span&gt;: token &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; (response.status &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; 401&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 容错处理：比如 Token 过期了，或者被后端改了密码&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        alert&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;登录失效，请重新登录&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        UserModule.&lt;/span&gt;&lt;span&gt;logout&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3. UI 交互：Q 弹的错误反馈&lt;a href=&quot;#3-ui-交互q-弹的错误反馈&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;当用户输错密码时，不要只弹一个冰冷的 &lt;code&gt;alert&lt;/code&gt;。我们用 CSS 动画模拟一个“摇头”的动作，这会让网页感觉更有生命力。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CSS (login.css):&lt;/strong&gt;
利用 &lt;code&gt;cubic-bezier&lt;/code&gt; 贝塞尔曲线，实现一种富有弹性的抖动。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;@keyframes&lt;/span&gt;&lt;span&gt; shake&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    0%&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;100%&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;transform&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;translateX&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;); }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    20%&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;60%&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;transform&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;translateX&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-10&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;); } &lt;/span&gt;&lt;span&gt;/* 向左猛甩 */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    40%&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;80%&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;transform&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;translateX&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;); }  &lt;/span&gt;&lt;span&gt;/* 向右猛甩 */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;/* 激活这个类名时，执行 0.5秒 的动画 */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;.login-box.shake&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    animation&lt;/span&gt;&lt;span&gt;: shake &lt;/span&gt;&lt;span&gt;0.5&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt; cubic-bezier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;.36&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.07&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.19&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.97&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;both&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;JS 调用:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;response.ok) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 添加类名触发动画&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    loginBox.classList.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;shake&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 500ms 动画结束后，移除类名，以便下次还能触发&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    setTimeout&lt;/span&gt;&lt;span&gt;(() &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        loginBox.classList.&lt;/span&gt;&lt;span&gt;remove&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;shake&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }, &lt;/span&gt;&lt;span&gt;500&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;第四章：安全性反思 (Production Note)&lt;a href=&quot;#第四章安全性反思-production-note&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;写到这里，我们已经完成了一个功能闭环。但作为一个有追求的开发者，必须诚实地指出当前实现的局限性。&lt;/p&gt;
&lt;p&gt;如果这是在公司级的生产环境，我们需要做以下升级：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;拒绝明文传输&lt;/strong&gt;：Token 和密码目前是在 HTTP 裸奔的。&lt;strong&gt;解决方案&lt;/strong&gt;：必须部署 SSL 证书，启用 &lt;strong&gt;HTTPS&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拒绝前端明文存储&lt;/strong&gt;：LocalStorage 容易被 XSS 攻击读取。&lt;strong&gt;解决方案&lt;/strong&gt;：使用 &lt;code&gt;HttpOnly Cookie&lt;/code&gt;，这样 JS 读不到，但浏览器发请求会自动带上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token 时效性&lt;/strong&gt;：目前的 Token 是永久有效的。&lt;strong&gt;解决方案&lt;/strong&gt;：引入 &lt;strong&gt;JWT (JSON Web Token)&lt;/strong&gt;，设置 &lt;code&gt;exp&lt;/code&gt; (过期时间) 字段。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;密码存储&lt;/strong&gt;：后端不应明文比对密码。&lt;strong&gt;解决方案&lt;/strong&gt;：数据库存储密码的哈希值（如 bcrypt），比对时使用 &lt;code&gt;bcrypt.CompareHashAndPassword&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第五章：demo 展示&lt;a href=&quot;#第五章demo-展示&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;你可以在自己的电脑上尝试运行以下代码&lt;/p&gt;
&lt;h3&gt;项目结构&lt;a href=&quot;#项目结构&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;demo/&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;├── main.go        # 后端：负责鉴权和 API&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;└── index.html     # 前端：包含页面、样式和 JS 逻辑&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1. 后端代码 (&lt;code&gt;main.go&lt;/code&gt;)&lt;a href=&quot;#1-后端代码-maingo&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;这里展示了中间件拦截和 Token 验证的核心逻辑。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; main&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	&quot;&lt;/span&gt;&lt;span&gt;net/http&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	&quot;&lt;/span&gt;&lt;span&gt;github.com/gin-gonic/gin&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// 🔐 配置上帝密码 (实际开发请用环境变量)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; APP_PASSWORD&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; &quot;miku&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; main&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	r &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; gin.&lt;/span&gt;&lt;span&gt;Default&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	// 1. 静态文件服务 (用来展示前端页面)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	r.&lt;/span&gt;&lt;span&gt;LoadHTMLFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;index.html&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	r.&lt;/span&gt;&lt;span&gt;GET&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt;gin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		c.&lt;/span&gt;&lt;span&gt;HTML&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;index.html&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	// 2. 登录接口 (公开)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	r.&lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/api/login&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt;gin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		var&lt;/span&gt;&lt;span&gt; json &lt;/span&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			Password &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; `json:&quot;password&quot;`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		if&lt;/span&gt;&lt;span&gt; c.&lt;/span&gt;&lt;span&gt;ShouldBindJSON&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;json) &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; nil&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; json.Password &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; APP_PASSWORD {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			// 登录成功，直接把密码当作 Token 返回 (简化演示)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			c.&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;gin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;H&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;				&quot;token&quot;&lt;/span&gt;&lt;span&gt;: APP_PASSWORD,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;				&quot;msg&quot;&lt;/span&gt;&lt;span&gt;:   &lt;/span&gt;&lt;span&gt;&quot;欢迎回来，主人！&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			c.&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;gin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;H&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;error&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;密码错误喵！&quot;&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	// 3. 需要权限的接口组&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	admin &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; r.&lt;/span&gt;&lt;span&gt;Group&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/admin&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	admin.&lt;/span&gt;&lt;span&gt;Use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;AuthMiddleware&lt;/span&gt;&lt;span&gt;()) &lt;/span&gt;&lt;span&gt;// 🛡️ 挂载鉴权中间件&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		admin.&lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/delete&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt;gin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			c.&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;gin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;H&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;success&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;data&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;文章已删除！&quot;&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	r.&lt;/span&gt;&lt;span&gt;Run&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;:8080&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// 🛡️ 核心中间件：检查请求头里有没有 Token&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; AuthMiddleware&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;gin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;HandlerFunc&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	return&lt;/span&gt;&lt;span&gt; func&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt;gin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		token &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; c.&lt;/span&gt;&lt;span&gt;GetHeader&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Authorization&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		if&lt;/span&gt;&lt;span&gt; token &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; APP_PASSWORD {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			// 🚫 拦截请求，不再往下执行&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			c.&lt;/span&gt;&lt;span&gt;AbortWithStatusJSON&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;gin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;H&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;error&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;无权访问，请先登录！&quot;&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			return&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		// ✅ 放行&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		c.&lt;/span&gt;&lt;span&gt;Next&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;h3&gt;2. 前端代码 (&lt;code&gt;index.html&lt;/code&gt;)&lt;a href=&quot;#2-前端代码-indexhtml&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;!&lt;/span&gt;&lt;span&gt;DOCTYPE&lt;/span&gt;&lt;span&gt; html&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt; lang&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;zh-CN&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;head&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt; charset&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;UTF-8&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;&amp;gt;Mikuweb 鉴权演示&amp;lt;/&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;style&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        /* ✨ 简单的居中样式 */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        body&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            font-family&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            background&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;#f0f2f5&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            display&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;flex&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            justify-content&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;center&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            align-items&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;center&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            height&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;vh&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        .card&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            background&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;white&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            padding&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;rem&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            border-radius&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;16&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            box-shadow&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; 4&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt; 12&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt; rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0.1&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            text-align&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;center&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            width&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;300&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        input&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            width&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            margin-top&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            padding&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            box-sizing&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;border-box&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        /* 🔴 核心：错误时的抖动动画 */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        .shake&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            animation&lt;/span&gt;&lt;span&gt;: shake &lt;/span&gt;&lt;span&gt;0.5&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt; cubic-bezier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;.36&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.07&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.19&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.97&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;both&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        @keyframes&lt;/span&gt;&lt;span&gt; shake&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            10%&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;90%&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;transform&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;translate3d&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-1&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;); }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            20%&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;80%&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;transform&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;translate3d&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;); }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            30%&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;50%&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;70%&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;transform&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;translate3d&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-4&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;); }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            40%&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;60%&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;transform&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;translate3d&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;); }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;/&lt;/span&gt;&lt;span&gt;style&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;head&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;card&quot;&lt;/span&gt;&lt;span&gt; id&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;login-box&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;h2&lt;/span&gt;&lt;span&gt;&amp;gt;🔐 请登录&amp;lt;/&lt;/span&gt;&lt;span&gt;h2&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;input&lt;/span&gt;&lt;span&gt; type&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;password&quot;&lt;/span&gt;&lt;span&gt; id&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;password&quot;&lt;/span&gt;&lt;span&gt; placeholder&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;输入 miku 试试&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt; onclick&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;handleLogin&lt;/span&gt;&lt;span&gt;()&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;登录&amp;lt;/&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;card&quot;&lt;/span&gt;&lt;span&gt; id&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;admin-box&quot;&lt;/span&gt;&lt;span&gt; style&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;display: none;&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;h2&lt;/span&gt;&lt;span&gt;&amp;gt;👋 管理员模式&amp;lt;/&lt;/span&gt;&lt;span&gt;h2&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&amp;gt;Token 已保存到 LocalStorage&amp;lt;/&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt; onclick&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;sensitiveAction&lt;/span&gt;&lt;span&gt;()&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;🗑️ 测试删除文章&amp;lt;/&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt; onclick&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;logout&lt;/span&gt;&lt;span&gt;()&quot;&lt;/span&gt;&lt;span&gt; style&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;background: #ff4757; color: white;&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;退出登录&amp;lt;/&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 🔄 页面加载时检查状态&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; token&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; localStorage.&lt;/span&gt;&lt;span&gt;getItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;auth_token&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; (token) &lt;/span&gt;&lt;span&gt;toggleView&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 1️⃣ 登录逻辑&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    async&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; handleLogin&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        const&lt;/span&gt;&lt;span&gt; pwd&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.&lt;/span&gt;&lt;span&gt;getElementById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;password&apos;&lt;/span&gt;&lt;span&gt;).value;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        const&lt;/span&gt;&lt;span&gt; box&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.&lt;/span&gt;&lt;span&gt;getElementById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;login-box&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        const&lt;/span&gt;&lt;span&gt; res&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;/api/login&apos;&lt;/span&gt;&lt;span&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            method: &lt;/span&gt;&lt;span&gt;&apos;POST&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            headers: {&lt;/span&gt;&lt;span&gt;&apos;Content-Type&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;application/json&apos;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            body: &lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;({ password: pwd })&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        if&lt;/span&gt;&lt;span&gt; (res.ok) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            const&lt;/span&gt;&lt;span&gt; data&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; res.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            localStorage.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;auth_token&apos;&lt;/span&gt;&lt;span&gt;, data.token); &lt;/span&gt;&lt;span&gt;// 保存 Token&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            toggleView&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            alert&lt;/span&gt;&lt;span&gt;(data.msg);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        } &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            // ❌ 触发抖动动画&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            box.classList.&lt;/span&gt;&lt;span&gt;remove&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;shake&apos;&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;// 重置动画&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            void&lt;/span&gt;&lt;span&gt; box.offsetWidth;          &lt;/span&gt;&lt;span&gt;// 强制重绘&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            box.classList.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;shake&apos;&lt;/span&gt;&lt;span&gt;);    &lt;/span&gt;&lt;span&gt;// 再次添加&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 2️⃣ 敏感操作 (带 Token 请求)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    async&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; sensitiveAction&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        const&lt;/span&gt;&lt;span&gt; res&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;/admin/delete&apos;&lt;/span&gt;&lt;span&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            method: &lt;/span&gt;&lt;span&gt;&apos;POST&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            headers: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                // 🗝️ 核心：把 Token 亮给后端看&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                &apos;Authorization&apos;&lt;/span&gt;&lt;span&gt;: localStorage.&lt;/span&gt;&lt;span&gt;getItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;auth_token&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        if&lt;/span&gt;&lt;span&gt; (res.status &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; 401&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            alert&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Token 失效，请重新登录！&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            logout&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        } &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            const&lt;/span&gt;&lt;span&gt; data&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; res.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            alert&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;操作成功：&quot;&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; data.data);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 3️⃣ 辅助功能&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    function&lt;/span&gt;&lt;span&gt; logout&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        localStorage.&lt;/span&gt;&lt;span&gt;removeItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;auth_token&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        location.&lt;/span&gt;&lt;span&gt;reload&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    function&lt;/span&gt;&lt;span&gt; toggleView&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;isLoggedIn&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        document.&lt;/span&gt;&lt;span&gt;getElementById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;login-box&apos;&lt;/span&gt;&lt;span&gt;).style.display &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; isLoggedIn &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &apos;none&apos;&lt;/span&gt;&lt;span&gt; :&lt;/span&gt;&lt;span&gt; &apos;block&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        document.&lt;/span&gt;&lt;span&gt;getElementById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;admin-box&apos;&lt;/span&gt;&lt;span&gt;).style.display &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; isLoggedIn &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &apos;block&apos;&lt;/span&gt;&lt;span&gt; :&lt;/span&gt;&lt;span&gt; &apos;none&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;总结&lt;a href=&quot;#总结&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;通过手搓这套系统，我们实际上复习了 Web 开发中最重要的几个概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;中间件模式&lt;/strong&gt;：如何解耦业务与鉴权。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RESTful 规范&lt;/strong&gt;：利用 Header 传递元数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态管理&lt;/strong&gt;：前端如何利用 Storage 维持会话。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交互细节&lt;/strong&gt;：如何用 CSS 提升用户体验。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;哪怕是最简单的“单密码”系统，只要用心雕琢，也能成为你技术栈中闪光的一环。&lt;/p&gt;</content:encoded><category>category:笔记</category></item><item><title>手搓深色模式：一行 CSS 类的轻量级无依赖方案（支持 iframe 同步）</title><link>https://xunyi.cloud/post/note/front-end/handcrafted-dark-mode</link><guid isPermaLink="false">note/front-end/handcrafted-dark-mode</guid><description>在现代 Web 开发中，深色模式（Dark Mode）几乎成了标配。虽然有很多 UI 库自带这个功能，但作为一个喜欢掌控细节的开发者，自己“手搓”一套方案不仅代码量小，而且能完全满足定制化需求（比如复杂的 iframe 嵌套场景）。
今天就来分享一套轻量级、无依赖、支持 iframe</description><pubDate>Sat, 29 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在现代 Web 开发中，深色模式（Dark Mode）几乎成了标配。虽然有很多 UI 库自带这个功能，但作为一个喜欢掌控细节的开发者，自己“手搓”一套方案不仅代码量小，而且能完全满足定制化需求（比如复杂的 &lt;code&gt;iframe&lt;/code&gt; 嵌套场景）。&lt;/p&gt;
&lt;p&gt;今天就来分享一套&lt;strong&gt;轻量级、无依赖、支持 iframe 同步&lt;/strong&gt;的深浅色模式切换方案。&lt;/p&gt;
&lt;h2&gt;核心原理：一行 CSS 类的魔法&lt;a href=&quot;#核心原理一行-css-类的魔法&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;我们的实现逻辑非常简单粗暴但有效：
&lt;strong&gt;通过给 &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; 标签添加一个 &lt;code&gt;dark-mode&lt;/code&gt; 类来控制全局样式。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;默认状态&lt;/strong&gt;：&lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; 无类名 -&amp;gt; 浅色模式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深色状态&lt;/strong&gt;：&lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; 有 &lt;code&gt;.dark-mode&lt;/code&gt; 类名 -&amp;gt; 深色模式。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第一步：CSS 样式的分层设计&lt;a href=&quot;#第一步css-样式的分层设计&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;为了维护方便，我们建议采用&lt;strong&gt;覆盖式&lt;/strong&gt;写法。默认写浅色样式，然后通过 CSS 优先级覆盖深色样式。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;/* === 默认样式 (浅色模式) === */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;.article-card&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    background&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;#fff&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    color&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;#333&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    transition&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;background&lt;/span&gt;&lt;span&gt; 0.3&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt; 0.3&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;/* 加上过渡动画更丝滑 */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;/* === 深色模式 (覆盖样式) === */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;/* 核心技巧：利用 body.dark-mode 提高权重 */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;.dark-mode&lt;/span&gt;&lt;span&gt; .article-card&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    background&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;#2a2a2a&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    color&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;#f0f0f0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：如果你的项目很大，建议将深色样式拆分到单独的文件中，便于管理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;第二步：JavaScript 逻辑核心&lt;a href=&quot;#第二步javascript-逻辑核心&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;我们需要一个“大脑”来处理三个任务：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;切换&lt;/strong&gt;：点击按钮修改 &lt;code&gt;class&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;记忆&lt;/strong&gt;：使用 &lt;code&gt;localStorage&lt;/code&gt; 记住用户的选择。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同步&lt;/strong&gt;：确保刷新页面后主题不丢失。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在你的全局 JS 文件（例如 &lt;code&gt;navbar.js&lt;/code&gt;）中加入以下逻辑：&lt;/p&gt;
&lt;h3&gt;1. 页面加载时的自动检测&lt;a href=&quot;#1-页面加载时的自动检测&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;// 页面加载瞬间，立即读取偏好&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// 放在 DOMContentLoaded 或 &amp;lt;head&amp;gt; 中可以减少闪烁&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; savedTheme&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; localStorage.&lt;/span&gt;&lt;span&gt;getItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;theme&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (savedTheme &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &apos;dark&apos;&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    document.body.classList.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;dark-mode&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 切换按钮的点击事件&lt;a href=&quot;#2-切换按钮的点击事件&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;themeToggle.&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;click&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 1. 切换当前 body 的类&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; isDark&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.body.classList.&lt;/span&gt;&lt;span&gt;toggle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;dark-mode&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 2. 保存状态到本地存储&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    localStorage.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;theme&apos;&lt;/span&gt;&lt;span&gt;, isDark &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &apos;dark&apos;&lt;/span&gt;&lt;span&gt; :&lt;/span&gt;&lt;span&gt; &apos;light&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 3. 更新按钮文字或图标&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    this&lt;/span&gt;&lt;span&gt;.textContent &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; isDark &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &apos;🌞&apos;&lt;/span&gt;&lt;span&gt; :&lt;/span&gt;&lt;span&gt; &apos;🌙&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 4. (进阶) 同步给子页面，见下文&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    syncThemeToIframe&lt;/span&gt;&lt;span&gt;(isDark);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;第三步：攻克难点—— Iframe 跨页面同步&lt;a href=&quot;#第三步攻克难点-iframe-跨页面同步&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;很多传统网页架构使用 &lt;code&gt;iframe&lt;/code&gt; 来加载内容，这就导致了一个痛点：&lt;strong&gt;主页变黑了，iframe 里面还是白的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们需要实现主页面与 iframe 的双向通信。&lt;/p&gt;
&lt;h3&gt;场景 A：主页面控制 iframe（父传子）&lt;a href=&quot;#场景-a主页面控制-iframe父传子&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;当我们在主导航栏点击切换时，需要手动去抓取 iframe 的 DOM 并修改它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; syncThemeToIframe&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;isDark&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; iframe&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.&lt;/span&gt;&lt;span&gt;getElementById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;contentFrame&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 必须确保 iframe 已加载且同源&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; (iframe &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; iframe.contentDocument &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; iframe.contentDocument.body) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 同步 toggle 类名&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        iframe.contentDocument.body.classList.&lt;/span&gt;&lt;span&gt;toggle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;dark-mode&apos;&lt;/span&gt;&lt;span&gt;, isDark);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;场景 B：Iframe 加载时的自我感知（子查父/存储）&lt;a href=&quot;#场景-biframe-加载时的自我感知子查父存储&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;当 iframe 里的页面被刷新或跳转时，它需要知道当前是什么模式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方法 1：读取共享的 LocalStorage（推荐）&lt;/strong&gt;
只要 iframe 和主页面同源（Domain 相同），它们共享 &lt;code&gt;localStorage&lt;/code&gt;。
在 iframe 内部引用的 JS 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;// iframe 内部脚本&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;document.&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;DOMContentLoaded&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 直接读取统一的存储键值&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; (localStorage.&lt;/span&gt;&lt;span&gt;getItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;theme&apos;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &apos;dark&apos;&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        document.body.classList.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;dark-mode&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方法 2：监听主框架的消息（更严谨）&lt;/strong&gt;
在主页面的 &lt;code&gt;main-frame.js&lt;/code&gt; 中监听 iframe 的加载事件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;iframe.&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;load&apos;&lt;/span&gt;&lt;span&gt;, () &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 只要主页面是黑的，加载完新的 iframe 页面后，强制把它也变黑&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; isParentDark&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.body.classList.&lt;/span&gt;&lt;span&gt;contains&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;dark-mode&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; (isParentDark) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        iframe.contentDocument.body.classList.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;dark-mode&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;效果展示&lt;a href=&quot;#效果展示&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;figure&gt;&lt;img src=&quot;/img/posts/note/front-end/handcrafted-dark-mode/6dfa6ea68709aad6d6bfae6257532238.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;&lt;/figure&gt;
&lt;figure&gt;&lt;img src=&quot;/img/posts/note/front-end/handcrafted-dark-mode/b817b52e27e981fc84851a4793349b62.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;&lt;/figure&gt;
&lt;h2&gt;总结与数据流图&lt;a href=&quot;#总结与数据流图&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这一套方案的数据流向非常清晰：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;用户动作&lt;/strong&gt; -&amp;gt; 触发 JS 事件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DOM 操作&lt;/strong&gt; -&amp;gt; 实时修改 CSS 类（视觉反馈）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持久化&lt;/strong&gt; -&amp;gt; 写入 &lt;code&gt;localStorage&lt;/code&gt;（防止刷新丢失）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传递&lt;/strong&gt; -&amp;gt; 穿透 &lt;code&gt;iframe&lt;/code&gt; 边界同步状态。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;用户点击 &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;   ↓&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[主页面 body 变黑] ──(同步)──&amp;gt; [Iframe body 变黑]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;   ↓&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[写入 localStorage]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;   ↓&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;(用户刷新页面)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;   ↓&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[读取 localStorage] ──&amp;gt; [恢复深色模式]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;注意事项&lt;a href=&quot;#注意事项&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;同源策略&lt;/strong&gt;：iframe 操作 &lt;code&gt;contentDocument&lt;/code&gt; 或共享 &lt;code&gt;localStorage&lt;/code&gt; 必须要求主页面和子页面在同一个域名下。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;样式优先级&lt;/strong&gt;：切记深色模式的 CSS 选择器权重必须高于默认样式（使用 &lt;code&gt;body.dark-mode .class&lt;/code&gt; 即可完美解决）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;闪烁问题&lt;/strong&gt;：将读取 &lt;code&gt;localStorage&lt;/code&gt; 的脚本放在 HTML 的 &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; 底部执行，可以最大程度避免页面先白后黑的“闪烁”现象。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;希望这篇教程能帮你轻松搞定网页的“夜间模式”！快去试试吧！&lt;/p&gt;
&lt;hr /&gt;</content:encoded><category>category:笔记</category></item><item><title>手搓 Markdown 渲染器：Fetch API 与三个经典工具库的安全渲染流水线</title><link>https://xunyi.cloud/post/note/front-end/handcrafted-markdown-renderer</link><guid isPermaLink="false">note/front-end/handcrafted-markdown-renderer</guid><description>在现代前端开发中，我们习惯了用 React、Vue 配合各种复杂的插件来渲染内容。但有时，我们只是想在一个简单的静态网页上展示一篇 README 或技术文档，引入一套重型框架显得有些“杀鸡用牛刀”。
今天，我们就回归本源，利用浏览器原生的 Fetch API</description><pubDate>Sat, 29 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在现代前端开发中，我们习惯了用 React、Vue 配合各种复杂的插件来渲染内容。但有时，我们只是想在一个简单的静态网页上展示一篇 README 或技术文档，引入一套重型框架显得有些“杀鸡用牛刀”。&lt;/p&gt;
&lt;p&gt;今天，我们就回归本源，利用浏览器原生的 &lt;strong&gt;Fetch API&lt;/strong&gt; 配合三个经典的工具库，&lt;strong&gt;手搓&lt;/strong&gt;一个安全、漂亮且支持代码高亮的 Markdown 渲染器。&lt;/p&gt;
&lt;h2&gt;核心架构：渲染流水线&lt;a href=&quot;#核心架构渲染流水线&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;要在网页上把 Markdown 变成 HTML，其实就是建立一条数据处理的“流水线”。整个过程可以分为四个步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;取货 (Fetch)&lt;/strong&gt;：通过网络请求获取 &lt;code&gt;.md&lt;/code&gt; 文件的纯文本内容。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;翻译 (Parse)&lt;/strong&gt;：将 Markdown 语法转换成 HTML 字符串（使用 &lt;code&gt;marked&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安检 (Sanitize)&lt;/strong&gt;：清洗 HTML，移除恶意脚本防止 XSS 攻击（使用 &lt;code&gt;DOMPurify&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;装修 (Highlight &amp;amp; Style)&lt;/strong&gt;：给代码块上色，并应用排版样式（使用 &lt;code&gt;highlight.js&lt;/code&gt; 和 GitHub CSS）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;1. 准备工具 (The Stack)&lt;a href=&quot;#1-准备工具-the-stack&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;我们要用到三个“前端老字号”库，直接通过 CDN 引入，无需 npm 安装，无需打包。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Marked&lt;/strong&gt;: 负责核心的 Markdown 解析，速度快，体积小。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DOMPurify&lt;/strong&gt;: 负责安全。&lt;strong&gt;（极其重要）&lt;/strong&gt; 因为 Markdown 允许嵌入 HTML，如果不清洗，黑客可以直接写入 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; 攻击你的用户。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Highlight.js&lt;/strong&gt;: 负责自动检测代码语言并高亮。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 核心代码实现&lt;a href=&quot;#2-核心代码实现&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;h3&gt;第一步：引入资源&lt;a href=&quot;#第一步引入资源&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;除了 JS 库，为了让排版好看，我们直接引入 &lt;strong&gt;GitHub Markdown CSS&lt;/strong&gt;，让你的文章看起来像在 GitHub 上一样专业。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.0/github-markdown-light.min.css&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/marked/marked.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.8/purify.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第二步：编写渲染逻辑&lt;a href=&quot;#第二步编写渲染逻辑&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;这是整个引擎的心脏。我们通过 &lt;code&gt;fetch&lt;/code&gt; 读取本地或远程的文件，然后依次调用库函数进行处理。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const contentDiv = document.getElementById(&apos;content&apos;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const mdUrl = &apos;./post.md&apos;; // 你的文件路径&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;fetch(mdUrl)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .then(response =&amp;gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        if (!response.ok) throw new Error(&quot;文件加载失败 QAQ&quot;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return response.text(); // 拿到纯文本&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    })&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .then(text =&amp;gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 1. 翻译：Markdown -&amp;gt; HTML&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        const dirtyHtml = marked.parse(text);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 2. 安检：清洗脏代码 (防 XSS)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        const cleanHtml = DOMPurify.sanitize(dirtyHtml);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 3. 上架：插入页面&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        contentDiv.innerHTML = cleanHtml;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 4. 装修：代码高亮&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        contentDiv.querySelectorAll(&apos;pre code&apos;).forEach((block) =&amp;gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            hljs.highlightElement(block);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    })&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .catch(err =&amp;gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        console.error(err);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        contentDiv.innerHTML = `&amp;lt;p style=&quot;color:red&quot;&amp;gt;加载出错了: ${err.message}&amp;lt;/p&amp;gt;`;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 常见坑点：CORS 跨域&lt;a href=&quot;#3-常见坑点cors-跨域&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;很多新手在写这段代码时，会直接双击 &lt;code&gt;index.html&lt;/code&gt; 打开，然后发现控制台报错：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Access to fetch at ‘file://…’ from origin ‘null’ has been blocked by CORS policy.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;原因：出于安全策略，浏览器禁止网页直接通过 file:// 协议读取硬盘文件。&lt;/p&gt;
&lt;p&gt;解决：你必须启动一个本地 HTTP 服务器。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VS Code 用户&lt;/strong&gt;：安装 “Live Server” 插件，右键点击 HTML 文件选择 “Open with Live Server”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python 用户&lt;/strong&gt;：终端运行 &lt;code&gt;python -m http.server&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;完整演示代码&lt;a href=&quot;#完整演示代码&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;你可以新建一个 &lt;code&gt;index.html&lt;/code&gt;，同级目录下放一个 &lt;code&gt;post.md&lt;/code&gt;，然后把下面的代码复制进去体验。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;html lang=&quot;zh-CN&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;head&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;title&amp;gt;原生 Markdown 渲染器 Demo&amp;lt;/title&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.0/github-markdown-light.min.css&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;style&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        body {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            background-color: #f6f8fa;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            padding: 20px;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            font-family: -apple-system, sans-serif;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        /* 模拟一张 A4 纸的阅读体验 */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        .page-container {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            max-width: 850px;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            margin: 0 auto;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            background: white;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            border: 1px solid #d0d7de;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            border-radius: 6px;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            padding: 40px;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            box-shadow: 0 2px 10px rgba(0,0,0,0.05);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        /* 必须加上这个类名，github-markdown-css 才会生效 */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        .markdown-body {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            box-sizing: border-box;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            min-width: 200px;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            max-width: 100%;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        .loading { text-align: center; color: #666; margin-top: 50px; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;/style&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;body&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;div class=&quot;page-container&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;div id=&quot;content&quot; class=&quot;markdown-body&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &amp;lt;div class=&quot;loading&quot;&amp;gt;正在加载文章... (∠・ω&amp;lt; )⌒★&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/marked/marked.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.8/purify.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;script&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 配置：你的 Markdown 文件路径&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        const FILE_PATH = &apos;./post.md&apos;; &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        const contentDiv = document.getElementById(&apos;content&apos;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // 开始加载&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        fetch(FILE_PATH)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            .then(response =&amp;gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                if (!response.ok) throw new Error(`状态码 ${response.status}`);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                return response.text();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            })&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            .then(text =&amp;gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                // 1. Parse: 转换&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                const rawHtml = marked.parse(text);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                // 2. Sanitize: 清洗 (安全第一!)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                const safeHtml = DOMPurify.sanitize(rawHtml);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                // 3. Render: 注入&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                contentDiv.innerHTML = safeHtml;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                // 4. Highlight: 高亮代码块&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                contentDiv.querySelectorAll(&apos;pre code&apos;).forEach((block) =&amp;gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    hljs.highlightElement(block);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            })&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            .catch(error =&amp;gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                console.error(error);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                contentDiv.innerHTML = `&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    &amp;lt;div style=&quot;text-align:center; color:#cf222e;&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        &amp;lt;h3&amp;gt;加载失败&amp;lt;/h3&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        &amp;lt;p&amp;gt;${error.message}&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        &amp;lt;small&amp;gt;提示：请确保使用了 Live Server 或本地服务器运行&amp;lt;/small&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    &amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                `;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;/script&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;总结&lt;a href=&quot;#总结&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;看，我们没有使用任何复杂的构建工具（Webpack/Vite），仅用了不到 50 行代码，就实现了一个功能完备的 Markdown 阅读器。&lt;/p&gt;
&lt;p&gt;这种“手搓”的方式非常适合个人博客、简单的文档站点，或者通过这一过程去深入理解浏览器如何处理文本和 DOM。&lt;/p&gt;
&lt;p&gt;Happy Coding! (∠・ω&amp;lt; )⌒★&lt;/p&gt;</content:encoded><category>category:笔记</category></item><item><title>把苹果的液态玻璃搬进网页：从毛玻璃到真·液态玻璃的完整实现</title><link>https://xunyi.cloud/post/note/front-end/liquid-glass-web-implementation</link><guid isPermaLink="false">note/front-end/liquid-glass-web-implementation</guid><description>液态玻璃（Liquid Glass）的前世今生与 Web 实现：SVG feDisplacementMap + Canvas 位移图打造会折射的玻璃，附可拖动 Demo。</description><pubDate>Sat, 29 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;从 macOS 到 HarmonyOS：液态玻璃的前世今生&lt;a href=&quot;#从-macos-到-harmonyos液态玻璃的前世今生&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;2024 年 WWDC，苹果发布了全新设计语言 &lt;strong&gt;Liquid Glass（液态玻璃）&lt;/strong&gt;，随 macOS Sequoia 与 iOS 18 铺开。它不再是 2013 年 iOS 7 那层简单的毛玻璃（Frosted Glass），而是一种会「折射」的材质：窗口边缘像透镜一样把背后的内容扭曲，高光随视角流动，玻璃仿佛真的有了厚度和曲率。&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://gastigado.cnies.org/d/public/images.jpeg&quot; alt=&quot;liquid_glass&quot; loading=&quot;lazy&quot; /&gt;&lt;figcaption&gt;liquid_glass&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;苹果之后，各大厂商迅速跟进。&lt;strong&gt;华为 HarmonyOS NEXT&lt;/strong&gt; 把「玻璃拟态」作为核心设计语言贯穿全系统；小米、OPPO 的系统中也不难看到类似材质的影子。一时间，「玻璃拟态」成了继拟物化、扁平化、新拟态（Neumorphism）之后的新一轮质感竞赛。&lt;/p&gt;
&lt;p&gt;对前端开发者来说，这层材质有没有办法用 Web 技术复刻？答案是：&lt;strong&gt;能&lt;/strong&gt;。这篇文章就用我博客里真实跑着的一套实现，把原理拆开讲透，文末附一个开箱即用的单文件 Demo。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文代码来自本站开屏页的液态玻璃 Dock、顶栏与搜索框，你可以随时来 &lt;a href=&quot;https://xunyi.cloud&quot;&gt;xunyi.cloud&lt;/a&gt; 亲手摸一摸这些玻璃。&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://gastigado.cnies.org/d/public/%E6%88%AA%E5%B1%8F2026-08-30%2012.32.00.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;h2&gt;三层递进：从模糊到折射&lt;a href=&quot;#三层递进从模糊到折射&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;理解液态玻璃，最好把它拆成三层，每一层都能独立实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;┌─────────────────────────────────┐&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;│ ③ 折射层：背景内容边缘扭曲       │  ← Liquid Glass 的灵魂&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;├─────────────────────────────────┤&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;│ ② 模糊层：背景内容整体虚化       │  ← 传统毛玻璃&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;├─────────────────────────────────┤&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;│ ① 质感层：半透明底 + 高光 + 阴影  │  ← 任何「玻璃感」的底色&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;└─────────────────────────────────┘&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第一层：质感&lt;a href=&quot;#第一层质感&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;半透明白底、1px 浅色边框、顶部一道斜向高光、底部柔和的投影：&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;.glass&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  background&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0.4&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  border&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt; solid&lt;/span&gt;&lt;span&gt; rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0.6&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  box-shadow&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; 10&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt; 36&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt; rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;18&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;34&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0.12&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;              inset&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0.75&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;.glass::before&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  content&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  position&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;absolute&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  inset&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  background&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;linear-gradient&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;120&lt;/span&gt;&lt;span&gt;deg&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;              rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0.56&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;40&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这层决定「像不像玻璃」，但还不会「动」。&lt;/p&gt;
&lt;h3&gt;第二层：模糊&lt;a href=&quot;#第二层模糊&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;backdrop-filter&lt;/code&gt; 一行代码让元素背后的内容虚化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;backdrop-filter&lt;/span&gt;&lt;span&gt;: blur(24px) saturate(1&lt;/span&gt;&lt;span&gt;.3&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是 2013 年至今的「毛玻璃」。但它有个致命的局限：&lt;strong&gt;整个玻璃面均匀模糊，边缘没有任何光学变化&lt;/strong&gt;——而真实的玻璃，越靠近边缘，背后的景物弯折越厉害。&lt;/p&gt;
&lt;h3&gt;第三层：折射&lt;a href=&quot;#第三层折射&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;液态玻璃的关键在于：&lt;strong&gt;模糊不是均匀的，边缘要产生「透镜畸变」&lt;/strong&gt;。背后的文字和图像经过玻璃边缘时，应该被轻微地拉扯、弯曲。&lt;/p&gt;
&lt;p&gt;浏览器没有现成的「折射」CSS 属性，但 SVG 的滤镜体系里藏着一个完美的原语——&lt;code&gt;feDisplacementMap&lt;/code&gt;。它能拿一张「位移图」，按图上每个像素的 R/G 通道值，把目标画面往横/竖方向推开。我们要做的，就是&lt;strong&gt;自己画一张位移图&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;核心实现：用 Canvas 画一张位移图&lt;a href=&quot;#核心实现用-canvas-画一张位移图&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;整体渲染管线如下：&lt;/p&gt;
&lt;pre&gt;flowchart LR
  A[玻璃容器 rect] --&amp;gt; B[逐像素 SDF 计算]
  B --&amp;gt; C[边缘位移强度图]
  C --&amp;gt; D[Canvas 像素数组 R/G 编码]
  D --&amp;gt; E[canvas.toDataURL 喂给 feImage]
  E --&amp;gt; F[feDisplacementMap 扭曲背景]
  F --&amp;gt; G[backdrop-filter: url#filter + blur]&lt;/pre&gt;
&lt;h3&gt;第一步：用 SDF 描述「玻璃的形状」&lt;a href=&quot;#第一步用-sdf-描述玻璃的形状&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;SDF（Signed Distance Field，有符号距离场）能回答一个问题：&lt;strong&gt;任意一个像素到玻璃边缘的距离是多少？&lt;/strong&gt; 对于圆角矩形，SDF 函数长这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; roundedRectSdf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; number&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;y&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; number&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;halfW&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; number&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;halfH&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; number&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; number&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  // 把坐标变换到矩形中心，先向内收缩一个圆角半径&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; qx&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;abs&lt;/span&gt;&lt;span&gt;(x) &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; halfW &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; r;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; qy&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;abs&lt;/span&gt;&lt;span&gt;(y) &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; halfH &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; r;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  // 内部点取负值，外部点取到圆角矩形的欧氏距离&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;(Math.&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(qx, qy), &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;hypot&lt;/span&gt;&lt;span&gt;(Math.&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(qx, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;), Math.&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(qy, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)) &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; r;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第二步：把距离映射成「折射强度」&lt;a href=&quot;#第二步把距离映射成折射强度&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;玻璃的边缘折射不是突然出现的，而是有一段过渡带。用 &lt;code&gt;smoothStep&lt;/code&gt; 把距离值揉成 0~1 的强度：&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; smoothStep&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; number&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; number&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;t&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; number&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; n&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, Math.&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, (t &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; a) &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; (b &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; a)));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; n &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; n &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt; -&lt;/span&gt;&lt;span&gt; 2&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt; n);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// edgeDistance 由 SDF 得出；cornerSoftness 控制过渡带宽度&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; edgeDisplacement&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; smoothStep&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, edgeDistance &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; cornerSoftness);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; scaled&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; smoothStep&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, edgeDisplacement);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// 边缘再加一圈「增强环」，模拟光线掠过玻璃棱边的亮度&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; edgeRing&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt; -&lt;/span&gt;&lt;span&gt; smoothStep&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;0.16&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0.24&lt;/span&gt;&lt;span&gt;, edgeDistance);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; edgeBoost&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; edgeRing &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; edgeRefractionStrength;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;第三步：编码进位移图&lt;a href=&quot;#第三步编码进位移图&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;浏览器要求 &lt;code&gt;feDisplacementMap&lt;/code&gt; 的位移图里：&lt;strong&gt;R 通道 = X 方向位移，G 通道 = Y 方向位移&lt;/strong&gt;，取值范围 0255 映射到 -11。我们把每个像素算出的位移向量归一化后写进 Canvas：&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; data&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Uint8ClampedArray&lt;/span&gt;&lt;span&gt;(width &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; height &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; 4&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// 先算出全图最大位移，用于归一化&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// displacement 数组存每个像素的 (dx, dy)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;let&lt;/span&gt;&lt;span&gt; i &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;; i &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt; data.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt;; i &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; 4&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; r&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (dx &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; maxScale) &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; 2&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; 0.5&lt;/span&gt;&lt;span&gt;;  &lt;/span&gt;&lt;span&gt;// 映射到 0..1&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; g&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (dy &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; maxScale) &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; 2&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; 0.5&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  data[i]     &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;round&lt;/span&gt;&lt;span&gt;(r &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; 255&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  data[i &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;round&lt;/span&gt;&lt;span&gt;(g &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; 255&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  data[i &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; 3&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 255&lt;/span&gt;&lt;span&gt;;                    &lt;/span&gt;&lt;span&gt;// 全不透明&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;ctx.&lt;/span&gt;&lt;span&gt;putImageData&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; ImageData&lt;/span&gt;&lt;span&gt;(data, width, height), &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;第四步：把 Canvas 接进 SVG 滤镜&lt;a href=&quot;#第四步把-canvas-接进-svg-滤镜&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;这是整个方案里最巧妙的一步：SVG 的 &lt;code&gt;feImage&lt;/code&gt; 可以直接引用 &lt;code&gt;canvas.toDataURL()&lt;/code&gt; 生成的 data URI，于是「JS 画的图」无缝变成「滤镜的输入」：&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; svg&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.&lt;/span&gt;&lt;span&gt;createElementNS&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;http://www.w3.org/2000/svg&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;svg&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; filter&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.&lt;/span&gt;&lt;span&gt;createElementNS&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;http://www.w3.org/2000/svg&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;filter&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; feImage&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.&lt;/span&gt;&lt;span&gt;createElementNS&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;http://www.w3.org/2000/svg&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;feImage&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; feDisplacement&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.&lt;/span&gt;&lt;span&gt;createElementNS&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;http://www.w3.org/2000/svg&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;feDisplacementMap&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;feImage.&lt;/span&gt;&lt;span&gt;setAttributeNS&lt;/span&gt;&lt;span&gt;(xlinkNS, &lt;/span&gt;&lt;span&gt;&apos;href&apos;&lt;/span&gt;&lt;span&gt;, canvas.&lt;/span&gt;&lt;span&gt;toDataURL&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;feDisplacement.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;in&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;SourceGraphic&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;feDisplacement.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;in2&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;`${&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;}-map`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;feDisplacement.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;xChannelSelector&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;R&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;feDisplacement.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;yChannelSelector&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;G&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;filter.&lt;/span&gt;&lt;span&gt;append&lt;/span&gt;&lt;span&gt;(feImage, feDisplacement);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;svg.&lt;/span&gt;&lt;span&gt;append&lt;/span&gt;&lt;span&gt;(filter);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;document.body.&lt;/span&gt;&lt;span&gt;appendChild&lt;/span&gt;&lt;span&gt;(svg);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;最后，把整条滤镜链挂到玻璃元素的 &lt;code&gt;backdrop-filter&lt;/code&gt; 上，与基础的 blur/对比度叠加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;target.style.backdropFilter &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  `url(#${&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;}) blur(${&lt;/span&gt;&lt;span&gt;blur&lt;/span&gt;&lt;span&gt;}px) contrast(${&lt;/span&gt;&lt;span&gt;contrast&lt;/span&gt;&lt;span&gt;}) brightness(${&lt;/span&gt;&lt;span&gt;brightness&lt;/span&gt;&lt;span&gt;}) saturate(${&lt;/span&gt;&lt;span&gt;saturate&lt;/span&gt;&lt;span&gt;})`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;url(#id)&lt;/code&gt; 引用的滤镜作用于元素背后的画面（backdrop），于是背景先被位移图「折射」，再被 CSS 滤镜模糊增亮——三层效果至此全部打通。&lt;/p&gt;
&lt;h2&gt;让玻璃「活」起来：鼠标跟随折射&lt;a href=&quot;#让玻璃活起来鼠标跟随折射&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;静态折射已经很像玻璃了，但苹果的液态玻璃还会随视角微微流动。Web 上没有视角，但&lt;strong&gt;鼠标就是最好的视角&lt;/strong&gt;：&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (interactive) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  target.&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;mousemove&apos;&lt;/span&gt;&lt;span&gt;, (&lt;/span&gt;&lt;span&gt;event&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; rect&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; target.&lt;/span&gt;&lt;span&gt;getBoundingClientRect&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 鼠标在玻璃上的归一化坐标 (0~1)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    mouseX &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (event.clientX &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; rect.left) &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; rect.width;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    mouseY &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (event.clientY &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; rect.top) &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; rect.height;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    render&lt;/span&gt;&lt;span&gt;();  &lt;/span&gt;&lt;span&gt;// 重新生成位移图&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;在逐像素循环里，把鼠标位置当作一个「引力源」，离鼠标越近的像素位移越小，越远则被推开：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; dxMouse&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; uvX &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; mouseX;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; dyMouse&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; uvY &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; mouseY;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; mouseDistance&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;hypot&lt;/span&gt;&lt;span&gt;(dxMouse, dyMouse);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// 高斯衰减：只在鼠标附近小范围产生影响&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; mouseInfluence&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;exp&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;mouseDistance &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; 16&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; 0.06&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;mapped &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; { x: mapped.x &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; dxMouse &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; mouseInfluence,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;           y: mapped.y &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; dyMouse &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; mouseInfluence };&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每帧重绘一张 Canvas 位图看起来重，但实际上：&lt;code&gt;dpr&lt;/code&gt; 上限 2、玻璃通常只有几百乘几十像素，加上只在 mousemove/resize 时重绘，性能开销完全可控。&lt;/p&gt;
&lt;h2&gt;性能与降级：优雅地退化为毛玻璃&lt;a href=&quot;#性能与降级优雅地退化为毛玻璃&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Canvas 不可用时&lt;/strong&gt;（极端环境）：跳过位移图，&lt;code&gt;backdrop-filter&lt;/code&gt; 只保留 &lt;code&gt;blur/contrast/brightness/saturate&lt;/code&gt;，玻璃降级成第二层的毛玻璃，视觉不崩&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ResizeObserver&lt;/strong&gt; 监听尺寸变化自动重绘位移图；&lt;code&gt;prefers-reduced-motion&lt;/code&gt; 用户关闭鼠标跟随&lt;/li&gt;
&lt;li&gt;位移图只在需要时生成，常驻成本只是几张几十 KB 的位图&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;完整可运行 Demo&lt;a href=&quot;#完整可运行-demo&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;下面是一个零依赖的单文件 Demo，保存成 &lt;code&gt;.html&lt;/code&gt; 双击打开即可玩。它是一块&lt;strong&gt;椭圆&lt;/strong&gt;液态玻璃，背景铺了光晕、条纹和 emoji 网格——&lt;strong&gt;直接用鼠标拖动玻璃&lt;/strong&gt;，就像捏着一块透镜扫过桌面，边缘折射与高光实时变化：&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;!&lt;/span&gt;&lt;span&gt;doctype&lt;/span&gt;&lt;span&gt; html&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt; lang&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;zh&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;head&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt; charset&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;&amp;gt;Liquid Glass Demo · 拖动我&amp;lt;/&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;style&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  *&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;box-sizing&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;border-box&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  body&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;margin&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;vh&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;overflow&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;hidden&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;position&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;relative&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;         font-family&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;-apple-system&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;PingFang SC&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;         background&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;#0f172a&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;user-select&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;none&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  /* 复杂背景：三色光晕 + 斜向条纹 + emoji 网格 */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  .scene&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;position&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;absolute&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;inset&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;overflow&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;hidden&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;           background&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;             radial-gradient&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;circle&lt;/span&gt;&lt;span&gt; at&lt;/span&gt;&lt;span&gt; 18&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt; 22&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;57&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;197&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;187&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.4&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;transparent&lt;/span&gt;&lt;span&gt; 42&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;             radial-gradient&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;circle&lt;/span&gt;&lt;span&gt; at&lt;/span&gt;&lt;span&gt; 82&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt; 72&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;192&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;132&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;252&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.45&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;transparent&lt;/span&gt;&lt;span&gt; 46&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;             radial-gradient&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;circle&lt;/span&gt;&lt;span&gt; at&lt;/span&gt;&lt;span&gt; 60&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt; 26&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;215&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;106&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.28&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;transparent&lt;/span&gt;&lt;span&gt; 36&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;); }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  .stripes&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;position&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;absolute&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;inset&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;-20&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;             background&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;repeating-linear-gradient&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;45&lt;/span&gt;&lt;span&gt;deg&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;               rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.07&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; 24&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;transparent&lt;/span&gt;&lt;span&gt; 24&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt; 48&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;); }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  .grid&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;position&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;absolute&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;inset&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;display&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;flex&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;flex-wrap&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;wrap&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          gap&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;8&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;padding&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;opacity&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;.92&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  .grid&lt;/span&gt;&lt;span&gt; span&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;width&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;34&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;34&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;display&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;grid&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;place-items&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;center&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;font-size&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  .word&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;position&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;absolute&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;left&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;top&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;44&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;transform&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;translate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-50&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;-50&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          font-size&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;42&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;font-weight&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;800&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;letter-spacing&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;.18&lt;/span&gt;&lt;span&gt;em&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          color&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.95&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;text-shadow&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; 4&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt; 24&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt; rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.4&lt;/span&gt;&lt;span&gt;); }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  .hint&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;position&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;absolute&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;left&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;bottom&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;22&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;transform&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;translateX&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-50&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          color&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.65&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;font-size&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;14&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;letter-spacing&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;.1&lt;/span&gt;&lt;span&gt;em&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  /* 椭圆液态玻璃（可拖动） */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  .glass&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;position&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;absolute&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;width&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;300&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;180&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;left&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;top&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;           transform&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;translate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-50&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;-50&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;border-radius&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;grab&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;           border&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt; solid&lt;/span&gt;&lt;span&gt; rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.55&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;           background&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.16&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;           box-shadow&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; 24&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt; 60&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt; rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.35&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;inset&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; 2&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.6&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;           touch-action&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;none&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  .glass.dragging&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;grabbing&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  .glass::before&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;content&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;position&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;absolute&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;inset&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;border-radius&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;inherit&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    background&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;linear-gradient&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;120&lt;/span&gt;&lt;span&gt;deg&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;.5&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;8&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;rgba&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;255&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;40&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;); }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;style&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;head&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;scene&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;stripes&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;grid&quot;&lt;/span&gt;&lt;span&gt; id&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;grid&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;word&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;LIQUID GLASS&amp;lt;/&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;/&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;glass&quot;&lt;/span&gt;&lt;span&gt; id&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;glass&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;hint&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;🖱 拖动玻璃，看背后的世界被折射&amp;lt;/&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; emojis&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&apos;🌸&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;🌿&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;🍀&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;💠&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;🔷&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;🟣&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;🫧&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;✨&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;🎐&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;🌊&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;🍃&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&apos;💧&apos;&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; grid&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.&lt;/span&gt;&lt;span&gt;getElementById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;grid&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;let&lt;/span&gt;&lt;span&gt; i &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;; i &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt; 180&lt;/span&gt;&lt;span&gt;; i&lt;/span&gt;&lt;span&gt;++&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; s&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.&lt;/span&gt;&lt;span&gt;createElement&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;span&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  s.textContent &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; emojis[i &lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt; emojis.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  grid.&lt;/span&gt;&lt;span&gt;appendChild&lt;/span&gt;&lt;span&gt;(s);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; NS&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; &apos;http://www.w3.org/2000/svg&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; XNS&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; &apos;http://www.w3.org/1999/xlink&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; target&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.&lt;/span&gt;&lt;span&gt;getElementById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;glass&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; svg&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.&lt;/span&gt;&lt;span&gt;createElementNS&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;NS&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;svg&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;svg.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;width&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;); svg.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;height&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;svg.style.position &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &apos;fixed&apos;&lt;/span&gt;&lt;span&gt;; svg.style.pointerEvents &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &apos;none&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; filter&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.&lt;/span&gt;&lt;span&gt;createElementNS&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;NS&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;filter&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;filter.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;id&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;lg&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;filter.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;filterUnits&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;userSpaceOnUse&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;filter.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;color-interpolation-filters&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;sRGB&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; feImage&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.&lt;/span&gt;&lt;span&gt;createElementNS&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;NS&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;feImage&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;feImage.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;id&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;lg-map&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; feMap&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.&lt;/span&gt;&lt;span&gt;createElementNS&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;NS&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;feDisplacementMap&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;feMap.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;in&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;SourceGraphic&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;feMap.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;in2&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;lg-map&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;feMap.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;xChannelSelector&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;R&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;feMap.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;yChannelSelector&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;G&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;filter.&lt;/span&gt;&lt;span&gt;append&lt;/span&gt;&lt;span&gt;(feImage, feMap); svg.&lt;/span&gt;&lt;span&gt;append&lt;/span&gt;&lt;span&gt;(filter);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;document.body.&lt;/span&gt;&lt;span&gt;appendChild&lt;/span&gt;&lt;span&gt;(svg);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; canvas&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; document.&lt;/span&gt;&lt;span&gt;createElement&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;canvas&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; ctx&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; canvas.&lt;/span&gt;&lt;span&gt;getContext&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;2d&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; smooth&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;t&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; n&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;Math.&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,Math.&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,(t&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;a)&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;(b&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;a)));&lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; n&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;n);};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// 椭圆 SDF：坐标按长短轴缩放成单位圆后算距离，再按短轴还原尺度&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; ellipseSdf&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;y&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;rx&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ry&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; len&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;hypot&lt;/span&gt;&lt;span&gt;(x &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; rx, y &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; ry);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; (len &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;(rx, ry);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; render&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; rect&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; target.&lt;/span&gt;&lt;span&gt;getBoundingClientRect&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; dpr&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;(devicePixelRatio &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; w&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, Math.&lt;/span&gt;&lt;span&gt;floor&lt;/span&gt;&lt;span&gt;(rect.width &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; dpr));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; h&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, Math.&lt;/span&gt;&lt;span&gt;floor&lt;/span&gt;&lt;span&gt;(rect.height &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; dpr));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  canvas.width &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; w; canvas.height &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; h;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  feImage.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;width&apos;&lt;/span&gt;&lt;span&gt;, rect.width);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  feImage.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;height&apos;&lt;/span&gt;&lt;span&gt;, rect.height);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; data&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Uint8ClampedArray&lt;/span&gt;&lt;span&gt;(w &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; h &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; 4&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; disp&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; []; &lt;/span&gt;&lt;span&gt;let&lt;/span&gt;&lt;span&gt; maxScale &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;let&lt;/span&gt;&lt;span&gt; i &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;; i &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt; data.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt;; i &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; 4&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (i &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; 4&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt; w, &lt;/span&gt;&lt;span&gt;y&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;floor&lt;/span&gt;&lt;span&gt;(i &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; 4&lt;/span&gt;&lt;span&gt; /&lt;/span&gt;&lt;span&gt; w);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; ux&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; x &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; w &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; 0.5&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;uy&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; y &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; h &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; 0.5&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 椭圆边缘折射 + 边缘增强环（透镜棱边更亮）&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; edge&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; ellipseSdf&lt;/span&gt;&lt;span&gt;(ux, uy, &lt;/span&gt;&lt;span&gt;0.5&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0.5&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; strength&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; smooth&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;smooth&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, edge &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; 0.14&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; ring&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt; -&lt;/span&gt;&lt;span&gt; smooth&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;0.14&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0.2&lt;/span&gt;&lt;span&gt;, edge);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; boost&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; ring &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; 0.7&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; mx&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; ux &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; strength &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; 0.5&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;my&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; uy &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; strength &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; 0.5&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; dx&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (mx &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; w &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; x) &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; boost, &lt;/span&gt;&lt;span&gt;dy&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (my &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; h &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; y) &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; boost;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    maxScale &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(maxScale, Math.&lt;/span&gt;&lt;span&gt;abs&lt;/span&gt;&lt;span&gt;(dx), Math.&lt;/span&gt;&lt;span&gt;abs&lt;/span&gt;&lt;span&gt;(dy));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    disp.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(dx, dy);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  maxScale &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(maxScale &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; 0.5&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0.0001&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  let&lt;/span&gt;&lt;span&gt; j &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;let&lt;/span&gt;&lt;span&gt; i &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;; i &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt; data.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt;; i &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; 4&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    data[i]     &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;round&lt;/span&gt;&lt;span&gt;((disp[j&lt;/span&gt;&lt;span&gt;++&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; maxScale &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; 2&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; 0.5&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; 255&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    data[i &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Math.&lt;/span&gt;&lt;span&gt;round&lt;/span&gt;&lt;span&gt;((disp[j&lt;/span&gt;&lt;span&gt;++&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; maxScale &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; 2&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; 0.5&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; 255&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    data[i &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; 3&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 255&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ctx.&lt;/span&gt;&lt;span&gt;putImageData&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; ImageData&lt;/span&gt;&lt;span&gt;(data, w, h), &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  feImage.&lt;/span&gt;&lt;span&gt;setAttributeNS&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;XNS&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;href&apos;&lt;/span&gt;&lt;span&gt;, canvas.&lt;/span&gt;&lt;span&gt;toDataURL&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  feMap.&lt;/span&gt;&lt;span&gt;setAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;scale&apos;&lt;/span&gt;&lt;span&gt;, maxScale &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; dpr);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;target.style.backdropFilter &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &apos;url(#lg) blur(.4px) contrast(1.16) brightness(1.05) saturate(1.3)&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// 拖动：把玻璃当透镜扫过背景，折射实时重绘&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;let&lt;/span&gt;&lt;span&gt; dragging &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; false&lt;/span&gt;&lt;span&gt;, offX &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;, offY &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;target.&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;pointerdown&apos;&lt;/span&gt;&lt;span&gt;, (&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  dragging &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  target.classList.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;dragging&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  target.&lt;/span&gt;&lt;span&gt;setPointerCapture&lt;/span&gt;&lt;span&gt;(e.pointerId);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; r&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; target.&lt;/span&gt;&lt;span&gt;getBoundingClientRect&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  offX &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; e.clientX &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; r.left; offY &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; e.clientY &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; r.top;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;target.&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;pointermove&apos;&lt;/span&gt;&lt;span&gt;, (&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;dragging) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  target.style.left &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (e.clientX &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; offX) &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &apos;px&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  target.style.top  &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (e.clientY &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; offY) &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &apos;px&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  render&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;target.&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;pointerup&apos;&lt;/span&gt;&lt;span&gt;, () &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  dragging &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  target.classList.&lt;/span&gt;&lt;span&gt;remove&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;dragging&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; ResizeObserver&lt;/span&gt;&lt;span&gt;(render).&lt;/span&gt;&lt;span&gt;observe&lt;/span&gt;&lt;span&gt;(target);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;render&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;本站的落地：Dock、顶栏与搜索框&lt;a href=&quot;#本站的落地dock顶栏与搜索框&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这套实现没有停留在 Demo，而是真实跑在 &lt;a href=&quot;https://xunyi.cloud&quot;&gt;xunyi.cloud&lt;/a&gt; 的各个玻璃面上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;开屏 Dock&lt;/strong&gt;：液态玻璃底座 + 折射跟随鼠标，配合 rAF 惯性放大、相邻图标挤压的 macOS 手感&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;博客顶栏&lt;/strong&gt;：跨页 persist 的通栏玻璃条，滚动时玻璃变实，夜间切换薰衣草紫&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;搜索弹窗&lt;/strong&gt;、&lt;strong&gt;音乐面板&lt;/strong&gt;：同一套 &lt;code&gt;createLiquidGlass&lt;/code&gt; 工具，参数各自微调&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;落地过程中踩过的坑也值得记一笔：&lt;code&gt;backdrop-filter&lt;/code&gt; 上同时挂 &lt;code&gt;url(#filter)&lt;/code&gt; 与 CSS 滤镜时，顺序与 &lt;code&gt;filterUnits&lt;/code&gt; 设置都会影响渲染；Safari 需要 &lt;code&gt;-webkit-backdrop-filter&lt;/code&gt; 前缀；而 &lt;code&gt;canvas.toDataURL()&lt;/code&gt; 每帧生成 data URI 有 GC 压力，适合「按需重绘」而不是逐帧循环。&lt;/p&gt;
&lt;h2&gt;写在最后&lt;a href=&quot;#写在最后&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;从 2013 年的毛玻璃到 2024 年的液态玻璃，苹果花了十一年把一层材质雕琢成品牌语言；而 Web 用 &lt;code&gt;backdrop-filter&lt;/code&gt;、SVG &lt;code&gt;feDisplacementMap&lt;/code&gt; 和一张 Canvas 位移图，就能让同样的质感在任何浏览器里流动。玻璃之下，折射的其实是我们对「细节」这件事的执着。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本站全部玻璃实现已开源在博客仓库，欢迎取用与魔改。&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><category>category:笔记</category></item><item><title>初音画廊 BGM 进化史：从 APlayer 到 iframe 架构</title><link>https://xunyi.cloud/post/note/front-end/miku-gallery-bgm-iframe</link><guid isPermaLink="false">note/front-end/miku-gallery-bgm-iframe</guid><description>
前言
最近在搓我的 Web 期末大作业——一个初音未来（Miku）主题的画廊网站。页面做得很漂亮，但总觉得少了点灵魂……没错，是 BGM！
本以为加个播放器很简单，结果没想到为了实现“切换页面音乐不断”，我把整个网站的架构都重构了一遍。今天就来分享一下这个从 APlayer 到 iframe</description><pubDate>Sat, 29 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;前言&lt;/strong&gt;
最近在搓我的 Web 期末大作业——一个初音未来（Miku）主题的画廊网站。页面做得很漂亮，但总觉得少了点灵魂……没错，是 BGM！
本以为加个播放器很简单，结果没想到为了实现“切换页面音乐不断”，我把整个网站的架构都重构了一遍。今天就来分享一下这个从 APlayer 到 iframe 架构的进化过程。
​&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;第一阶段：初见 APlayer&lt;a href=&quot;#第一阶段初见-aplayer&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;​
为了给我的“Miku 画廊”加上音乐，我选中了颜值超高的开源播放器 &lt;strong&gt;APlayer&lt;/strong&gt;。
​&lt;/p&gt;
&lt;h3&gt;1. 引入播放器&lt;a href=&quot;#1-引入播放器&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;​
这个步骤很简单，不用 npm，直接用 CDN 引入 CSS 和 JS 就能用，非常适合我们这种交作业的静态页面项目。
​&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;link&lt;/span&gt;&lt;span&gt; rel&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;stylesheet&quot;&lt;/span&gt;&lt;span&gt; href&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;https://cdn.jsdelivr.net/npm/aplayer/dist/APlayer.min.css&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt; src&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;https://cdn.jsdelivr.net/npm/aplayer/dist/APlayer.min.js&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h3&gt;2. 初始化&lt;a href=&quot;#2-初始化&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;​
我在页面底部加了一个 &lt;code&gt;&amp;lt;div id=&quot;aplayer&quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt;，然后写了段 JS：
​&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; ap&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; APlayer&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    container: document.&lt;/span&gt;&lt;span&gt;getElementById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;aplayer&apos;&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    fixed: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,          &lt;/span&gt;&lt;span&gt;// 开启吸底模式，让它一直悬浮在左下角&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    theme: &lt;/span&gt;&lt;span&gt;&apos;#39c5bb&apos;&lt;/span&gt;&lt;span&gt;,     &lt;/span&gt;&lt;span&gt;// Miku专属的葱绿色！&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    audio: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            name: &lt;/span&gt;&lt;span&gt;&apos;World is Mine&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            artist: &lt;/span&gt;&lt;span&gt;&apos;Hatsune Miku&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            url: &lt;/span&gt;&lt;span&gt;&apos;music/world_is_mine.mp3&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            cover: &lt;/span&gt;&lt;span&gt;&apos;img/miku_cover.jpg&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;​
&lt;strong&gt;效果：&lt;/strong&gt; 页面一打开，左下角就出现了播放器，点击播放，Miku 的歌声就出来了。完美？并没有。
​&lt;a href=&quot;#效果-页面一打开左下角就出现了播放器点击播放miku-的歌声就出来了完美并没有&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;第二阶段：遇到的痛点&lt;a href=&quot;#第二阶段遇到的痛点&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;h2&gt;​
我兴奋地点击导航栏去查看“精选展厅”页面，结果……
&lt;strong&gt;音乐停了！&lt;/strong&gt; 😱
​
&lt;strong&gt;原因分析：&lt;/strong&gt;
传统的网页跳转（&lt;code&gt;a href=&quot;page2.html&quot;&lt;/code&gt;）本质上是浏览器&lt;strong&gt;销毁当前页面，加载新页面&lt;/strong&gt;。
当前页面被销毁了，依附在页面上的 JS 播放器自然也就“挂了”。到了新页面，播放器重新初始化，音乐又从头开始放。
​
这对于听歌体验来说简直是灾难！我想要的是网易云音乐那种“页面随便切，歌声永流传”的效果。
​&lt;a href=&quot;#我兴奋地点击导航栏去查看精选展厅页面结果音乐停了-原因分析传统的网页跳转a-hrefpage2html本质上是浏览器销毁当前页面加载新页面当前页面被销毁了依附在页面上的-js-播放器自然也就挂了到了新页面播放器重新初始化音乐又从头开始放这对于听歌体验来说简直是灾难我想要的是网易云音乐那种页面随便切歌声永流传的效果&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;第三阶段：重构！iframe 主框架大法&lt;a href=&quot;#第三阶段重构iframe-主框架大法&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;​
为了解决这个问题，我参考了大佬的思路，决定使用 &lt;strong&gt;iframe（内嵌框架）&lt;/strong&gt; 技术。
​&lt;/p&gt;
&lt;h3&gt;核心思路&lt;a href=&quot;#核心思路&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;​
把网页拆成“外壳”和“内容”两部分：
​&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;外壳 (Shell)&lt;/strong&gt;：包含侧边导航栏 + 音乐播放器。&lt;strong&gt;这个外壳永远不会刷新&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内容 (Content)&lt;/strong&gt;：用一个 &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; 标签来显示具体的子页面（如首页、画廊页）。切换页面时，只替换 iframe 里的链接。
​&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1. 文件结构调整&lt;a href=&quot;#1-文件结构调整&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;​
我重新规划了项目结构：
​&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;static/&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;├── css/&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;│   ├── main-frame.css      # 专门给iframe布局用的&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;│   └── navbar.css          # 导航栏样式&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;├── html/&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;│   ├── main.html           # 【新入口】主框架外壳&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;│   ├── index.html          # 原来的首页（现在变成子页面）&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;│   └── ...                 # 其他页面&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;└── js/&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ├── main-frame.js       # 控制iframe切换&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    └── music_aplayer.js    # 播放器配置&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h3&gt;2. 编写主框架 (main.html)&lt;a href=&quot;#2-编写主框架-mainhtml&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;​
这是整个系统的核心。我在 &lt;code&gt;main.html&lt;/code&gt; 里写了这样的布局：
​&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;iframe&lt;/span&gt;&lt;span&gt; src&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;index.html&quot;&lt;/span&gt;&lt;span&gt; id&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;contentFrame&quot;&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;content-frame&quot;&lt;/span&gt;&lt;span&gt; name&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;contentFrame&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;iframe&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt; id&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;aplayer&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt; src&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;../js/navbar.js&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt; src&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;../js/main-frame.js&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt; src&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;../js/music_aplayer.js&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h3&gt;3. CSS 布局魔法 (main-frame.css)&lt;a href=&quot;#3-css-布局魔法-main-framecss&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;​
为了让 iframe 看起来不像个旧时代的网页，我们需要去掉它的边框，并处理好层级关系（z-index）：
​&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;.content-frame&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    position&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;fixed&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    top&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;left&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;right&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;bottom&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    width&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    border&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;none&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    z-index&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;/* 层级最低，在最底层 */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    transition&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;left&lt;/span&gt;&lt;span&gt; 0.3&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt; ease&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;/* 加个动画，侧边栏展开时丝滑移动 */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​
同时，侧边栏的 &lt;code&gt;z-index&lt;/code&gt; 设为 1000，播放器的 &lt;code&gt;fixed&lt;/code&gt; 模式会自动处理层级，确保它们浮在内容之上。
​&lt;/p&gt;
&lt;h3&gt;4. 让导航栏控制 iframe (main-frame.js)&lt;a href=&quot;#4-让导航栏控制-iframe-main-framejs&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;​
最关键的一步，不能让 &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; 标签直接跳转，而是要拦截点击事件，把目标地址赋给 iframe：
​&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;// 伪代码逻辑&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;link.&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;click&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    e.&lt;/span&gt;&lt;span&gt;preventDefault&lt;/span&gt;&lt;span&gt;(); &lt;/span&gt;&lt;span&gt;// 阻止浏览器跳转&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; targetUrl&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;href&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // 修改 iframe 的地址，实现页面局部刷新&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    document.&lt;/span&gt;&lt;span&gt;getElementById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;contentFrame&apos;&lt;/span&gt;&lt;span&gt;).src &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; targetUrl;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;​&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;✨ 最终效果&lt;a href=&quot;#-最终效果&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;​
现在，我的大作业变成了这样：
​&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;打开 &lt;code&gt;main.html&lt;/code&gt;&lt;/strong&gt;：首先看到首页，音乐播放器就位。
&lt;figure&gt;&lt;img src=&quot;/img/posts/note/front-end/miku-gallery-bgm-iframe/f58ef215acbdf320535b82b08c2184b2.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;&lt;/figure&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;点击“画师副展厅”&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;浏览器地址栏不变。&lt;/li&gt;
&lt;li&gt;页面中间的内容平滑变成了新页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;左下角的 APlayer 进度条还在继续走，Miku 的歌声完全没有中断！&lt;/strong&gt; 3.  &lt;strong&gt;响应式适配&lt;/strong&gt;：在手机上，侧边栏会自动变成遮罩模式，iframe 保持不动，体验非常 App 化。
​&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;📝 踩坑总结&lt;a href=&quot;#-踩坑总结&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;路径问题&lt;/strong&gt;：音乐文件的路径要基于 &lt;code&gt;main.html&lt;/code&gt; 的位置写（比如 &lt;code&gt;../images/music/xxx.mp3&lt;/code&gt;），而不是子页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;子页面导航&lt;/strong&gt;：因为子页面被嵌在 iframe 里，如果不做处理，子页面里可能又会显示一个导航栏。我在 &lt;code&gt;navbar.js&lt;/code&gt; 里加了个判断 &lt;code&gt;if (window.self !== window.top)&lt;/code&gt;，如果是被嵌套的页面，就自动隐藏导航栏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LRC 歌词&lt;/strong&gt;：APlayer 支持 &lt;code&gt;.lrc&lt;/code&gt; 文件，只要配置好 &lt;code&gt;lrcType: 3&lt;/code&gt;，歌词就能跟着动，这波大作业稳了！
​&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;结语&lt;/strong&gt;
虽然只是加个播放器，却逼着我学懂了 iframe 和单页应用（SPA）的雏形逻辑。看到网页在切换时那丝滑的体验，感觉一切折腾都值了！&lt;/p&gt;
&lt;p&gt;Ciallo～(∠・ω&amp;lt; )⌒★ 下次继续折腾！&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><category>category:笔记</category></item><item><title>Arch Linux 美化终极指南：将 KDE Plasma 100% 变身为 macOS 风格</title><link>https://xunyi.cloud/post/tools/linux/arch-kde-macos-theming</link><guid isPermaLink="false">tools/linux/arch-kde-macos-theming</guid><description>Arch Linux + KDE Plasma 是许多 Linux 爱好者心中的“梦幻组合”，它提供了极致的自由度和强大的性能。而 macOS 以其优雅、简洁的设计语言备受推崇。那么，我们能否将二者合一，在强大的 Arch 系统上享受到 macOS 的视觉体验呢？
在这里插入图片描述</description><pubDate>Sat, 29 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Arch Linux + KDE Plasma 是许多 Linux 爱好者心中的“梦幻组合”，它提供了极致的自由度和强大的性能。而 macOS 以其优雅、简洁的设计语言备受推崇。那么，我们能否将二者合一，在强大的 Arch 系统上享受到 macOS 的视觉体验呢？&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;/img/posts/tools/linux/arch-kde-macos-theming/4f44c9e0bd3e507c6ec37a3d650e2337.webp&quot; alt=&quot;在这里插入图片描述&quot; loading=&quot;lazy&quot; /&gt;&lt;figcaption&gt;在这里插入图片描述&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;答案是肯定的！KDE Plasma 的超高可定制性让这一切变得轻而易举。&lt;/p&gt;
&lt;p&gt;本指南将一步步教你，如何&lt;strong&gt;仅使用 KDE Plasma 的原生组件和官方商店&lt;/strong&gt;，将你的桌面环境完美复刻成 macOS 风格。这种方法可以确保最佳的稳定性和未来的系统兼容性，让你告别第三方插件可能带来的困扰。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;核心改造元素&lt;a href=&quot;#核心改造元素&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;我们将围绕 macOS 的四大核心视觉元素进行改造：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;全局主题&lt;/strong&gt;：统一的配色、窗口和图标风格。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;顶部菜单栏&lt;/strong&gt;：集成了全局菜单、时间和系统托盘。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;底部程序坞 (Dock)&lt;/strong&gt;：悬浮、居中、带有程序图标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;细节微调&lt;/strong&gt;：字体、窗口按钮位置、动态壁纸等。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;第一步：奠定基础 —— 安装全局主题&lt;a href=&quot;#第一步奠定基础--安装全局主题&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这是最快的一步，一个好的全局主题能立刻让你的桌面改头换面。我们将使用非常流行的 &lt;code&gt;WhiteSur&lt;/code&gt; 主题。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开 &lt;strong&gt;“系统设置 (System Settings)”&lt;/strong&gt; -&amp;gt; &lt;strong&gt;“外观 (Appearance)”&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;在左侧选择 &lt;strong&gt;“全局主题 (Global Theme)”&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;点击右下角的 &lt;strong&gt;“获取新的全局主题… (Get New Global Themes…)”&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;在弹出的窗口中，搜索 &lt;code&gt;WhiteSur&lt;/code&gt;，找到 &lt;code&gt;WhiteSur Dark&lt;/code&gt; 或 &lt;code&gt;WhiteSur Light&lt;/code&gt; 并点击 &lt;strong&gt;“安装 (Install)”&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;安装后，返回“全局主题”页面，选中 &lt;code&gt;WhiteSur&lt;/code&gt;，点击右下角的 &lt;strong&gt;“应用 (Apply)”&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：应用后，你的配色、窗口样式、图标、光标等大部分元素都应该已经很有 macOS 的感觉了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;第二步：打造灵魂 —— 顶部全局菜单栏&lt;a href=&quot;#第二步打造灵魂--顶部全局菜单栏&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这是 macOS 的精髓。我们将把应用菜单（文件、编辑…）从窗口内部移到屏幕顶部。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;进入编辑模式&lt;/strong&gt;：在桌面空白处右键 -&amp;gt; &lt;strong&gt;“进入编辑模式 (Enter Edit Mode)”&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;清理顶部面板&lt;/strong&gt;：如果顶部面板上有“任务管理器”（显示窗口列表的那个），可以右键将它移除，因为这个功能将由我们下面的 Dock 实现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;添加全局菜单&lt;/strong&gt;：点击 &lt;strong&gt;“添加部件… (Add Widgets…)”&lt;/strong&gt;，搜索并拖拽 &lt;strong&gt;“全局菜单 (Global Menu)”&lt;/strong&gt; 部件到顶部面板的最左侧。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;布局调整&lt;/strong&gt;：为了让右侧的系统托盘、时钟等靠右显示，从部件列表里拖拽一个 &lt;strong&gt;“间隔 (Spacer)”&lt;/strong&gt; 到全局菜单和系统托盘之间。这个“间隔”会自动伸长，把右边的所有东西都推到屏幕最右边。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;退出编辑模式&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;第三步：创建标志 —— “插件式”悬浮程序坞 (Dock)&lt;a href=&quot;#第三步创建标志--插件式悬浮程序坞-dock&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;虽然 KDE 原生面板功能强大，但配置起来略显繁琐。一个更简单、更“插件化”的方法是直接从社区商店下载一个现成的 Dock 部件。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;移除旧面板&lt;/strong&gt;：确保屏幕底部没有多余的面板。可以在“编辑模式”下右键点击它并选择“移除”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;获取新部件&lt;/strong&gt;：在桌面空白处右键 -&amp;gt; &lt;strong&gt;“添加部件… (Add Widgets…)”&lt;/strong&gt;。在打开的侧边栏中，点击 &lt;strong&gt;“获取新部件… (Get New Widgets…)”&lt;/strong&gt; -&amp;gt; &lt;strong&gt;“下载 Plasma 部件 (Download Plasma Widgets)”&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;搜索并安装 Dock&lt;/strong&gt;：在弹出的商店窗口中，搜索 &lt;code&gt;dock&lt;/code&gt;。你会看到 &lt;code&gt;Floating Dock&lt;/code&gt; 等多个选项。可以按评分或下载量排序，选择一个你喜欢的进行&lt;strong&gt;安装&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;添加 Dock 到桌面&lt;/strong&gt;：安装后关闭商店。回到“添加部件”列表，搜索你刚刚安装的部件名字（如 &lt;code&gt;Floating Dock&lt;/code&gt;），然后&lt;strong&gt;直接把它拖拽到桌面的底部&lt;/strong&gt;，一个配置好的 Dock 就出现了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置与固定程序&lt;/strong&gt;：退出编辑模式。这些 Dock 部件通常都有自己独立的配置选项（右键点击它即可找到），比手动配置面板更直观。最后，从你的主菜单拖拽常用的程序图标到 Dock 上即可固定。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;重要提示&lt;/strong&gt;：社区开发的第三方部件可能更新不及时，在 KDE Plasma 大版本更新后有失效的风险。虽然此方法更简单，但稳定性和兼容性可能不如使用原生面板的方案。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;第四步：注入灵魂 —— 细节是魔鬼&lt;a href=&quot;#第四步注入灵魂--细节是魔鬼&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;窗口按钮左置&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进入 &lt;strong&gt;“系统设置” -&amp;gt; “外观” -&amp;gt; “窗口装饰 (Window Decorations)”&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;点击“标题栏按钮 (Titlebar Buttons)”标签页，直接用鼠标把“关闭、最小化、最大化”图标从右边拖到左边。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;更换字体&lt;/strong&gt;：
macOS 的字体优雅清晰。我们可以安装一个优秀的开源替代品 &lt;code&gt;Inter&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;安装字体&lt;/strong&gt;：
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; pacman&lt;/span&gt;&lt;span&gt; -S&lt;/span&gt;&lt;span&gt; inter-font&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用字体&lt;/strong&gt;：进入 &lt;strong&gt;“系统设置” -&amp;gt; “外观” -&amp;gt; “字体 (Fonts)”&lt;/strong&gt;，将除“固定宽度”外的所有字体都设置为 &lt;code&gt;Inter&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;添加动态壁纸 (可选)&lt;/strong&gt;：
macOS 近年的动态壁纸令人印象深刻。在 KDE 上，实现这一效果最简单且功能最强大的方法，通常是&lt;strong&gt;从社区商店安装一个专门的动态壁纸插件&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;获取并安装插件&lt;/strong&gt;：右键桌面 -&amp;gt; &lt;strong&gt;“配置桌面和壁纸…”&lt;/strong&gt;，在设置窗口的右下角点击 &lt;strong&gt;“获取新插件… (Get New Plugins…)”&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在弹出的商店窗口中，搜索 &lt;code&gt;video wallpaper&lt;/code&gt;。你会看到 &lt;code&gt;Smart Video Wallpaper Reborn&lt;/code&gt; 等多个选项。选择一个评分和下载量高的进行&lt;strong&gt;安装&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;应用插件&lt;/strong&gt;：安装后，&lt;strong&gt;注销再重新登录&lt;/strong&gt;（或重启电脑）以确保插件被加载。再次进入“配置桌面和壁纸…”设置，在 &lt;strong&gt;“壁纸类型 (Wallpaper Type)”&lt;/strong&gt; 的下拉菜单中，你应该就能找到新安装的插件（如 &lt;code&gt;Smart Video Wallpaper Reborn&lt;/code&gt;）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;设置视频&lt;/strong&gt;：选中它，你就可以在下方的设置区域添加你喜欢的视频文件，并进行详细的配置了。
&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;/img/posts/tools/linux/arch-kde-macos-theming/6c77199efe593a702668099974798e83.webp&quot; alt=&quot;在这里插入图片描述&quot; loading=&quot;lazy&quot; /&gt;&lt;figcaption&gt;在这里插入图片描述&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;性能优化提示：开启硬件加速（强烈推荐）&lt;/strong&gt;
动态壁纸会消耗更多资源。为了让它更流畅、更省电，强烈建议开启硬件加速，让显卡来处理视频解码。这需要安装对应的驱动：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# 适用于 Intel 核显&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; pacman&lt;/span&gt;&lt;span&gt; -S&lt;/span&gt;&lt;span&gt; intel-media-driver&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;安装后重启即可生效，这对你系统上所有的视频播放都有好处。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;美化登录屏幕 (可选)&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进入 &lt;strong&gt;“系统设置” -&amp;gt; “开机和关机 (Startup &amp;amp; Shutdown)” -&amp;gt; “登录屏幕 (SDDM)”&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;点击 &lt;strong&gt;“获取新的 SDDM 主题…”&lt;/strong&gt;，搜索 &lt;code&gt;macOS&lt;/code&gt; 或 &lt;code&gt;WhiteSur&lt;/code&gt;，安装并应用你喜欢的主题。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;结语&lt;a href=&quot;#结语&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;恭喜你！通过以上步骤，你已经成功地在强大的 Arch Linux 上，拥有了一个媲美 macOS 的桌面环境。这正是 KDE Plasma 和 Arch Linux 的魅力所在——只要你敢想，一切皆可定制。&lt;/p&gt;
&lt;p&gt;现在，享受你的专属桌面吧！&lt;/p&gt;</content:encoded><category>category:工具</category></item><item><title>Arch Linux 终端美化：Starship 与 Yazi 安装配置全指南</title><link>https://xunyi.cloud/post/tools/linux/arch-terminal-starship-yazi</link><guid isPermaLink="false">tools/linux/arch-terminal-starship-yazi</guid><description>前言
本指南将整合两个强大的终端工具——Starship 和 Yazi——的安装与配置流程，帮助你将 Arch Linux 的终端体验提升到一个新的水平。Starship 能够为你提供一个美观、信息丰富的命令提示符，而 Yazi 则是一个高效、带预览功能的终端文件管理器。</description><pubDate>Sat, 29 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;a href=&quot;#前言&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;本指南将整合两个强大的终端工具——Starship 和 Yazi——的安装与配置流程，帮助你将 Arch Linux 的终端体验提升到一个新的水平。Starship 能够为你提供一个美观、信息丰富的命令提示符，而 Yazi 则是一个高效、带预览功能的终端文件管理器。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;第一部分：Starship - 高度可定制的终端提示符&lt;a href=&quot;#第一部分starship---高度可定制的终端提示符&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Starship 是一个用 Rust 编写的、速度极快、高度可定制的终端提示符。它能够在你的命令行中，根据当前目录和环境，智能地显示 Git 分支、项目版本号等关键信息。
&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;/img/posts/tools/linux/arch-terminal-starship-yazi/96597c116c7ef41da36a7ccb9d048265.webp&quot; alt=&quot;在这里插入图片描述&quot; loading=&quot;lazy&quot; /&gt;&lt;figcaption&gt;在这里插入图片描述&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;1.1 安装字体 (获取图标支持)&lt;a href=&quot;#11-安装字体-获取图标支持&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Starship 使用了许多 Nerd Font 字体库中的特殊图标。为了能正常显示这些图标，而不是乱码方块，我们必须先安装并配置一款 Nerd Font 字体。&lt;/p&gt;
&lt;h4&gt;1.1.1 安装字体包&lt;a href=&quot;#111-安装字体包&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;FiraCode Nerd Font&lt;/code&gt; 是一款优秀且常用的选择。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; pacman&lt;/span&gt;&lt;span&gt; -S&lt;/span&gt;&lt;span&gt; ttf-firacode-nerd&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.1.2 刷新系统字体缓存&lt;a href=&quot;#112-刷新系统字体缓存&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;为了让系统能立刻识别到新安装的字体，需要手动刷新字体缓存。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;fc-cache&lt;/span&gt;&lt;span&gt; -fv&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.1.3 在终端模拟器中配置字体&lt;a href=&quot;#113-在终端模拟器中配置字体&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;这是最关键的一步。你需要&lt;strong&gt;打开你所使用的终端软件（如 Konsole）的设置&lt;/strong&gt;，并将字体更改为 &lt;code&gt;FiraCode Nerd Font&lt;/code&gt;。&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;/img/posts/tools/linux/arch-terminal-starship-yazi/00d588710911b3dba787025911ae379e.webp&quot; alt=&quot;在这里插入图片描述&quot; loading=&quot;lazy&quot; /&gt;&lt;figcaption&gt;在这里插入图片描述&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;以 &lt;strong&gt;Konsole&lt;/strong&gt; 为例：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开 Konsole。&lt;/li&gt;
&lt;li&gt;点击菜单栏 &lt;code&gt;设置&lt;/code&gt; -&amp;gt; &lt;code&gt;编辑当前方案...&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;选择 &lt;code&gt;外观&lt;/code&gt; 标签页，点击 &lt;code&gt;选择字体...&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;在字体列表中搜索并选中 &lt;code&gt;FiraCode Nerd Font&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;点击 &lt;code&gt;确定&lt;/code&gt; 和 &lt;code&gt;应用&lt;/code&gt; 保存设置。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1.2 安装 Starship&lt;a href=&quot;#12-安装-starship&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Starship 主程序可以直接从 Arch 官方仓库安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; pacman&lt;/span&gt;&lt;span&gt; -S&lt;/span&gt;&lt;span&gt; starship&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3 在 Shell 中启用 Starship&lt;a href=&quot;#13-在-shell-中启用-starship&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;为了让 Starship 能够接管默认的终端提示符，需要在你的 Shell 配置文件中添加一行启动命令。&lt;strong&gt;这一步是让配置永久生效的关键&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;1.3.1 确认你的 Shell&lt;a href=&quot;#131-确认你的-shell&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;首先，确认你当前使用的 Shell 类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; $SHELL&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本教程以最常见的 &lt;code&gt;/bin/bash&lt;/code&gt; 为例。&lt;/p&gt;
&lt;h4&gt;1.3.2 添加配置到 &lt;code&gt;.bashrc&lt;/code&gt;&lt;a href=&quot;#132-添加配置到-bashrc&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;运行以下命令，将 Starship 的启动脚本自动追加到 Bash 的配置文件末尾。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &apos;eval &quot;$(starship init bash)&quot;&apos;&lt;/span&gt;&lt;span&gt; &amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt; ~/.bashrc&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加完成后，&lt;strong&gt;完全关闭并重新打开你的终端窗口&lt;/strong&gt;，Starship 提示符就会自动生效了。&lt;/p&gt;
&lt;h3&gt;1.4 (可选) 个性化你的 Starship&lt;a href=&quot;#14-可选-个性化你的-starship&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;你可以通过创建一个 TOML 格式的配置文件来进行高度定制。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建配置文件&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; ~/.config&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;touch&lt;/span&gt;&lt;span&gt; ~/.config/starship.toml&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;编辑配置文件&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;nano&lt;/span&gt;&lt;span&gt; ~/.config/starship.toml&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;添加自定义配置&lt;/strong&gt;：
以下是一个简单的示例，它修改了提示符的结束符号，并设置主机名仅在 SSH 连接时显示。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# ~/.config/starship.toml&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 修改命令成功或失败时显示的符号&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;character&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;success_symbol = &lt;/span&gt;&lt;span&gt;&quot;[➜](bold green)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;error_symbol = &lt;/span&gt;&lt;span&gt;&quot;[✗](bold red)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 默认隐藏主机名，仅在 SSH 远程连接时显示&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;hostname&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;ssh_only = &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;disabled = &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;保存文件后，新配置会立即在新的终端提示符上生效。更多可用的配置项可以查阅 &lt;a href=&quot;https://starship.rs/config/&quot;&gt;Starship 官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;第二部分：Yazi - 高效的终端文件管理器&lt;a href=&quot;#第二部分yazi---高效的终端文件管理器&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;本部分旨在记录在 Arch Linux 上安装和配置 &lt;code&gt;Yazi&lt;/code&gt; 的完整过程。特别针对通过 &lt;code&gt;pacman&lt;/code&gt; 安装后可能遇到的&lt;strong&gt;旧版本 Yazi&lt;/strong&gt; 的情况，该版本&lt;strong&gt;没有自动生成配置文件的命令&lt;/strong&gt;，且部分&lt;strong&gt;配置项与新版本不同&lt;/strong&gt;。本指南将提供一套确保可以成功配置的“手动挡”方案。&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;/img/posts/tools/linux/arch-terminal-starship-yazi/e208224d78f3d93ab4fe5dfa8acaadb8.webp&quot; alt=&quot;在这里插入图片描述&quot; loading=&quot;lazy&quot; /&gt;&lt;figcaption&gt;在这里插入图片描述&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h3&gt;2.1 安装&lt;a href=&quot;#21-安装&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;h4&gt;2.1.1 安装 Yazi 主程序&lt;a href=&quot;#211-安装-yazi-主程序&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Yazi 位于 Arch Linux 的官方软件仓库中，可以直接通过 &lt;code&gt;pacman&lt;/code&gt; 安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; pacman&lt;/span&gt;&lt;span&gt; -S&lt;/span&gt;&lt;span&gt; yazi&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.1.2 安装预览功能依赖&lt;a href=&quot;#212-安装预览功能依赖&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;为了让 Yazi 能够预览图片、视频、PDF、压缩包等文件，我们需要安装一系列的依赖工具。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;从官方仓库安装：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; pacman&lt;/span&gt;&lt;span&gt; -S&lt;/span&gt;&lt;span&gt; p7zip&lt;/span&gt;&lt;span&gt; ffmpegthumbnailer&lt;/span&gt;&lt;span&gt; poppler&lt;/span&gt;&lt;span&gt; bat&lt;/span&gt;&lt;span&gt; ueberzugpp&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;p7zip&lt;/code&gt;: 用于预览压缩文件（.7z, .zip, .rar 等）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ffmpegthumbnailer&lt;/code&gt;: 用于生成视频文件的缩略图。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;poppler&lt;/code&gt;: 用于预览 PDF 文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bat&lt;/code&gt;: 用于代码文件语法高亮。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ueberzugpp&lt;/code&gt;: 用于在终端中显示图片。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;从 AUR 仓库安装：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;字体预览工具 &lt;code&gt;fontpreview-ueberzug&lt;/code&gt; 需要从 AUR 安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;yay&lt;/span&gt;&lt;span&gt; -S&lt;/span&gt;&lt;span&gt; fontpreview-ueberzug&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 核心配置 (手动创建)&lt;a href=&quot;#22-核心配置-手动创建&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;由于我们面对的 Yazi 版本可能没有 &lt;code&gt;yazi init&lt;/code&gt; 命令，我们需要手动创建配置文件来启用和优化预览功能。&lt;/p&gt;
&lt;h4&gt;2.2.1 创建配置目录&lt;a href=&quot;#221-创建配置目录&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; ~/.config/yazi/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.2 创建并编辑主配置文件&lt;a href=&quot;#222-创建并编辑主配置文件&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;使用文本编辑器创建一个新的 &lt;code&gt;yazi.toml&lt;/code&gt; 文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;nano&lt;/span&gt;&lt;span&gt; ~/.config/yazi/yazi.toml&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.3 写入核心配置&lt;a href=&quot;#223-写入核心配置&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;将以下内容&lt;strong&gt;完整复制&lt;/strong&gt;并粘贴到你打开的 &lt;code&gt;yazi.toml&lt;/code&gt; 文件中。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# --- 插件配置 ---&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 告诉 Yazi 使用 ueberzugpp 作为图片预览工具&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;image_previewer = &lt;/span&gt;&lt;span&gt;&quot;ueberzugpp&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# --- 预览质量配置 ---&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;preview&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 提高 PDF 预览的清晰度 (DPI)，默认是 150&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;pdf_poppler_dpi = &lt;/span&gt;&lt;span&gt;300&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 提高图片预览的质量&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 注意：此旧版本的范围是 50-90，而不是新版的 1-10&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;image_quality = &lt;/span&gt;&lt;span&gt;85&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;完成后，保存并退出编辑器 (&lt;code&gt;Ctrl + O&lt;/code&gt;, &lt;code&gt;Enter&lt;/code&gt;, &lt;code&gt;Ctrl + X&lt;/code&gt;)。&lt;/p&gt;
&lt;h3&gt;2.3 强烈推荐：更新系统&lt;a href=&quot;#23-强烈推荐更新系统&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;我们遇到的所有配置问题（没有 &lt;code&gt;init&lt;/code&gt; 命令、配置项范围不同）都源于软件版本过旧。在 Arch Linux 这样的滚动发行版上，保持系统更新是最佳实践。&lt;/p&gt;
&lt;p&gt;运行以下命令可以更新你系统上所有的软件包，包括 Yazi：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; pacman&lt;/span&gt;&lt;span&gt; -Syu&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新后，你可能会获得一个拥有 &lt;code&gt;yazi init&lt;/code&gt; 命令的新版本 Yazi，未来的配置会更加方便。&lt;/p&gt;
&lt;h3&gt;2.4 (可选) Shell 集成&lt;a href=&quot;#24-可选-shell-集成&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;为了在退出 Yazi 后，终端能自动跳转到最后浏览的目录，可以将以下函数添加到你的 Shell 配置文件中 (如 &lt;code&gt;~/.bashrc&lt;/code&gt; 或 &lt;code&gt;~/.zshrc&lt;/code&gt;)。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Yazi: cd on quit&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; y&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    local&lt;/span&gt;&lt;span&gt; tmp&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;$(&lt;/span&gt;&lt;span&gt;mktemp&lt;/span&gt;&lt;span&gt; -t&lt;/span&gt;&lt;span&gt; &quot;yazi-cwd.XXXXX&quot;)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    yazi&lt;/span&gt;&lt;span&gt; --cwd-file=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$tmp&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;-f&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$tmp&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; ]; &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        cd&lt;/span&gt;&lt;span&gt; &quot;$(&lt;/span&gt;&lt;span&gt;cat&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$tmp&lt;/span&gt;&lt;span&gt;&quot;)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        rm&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$tmp&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    fi&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;添加后，使用 &lt;code&gt;source ~/.bashrc&lt;/code&gt; (或 &lt;code&gt;~/.zshrc&lt;/code&gt;)使其生效，之后便可以用 &lt;code&gt;y&lt;/code&gt; 命令来启动 Yazi。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;总结&lt;a href=&quot;#总结&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;通过以上步骤，你已经成功地为你的 Arch Linux 终端配置了 Starship 提示符和 Yazi 文件管理器。这两个工具的结合，不仅能让你的终端界面更加现代化和美观，也能极大地提升你在命令行下的工作效率。建议查阅它们的官方文档，探索更多高级定制选项。&lt;/p&gt;</content:encoded><category>category:工具</category></item><item><title>WSL 疑难杂症：设置搜索无结果问题的解决方法</title><link>https://xunyi.cloud/post/tools/linux/wsl-settings-not-found-fix</link><guid isPermaLink="false">tools/linux/wsl-settings-not-found-fix</guid><description>问题如下
image-20251026165815387
解决方法如下
1.搜索栏搜索“WSL Settings”。
image-20251026170229286
2.更改网络设置为”Mirrored”。
image-20251026170331852
3.终端重启 WSL 服务。
wsl</description><pubDate>Sat, 29 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;h4&gt;问题如下&lt;a href=&quot;#问题如下&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;figure&gt;&lt;img src=&quot;/img/posts/tools/linux/wsl-settings-not-found-fix/1799b768d51f42e1a0e81700b31a9d5c.webp&quot; alt=&quot;image-20251026165815387&quot; loading=&quot;lazy&quot; /&gt;&lt;figcaption&gt;image-20251026165815387&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h4&gt;解决方法如下&lt;a href=&quot;#解决方法如下&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;1.搜索栏搜索“WSL Settings”。&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;/img/posts/tools/linux/wsl-settings-not-found-fix/aad924cfdf6ca08361ebf1c4b0cc3828.webp&quot; alt=&quot;image-20251026170229286&quot; loading=&quot;lazy&quot; /&gt;&lt;figcaption&gt;image-20251026170229286&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;2.更改网络设置为”Mirrored”。&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;/img/posts/tools/linux/wsl-settings-not-found-fix/fbe7220e175d0b8d902d8d10839c5e30.webp&quot; alt=&quot;image-20251026170331852&quot; loading=&quot;lazy&quot; /&gt;&lt;figcaption&gt;image-20251026170331852&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;3.终端重启 WSL 服务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;wsl&lt;/span&gt;&lt;span&gt; --shutdown&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;</content:encoded><category>category:工具</category></item></channel></rss>