news 2026/9/19 13:46:58

纯前端人格测试应用开发实战:架构、计分与分享卡片全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
纯前端人格测试应用开发实战:架构、计分与分享卡片全解析

先交代一下背景:我一直在做“轻工具”系列的小网页,原则是打开即用、用完就走,不搞复杂的账号体系。前阵子有位朋友找我做一份团队沟通用的性格测试,需求很直接:用户答完题,拿到一份好看的结果,并且愿意把结果分享给同事。我第一反应是拉一个后端服务,存每个用户的答题记录,再做一个管理后台做统计,甚至想好了用哪个云数据库。做到一半我发现不对劲:这个项目的核心价值根本不在数据收集,而在“答题体验”和“结果呈现”。于是我推倒重来,把它做成纯前端项目,也就是你现在看到的这个 SBTI 人格测试。全文源码已经同步放到了 GitHub 和 Gitee,搜“sbti 纯前端人格测试”就能找到,这里先把源码拆开讲一遍。

我会从目录结构、状态管理、计分算法、结果页交互、部署扩展五个方面展开。如果你也想做一个类似的心理测试、性格测评或者任何问卷类应用,这篇文章里提到的取舍和细节可以直接抄作业。

1. 纯前端做心理测试:省掉服务器不是降级,而是主动选择

先回答一个很多人会问的问题:心理测试这种应用,不是天然需要后端来记录用户数据吗?其实要看你的目的。如果你的目标是做专业的心理学量表,采集大量样本做信效度分析,那确实需要后端和数据库。但如果你的目标只是让用户快速了解自己的性格倾向,顺便把这个工具传播出去,那么纯前端反而更合适。

SBTI 这个项目最终的数据流非常简单:用户打开页面,题目从静态 JSON 文件里读取,答案写到浏览器 localStorage,做完以后直接用本地数据计算结果,最后生成一张分享图。整个过程不经过任何一台我自己的服务器,唯一的外部请求是加载页面本身的静态资源。这对用户来说有一个隐性的心理优势:你的答题结果没有上传到某个未知的服务器,隐私焦虑会低很多。

1.1 我为什么放弃了第一版“带后端”的架构

第一版我用的是一个非常标准的开发框架:后端提供题目列表接口和答案提交接口,前端从接口拉数据,答完提交,后端算完类型再返回结果。这个架构本身没问题,但我很快发现三个让我难受的点。

第一个是部署成本。后端代码意味着至少一台云服务器或者容器实例,需要考虑 HTTPS 证书、数据库备份、接口万一被刷了怎么办。哪怕用一个很轻量的大服务,也要花精力维护。而且我预期这个工具不会有很高的并发,却要承担完整的运维负担,这让我觉得很不值。

第二个是测试成本。没有后端以后,我可以在本地直接把整个流程跑完,改动任何题目文案都不用重启服务。而有后端时,我得同时改前端、改接口、改数据库初始化脚本,才能看到一道新题长什么样。对一个以内容为主要产品形态的项目来说,这个成本太高了。

第三个是结果页的分享闭环。带后端的架构里,分享链接通常要带一个用户 ID 或者结果 ID,别人打开以后要向后端请求一次才能看到结果。如果后端挂了,整条分享链就断了。纯前端项目把类型参数直接放在 URL 里,例如?type=ESTJ,别人打开链接的时候不需要请求任何接口,结果页秒开,这个体验是带后端方案很难做到的。

1.2 纯前端的能力边界和取舍

当然,纯前端并不是万能药。我在这版里主动放弃了一些功能,这些取舍值得你参考。

第一个被砍掉的是“按用户维度统计倾向分布”。没有后端,我就无法精确知道哪些类型占比最高、用户都在第几题犹豫最久。如果我真的想要一个粗略的分布数据,可以通过一个很轻量的事件上报接口来做,或者干脆在分享页里加一个投票按钮,让用户自己选择结果类型,然后把结果发到服务器。但这会引入外部存储,我暂时不需要。

第二个被砍掉的是“跨设备历史记录”。localStorage 是浏览器本地的,用户换台电脑或者换个手机,历史结果就没了。对大多数性格测试用户来说,这其实可以接受,他们很少会在另一个设备上查自己三个月前的结果。

第三个被砍掉的是复杂的量表逻辑。心理测量学里有反向计分、随机题序、答题时间记录、测谎题等等。我在第一版里全部做了,后来发现投入产出比太低。比如反向计分,本质上只是把选项分值反转,我完全可以在数据层做一层映射,不需要专门设计一套逻辑。测谎题在这个场景里也没必要,这个工具的定位是“自我探索和团队破冰”,不是员工招聘评估。

我保留了一个比较重要的能力:给用户一个明确的“答题说明”。真正的心理学测试对施测流程有严格规范,但作为轻工具,我会在开头写清楚“本测试结果仅供参考,不构成任何心理诊断”,然后用口语化的引导降低用户的戒备心。这种说明不只是一种免责,也是让用户认真答题的必要前提。

2. 源码目录拆解:数据、状态、视图三层如何协作

整个项目我用的技术栈是 Vue 3 + Vite + Pinia,没有引入 UI 组件库。选择 Vue 3 而不是 React,纯粹是因为个人习惯:模板写法更接近 HTML,对后做静态页面的人来说门槛更低。Vite 负责开发和构建,最终产出 dist 目录,里面全是静态文件。不需要任何运行时环境,放到 Nginx、GitHub Pages、Gitee Pages 或者任何一个 CDN 上都行。

源码目录大概长这样:

sbti-personality-test/ ├── index.html ├── package.json ├── vite.config.js └── src/ ├── main.js ├── App.vue ├── data/ │ ├── questions.json │ └── typeDescriptions.json ├── stores/ │ └── testStore.js ├── composables/ │ ├── useScoring.js │ └── useShareImage.js ├── components/ │ ├── WelcomeView.vue │ ├── QuestionView.vue │ └── ResultView.vue └── utils/ ├── storage.js └── shuffle.js

这个结构不是一开始就定好的,是我写了三四个版本以后慢慢摸索出来的。核心思想只有一句话:数据和视图分离,状态是唯一的数据源。

2.1 数据层:把题目和结果文案从代码里彻底剥离开

questions.json是题目的唯一数据源,每一题的字段设计得非常简单:

{ "id": "q01", "text": "团队一起讨论新方案时,你更倾向于哪种状态?", "dimension": ["EI", "SN"], "options": [ { "label": "A", "text": "话多,想到哪说到哪,边说边理思路", "score": { "E": 1, "S": 1 } }, { "label": "B", "text": "先听别人讲,整理好想法后再发言", "score": { "I": 1, "N": 1 } } ] }

这里有个很关键的设计:一道题最多影响两个维度,每个选项的分值是一个对象,里面的 key 是类型维度上的某一端字母,value 是增加的分值。例如第一题选项 A 会让「外向 E」和「实感 S」各加 1 分,选项 B 会让「内向 I」和「直觉 N」各加 1 分。

我不建议让一道题同时影响三个以上维度,因为那会让计分逻辑很难解释。用户可能会问:为什么我选了某个选项,却同时改变了三个分数?这在题目内容上很难自圆其说。两道题分别度量两个维度,结果呈现会清晰很多。

typeDescriptions.json则维护 16 种类型的结果文案。每种类型包含标题、一句话概述、典型行为、适合的合作方式、可能的盲区、适合的例句和给用户的建议。这些文案是整个项目里最花时间的部分,我前前后后改了四轮。写文案的时候我会刻意避免一种写法:给某个类型贴上绝对的“好人”或“坏人”标签。我希望每个类型既有优势描述,也有盲区提醒,这样测试结果才显得可信,而不是一堆彩虹屁。

2.2 状态层:用 Pinia 管理答题进度,而不是让组件各自为战

答题应用最怕的事情是组件之间互相传状态。比如当前在第几题,这个值如果放在 QuestionView 里,那么用户点“上一题”的时候,ResultView 或者 ProgressBar 要同步更新,就得靠事件总线或者 props 一层层传,非常痛苦。我直接用 Pinia 写了一个testStore,所有页面共享同一个状态对象。

核心状态就四个字段:

export const useTestStore = defineStore('test', () => { const currentIndex = ref(0) const answers = ref({}) const startedAt = ref(null) const finishedAt = ref(null) function setAnswer(questionId, score, scrollToNext = true) { answers.value[questionId] = score if (scrollToNext && currentIndex.value < questions.length - 1) { currentIndex.value += 1 } } function prev() { if (currentIndex.value > 0) currentIndex.value -= 1 } function reset() { answers.value = {} currentIndex.value = 0 startedAt.value = null finishedAt.value = null } return { currentIndex, answers, startedAt, finishedAt, setAnswer, prev, reset } })

setAnswer里做了一件非常重要的事:把“保存答案”和“跳转下一题”绑定在一起。这看起来好像违反了单一职责原则,但在这个场景里其实是刻意的。因为问卷的交互规则就是“选择即答完,答完即下一题”,把这两个动作拆开反而容易引入中间态,比如用户选了答案但没跳到下一题,或者跳到了下一题但答案没存上。

如果你想支持“先多选再统一提交”的模式,那确实需要拆开。但 SBTI 的交互定位是轻快,我选择了前者。

2.3 视图层:QuestionView、ResultView 之间通信的最小路径

视图层只有三个大组件:WelcomeView、QuestionView、ResultView。App.vue 里通过一个stage计算属性决定当前渲染哪个页面:

const stage = computed(() => { if (!store.startedAt) return 'welcome' if (store.currentIndex < questions.length) return 'question' return 'result' })

也就是说,用户只要一开始答题,就进入 question 状态;答完最后一题,currentIndex 等于题目总数,自动切换为 result。这个逻辑非常简单,没有路由切换,没有动画转移,但我建议所有问卷类项目都先按这个方式做,因为状态机够简单,出 bug 的概率最低。

QuestionView 里我单独抽了一个QuestionOption子组件。它的 props 只有一个 option 对象和当前是否选中,点击事件冒泡到 QuestionView,再交给 store 处理。这么拆的原因有两个:一是以后想做“图片选项”或者“拖拽排序”时,可以直接替换这个子组件而不影响题目容器;二是每个选项的选中态样式需要频繁切换,独立组件在渲染性能上更可控。

ProgressBar 是我从 styled-components 思路里借过来的一个小组件。它不读取题目内容,只读取currentIndex / questions.length这个比例,用来渲染进度条宽度。把它和 QuestionView 分开,最大的好处是接口稳定,任何页面想要显示进度,直接传一个 0 到 1 的数字就行。

3. 计分与类型判定:每道题的分数是怎么变成“四个字母”的

如果你去看很多开源测试项目,会发现计分逻辑通常写在组件里,比如在点击选项时直接score.E += 1。这在项目很小的时候没问题,但一旦你想加“跳过本题”“撤销上一题”“随机打乱题序”这些功能,分散的计分逻辑会让你改到怀疑人生。所以这个项目的计分被放在一个独立的 composable 里,叫useScoring

SBTI 有四个维度,每个维度两端分别是:

  • 精力倾向:E(外向)/ I(内向)
  • 信息接收:S(实感)/ N(直觉)
  • 决策偏好:T(思考)/ F(情感)
  • 行为方式:J(计划)/ P(随性)

每一道题在设计的时候,会落到其中一两个维度上。比如一道关于“临时开会时你的反应”的题目,可能同时影响 J/P 和 E/I 两个维度。我不让题目单独落在 T/F 上,是因为 T/F 和开会场景的关联度太弱,硬设计会让题目显得很刻意。

3.1 维度映射:一道题为什么要影响两个维度

有人会困惑:为什么不直接把题目标注为测量某个单一维度?答案是,在行为场景里,几乎不存在只影响一个维度的选择。你选择“提前安排行程”还是“到了再说”,表面上看是 J/P 这个维度,但背后可能也反映出你是更享受计划带来的安全感,还是更享受临时变化的刺激感,这就又和 E/I 或 S/N 沾边了。

但我不建议让一道题同时影响三个维度。原因前面说过,产品文案上很难解释。所以我给每个选项的score字段最多两个 key,这在数据上是允许的,但产品设计朝尽可能固定为两个 key 去靠。这样计分逻辑可以很简单,就是一个累加过程。

3.2 计分过程和边界处理(平局、缺答、中途退出)

useScoring.js的核心代码不长:

export function useScoring() { function computeResult(answers, questions) { const raw = { E: 0, I: 0, S: 0, N: 0, T: 0, F: 0, J: 0, P: 0 } questions.forEach((q) => { const selected = answers[q.id] if (!selected) return Object.keys(selected).forEach((key) => { raw[key] += selected[key] }) }) const dimensions = [ { left: 'E', right: 'I' }, { left: 'S', right: 'N' }, { left: 'T', right: 'F' }, { left: 'J', right: 'P' } ] const letters = dimensions.map((d) => { const leftScore = raw[d.left] const rightScore = raw[d.right] if (leftScore === rightScore) { return { letter: d.left, diff: 0 } } return leftScore > rightScore ? { letter: d.left, diff: leftScore - rightScore } : { letter: d.right, diff: rightScore - leftScore } }) return { raw, letters } } return { computeResult } }

所有维度分数累加完成以后,每个维度比较左右两边的总分数,谁高取谁。这里有两个边界情况需要单独处理。

第一个边界是平局。如果某个维度上左右分数完全一样,我默认取左边字母,同时把diff设为 0。在结果页我会隐藏这个维度的类型字母,而是显示一个“在该维度上倾向不明显”的提示。这么做是因为,如果一个用户在很多题上都犹豫不定,导致分数严重接近,硬给他一个类型反而会显得不专业。

第二个边界是缺答。正常情况下用户答完所有题才会进入结果页,但 URL 里如果有人直接手动拼接了一个?type=ENTJ参数,或者用浏览器控制台手动调用了结果页组件,就可能导致部分答案缺失。我在computeResult里对每一题都做了if (!selected) return,保证缺答不会导致代码崩溃,只是那几个维度的分数会偏低。

3.3 输出不强求“完全匹配”,而是给出倾向强度

我在第一版里只输出四个字母,比如“ESTJ”,然后配一段对应的描述。后来朋友试用反馈说:“我看不懂 ESTJ 和 ENTJ 有什么区别,而且我感觉自己每个维度只有一点点偏向,为什么结果说得那么绝对。”

这个反馈让我意识到,普通用户需要的不是类型标签,而是“我大概偏向哪边”。所以在结果页里,四个维度不再只是字母,而是用一个双端进度条来展示左右分数比例。比如 E 68 分、I 32 分,进度条就会明显偏向 E 端,旁边写“你更依赖外部互动来获取能量”。如果两边是 51 比 49,进度条几乎居中,我就不强推某个字母,而是提示“你在这一维度上比较灵活,具体表现取决于场景”。

这里有一个产品设计上的小心思:百分比条是纯 CSS 渲染的,我只需要算出两个分数分别占总分的比例就行,完全不需要图表库。很多前端开发者一听到“可视化”就想着引入 ECharts,但这个场景用三个 div 加一个 CSS flex 就能解决,性能和加载体积都更优。

4. 结果页和分享卡片:流量回流都藏在这些交互细节里

结果页是整个项目里最容易出彩,也最容易做砸的地方。用户辛辛苦苦答完 40 道题,等的就是这一眼。如果结果页做得像一份平淡的报告,他大概率不会转发;如果做成一份“懂他”的卡片,他转发到群里的概率会高很多。

4.1 结果页的信息层次

我定义的结果页信息结构是四层:

  1. 第一屏:类型字母 + 一句话身份标签。例如“ESTJ · 组织者”,下面一句“你习惯先定目标再想路径,擅长把混乱变成秩序”。这层要足够短,让用户截图时能截到重点。
  2. 第二屏:四个维度的倾向强度条。让用户看到量化的偏向,避免“绝对化”的感觉。
  3. 第三屏:该类型的优势、适合的协作方式、可能的盲区。这层是给愿意往下滚的人看的,也是真正能引发“对,我就是这样”认同感的内容。
  4. 第四屏:一个“生成分享卡片”按钮和一个“重新测试”按钮。

为什么要按这个顺序?因为从传播角度,用户第一眼看到的是“我被定义成了什么”,如果这个定义足够精准,他才会愿意往下看细节,才会愿意分享。如果把一堆长篇大论放在第一屏,反而稀释了冲击力。

我在文案里还会刻意控制每段描述不超过三行。超过三行读者基本不会看完,而且截图分享到群里以后,过长的文字很容易被折叠。这是一个反直觉但是真实存在的传播规律:让卡片上的字越少,它被传播的概率越高。

4.2 Canvas 分享卡片实现及中文字体踩坑

“生成分享卡片”这个功能,第一版我用的是 HTML 转图片方案,也就是把 DOM 节点复制到 canvas 上,然后调用canvas.toDataURL()。这个方案在 PC 端 Chrome 上表现很好,但在移动端兼容性很糟糕,尤其是 iPhone 的 Safari 对 canvas 内容绘制有各种诡异的限制,最后我改成了纯 Canvas 绘制方案。

核心思路是:在页面上放一个尺寸为 1200×630 的 canvas,把所有需要展示的内容直接画上去。这样生成图片时不需要依赖 DOM 解析,兼容性最稳。代码大致是这样:

const canvas = document.createElement('canvas') canvas.width = 1200 canvas.height = 630 const ctx = canvas.getContext('2d') ctx.fillStyle = '#ffffff' ctx.fillRect(0, 0, 1200, 630) ctx.fillStyle = '#2b2b2b' ctx.font = 'bold 64px "PingFang SC", "Microsoft YaHei", sans-serif' ctx.textAlign = 'center' ctx.fillText(result.letters.map(l => l.letter).join(''), 600, 220) ctx.font = '32px "PingFang SC", "Microsoft YaHei", sans-serif' ctx.fillStyle = '#666666' ctx.fillText(result.title, 600, 300) ctx.fillStyle = '#999999' ctx.font = '26px "PingFang SC", "Microsoft YaHei", sans-serif' ctx.fillText('长按保存或分享给朋友,一起来测', 600, 520)

这里面最大的坑是字体。Canvas 绘制中文字体时,如果用户设备上找不到你指定的字体名称,就会直接回退到默认字体,导致卡片上的字变成奇怪的宋体或者系统默认字体。我的解决方案是写多个字体栈,并优先使用系统自带的中文字体,比如 iOS 用 PingFang SC、Windows 用 Microsoft YaHei、其他设备用 sans-serif。因为没有加载远程字体,所以不会出现跨域字体污染 canvas 导致toDataURL()失败的情况。

还有一个细节:Canvas 里的换行需要手动计算文本宽度。fillText不会自动换行,如果标题太长就会溢出画布。我写了一个简单的wrapText函数,根据配置的最大宽度把字符串拆成多行。

4.3 用 URL Scheme 让分享链接直达结果

分享卡片有两种形态:一种是完整的图片,另一种是图片底部带一个二维码或者 URL。我选择在图片上叠一个短链接,让用户可以用“复制链接”的方式直接分享文本链接。

这个链接就是https://你的域名/?type=ESTJ。我在 App.vue 的初始化逻辑里做了判断:

const params = new URLSearchParams(window.location.search) const sharedType = params.get('type') if (sharedType) { store.finishedAt = Date.now() store.forcedResult = sharedType }

这样别人打开链接后,不需要重新答题,直接看到 ESTJ 的结果页。而在结果页里,他会看到一个很明显的“我也要测”按钮,点击后清空forcedResult,让用户进入正常的答题流。这就形成了一个回流闭环:老用户分享 -> 新用户打开结果页 -> 新用户开始答题 -> 新用户分享。

有人会担心直接把类型写在 URL 里会不会被别人伪造。我的答案是不会。这个产品的定位是轻工具,不是严肃测评,如果有人想手动改 URL 让自己变成某个类型,那是他自己的选择,对产品没有坏处。而且为了减少参数污染,我在生成分享链接的时候只带类型缩写,不带任何答案,这样分享链接不会暴露用户的答题过程。

5. 从源码到上线:部署、扩展和一个最容易踩的坑

这部分我写清楚两个问题:怎么把源码跑起来,怎么把它改造成你自己的测试应用。如果你对 Vue 不熟,前几段可以直接跳到你需要的段落。

5.1 本地跑起来只需要三步

假设你已经拉到了源码,本地需要 Node.js 16 以上。在项目根目录依次执行:

npm install npm run dev

Vite 会启动一个开发服务器,默认端口是 5173,浏览器打开就能看到欢迎页。开发模式下,题目和结果的修改都是热更新,你改完questions.json里的一道题,页面会立刻刷新,不需要重启服务,这个体验非常适合反复调题目文案。

要发布到线上:

npm run build

构建完成后,dist目录里就是所有静态文件。你把它上传到任何一个静态托管服务就行。如果是 Nginx,配置根目录指向 dist 即可;如果是 GitHub Pages,把 dist 内容推到仓库的 gh-pages 分支即可;如果是 Gitee Pages 也一样,只是国内访问速度可能更好一点。这个过程我一共用了不到十分钟,比配置服务器快太多。

5.2 把 SBTI 换成任意量表的改造方法

很多朋友拿到源码以后,最想做的事应该是把题目换成自己的。这个项目里,你只需要替换两个文件:

  • src/data/questions.json:换成你自己的题目,注意保留idtextdimensionoptions这四个字段结构。
  • src/data/typeDescriptions.json:换成你自己的结果文案,注意每个类型 ID 要和计分得出的四个字母对应。

如果你要改的题目不是四个维度,而是三个维度或者五个维度,你需要同步改useScoring.js里的dimensions数组,以及ResultView.vue里渲染维度条的部分。如果量表选项不再是文字,而是图片或者音频,你需要扩展QuestionOption组件,让它能根据type字段渲染不同形态的选项。

这个改动成本很小,因为数据层和视图层已经完全分离了。我当初就是刻意把它做成一个“可以换内容”的壳,而不是一个写死的 SBTI 应用。这也是为什么文件结构里没有把题目写死在组件里的原因。

5.3 localStorage 不可用、hash 路由与资源加载三个部署细节

部署上线以后,有三个细节特别容易踩坑。

第一个是 localStorage 不可用。用户在隐私模式或者设置了禁止网站存储数据时,localStorage.setItem可能会直接抛异常。如果不处理,用户一点“开始测试”就会白屏。我的解决方式是在utils/storage.js里包一层 try/catch,失败时回退到内存模式,也就是把数据放在一个普通对象里。这样至少保证当前会话内能正常使用,只是刷新后会丢失进度。

第二个是路由模式。这是一个纯前端项目,我建议用 hash 路由而不是 history 路由。如果你在 GitHub Pages 或 Gitee Pages 上部署,history 模式刷新二级页面时经常出现 404,因为静态服务器没有配置重写规则。hash 模式不存在这个问题,而且对于这种单页工具,URL 里带#并没有任何副作用。

第三个是第三方统计脚本的加载。我不想在这个项目里引入重量级分析平台,因为它们会阻塞页面渲染。后来我选择在index.html里用异步加载方式引入统计脚本,并且设置了defer。如果你想统计访问量,可以这样做:

<script defer src="https://your-analytics.example.com/script.js"></script>

这样页面主体渲染完成之后再加载脚本,不会因为统计服务挂掉而拖慢首屏。

还有一个容易被忽略的问题:资源路径。Vite 构建默认会生成绝对路径资源,也就是/assets/xxx.js。如果你的网站不是部署在域名根路径,而是部署在某个子路径下,比如https://yourname.github.io/repo/,那就需要在vite.config.js里设置base: './',这样构建出来的资源会变成相对路径,部署到任何子目录都不会崩。我之前吃过一次这个亏,发布到 GitHub Pages 后页面一片空白,控制台报一堆 404,原因就是这个。

最后说一点我自己的体会。做完这个纯前端测试项目以后,我对“轻工具”的理解更具体了:不是所有东西都需要一套完整的中后台,不是所有用户行为都必须被记录,不是所有结果都必须存到数据库。有时候,把产品边界刻意划小一点,反而能让用户在使用时感到更轻松,也让开发者能睡个安稳觉。

如果你对这个源码有兴趣,建议先跑起来,然后把questions.json换成你自己的题目试试。哪怕你最后不想用 SBTI,这套“数据驱动题目渲染 + 本地状态管理 + Canvas 分享卡片”的套路,也可以平移到其他问卷、测评甚至打卡类工具里。祝你能用最小的成本做出一个自己喜欢的小产品。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 13:45:31

Dolphin PDF转Markdown:一条命令完整跑通

Dolphin PDF转Markdown&#xff1a;一条命令完整跑通 【免费下载链接】Dolphin The official repo for “Dolphin: Document Image Parsing via Heterogeneous Anchor Prompting”, ACL, 2025. 项目地址: https://gitcode.com/GitHub_Trending/dolphin33/Dolphin Dolphi…

作者头像 李华
网站建设 2026/9/19 13:45:09

Keil MDK安装配置与调试避坑指南:从版本选择到工程管理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 13:45:03

普通话轻声与儿化音的声学原理及自动化训练方案

简介&#xff1a;本资源是一份专为普通话水平测试&#xff08;PSC&#xff09;考生设计的权威发音训练文档&#xff0c;聚焦轻声与儿化音两大核心难点&#xff0c;适用于语言学习者、师范生、播音主持备考人员及教师教学参考。文档严格依据《普通话水平测试用普通话词语表》编制…

作者头像 李华
网站建设 2026/9/19 13:44:33

PLC控制电路设计:继电器逻辑映射与硬软协同安全实现

简介&#xff1a;本资源是一份面向电气自动化、机电一体化专业初学者及现场工程师的《电气控制和PLC基本控制电路》教学课件&#xff0c;系统覆盖电动机典型控制逻辑与低压电器应用核心技能。内容紧扣实际工程需求&#xff0c;深入解析点动与自锁、正反转、顺序起动、自动来回、…

作者头像 李华
网站建设 2026/9/19 13:42:41

N_m3u8DL-RE 完整指南:m3u8 与 MPD 流媒体下载、直播录制实操

N_m3u8DL-RE 完整指南&#xff1a;m3u8 与 MPD 流媒体下载、直播录制实操 【免费下载链接】N_m3u8DL-RE Cross-Platform, modern and powerful stream downloader for MPD/M3U8/ISM. English/简体中文/繁體中文. 项目地址: https://gitcode.com/GitHub_Trending/nm3/N_m3u8D…

作者头像 李华