ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Codex 100个真实案例 - 用AI做在线白板工具(引导图+便签+导出)

Codex 100个真实案例 - 用AI做在线白板工具(引导图+便签+导出) 1. 从零做一个在线白板为什么我选 Codex 单文件方案在线白板工具本质上是一个「无限画布 可拖拽元素 关系连线 导出」的组合体。市面上 Miro、Excalidraw 功能确实强但要么收费、要么上手成本高很多时候我只是想快速画个流程图丢到群里或者开会时贴几张便签梳理需求。这种轻量场景自己用 Codex 生成一个单文件 HTML 白板反而更顺手。这篇要做的 Smart Board 包含四块核心能力HTML5 Canvas 实现无限画布滚轮缩放、空格拖拽平移、便签卡片双击新建/编辑、拖拽移动、六种配色、带箭头的连线引导便签之间画关系线、以及 jsPDF 导出 PNG/PDF。全程纯前端不需要 npm、不需要打包工具、不需要后端服务一个 index.html 双击就能跑。适合谁看想练手 Codex 提示词的前端同学、需要内部轻量白板工具的团队、以及想理解 Canvas 坐标变换的开发者。我会把可复制的 Codex 提示词、项目骨架、验证步骤都写清楚同时说明怎么通过 TaoToken 统一 Key/API 通道给白板接入 AI 能力比如让 AI 帮你自动生成便签内容或整理连线关系。先说结论这个项目最关键的三个技术点是「世界坐标与屏幕坐标互转」「Canvas 离屏导出」「便签命中检测」。把这三点讲透剩下的都是体力活。2. TaoToken 前置统一 Key 与 API 通道怎么接白板本身是纯前端但如果你想加「AI 帮我生成思维导图便签」「AI 自动整理连线」这类功能就需要一个稳定的模型调用通道。我试过把 Key 硬编码在前端结果就是一旦泄露就得全部重换而且不同模型的接口格式还不一样切换起来很麻烦。TaoToken 在这里的作用是提供一个统一的 API 入口你只需要维护一个 Key就能调用多种模型。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基础地址是 https://taotoken.net/api 这个不加 UTM。接入流程分三步第一步去控制台创建 API Key。打开 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 登录后在 API Keys 页面点新建复制生成的 Key形如 sk-xxxx这个 Key 只显示一次记得存好。对应的文档入口在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。第二步确认你要用的模型。如果你只是想验证白板的 AI 便签生成功能可以先去模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 试一下提示词效果确认输出格式符合预期再写进代码。第三步如果你打算长期用 Codex 做编码、或者把白板接成 Agent 工具链建议看下 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。它更适合高频调用场景比按次计费划算。注意前端直接暴露 Key 有泄露风险。生产环境建议加一层自己的后端代理前端只调你的代理接口由代理去请求 TaoToken。本文 Demo 为了演示方便会用一个简单的 fetch 封装你在实际部署时务必替换成后端转发。Key 管理页面统一在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 需要轮换或删除旧 Key 都在这里操作。3. 可复制配置Codex 提示词与项目骨架3.1 项目初始化提示词在终端里先建目录然后让 Codex 生成骨架。我用的提示词是这样的mkdir smart-board cd smart-board codex 创建一个在线白板工具项目 smart-board纯前端只需要三个文件 1. index.html - 主页面包含所有 HTMLCSSJS 2. templates.js - 预设模板数据思维导图、流程图、看板、SWOT 3. README.md - 使用说明 先搭建基础 HTML 结构和 CSS 样式工具栏固定在顶部状态栏固定在底部中间是 canvas。Codex 执行后会生成目录结构smart-board/ ├── index.html # 主文件HTMLCSSJS 全在这里 ├── templates.js # 预设模板数据 └── README.md # 使用说明3.2 无限画布核心坐标变换这是整个白板的地基。提示词要明确要求「世界坐标与屏幕坐标互转」codex 在 index.html 中实现无限画布引擎 InfiniteCanvas 1. 使用 Canvas 2D Context 渲染 2. 滚轮缩放以鼠标位置为中心 3. 空格左键拖拽平移或鼠标中键拖拽 4. 维护 scale、offsetX、offsetY 三个视图参数 5. 提供 screenToWorld(sx, sy) 和 worldToScreen(wx, wy) 方法 6. 绘制随缩放变化的网格背景 7. requestAnimationFrame 渲染循环 8. 中文注释核心代码逻辑是这样的class InfiniteCanvas { constructor(canvasElement) { this.canvas canvasElement; this.ctx canvasElement.getContext(2d); this.scale 1; this.offsetX 0; this.offsetY 0; this.minScale 0.1; this.maxScale 5; this.isPanning false; this.spacePressed false; this.resize(); window.addEventListener(resize, () this.resize()); this._bindEvents(); this._startRenderLoop(); } screenToWorld(sx, sy) { return { x: (sx - this.offsetX) / this.scale, y: (sy - this.offsetY) / this.scale, }; } worldToScreen(wx, wy) { return { x: wx * this.scale this.offsetX, y: wy * this.scale this.offsetY, }; } }滚轮缩放的关键是「缩放前后鼠标对应的世界坐标不变」canvas.addEventListener(wheel, (e) { e.preventDefault(); const beforeZoom this.screenToWorld(e.clientX, e.clientY); const zoomFactor e.deltaY 0 ? 0.9 : 1.1; this.scale Math.max(this.minScale, Math.min(this.maxScale, this.scale * zoomFactor)); this.offsetX e.clientX - beforeZoom.x * this.scale; this.offsetY e.clientY - beforeZoom.y * this.scale; }, { passive: false });3.3 便签卡片系统便签是白板里最常用的元素。提示词要覆盖数据模型、渲染、拖拽、编辑四块codex 实现便签卡片系统 1. 数据模型id、位置(x,y)、大小(w,h)、文字、背景色、字号、层级、分组ID 2. 渲染圆角矩形 阴影 文字自动换行 3. 拖拽移动鼠标点击便签可拖动 4. 双击编辑弹出 textarea 覆盖在便签上 5. 六种预设颜色 6. 命中检测 hitTest 方法 7. 中文注释便签渲染里有个细节值得说文字自动换行不能直接用fillText要逐字测量宽度。Codex 生成的_drawWrappedText方法处理了这个问题_drawWrappedText(ctx, text, x, y, maxWidth, lineHeight) { const lines text.split(\n); let currentY y; for (const line of lines) { let currentLine ; for (const char of line) { const testLine currentLine char; if (ctx.measureText(testLine).width maxWidth currentLine) { ctx.fillText(currentLine, x, currentY); currentLine char; currentY lineHeight; } else { currentLine testLine; } } ctx.fillText(currentLine, x, currentY); currentY lineHeight; } }3.4 连线引导系统连线要在便签边缘出发而不是从中心画否则箭头会被便签挡住。提示词codex 实现连线引导系统 1. 连线数据起始便签ID、目标便签ID、颜色、标签文字 2. 渲染贝塞尔曲线 箭头从便签边缘出发 3. 边缘点计算根据目标方向取便签矩形边界上的交点 4. 创建模式点击起始便签再点击目标便签 5. 中文注释边缘点计算是核心用矩形边界与中心连线的交点_getEdgePoint(note, targetX, targetY) { const cx note.x note.width / 2; const cy note.y note.height / 2; const dx targetX - cx, dy targetY - cy; if (dx 0 dy 0) return { x: cx, y: cy }; const scaleX Math.abs(dx) 0 ? (note.width / 2) / Math.abs(dx) : Infinity; const scaleY Math.abs(dy) 0 ? (note.height / 2) / Math.abs(dy) : Infinity; const s Math.min(scaleX, scaleY); return { x: cx dx * s, y: cy dy * s }; }3.5 导出 PNG 与 PDF导出要用离屏 Canvas不能直接截主画布否则会把工具栏也截进去。提示词codex 实现导出功能 1. 导出PNG离屏Canvas2倍分辨率 2. 导出PDF用 jsPDFCDN 引入 3. 自动计算所有便签的包围盒只导出有内容的区域 4. 导出时用临时 viewport把世界坐标映射到离屏画布 5. 中文注释包围盒计算_getBoundingBox() { const notes this.noteManager.notes; if (notes.length 0) return { x: 0, y: 0, width: 800, height: 600 }; let minX Infinity, minY Infinity, maxX -Infinity, maxY -Infinity; for (const n of notes) { minX Math.min(minX, n.x); minY Math.min(minY, n.y); maxX Math.max(maxX, n.x n.width); maxY Math.max(maxY, n.y n.height); } const pad 50; return { x: minX - pad, y: minY - pad, width: maxX - minX pad * 2, height: maxY - minY pad * 2 }; }PDF 导出用 jsPDF在 HTML 头部引入 CDNscript srchttps://cdn.jsdelivr.net/npm/jspdf2.5.1/dist/jspdf.umd.min.js/scriptexportPDF() { const bbox this._getBoundingBox(); const offscreen document.createElement(canvas); const scale 2; offscreen.width bbox.width * scale; offscreen.height bbox.height * scale; const ctx offscreen.getContext(2d); ctx.scale(scale, scale); ctx.fillStyle #ffffff; ctx.fillRect(0, 0, bbox.width, bbox.height); const tempViewport { scale: 1, offsetX: -bbox.x, offsetY: -bbox.y, worldToScreen(wx, wy) { return { x: wx this.offsetX, y: wy this.offsetY }; }, }; const sorted [...this.noteManager.notes].sort((a, b) a.zIndex - b.zIndex); for (const note of sorted) note.render(ctx, tempViewport); const imgData offscreen.toDataURL(image/png); const isLandscape bbox.width bbox.height; const pdf new jspdf.jsPDF({ orientation: isLandscape ? landscape : portrait, unit: px, format: [bbox.width, bbox.height], }); pdf.addImage(imgData, PNG, 0, 0, bbox.width, bbox.height); pdf.save(whiteboard_${new Date().toISOString().slice(0, 10)}.pdf); }3.6 接入 AI 能力可选如果你想加「AI 生成便签」功能可以在工具栏加一个按钮调用 TaoToken 的 API。前端封装示例async function generateNotesByAI(prompt) { const res await fetch(https://taotoken.net/api/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${YOUR_TAOTOKEN_KEY}, }, body: JSON.stringify({ model: gpt-4o-mini, messages: [ { role: system, content: 你是白板助手把用户需求拆成便签返回JSON数组每项含text和color字段。 }, { role: user, content: prompt }, ], }), }); const data await res.json(); return JSON.parse(data.choices[0].message.content); }拿到便签数组后遍历调用noteManager.createNote()即可批量生成。再次提醒生产环境请把这段请求放到你自己的后端前端不要直接持有 Key。4. 验证请求与成功结果4.1 启动项目cd smart-board # 直接用浏览器打开 index.html或者起个本地服务 python3 -m http.server 8080浏览器访问http://localhost:8080你应该看到顶部工具栏便签、连线、颜色、模板、导出按钮中间浅灰网格画布底部状态栏显示缩放比例和元素数量。4.2 功能验证清单按顺序验证这几项每项都有明确的成功标志操作预期结果滚轮上下滚动画布以鼠标位置为中心缩放状态栏百分比变化按住空格左键拖拽画布平移网格跟着移动双击空白处新建一张便签默认文字「双击编辑内容」双击便签弹出 textarea输入文字后失焦保存拖拽便签便签跟随鼠标移动松手后位置保存点击「连线」再点两张便签两便签之间出现带箭头的曲线点击「导出PNG」下载 PNG 文件只包含便签区域点击「导出PDF」下载 PDF 文件横竖版自适应刷新页面便签和连线还在localStorage 持久化4.3 验证 AI 接入如果做了在控制台执行generateNotesByAI(帮我规划一个电商App的开发流程).then(notes { notes.forEach((n, i) { noteManager.createNote(100 i * 220, 200, { text: n.text, color: n.color }); }); });成功的话画布上会批量出现便签。如果报 401检查 Key 是否正确如果报 404检查 API 地址是否拼错注意是https://taotoken.net/api后面接路径。5. 本篇常见错排查5.1 缩放后便签位置偏移现象滚轮缩放后便签跑到鼠标反方向去了。原因缩放时没有保持鼠标对应的世界坐标不变。检查wheel事件里是否先算了beforeZoom再更新offsetX/offsetY。正确顺序是「先记录缩放前世界坐标 → 更新 scale → 反推 offset」。5.2 便签拖拽时抖动现象拖动便签时位置一跳一跳的。原因dragOffsetX计算时机不对。应该在mousedown时算一次world.x - note.xmousemove里直接用这个固定偏移量而不是每次都重新算。5.3 导出 PNG 是空白现象点击导出下载的图片全白。原因离屏 Canvas 的tempViewport没设置对。检查offsetX是否等于-bbox.xoffsetY是否等于-bbox.y。如果便签坐标是负数画布左上角外包围盒的x也是负数-bbox.x就是正数映射才对。5.4 jsPDF 报错 jspdf is not defined现象控制台报jspdf is not defined。原因CDN 脚本没加载成功或者加载顺序在业务代码之后。确保script src...jspdf...放在head里业务代码放在body末尾。另外 jsPDF 2.x 的全局变量是jspdf.jsPDF不是jsPDF。5.5 连线箭头被便签挡住现象箭头画出来了但末端被目标便签覆盖。原因渲染顺序问题。连线应该在便签之前渲染或者用_getEdgePoint把终点缩到便签边缘。检查renderAll里是不是先画连线再画便签。5.6 localStorage 数据丢失现象刷新后便签没了。原因_save()没在拖拽结束、编辑保存、删除时调用。检查mouseup、blur、deleteNote三处是否都调了_save()。另外 localStorage 有 5MB 限制便签特别多时要注意。5.7 AI 接口返回 401/403现象调用 TaoToken API 报鉴权失败。排查顺序Key 是否复制完整有没有多余空格→ 请求头Authorization格式是否是Bearer sk-xxx→ Key 是否在控制台被禁用。如果都正常去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 确认 Key 状态。接入细节参考文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。6. 继续往下做从 Demo 到可用工具到这里你已经有一个能跑的白板 Demo 了。如果想让它在团队里真正用起来我建议按这个顺序加功能第一加撤销重做。用一个history数组存每次操作前的快照CtrlZ回退。这个用 Codex 生成很快提示词写「实现基于快照的撤销重做最多保留50步」就行。第二加多选和框选。按住Shift点便签多选或者拖拽出一个矩形框选。多选后可以批量改色、批量删除、CtrlG打组。第三加模板库。把思维导图、流程图、看板、SWOT 的便签布局定义成纯数据对象一键加载。这部分数据放templates.js里和渲染逻辑解耦。第四加 AI 整理。选中一堆便签点「AI 整理」让模型帮你归类、生成连线关系。这时候 TaoToken 的统一通道就派上用场了——你可以在一个 Key 下切换不同模型对比哪个整理效果更好。长期做这类编码和 Agent 集成Coding Plan 会比按次调用更省心 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。最后提醒一个我踩过的坑Canvas 渲染循环里不要做重计算。我一开始把便签排序放在render里便签一多就卡。后来改成只在增删改时排序渲染时直接遍历帧率立刻上来了。这个优化用 Codex 也能做提示词写「把便签排序从渲染循环里移出只在数据变更时执行」即可。
返回列表