写在开头:
2025下半年看到豆包的新闻之后,我就开始尝试用豆包写一个完整的小型前端应用,并同步记录着开发的过程。本文的绝大部分内容编写于去年,原以为后续手工接管可以在一两周内搞定,没想到拖拖拉拉到了今天才全部完工😂。
在去年6月底就完成了文章内容
新闻内容
早上刷到一篇新闻说豆包大模型高考水平能够读清北了,之前一直没咋用过豆包,正好我打算编写一个习惯打卡应用 来监督我日常读书、运动的进度,这次就拿它来练练手。
本次挑战将使用豆包的“AI编程”能力完成
一、开始 我计划是将习惯打卡应用 集成进我之前开发的理财记账本 项目,并随着后续其他应用的加入,从而形成一个all in one的个人数据管理系统。所以本次项目的技术栈仍然使用PrimeVue + Prisma + Nuxt + Mysql。
提示词:
我正在使用PrimeVue + Prisma + Nuxt + Mysql开发一个习惯打卡应用,其中Vue采用的是组合式用法,我现在已完成项目初始化工作,准备开始进行编码。
我的构想是包含四个页面:
(1)打卡项目:包含打卡项目的增查改删功能;包含字段:打卡项目标题、打卡项目描述、以及打卡项目封面(图片附件形式)。
(2)项目期数:创建对应项目的打卡期数,设置打卡日期范围以及进度的计算方式(按单次打卡累加,或是进度值展示);包含字段:打开日期范围、目标值、初始值、进度计算方式(累加/持续)、单日是否可多次打卡。
(3)打卡:进行打卡的页面,打卡时可以填写本次进度、添加备注和上传图片附件。
(4)概览:展示关键数据信息和图表,显示活动数、期数、打卡次数、目标进度等信息,以及通过项目和期数展示日历图表。
现在请告诉我后续的开发步骤是怎样的?
随后豆包写出了开发步骤,并对5、6、7、8这些关键步骤提供了一些示例代码:
数据库建模(Prisma)
后端 API 开发(Nuxt + Prisma)
前端组件开发(PrimeVue + 组合式 API)
路由配置(Nuxt)
图片上传功能
概览页面图表
进度计算逻辑
表单验证与错误处理
测试与优化
部署上线
从这来看没啥问题,那让我们继续进行吧。
二、数据库建模(Prisma) 豆包在第一步时提供了示例表结构文件,乍一看似乎没啥问题,但仔细想想还是有2项可优化的地方:
Cycle表的targetValue、initialValue字段是Int 类型,这意味着不支持小数 了。而像减肥、存钱等类型打卡项目中,小数是不可或缺的。
由于第一项的原因,导致Record表的progress字段类型不该为Int 类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // schema.prisma model Project { id Int @id @default(autoincrement ()) title String @unique description String? coverImage String? // 存储图片URL或路径 cycles Cycle[] // 关联多个期数 } model Cycle { id Int @id @default(autoincrement ()) startDate DateTime endDate DateTime targetValue Int initialValue Int @default(0 ) progressType String // 'accumulative '(累加)或 'continuous '(持续) multipleCheckins Boolean @default(false ) projectId Int project Project @relation(fields: [projectId ], references: [id ]) records Record[] // 关联多个打卡记录 } model Record { id Int @id @default(autoincrement ()) date DateTime @default(now ()) progress Int note String? image String? // 存储图片URL或路径 cycleId Int cycle Cycle @relation(fields: [cycleId ], references: [id ]) }
提示词:
现在让我们先开始第一个步骤:数据库建模(Prisma)。我觉得Cycle表的targetValue、initialValue字段使用Int并不合理,当我有一些减肥、存钱的打卡项目时,小数是不可或缺的。
豆包随即表示需求合理,并对targetValue、initialValue、progress字段类型进行修改,调整为Decimal(我在提示的时候故意只说了Cycle表的问题,豆包能顺带把Record表也更新 了,我还是挺满意的)。
提示词:
我的schema.prisma文件里还有其他应用的数据表,请给打卡项目的表名称增加一个Checkin的前缀用来更好的区分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 // schema.prisma model CheckinProject { id Int @id @default(autoincrement ()) title String @unique description String? coverImage String? // 存储图片URL或路径 cycles CheckinCycle[] // 关联多个期数 createdAt DateTime @default(now ()) updatedAt DateTime @updatedAt } model CheckinCycle { id Int @id @default(autoincrement ()) projectId Int project CheckinProject @relation(fields: [projectId ], references: [id ]) title String // 期数标题(例如:"第1期" ) startDate DateTime endDate DateTime targetValue Decimal // 目标值(支持小数) currentValue Decimal @default(0 ) // 当前进度值(支持小数) initialValue Decimal @default(0 ) // 初始值(支持小数) progressType String // 'accumulative '(累加)或 'continuous '(持续) multipleCheckins Boolean @default(false ) // 单日是否可多次打卡 records CheckinRecord[] // 关联多个打卡记录 createdAt DateTime @default(now ()) updatedAt DateTime @updatedAt } model CheckinRecord { id Int @id @default(autoincrement ()) cycleId Int cycle CheckinCycle @relation(fields: [cycleId ], references: [id ]) date DateTime @default(now ()) progress Decimal // 本次打卡进度(支持小数) note String? // 备注信息 image String? // 存储图片URL或路径 createdAt DateTime @default(now ()) }
以上就是最终生成的表结构了,现在让我们开始下一步。
三、后端 API 开发(Nuxt + Prisma) 提示词:
现在让我们开始第二个步骤:后端 API 开发(Nuxt + Prisma)。请完成下列接口:
1、图片公共上传接口
2、打卡项目页面的增、查、改、删功能;
3、项目期数页面的增、查、改、删功能;
4、打卡页面的增、删功能;
5、概览页面的汇总接口,统计总项目数、总期数、总打卡次数和平均进度完成情况;
豆包很快给出了这几个接口的代码,现在让我们一个个接口来查看(由于豆包提供的接口路径和我的命名风格不太一样,我对目录路径进行了修改,所以文章后续目录结构会有所差异 )。我们此处仅检查代码可用性和存在的一些优化点。
(1)图片公共上传接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import { defineEventHandler, readMultipartFormData, createError } from 'h3' ;import { v4 as uuidv4 } from 'uuid' ;import fs from 'fs' ;import path from 'path' ;export default defineEventHandler (async (event) => { try { const parts = await readMultipartFormData (event); if (!parts || parts.length === 0 ) { throw createError ({ statusCode : 400 , message : 'No file uploaded' }); } const file = parts[0 ]; if (!file.filename || !file.data ) { throw createError ({ statusCode : 400 , message : 'Invalid file' }); } const uploadDir = path.resolve ('public/uploads' ); if (!fs.existsSync (uploadDir)) { fs.mkdirSync (uploadDir, { recursive : true }); } const fileExt = path.extname (file.filename ); const uniqueFileName = `${uuidv4()} ${fileExt} ` ; const filePath = path.join (uploadDir, uniqueFileName); fs.writeFileSync (filePath, file.data ); return { url : `/uploads/${uniqueFileName} ` }; } catch (error) { console .error ('Upload error:' , error); throw createError ({ statusCode : 500 , message : 'Failed to upload file' }); } });
代码上没有问题,但它凭空引入了uuid包且没有告知需要执行 npm install uuid的安装命令,导致用户可能会有些疑惑。
图片上传成功
这里其实也有问题,它提供的代码中有一行import { prisma } from '~/server/prisma';。这是凭空变出来的引用 ,也没有提供对应文件的内容。(Ps:好在之前的项目中我有导出过Prisma类,直接替换路径就好了)
(2)增查改删接口 当运行代码时,就会发现无法通过GET/POST/PATCH/DEL等任何形式访问接口 。原因是这种写法在过去版本的 Nuxt 3(Beta 到 RC 阶段)是支持的,在Nuxt 3.8之后就不再支持了 !也就是说豆包关于代码库的AI训练并不是最新的 ,才导致提供了错误的代码。(当时我又问了Kimi,Kimi居然也死犟这种方式是可行的😥,后面又提问了ChatGPT,它提供了正确答案😁)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import { defineEventHandler, readBody, createError } from 'h3' ;import { prisma } from '~/server/prisma' ;export const get = defineEventHandler (async (event) => { const id = parseInt (event.context .params .id ); const project = await prisma.checkinProject .findUnique ({ where : { id }, include : { cycles : true } }); if (!project) { throw createError ({ statusCode : 404 , message : 'Project not found' }); } return project; }); export const patch = defineEventHandler (async (event) => { const id = parseInt (event.context .params .id ); const data = await readBody (event); return prisma.checkinProject .update ({ where : { id }, data }); }); export const del = defineEventHandler (async (event) => { const id = parseInt (event.context .params .id ); await prisma.checkinRecord .deleteMany ({ where : { cycle : { projectId : id } } }); await prisma.checkinCycle .deleteMany ({ where : { projectId : id } }); return prisma.checkinProject .delete ({ where : { id } }); });
提示语:
目前只有图片上传接口可用,其余接口在使用时会产生[nuxt] [request error] [unhandled] [500] Invalid lazy handler result. It should be a function:错误,我发现新版本的Nuxt已经不支持在一个ts文件中声明多个接口的方式了,请修复它。
之后豆包修正了错误,并对接口进行了文件拆分。但是新的问题又产生了:const id = parseInt(event.context.params.id)在TypeScript中会产生错误提示Vue: 'event.context.params' is possibly 'undefined'.,也就是说 event.context.params 有可能并不存在 id 属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { defineEventHandler, createError } from 'h3' ;import { prisma } from '~/server/prisma' ;export default defineEventHandler (async (event) => { const id = parseInt (event.context .params .id ); const project = await prisma.checkinProject .findUnique ({ where : { id }, include : { cycles : true } }); if (!project) { throw createError ({ statusCode : 404 , message : 'Project not found' }); } return project; });
提示语:
在TypeScript中 parseInt(event.context.params.id) 会报错,提示Vue: ‘event.context.params’ is possibly ‘undefined’。请修复这个问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import { defineEventHandler, createError } from 'h3' ;import { prisma } from '~/server/prisma' ;export default defineEventHandler (async (event) => { if (!event.context .params || !event.context .params .id ) { throw createError ({ statusCode : 400 , message : 'Missing project ID parameter' }); } const id = parseInt (event.context .params .id ); if (isNaN (id)) { throw createError ({ statusCode : 400 , message : 'Invalid project ID' }); } const project = await prisma.checkinProject .findUnique ({ where : { id }, include : { cycles : true } }); if (!project) { throw createError ({ statusCode : 404 , message : 'Project not found' }); } return project; });
在手工对豆包生成的所有接口进行检查和修复TypeScript类型错误 后,我发现部分接口存在一定设计差异,于是又与豆包沟通,进行调整:
提示语:
目前提供的部分接口与我设计存在一定差异,需要调整:
1、期数新增接口:需要对某项目下的其他期数记录做校验,不允许同项目间的记录时间范围出现重合。
2、打卡接口:我希望调整传入参数,通过传入项目ID和打卡日期来确定对应的期数ID(表结构可以不变)。
3、增加一个日历展示接口,传入日期后展示对应月份的每日打卡记录(默认展示当日全部项目的打卡记录,通过传入projectId参数来仅展示某个项目的打卡记录)。
在使用JetClient工具逐一验证接口可用和逻辑正确后,就可以开始前端页面搭建了。其余页面的接口比较类似,就不放出来占篇幅了。
四、前端组件开发(PrimeVue + 组合式 API) (1)打卡项目页面 提示语:
好的,现在我们来进行前端组件开发(PrimeVue + 组合式 API),先开始第一个页面:打卡项目页面。我希望用DataView组件+卡片形式展示这些项目。为了应对未来更多的项目,我觉得应该加上分页功能,这可能需要调整打卡项目列表接口。
然后豆包给出了几个文件的代码:
打卡项目列表接口:增加page和pageSize参数(可用 )
打卡项目页面(异常 )
打卡项目编辑弹窗(报错 )
打卡项目页面异常的原因是:豆包提供了不存在的组件插槽 ,在PrimeVue的官方文档 中,只提供了list和grid两种布局插槽,根本没有item插槽。
1 2 3 4 5 <DataView ... > <template #item ="{ item }" > ... </template > </DataView >
提示语:
DataView不包含item插槽,请检查一下代码。
豆包意识到了问题并改为使用grid插槽,但这个循环代码其实有点小问题:为什么不在插槽内获取数据 (<template #grid="{ items }">),而是直接循环外部的projects变量 ?(运行不会有问题,但这并不是推荐写法)
1 2 3 4 5 6 7 8 9 10 <DataView ... > <template #grid > <div class ="grid" > <div class ="col-12 md-6 lg-4 mb-4" v-for ="project in projects" :key ="project.id" > ... </div > </div > </template > </DataView >
提示语:
为什么不在grid插槽中获取数据,而是使用外部的projects变量去循环输出HTML结构?
然后豆包说“这是一个疏忽”并改正了它😂😂😂
还有一个严重的问题是:当你不断的提要求和问题时,AI会反复修改和重构单个(或多个)页面的全部(或部分)代码,直到某个瞬间你会发现,不同页面间的代码已经无法通信了 。这个问题不单指豆包,我用过的这几家AI都是如此。
所以从这一刻开始,我只能手工接管前端部分的开发工作 ,不然AI重构的过程,会把页面功能代码逐渐变成屎山…
2026年1月更新:
去年11月,在新闻上刷到了TRAE SOLO功能在国区正式上线的消息,于是我又把TRAE下了回来。原计划由个人完成的开发工作,逐步变成了尝试用TRAE去补全应用功能。 磨蹭了2个月,终于把应用完成了😂。由于是中途改用的TRAE,这里就不评价好不好用了,后续我会单独记录一篇“用TRAE创建完整的小型应用”的全过程(容我先画个饼😀)。
五、结尾 最后,附上一些应用操作截图,个人感觉PrimeVue的UI还挺能打的😁。
习惯打卡 - 项目管理页面(负责创建项目)
习惯打卡 - 周期计划(打卡项目按时间维度可以分计划进行)
我很满意这个页面的UI操作,如果打卡进度为”1 “的话,只需要点击左下角的项目,就可以 快速创建一条打卡记录 。另外,左上角的日历日期也做了些小设计,如果日期带短下划线 则表示当天存在打卡记录 。
习惯打卡 - 参与打卡
概览页面则展示了整个应用的数据情况,从全局→计划 维度进行逐级展示。
习惯打卡 - 进度分析
点击“各计划执行情况”表格右侧的按钮,则触发弹窗 展示每个计划的打卡进度情况 。最初想把计划的完整打卡记录用日历图展示,最后因为数据太多、看着眼花就放弃了。改为了类似Git的贡献图形式,鼠标移动到对应日期上会展示详细的进度数据,效果反而更好一些☺。
习惯打卡 - 进度分析 - 计划详情弹窗