使用豆包来写一个习惯打卡应用(挑战失败版)

写在开头:

2025下半年看到豆包的新闻之后,我就开始尝试用豆包写一个完整的小型前端应用,并同步记录着开发的过程。本文的绝大部分内容编写于去年,原以为后续手工接管可以在一两周内搞定,没想到拖拖拉拉到了今天才全部完工😂。

在去年6月底就完成了文章内容在去年6月底就完成了文章内容

新闻内容新闻内容

早上刷到一篇新闻说豆包大模型高考水平能够读清北了,之前一直没咋用过豆包,正好我打算编写一个习惯打卡应用来监督我日常读书、运动的进度,这次就拿它来练练手。

本次挑战将使用豆包的“AI编程”能力完成本次挑战将使用豆包的“AI编程”能力完成

一、开始

我计划是将习惯打卡应用集成进我之前开发的理财记账本项目,并随着后续其他应用的加入,从而形成一个all in one的个人数据管理系统。所以本次项目的技术栈仍然使用PrimeVue + Prisma + Nuxt + Mysql

提示词:

我正在使用PrimeVue + Prisma + Nuxt + Mysql开发一个习惯打卡应用,其中Vue采用的是组合式用法,我现在已完成项目初始化工作,准备开始进行编码。

我的构想是包含四个页面:

(1)打卡项目:包含打卡项目的增查改删功能;包含字段:打卡项目标题、打卡项目描述、以及打卡项目封面(图片附件形式)。

(2)项目期数:创建对应项目的打卡期数,设置打卡日期范围以及进度的计算方式(按单次打卡累加,或是进度值展示);包含字段:打开日期范围、目标值、初始值、进度计算方式(累加/持续)、单日是否可多次打卡。

(3)打卡:进行打卡的页面,打卡时可以填写本次进度、添加备注和上传图片附件。

(4)概览:展示关键数据信息和图表,显示活动数、期数、打卡次数、目标进度等信息,以及通过项目和期数展示日历图表。

现在请告诉我后续的开发步骤是怎样的?

随后豆包写出了开发步骤,并对5、6、7、8这些关键步骤提供了一些示例代码:

  1. 数据库建模(Prisma)
  2. 后端 API 开发(Nuxt + Prisma)
  3. 前端组件开发(PrimeVue + 组合式 API)
  4. 路由配置(Nuxt)
  5. 图片上传功能
  6. 概览页面图表
  7. 进度计算逻辑
  8. 表单验证与错误处理
  9. 测试与优化
  10. 部署上线

从这来看没啥问题,那让我们继续进行吧。

二、数据库建模(Prisma)

豆包在第一步时提供了示例表结构文件,乍一看似乎没啥问题,但仔细想想还是有2项可优化的地方:

  1. Cycle表的targetValue、initialValue字段是Int类型,这意味着不支持小数了。而像减肥、存钱等类型打卡项目中,小数是不可或缺的。
  2. 由于第一项的原因,导致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
// server/api/checkin/upload/index.ts
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);

// 返回URL(相对路径,便于部署)
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
// server/api/checkin/projects/[id].ts (获取、更新、删除)
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 } });
});

// server/api/checkin/projects/index.ts (创建、查询)
// ...

提示语:

目前只有图片上传接口可用,其余接口在使用时会产生[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
// server/api/checkin/projects/[id].ts (获取单个项目)
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;
});

// server/api/checkin/projects/[id].patch.ts (更新项目)
// ...

// server/api/checkin/projects/[id].delete.ts (删除项目)
// ...

// server/api/checkin/projects/index.ts (创建项目)
// ...

// server/api/checkin/projects/index.get.ts (获取所有项目)
// ...

提示语:

在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
// server/api/checkin/projects/[id].ts (获取单个项目)
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);

// 验证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;
});

// server/api/checkin/projects/[id].patch.ts (更新项目)
// ...

// server/api/checkin/projects/[id].delete.ts (删除项目)
// ...

// server/api/checkin/projects/index.ts (创建项目)
// ...

// server/api/checkin/projects/index.get.ts (获取所有项目)
// ...

手工对豆包生成的所有接口进行检查和修复TypeScript类型错误后,我发现部分接口存在一定设计差异,于是又与豆包沟通,进行调整:

提示语:

目前提供的部分接口与我设计存在一定差异,需要调整:

1、期数新增接口:需要对某项目下的其他期数记录做校验,不允许同项目间的记录时间范围出现重合。

2、打卡接口:我希望调整传入参数,通过传入项目ID和打卡日期来确定对应的期数ID(表结构可以不变)。

3、增加一个日历展示接口,传入日期后展示对应月份的每日打卡记录(默认展示当日全部项目的打卡记录,通过传入projectId参数来仅展示某个项目的打卡记录)。

在使用JetClient工具逐一验证接口可用和逻辑正确后,就可以开始前端页面搭建了。其余页面的接口比较类似,就不放出来占篇幅了。

四、前端组件开发(PrimeVue + 组合式 API)

(1)打卡项目页面

提示语:

好的,现在我们来进行前端组件开发(PrimeVue + 组合式 API),先开始第一个页面:打卡项目页面。我希望用DataView组件+卡片形式展示这些项目。为了应对未来更多的项目,我觉得应该加上分页功能,这可能需要调整打卡项目列表接口。

然后豆包给出了几个文件的代码:

  • 打卡项目列表接口:增加pagepageSize参数(可用
  • 打卡项目页面(异常
  • 打卡项目编辑弹窗(报错

打卡项目页面异常的原因是:豆包提供了不存在的组件插槽,在PrimeVue的官方文档中,只提供了listgrid两种布局插槽,根本没有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 ...>
<!-- 修改这里:使用grid插槽而不是item插槽 -->
<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的贡献图形式,鼠标移动到对应日期上会展示详细的进度数据,效果反而更好一些☺。

习惯打卡 - 进度分析 - 计划详情弹窗习惯打卡 - 进度分析 - 计划详情弹窗

使用豆包来写一个习惯打卡应用(挑战失败版)

作者:有点东西

链接: https://www.youdiandongxi.com/article/try-doubao-create-clockin-webapp.html

协议:本文采用 CC BY-NC-SA 4.0 隐私协议,转载请注明出处!

评论区