<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>清风晚月</title>
        <link>https://zongbao.wang</link>
        <description>且听风吟，且看月明</description>
        <language>zh-CN</language>
        <lastBuildDate>Sun, 30 Aug 2026 21:05:03 +0800</lastBuildDate>
        <atom:link href="https://zongbao.wang/rss" rel="self" type="application/rss+xml"/>
        
        <item>
            <title>给小站加了个 2048</title>
            <link>https://zongbao.wang/post/给小站加了个-2048</link>
            <guid>https://zongbao.wang/post/给小站加了个-2048</guid>
            <pubDate>Wed, 17 Jun 2026 12:31:29 +0800</pubDate>
            <description><![CDATA[给博客加了个 2048 小游戏，带方块移动动画和手机滑动支持。]]></description>
            <content:encoded><![CDATA[<h2 id="_1">游戏厅上线</h2>
<p>博客新增了一个小游戏——<strong>2048</strong>，经典的数字合并游戏。</p>
<hr />
<h2 id="_2">玩法</h2>
<p>使用方向键或滑动屏幕移动方块，相同数字相撞会合并，目标是合成 2048！</p>
<ul>
<li>方向键 / 触摸滑动控制</li>
<li>到达 2048 后可选择继续挑战</li>
<li>最高分自动保存</li>
</ul>
<h2 id="_3">技术实现</h2>
<h3 id="_4">方块移动动画</h3>
<p>每个方块是一个绝对定位的 DOM 元素，通过 CSS transition 实现平滑移动：</p>
<div class="codehilite"><pre><span></span><code><span class="na">.g2048-tile</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nl">transition:</span><span class="w"> </span><span class="nf">top</span><span class="w"> </span><span class="mi">0</span><span class="no">.12s</span><span class="w"> </span><span class="no">ease</span><span class="p">,</span><span class="w"> </span><span class="no">left</span><span class="w"> </span><span class="mi">0</span><span class="no">.12s</span><span class="w"> </span><span class="no">ease</span><span class="c1">;</span>
<span class="err">}</span>
</code></pre></div>

<p>游戏逻辑计算出每个方块的最终位置后直接设置 style.top 和 style.left，浏览器自动补间动画，性能消耗极低。</p>
<h3 id="_5">触摸滑动</h3>
<p>通过 touchstart / touchend 事件监听滑动方向，设置 30px 阈值防止误触，并在游戏网格上阻止页面滚动。</p>
<h3 id="_6">方块消失与合并</h3>
<p>合并时旧方块缩小的同时新方块弹出放大，视觉效果连贯：</p>
<ul>
<li>tile-new：从 0 到 1 缩放出现</li>
<li>tile-merged：合并时 1 -&gt; 1.15 -&gt; 1 脉冲</li>
<li>tile-removed：缩小淡出消失</li>
</ul>
<hr />
<h2 id="_7">页面布局</h2>
<p>游戏页面采用全宽布局，适配手机和电脑，最大宽度 560px 保证游戏网格大小适中。</p>]]></content:encoded>
        </item>
        
        <item>
            <title>从零开始搭建这个博客</title>
            <link>https://zongbao.wang/post/building-this-blog-from-scratch</link>
            <guid>https://zongbao.wang/post/building-this-blog-from-scratch</guid>
            <pubDate>Mon, 01 Jun 2026 15:10:55 +0800</pubDate>
            <description><![CDATA[使用 Flask + SQLite 从零搭建个人博客的全过程，包括技术选型、核心架构、前端亮点和部署方案。]]></description>
            <content:encoded><![CDATA[<h2 id="_1">缘起</h2>
<p>一直以来都想拥有一个属于自己的博客，记录技术思考和生活感悟。在网上看了不少方案，最终决定自己动手写一个——用 <strong>Flask</strong> + <strong>SQLite</strong> 的轻量组合，部署在云服务器上。</p>
<hr />
<h2 id="_2">技术选型</h2>
<table>
<thead>
<tr>
<th>模块</th>
<th>选择</th>
<th>原因</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>后端框架</strong></td>
<td>Flask 3.1</td>
<td>轻量灵活，Python 生态</td>
</tr>
<tr>
<td><strong>数据库</strong></td>
<td>SQLite + SQLAlchemy</td>
<td>单用户博客，无需 MySQL</td>
</tr>
<tr>
<td><strong>前端</strong></td>
<td>原生 HTML/CSS/JS</td>
<td>无框架依赖，加载轻快</td>
</tr>
<tr>
<td><strong>Markdown</strong></td>
<td>Python-Markdown + Pygments</td>
<td>文章编写体验好，代码高亮</td>
</tr>
<tr>
<td><strong>部署</strong></td>
<td>Gunicorn + Nginx</td>
<td>简单可靠，反向代理静态文件</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="_3">核心架构</h2>
<h3 id="_4">应用工厂模式</h3>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">create_app</span><span class="p">(</span><span class="n">config_name</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
    <span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="vm">__name__</span><span class="p">)</span>
    <span class="n">app</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">from_object</span><span class="p">(</span><span class="n">config_map</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">config_name</span><span class="p">))</span>

    <span class="c1"># 初始化扩展</span>
    <span class="n">db</span><span class="o">.</span><span class="n">init_app</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
    <span class="n">login_manager</span><span class="o">.</span><span class="n">init_app</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
    <span class="n">csrf</span><span class="o">.</span><span class="n">init_app</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>

    <span class="c1"># 注册蓝图</span>
    <span class="n">app</span><span class="o">.</span><span class="n">register_blueprint</span><span class="p">(</span><span class="n">main_bp</span><span class="p">)</span>       <span class="c1"># 公开路由</span>
    <span class="n">app</span><span class="o">.</span><span class="n">register_blueprint</span><span class="p">(</span><span class="n">auth_bp</span><span class="p">,</span> <span class="n">url_prefix</span><span class="o">=</span><span class="s1">&#39;/auth&#39;</span><span class="p">)</span>
    <span class="n">app</span><span class="o">.</span><span class="n">register_blueprint</span><span class="p">(</span><span class="n">admin_bp</span><span class="p">,</span> <span class="n">url_prefix</span><span class="o">=</span><span class="s1">&#39;/admin&#39;</span><span class="p">)</span>

    <span class="c1"># 初始化数据库</span>
    <span class="k">with</span> <span class="n">app</span><span class="o">.</span><span class="n">app_context</span><span class="p">():</span>
        <span class="n">db</span><span class="o">.</span><span class="n">create_all</span><span class="p">()</span>
        <span class="n">_init_admin</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">app</span>
</code></pre></div>

<h3 id="_5">数据库模型</h3>
<p>文章模型是核心，支持 Markdown 渲染、标签管理、分类关联：</p>
<div class="codehilite"><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">Post</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
    <span class="nb">id</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">Column</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">Integer</span><span class="p">,</span> <span class="n">primary_key</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
    <span class="n">title</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">Column</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="mi">200</span><span class="p">),</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
    <span class="n">slug</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">Column</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="mi">200</span><span class="p">),</span> <span class="n">unique</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">index</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
    <span class="n">content</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">Column</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>  <span class="c1"># 原始 Markdown</span>
    <span class="n">tags</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">Column</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="mi">500</span><span class="p">),</span> <span class="n">default</span><span class="o">=</span><span class="s1">&#39;&#39;</span><span class="p">)</span>  <span class="c1"># 逗号分隔</span>
    <span class="n">is_published</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">Column</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">Boolean</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
    <span class="n">author_id</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">Column</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">Integer</span><span class="p">,</span> <span class="n">db</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="s1">&#39;user.id&#39;</span><span class="p">))</span>
    <span class="n">category_id</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">Column</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">Integer</span><span class="p">,</span> <span class="n">db</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="s1">&#39;category.id&#39;</span><span class="p">))</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">content_html</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="kn">import</span><span class="w"> </span><span class="nn">markdown</span>
        <span class="n">md</span> <span class="o">=</span> <span class="n">markdown</span><span class="o">.</span><span class="n">Markdown</span><span class="p">(</span><span class="n">extensions</span><span class="o">=</span><span class="p">[</span><span class="s1">&#39;extra&#39;</span><span class="p">,</span> <span class="s1">&#39;codehilite&#39;</span><span class="p">,</span> <span class="s1">&#39;toc&#39;</span><span class="p">])</span>
        <span class="k">return</span> <span class="n">md</span><span class="o">.</span><span class="n">convert</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">content</span><span class="p">)</span>
</code></pre></div>

<hr />
<h2 id="_6">前端亮点</h2>
<h3 id="_7">暗夜模式</h3>
<p>通过 CSS 变量实现一键切换，状态保存在 localStorage：</p>
<div class="codehilite"><pre><span></span><code><span class="p">:</span><span class="nd">root</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nv">--bg</span><span class="p">:</span><span class="w"> </span><span class="mh">#f5f5f5</span><span class="p">;</span>
<span class="w">  </span><span class="nv">--font-color</span><span class="p">:</span><span class="w"> </span><span class="mh">#4c4948</span><span class="p">;</span>
<span class="w">  </span><span class="nv">--card-bg</span><span class="p">:</span><span class="w"> </span><span class="mh">#fff</span><span class="p">;</span>
<span class="p">}</span>
<span class="o">[</span><span class="nt">data-theme</span><span class="o">=</span><span class="s1">&#39;dark&#39;</span><span class="o">]</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nv">--bg</span><span class="p">:</span><span class="w"> </span><span class="mh">#0d0d0d</span><span class="p">;</span>
<span class="w">  </span><span class="nv">--font-color</span><span class="p">:</span><span class="w"> </span><span class="nb">rgba</span><span class="p">(</span><span class="mi">255</span><span class="p">,</span><span class="mi">255</span><span class="p">,</span><span class="mi">255</span><span class="p">,</span><span class="mf">0.75</span><span class="p">);</span>
<span class="w">  </span><span class="nv">--card-bg</span><span class="p">:</span><span class="w"> </span><span class="mh">#141414</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>

<h3 id="pjax">PJAX 无刷新导航</h3>
<p>页面切换只替换 <code>#pjax-container</code> 内的内容，无需整页加载：</p>
<div class="codehilite"><pre><span></span><code><span class="ow">new</span><span class="w"> </span><span class="nx">Pjax</span><span class="p">({</span>
<span class="w">  </span><span class="nx">selectors</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;#pjax-container&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;title&#39;</span><span class="p">]</span>
<span class="p">});</span>
</code></pre></div>

<h3 id="_8">一言 · 每日随机语录</h3>
<p>每次回到首页，从缓存池中随机挑一句诗词或名言展示：</p>
<div class="codehilite"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">pool</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">&#39;saying_pool&#39;</span><span class="p">))</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="p">[];</span>
<span class="nx">el</span><span class="p">.</span><span class="nx">textContent</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">pool</span><span class="p">[</span><span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">random</span><span class="p">()</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nx">pool</span><span class="p">.</span><span class="nx">length</span><span class="p">)];</span>
</code></pre></div>

<p>每天凌晨自动轮换 25 条，补充新句子。</p>
<hr />
<h3 id="neteaseminiplayer-v2">NeteaseMiniPlayer v2 · 悬浮音乐播放器</h3>
<p>在博客右下角集成了网易云音乐悬浮播放器，使用开源的 <a href="https://docs.nmp.hypcvgm.top/">NeteaseMiniPlayer v2</a> 组件，支持歌词显示、播放列表、随机播放等丰富功能。</p>
<h4 id="_9">基本用法</h4>
<p>在页面中放置一个自定义元素，组件会自动初始化：</p>
<div class="codehilite"><pre><span></span><code><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;netease-mini-player&quot;</span>
     <span class="na">data-playlist-id</span><span class="o">=</span><span class="s">&quot;496585734&quot;</span>
     <span class="na">data-embed</span><span class="o">=</span><span class="s">&quot;false&quot;</span>
     <span class="na">data-position</span><span class="o">=</span><span class="s">&quot;bottom-left&quot;</span>
     <span class="na">data-lyric</span><span class="o">=</span><span class="s">&quot;true&quot;</span>
     <span class="na">data-theme</span><span class="o">=</span><span class="s">&quot;auto&quot;</span>
     <span class="na">data-autoplay</span><span class="o">=</span><span class="s">&quot;false&quot;</span>
     <span class="na">data-default-minimized</span><span class="o">=</span><span class="s">&quot;true&quot;</span>
     <span class="na">data-auto-pause</span><span class="o">=</span><span class="s">&quot;true&quot;</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</code></pre></div>

<p>通过 <code>data-*</code> 属性即可配置播放器行为，无需编写任何 JavaScript。</p>
<h4 id="pjax_1">与 PJAX 配合</h4>
<p>PJAX 只刷新 <code>#pjax-container</code> 内的内容，而播放器元素放在容器<strong>外面</strong>，因此 PJAX 切换页面时播放器<strong>不会中断</strong>，音乐持续播放：</p>
<div class="codehilite"><pre><span></span><code><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&quot;pjax-container&quot;</span><span class="p">&gt;</span>
  <span class="cm">&lt;!-- 页面内容，PJAX 只替换这里 --&gt;</span>
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>

<span class="cm">&lt;!-- 播放器在容器外，PJAX 不影响 --&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;netease-mini-player&quot;</span> <span class="err">...</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</code></pre></div>

<h4 id="_10">音频保活</h4>
<p>为防止浏览器在后台标签页时暂停音频，使用了一个 Web Worker 定时唤醒 AudioContext：</p>
<div class="codehilite"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">audioWorker</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">Worker</span><span class="p">(</span><span class="s1">&#39;/static/js/audio-worker.js&#39;</span><span class="p">);</span>
<span class="nx">audioWorker</span><span class="p">.</span><span class="nx">postMessage</span><span class="p">({</span><span class="w"> </span><span class="nx">command</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;start&#39;</span><span class="w"> </span><span class="p">});</span>
</code></pre></div>

<p>Worker 内部每 15 秒重建一次 AudioContext，配合 <code>visibilitychange</code> 事件，切回页面时自动恢复播放。</p>
<h4 id="_11">暗夜模式适配</h4>
<p>播放器支持 <code>data-theme="auto"</code> 自动跟随系统主题。同时本博客在切换主题时也通过 JS 同步更新播放器的主题状态：</p>
<div class="codehilite"><pre><span></span><code><span class="kd">function</span><span class="w"> </span><span class="nx">syncPlayerTheme</span><span class="p">(</span><span class="nx">theme</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.netease-mini-player&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-theme&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">theme</span><span class="p">);</span>
<span class="w">  </span><span class="p">});</span>
<span class="p">}</span>
</code></pre></div>

<blockquote>
<p>📖 完整文档：<a href="https://docs.nmp.hypcvgm.top/">NeteaseMiniPlayer v2 官方文档</a></p>
</blockquote>
<h2 id="_12">部署</h2>
<p>使用 Gunicorn 启动 3 个 Worker，Nginx 反向代理并直接托管静态文件：</p>
<div class="codehilite"><pre><span></span><code><span class="k">location</span><span class="w"> </span><span class="s">/static/</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kn">alias</span><span class="w"> </span><span class="s">/home/Blogs/app/static/</span><span class="p">;</span>
<span class="w">    </span><span class="kn">expires</span><span class="w"> </span><span class="s">30d</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">location</span><span class="w"> </span><span class="s">/</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kn">proxy_pass</span><span class="w"> </span><span class="s">http://127.0.0.1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>

<hr />
<h2 id="_13">写在最后</h2>
<p>这个博客虽然简单，但麻雀虽小五脏俱全。从数据库设计到前端交互，从部署上线到日常维护，每一个环节都是亲手搭建的。后续还会继续完善——评论回复通知、图片管理、文章归档等功能已经在路上了。</p>
<p>感谢你读到这儿，欢迎在评论区留言交流 🚀</p>]]></content:encoded>
        </item>
        
    </channel>
</rss>