尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Vue3+Element Plus在线编程闯关网站设计:从关卡模型到判题服务

Vue3+Element Plus在线编程闯关网站设计:从关卡模型到判题服务 简介这份基于 Vue3 与 Element Plus 的在线编程闯关挑战网站设计源码面向需要学习 Vue 前端开发、希望搭建在线编程练习平台的学习者适合有一定前端基础、想要通过完整项目提升实战能力的读者可用于课程设计、毕业设计或项目自学。压缩包共 132 个文件大小约 5.42MB文件类型以 Vue 组件、JavaScript 脚本、SQL 数据库为主同时包含 JSON 配置、CSS 样式、SVG 图标、PNG 与 JPG 图片等覆盖从页面展示到数据存储的完整开发链路。已有 287 人学习下载。资源内除核心功能页面外还提供登录页、首页效果截图与数据库脚本便于对照理解闯关题目的读取、提交与结果反馈过程目录划分较为清晰适合在此基础上扩展新关卡或调整界面风格。1. 在线编程闯关网站的 Vue3 落地路线先想清楚评测再谈界面很多开发者在搜索引擎里找“基于Vue3和Element Plus的在线编程闯关挑战网站设计源码”目的往往不是抄一套代码而是搞清楚一个真实可运行的 OJ 类前端项目是怎么组织的。这类网站的界面难度并不高真正卡壳的是关卡数据放哪、解锁进度怎么管理、用户提交的代码怎么在页面上跑起来并返回判定结果。基于 Vue3 Element Plus前两件事能快速成型最后一件事需要配一小段判题服务。下文按“环境骨架 → 关卡模型 → 编辑器交互 → 判题服务 → 工程化收尾”的顺序把在线编程闯关网站的设计源码拆开讲适合正在学 Vue3 做作品集、或需要给团队搭内部训练系统的前端开发者。2. 从零配置 Vue3 与 Element Plus 环境布局骨架与编辑器接入标题里的 Elment 是 Element Plus 的笔误搜索和安装时记得用正确拼写。一个在线编程闯关项目的前端工程初始化动作并不复杂麻烦的是后续把代码编辑器、路由、状态层接进来时版本对不上。先给出推荐的环境组合能省掉大部分“装完跑不起来”的抱怨。2.1 环境版本选择与 Vue3 安装Vite 5 Node 18 的推荐组合Vite 5 要求 Node.js 18 及以上版本如果你本机还在用 Node 16建议先升级再用 nvm 管理版本。下表是我在搭建类似在线编程与 OJ 评测系统时的基准配置工具推荐版本说明Node.js18.x LTSVite 5 与 Element Plus 均要求 18Vue3.4.xVite 官方模板默认版本Vite5.x开发服务器与构建工具Element Plus2.7.x当前稳定版组件 API 与 Vue3 对齐CodeMirror 66.x在线编程编辑器内核打开终端用 npm 初始化 Vue3 工程并安装依赖npm create vitelatest code-quest -- --template vue cd code-quest npm install npm install vue-router4 pinia npm install element-plus element-plus/icons-vue npm install vue-codemirror codemirror codemirror/lang-javascript codemirror/lang-python命令里--template vue让 Vite 生成 Vue3 单文件组件模板省去手动写入口文件。element-plus和图标库是并列的包图标库负责提供菜单折叠、返回、播放等操作类图标vue-codemirror是对 CodeMirror 6 的 Vue3 封装后面章节会用它搭在线编辑器。2.2 集成 Element Plus完整引入还是按需引入开发前期建议直接在入口文件完整引入 Element Plus理由是少配置、少报错等最后做工程化收尾再换成按需加载。完整引入的 main.js 这样写import { createApp } from vue import { createPinia } from pinia import ElementPlus from element-plus import zhCn from element-plus/es/locale/lang/zh-cn import element-plus/dist/index.css import * as ElementPlusIconsVue from element-plus/icons-vue import App from ./App.vue import router from ./router const app createApp(App) for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } app.use(createPinia()) app.use(router) app.use(ElementPlus, { locale: zhCn }) app.mount(#app)这里逐层说明app.use(ElementPlus, { locale: zhCn })会把全部组件注册到全局同时把分页器、日期选择这类组件的文案切成中文图标组件通过遍历ElementPlusIconsVue注册这样模板里可以直接写el-iconBack //el-icon而不需要逐个 import。createPinia()和router要先于mount注册否则组件里的useStore()和useRoute()会拿不到实例。2.3 页面骨架与路由设计侧边关卡栏 主操作区在线编程闯关网站的空间布局基本固定左侧窄栏放关卡列表和进度右侧主区域放题目描述与代码编辑器。路由配置按功能模块拆成两个页面一个管关卡列表一个管关卡详情与编辑器import { createRouter, createWebHistory } from vue-router const routes [ { path: /, component: () import(/views/Layout.vue), children: [ { path: , redirect: /levels }, { path: levels, component: () import(/views/LevelList.vue) }, { path: level/:id, component: () import(/views/LevelDetail.vue) } ] } ] export default createRouter({ history: createWebHistory(), routes })Layout.vue是壳组件左侧用el-aside嵌套el-menu渲染关卡导航右侧el-main放router-view。level/:id这种动态路由是关键进入关卡详情后组件里用useRoute().params.id去加载对应的关卡配置。深度嵌套的/levels和/level/:id对应了“先看列表再做题”的自然流程也能让浏览器的前进后退行为符合直觉。2.4 CodeMirror 编辑器接入最短路径跑通“能写代码”先做一个最小编辑器组件不接题目数据只验证 CodeMirror 在 Vue3 里能正常输入和取值。这个组件是后面所有在线编程功能的基础template div classeditor-box Codemirror v-modelcode placeholder在此输入代码 :extensionsextensions :style{ height: 100% } / /div /template script setup import { ref, shallowRef } from vue import { Codemirror } from vue-codemirror import { javascript } from codemirror/lang-javascript const code ref(function twoSum(nums, target) {\n // 在这里实现\n}) const extensions [javascript()] /scriptv-model双向绑定用户编辑内容extensions是 CodeMirror 6 的语言扩展数组后续切换 Python、JavaScript 语法时只需要替换这个数组。组件内的code变量会在用户停笔时自动更新后面提交判题时直接读它即可。3. 闯关关卡的数据模型与 Pinia 状态管理从关卡配置到进度持久化在线编程闯关网站的内容核心是关卡配置题目描述、模板代码、测试用例、难度、解锁条件。这些数据如果散落在各个组件里后期加一个关卡要改三四个文件。这个章节先把关卡资源组织成一份份 JSON再用 Pinia 把跳关、解锁、通过记录管起来。3.1 关卡资源的 JSON 结构关卡信息、模板代码与测试用例每个关卡建议用一个独立模块文件描述约定字段名前端渲染和判题接口都遵循这个结构// src/levels/py-01-two-sum.js export default { id: py-01, title: 两数之和, difficulty: 1, category: 数组, language: python, description: 给定一个整数数组 nums 和一个目标值 target 请你在数组中找出和为目标值的那两个整数并返回它们的下标。, template: def twoSum(nums, target): # 在这里编写代码 pass, solution: def twoSum(nums, target): seen {} for i, num in enumerate(nums): if target - num in seen: return [seen[target - num], i] seen[num] i, testCases: [ { input: [[2, 7, 11, 15], 9], expected: [0, 1] }, { input: [[3, 2, 4], 6], expected: [1, 2] }, { input: [[3, 3], 6], expected: [0, 1] } ] }language字段是编辑器切换语法的依据template是用户进入关卡后编辑器里的初始代码设计上要比solution少关键几行既给了思考起点又不至于直接暴露答案testCases里的input是数组判题时展开后作为函数参数传入expected是标准输出。这里有个容易踩的约定input里的第一个元素如果是数组类型传参时要特别注意语言之间的类型差异JavaScript 的[2, 7, 11, 15]到 Python 里就是 list语义一致但判题函数内部要做一次深比较不能只做浅比较。3.2 拿到设计源码后从哪些文件定位关卡配置与判题接口如果你手头已经有一套“在线编程闯关挑战网站设计源码”先不要急着看页面组件按下面顺序找打开package.json看依赖确认是纯前端还是前后端分离接着看src/router的页面路径找到展示关卡列表的那个视图再去src/api或src/levels目录搜索testCases关键字通常能找到全部关卡数据。常见做法是关卡数据由后端接口下发前端通过/api/levels拉取这样新增关卡不用发版。但纯前端的“设计源码”会把关卡放在src/levels目录里图片素材和题目描述往往也一起放。判题接口如果存在一般叫/api/judge或/api/submit在后端项目里搜judge或runCode就能定位。理解这两处的位置整个源码的阅读成本会低很多。3.3 Pinia 管理解锁状态与 localStorage 双写闯关机制最简单的实现是“通过当前关解锁下一关”用 Pinia 保存通关记录同时写入 localStorage 防止刷新丢失。下面这个 store 覆盖了最常见的状态管理诉求import { defineStore } from pinia const STORAGE_KEY code-quest-progress export const useLevelStore defineStore(level, { state: () ({ currentLevelId: null, progress: JSON.parse(localStorage.getItem(STORAGE_KEY) || {}) }), getters: { isPassed: (state) (id) !!state.progress[id]?.passed, isUnlocked: (state) (id) { return true } }, actions: { markPassed(id) { this.progress[id] { passed: true, passedAt: Date.now() } localStorage.setItem(STORAGE_KEY, JSON.stringify(this.progress)) } } })这段代码的关键在于progress的状态初始化直接从localStorage读避免页面刷新后全部重置。markPassed写入通关时间和标记后立即落本地存储这样下次打开网站仍能恢复进度。你做闯关解锁规则时可以在isUnlocked里读上一个关卡的passed状态形如return index 0 || isPassed(prevId)业务语义就完全收口在 store 层。3.4 Element Plus 渲染关卡列表卡片、步骤条与锁定的组合关卡列表的常见形式是用el-card包裹每个关卡左上角显示序号中间显示标题和分类标签右下角显示通过状态。加一个el-steps放在列表顶部可以把整体闯关进度直观展示出来template div classlevel-list el-steps :activepassedCount align-center el-step v-forlevel in levels :keylevel.id :titlelevel.title / /el-steps el-row :gutter12 el-col v-for(level, index) in levels :keylevel.id :span8 el-card :class{ locked: !store.isUnlocked(level.id) } clickgoLevel(level.id, index) template #header span第 {{ index 1 }} 关/span el-tag{{ level.category }}/el-tag /template p{{ level.title }}/p el-icon v-ifstore.isPassed(level.id)SuccessFilled //el-icon /el-card /el-col /el-row /div /templateel-steps的active绑定通过关卡数量起到进度条作用el-card上的lockedclass 用来做半透明遮罩点击时判断isUnlocked再决定跳转还是弹出ElMessage提示。注意el-col的span可以根据关卡数量调整数量多时改成:span6一屏显示 4 个。4. 在线编程编辑器的交互实现语言切换、快捷键与判题结果展示编辑器外壳搭好、关卡数据就位之后这一章把编辑器从“能打字”升级成“能做题”语法跟随关卡语言切换、CtrlEnter 触发运行、判题结果用 Element Plus 的通知组件反馈给用户。交互体验直接决定这个闯关网站是“玩具”还是“可用的练习平台”。4.1 按关卡语言切换 CodeMirror 语法扩展不同关卡使用的语言可能不同编辑器要根据level.language动态加载对应的语法扩展。用computed来组合扩展数组是 Vue3 里最直观的做法import { computed } from vue import { javascript } from codemirror/lang-javascript import { python } from codemirror/lang-python import { oneDark } from codemirror/theme-one-dark const props defineProps({ language: { type: String, default: javascript }, modelValue: { type: String, default: } }) const extensions computed(() { const exts [] if (props.language python) { exts.push(python()) } else { exts.push(javascript()) } return exts })编辑器组件本身不直接修改modelValue而是通过update:modelValue事件触发父组件的v-model更新保持单向数据流。oneDark主题只有在技能偏好为暗色时引入否则在亮色页面里会显得突兀。4.2 提交运行的快捷键CtrlEnter 与工具栏按钮在线编程场景里“运行”是最频繁的操作除了工具栏按钮还要支持CtrlEnter。在Codemirror组件上监听keydown事件取到按键组合后触发同一段运行逻辑可以避免按钮和快捷键行为分叉function handleKeydown(cm, event) { if ((event.ctrlKey || event.metaKey) event.key Enter) { event.preventDefault() runCode() } } function runCode() { running.value true emit(run, props.modelValue) }metaKey是为了兼容 Mac 键盘上的 Command 键否则用户用 Mac 按CmdEnter没有反应这个细节会直接影响 Mac 用户的做题体验。emit(run)把当前用户代码抛给父组件父组件负责调用后端判题接口这样编辑器组件不关心网络请求职责单一测试时也容易 mock。Edge 浏览器上偶尔会遇到键盘事件被浏览器插件抢占的情况判断里加上event.preventDefault()后能规避大部分冲突。4.3 判题结果展示El-Result、El-Alert 与 El-Dialog 的组合判题结果通常在编辑器下方直接展示全部通过时给一个大的成功标识失败时给出具体用例编号。用el-result做成功态用el-alert做失败态这种组合比“弹窗 Toast”更适合闯关场景因为用户可以边改代码边盯结果区不用反复关闭弹窗template div classjudge-result el-result v-ifresult result.status PASS iconsuccess title挑战成功 sub-title所有测试用例均已通过 / el-alert v-else-ifresult result.status FAIL :title失败于第 ${result.failedCase} 个用例 typeerror :description期望: ${result.expected}实际得到: ${result.actual} show-icon closable / /div /template判题结果对象result里至少包含status、failedCase、expected和actual四个字段。FAIL状态下把期望值和实际值并列展示用户定位问题的时间会明显缩短。el-result的 sub-title 可以放一句“点击下一关继续挑战”的引导文案闯关的连续性就补上了。5. 判题评测怎么做Node.js 本地评测服务与测试用例对比协议在线编程与 OJ 评测系统的核心是三件事收代码、跑用例、比输出。前端页面只负责收集用户代码和展示结果真正的判题动作必须放到服务端。这里用 Node.js 写一个轻量评测服务演示 JavaScript 与 Python 两种语言的判题思路。注意这是教学级实现生产环境要做容器级隔离。5.1 为什么判题不能全部放在前端完成浏览器里虽然能用 Web Worker 跑 JavaScript但存在两个硬伤一是用户可以打开开发者工具直接看请求和源码绕过评测逻辑二是 Python 这类语言无法在浏览器里原生执行。即使是纯 JavaScript 关卡如果把判题逻辑放到前端用户用fetch拿不到服务端业务数据那这个闯关网站就退化成“不能验证的静态代码输入框”。我一般会在项目里单独开一个server/目录用 Express 提供两个接口POST /api/judge统一收评测请求内部按语言分发GET /api/levels返回关卡配置。下面这段代码是 JavaScript 语言判题的核心逻辑重点在node:vm模块的用法const express require(express) const vm require(node:vm) const app express() app.use(express.json({ limit: 64kb })) app.post(/api/judge, (req, res) { const { code, language, testCases } req.body if (language ! javascript) { return res.status(400).json({ error: unsupported language }) } const result testCases.map((tc, index) { try { const wrapped ${code}\n;JSON.stringify(fn(...${JSON.stringify(tc.input)})); const output vm.runInContext(wrapped, vm.createContext({}), { timeout: 2000 }) const actual JSON.parse(output) const passed JSON.stringify(actual) JSON.stringify(tc.expected) return { index, passed, actual, expected: tc.expected } } catch (err) { return { index, passed: false, error: err.message } } }) res.json({ status: result.every(r r.passed) ? PASS : FAIL, result }) }) app.listen(3000, () console.log(judge server on :3000))timeout: 2000表示每个用例最多执行 2 秒防止用户代码里写while (true) {}拖垮进程但要注意timeout只对同步代码生效Promise或setTimeout不会被中断。JSON.stringify(tc.input)把测试用例的输入参数转换成代码字面量...展开后作为函数参数传入这里拼接的是开发者在关卡里写好的数据不是用户输入所以没有注入风险。如果用户代码里有require或processvm.createContext({})创建的空上下文会直接抛ReferenceError这也是一个轻量隔离的体现。提示node:vm不能当作安全沙箱使用它只做执行环境隔离无法防御资源耗尽型攻击。生产环境应当把判题服务部署在容器中用 cgroup 限制 CPU 和内存。5.2 Python 判题的独立进程方案与超时清理Python 没有浏览器内置运行时判题时必须调用系统 Python 解释器。常见做法是先把用户代码和测试用例拼成临时 Python 文件再用child_process.spawn启动子进程执行。超时和僵尸进程是需要特别处理的两个点const { spawn } require(child_process) const fs require(fs) const os require(os) const path require(path) app.post(/api/judge/python, (req, res) { const { code, testCases } req.body const tmpDir fs.mkdtempSync(path.join(os.tmpdir(), code-quest-)) const scriptFile path.join(tmpDir, main.py) const runner ${code} import json cases ${JSON.stringify(testCases)} for i, c in enumerate(cases): result two_sum(*c[input]) print(json.dumps({case: i, actual: result})) fs.writeFileSync(scriptFile, runner) const child spawn(python3, [scriptFile], { timeout: 3000 }) const stdout [] child.stdout.on(data, chunk stdout.push(chunk.toString())) child.on(error, err res.status(500).json({ error: err.message })) child.on(close, code { fs.rmSync(tmpDir, { recursive: true, force: true }) if (code 0) { res.json({ status: PASS, output: stdout.join() }) } else { res.json({ status: RUNTIME_ERROR, output: stdout.join() }) } }) })mkdtempSync创建临时目录判题结束后rmSync强制清理避免用户代码文件堆积在服务器上占用空间。timeout: 3000是spawn自带的超时选项但要注意 Python 的死循环在超时后进程可能没有立即退出更稳妥的做法是在close事件里检查退出码的同时调用child.kill(SIGKILL)确保进程树被清理干净。如果用户代码里存在语法错误Python 会在启动阶段就输出到 stderr 并退出此时拿到的 stdout 为空把 stderr 也收集起来返回前端排查问题会快很多。JavaScript 与 Python 判题方式的对比如下语言执行方式超时处理典型风险JavaScriptnode:vm 进程内执行同步代码用 timeout 参数异步死循环无法截断Pythonspawn 独立子进程timeout kill SIGKILL死循环占 CPU需强制杀进程5.3 评测结果协议把五个状态码固化成前后端约定判题接口的返回格式如果不提前约定前端展示和后端实现会各自发明一套联调时浪费时间。我在多个项目里用过这套状态码稳定且语义清晰状态码含义前端展示PASS所有用例通过el-result 成功提示FAIL有用例未通过显示失败用例与期望值TIMEOUT执行超过时限提示检查死循环RUNTIME_ERROR运行异常显示异常堆栈SYSTEM_ERROR评测服务自身出错提示稍后重试这个协议表放进项目 README 或接口文档里后端和前端照着实现能少一半沟通成本。6. 工程化收尾Markdown 渲染、按需引入与评测链路验证前面的功能链路已经能跑通这章补三个上线前值得做的事题目描述用 Markdown 渲染但禁用 HTMLElement Plus 切换到按需引入以压缩体积以及用三组 curl 验证整个评测链路。在线编程网站的题目描述往往包含换行和代码块直接渲染description字段里的换行符会很难看。6.1 关卡描述用 markdown-it 渲染并禁用 HTML安装 markdown-it把题目描述从纯文本升级成带格式的富文本说明npm install markdown-itimport MarkdownIt from markdown-it const md new MarkdownIt({ html: false, linkify: true }) const renderDescription (description) md.render(description)html: false会转义script和iframe防止关卡描述里嵌入恶意 HTMLlinkify: true会允许 URL 自动变成链接方便引用外部资料。渲染出来的 HTML 在组件内用v-html输出但要给外层容器加一个 scoped 样式把code、pre标签的字体和背景统一否则 Element Plus 的全局样式可能把题目里的行内代码渲染得很大。6.2 Vue3 性能优化按需引入 Element Plus 与路由懒加载完整引入 Element Plus 在开发期很方便但打包后样式和组件体积会明显偏大。切换到自动按需引入插件能只打包用到的组件和样式npm install -D unplugin-auto-import unplugin-vue-componentsimport AutoImport from unplugin-auto-import/vite import Components from unplugin-vue-components/vite import { ElementPlusResolver } from unplugin-vue-components/resolvers export default { plugins: [ AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ] }配置两个插件后模板里写的el-button会被解析成ElementPlusButton并自动引入对应样式。注意原 main.js 里app.use(ElementPlus)要删掉否则按需引入不生效。路由懒加载在前面章节已经用() import()处理过这一章检查一下编辑器页面是否拆出来了避免把 Codemirror 打进首屏 bundle 拖慢加载。6.3 上线前验证三组 curl 检查判题链路与异常处理本地起服务后用三组 curl 快速验证判题接口的稳定性。第一组是正常通过场景curl -X POST http://localhost:3000/api/judge \ -H Content-Type: application/json \ -d { code: function fn(a, b) { return a b }, language: javascript, testCases: [ { input: [1, 2], expected: 3 } ] }第二组验证运行时报错能否被捕获curl -X POST http://localhost:3000/api/judge \ -H Content-Type: application/json \ -d { code: function fn() { throw new Error(\boom\) }, language: javascript, testCases: [{ input: [], expected: 1 }] }第三组验证超时限制是否生效代码里写while(true){}再观察 2 秒后返回的状态码。如果返回 JSON 里result[0].error包含ERR_SCRIPT_EXECUTION_TIMEOUT说明超时逻辑生效如果接口长时间不返回说明timeout没有挡住同步死循环需要回到 5.1 节检查vm.runInContext的传参是否正确。一次把这三组命令跑通判题链路的基本可靠性就有保障了。本文还有配套的精品资源点击获取
返回列表