<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>前端 - tag - 沐木</title><link>https://oldletter.cn/tags/%E5%89%8D%E7%AB%AF/</link><description>前端 - tag - 沐木</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Mon, 20 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://oldletter.cn/tags/%E5%89%8D%E7%AB%AF/" rel="self" type="application/rss+xml"/><item><title>问卷考试系统设计（二）：题目 DSL 设计——用 JSON 描述一切题型</title><link>https://oldletter.cn/posts/qnr-02-dsl-design/</link><pubDate>Sun, 05 Apr 2026 00:00:00 +0000</pubDate><author>mumu</author><guid>https://oldletter.cn/posts/qnr-02-dsl-design/</guid><description><![CDATA[<p><strong>上一篇：<a href="/posts/qnr-01-architecture/" rel="">问卷考试系统设计（一）：架构选型与数据模型</a></strong></p>
<p>上一篇聊了整体架构和数据模型，这篇讲整个系统核心的部分：怎么用一套统一的 JSON 结构，把单选、多选、量表、矩阵、排序、填空……所有题型都描述清楚。然后让引擎把这些 JSON 跑起来，条件跳题、题目分组、随机排序，这些才是让问卷从&quot;能编辑&quot;变成&quot;能填写&quot;的关键。</p>
<hr>
<h2 id="为什么用-json-而不是多张表">为什么用 JSON 而不是多张表</h2>
<h3 id="传统建模的麻烦">传统建模的麻烦</h3>
<p>直观的做法，每种题型一张表：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-sql">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title=""><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">question_single_choice</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">BIGINT</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w"> </span><span class="n">stem</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">option_a</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w"> </span><span class="n">option_b</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w"> </span><span class="n">option_c</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w"> </span><span class="n">option_d</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">correct_answer</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">question_likert</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">BIGINT</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w"> </span><span class="n">stem</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">scale_type</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">20</span><span class="p">),</span><span class="w"> </span><span class="n">is_reverse</span><span class="w"> </span><span class="nb">BOOLEAN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- 10 种题型 10 张表……</span></span></span></code></pre></div></div>
<p>问题很明显：</p>]]></description></item><item><title>问卷考试系统设计（五）：前端引擎——渲染、状态管理与防作弊</title><link>https://oldletter.cn/posts/qnr-05-frontend/</link><pubDate>Mon, 20 Apr 2026 00:00:00 +0000</pubDate><author>mumu</author><guid>https://oldletter.cn/posts/qnr-05-frontend/</guid><description><![CDATA[<p><strong>上一篇：<a href="/posts/qnr-04-quality-control/" rel="">问卷考试系统设计（四）：质量控制</a></strong></p>
<p>后端写好了，接口也通了。前端要解决四个问题：怎么把后端返回的 JSON 渲染成可交互的答题界面、状态怎么管、考试场景怎么防作弊、手机上表格挤成一团怎么办。</p>
<p>前端答题状态要围绕一份本地快照流转，草稿和提交使用同一套校验结果：</p>
<div class="mermaid" id="id-2" data-mermaid-definition="c3RhdGVEaWFncmFtLXYyCiAgICAgICAgICAgICAgICAgICAgWypdIC0tPiBMb2FkaW5nRHNsCiAgICAgICAgICAgICAgICAgICAgTG9hZGluZ0RzbCAtLT4gSW5pdFN0b3JlOiDliqDovb3pl67ljbcgRFNMCiAgICAgICAgICAgICAgICAgICAgSW5pdFN0b3JlIC0tPiBBbnN3ZXJpbmc6IOWIneWni&#43;WMliBQaW5pYQogICAgICAgICAgICAgICAgICAgIEFuc3dlcmluZyAtLT4gUmVjb21wdXRlTG9naWM6IOmAiemhueWPmOWMlgogICAgICAgICAgICAgICAgICAgIFJlY29tcHV0ZUxvZ2ljIC0tPiBBbnN3ZXJpbmc6IOabtOaWsOi3s&#43;mimOe7k&#43;aenAogICAgICAgICAgICAgICAgICAgIEFuc3dlcmluZyAtLT4gU2F2aW5nRHJhZnQ6IOWumuaXtuaIluWkseeEpgogICAgICAgICAgICAgICAgICAgIFNhdmluZ0RyYWZ0IC0tPiBBbnN3ZXJpbmc6IOS/neWtmOaIkOWKnwogICAgICAgICAgICAgICAgICAgIEFuc3dlcmluZyAtLT4gU3VibWl0Q2hlY2s6IOeCueWHu&#43;aPkOS6pAogICAgICAgICAgICAgICAgICAgIFN1Ym1pdENoZWNrIC0tPiBBbnN3ZXJpbmc6IOagoemqjOWksei0pQogICAgICAgICAgICAgICAgICAgIFN1Ym1pdENoZWNrIC0tPiBTdWJtaXRBbnN3ZXI6IOagoemqjOmAmui/hwogICAgICAgICAgICAgICAgICAgIFN1Ym1pdEFuc3dlciAtLT4gU3VibWl0U2VjdXJpdHk6IOmZhOW4pui0qOmHj&#43;S4jumYsuS9nOW8iuaVsOaNrgogICAgICAgICAgICAgICAgICAgIFN1Ym1pdFNlY3VyaXR5IC0tPiBEb25lCiAgICAgICAgICAgICAgICAgICAgRG9uZSAtLT4gWypd">stateDiagram-v2
                    [*] --&gt; LoadingDsl
                    LoadingDsl --&gt; InitStore: 加载问卷 DSL
                    InitStore --&gt; Answering: 初始化 Pinia
                    Answering --&gt; RecomputeLogic: 选项变化
                    RecomputeLogic --&gt; Answering: 更新跳题结果
                    Answering --&gt; SavingDraft: 定时或失焦
                    SavingDraft --&gt; Answering: 保存成功
                    Answering --&gt; SubmitCheck: 点击提交
                    SubmitCheck --&gt; Answering: 校验失败
                    SubmitCheck --&gt; SubmitAnswer: 校验通过
                    SubmitAnswer --&gt; SubmitSecurity: 附带质量与防作弊数据
                    SubmitSecurity --&gt; Done
                    Done --&gt; [*]</div><hr>
<h2 id="一注册表模式一套-dsl-驱动所有题型渲染">一、注册表模式：一套 DSL 驱动所有题型渲染</h2>
<h3 id="硬编码的坑">硬编码的坑</h3>
<p>直观的写法是 if-else：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-vue">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title=""><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-vue" data-lang="vue"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">SingleChoice</span> <span class="nt">v-if</span><span class="o">=</span><span class="s">&#34;q.type === &#39;single_choice&#39;&#34; :question=&#34;q&#34;</span><span class="p"> /&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">MultipleChoice</span> <span class="nt">v-else-if</span><span class="o">=</span><span class="s">&#34;q.type === &#39;multiple_choice&#39;&#34; :question=&#34;q&#34;</span><span class="p"> /&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">LikertScale</span> <span class="nt">v-else-if</span><span class="o">=</span><span class="s">&#34;q.type === &#39;likert&#39;&#34; :question=&#34;q&#34;</span><span class="p"> /&gt;</span></span></span></code></pre></div></div>
<p>每加一种题型，渲染层、校验层、状态层都得改。后面会越来越难维护。</p>]]></description></item></channel></rss>