记账网站-post功能

加 POST 新增账单功能

改文件:

复制代码
app/api/transactions/route.ts

现在这个文件里已经有 GET(),我们就在它下面继续加 POST()

把下面这段放到 GET() 后面:

复制代码
export async function POST(request: Request) {
  try {
    const body = await request.json();

    const {
      type,
      amount,
      category,
      note,
      transaction_date,
    } = body;

    const [result] = await pool.query(
      `
      INSERT INTO transactions
      (type, amount, category, note, transaction_date)
      VALUES (?, ?, ?, ?, ?)
      `,
      [
        type,
        amount,
        category,
        note || null,
        transaction_date,
      ]
    );

    return NextResponse.json(
      {
        message: "新增账单成功",
        result,
      },
      { status: 201 }
    );
  } catch (error) {
    console.error("新增账单失败:", error);

    return NextResponse.json(
      { error: "新增账单失败" },
      { status: 500 }
    );
  }
}

这样你的这个接口就同时支持:

复制代码
GET  /api/transactions
→ 查询账单

POST /api/transactions
→ 新增账单

测试 POST 接口本身能不能成功插入数据

保持 npm run dev 终端运行,然后新终端执行:

复制代码
Invoke-RestMethod `
  -Uri "http://localhost:3000/api/transactions" `
  -Method POST `
  -ContentType "application/json" `
  -Body '{
    "type": "expense",
    "amount": 18.5,
    "category": "餐饮",
    "note": "测试晚饭",
    "transaction_date": "2026-08-14"
  }'

如果成功,你应该会看到类似:

复制代码
message
-------
新增账单成功

然后再打开:

复制代码
http://localhost:3000/api/transactions

刷新。

如果里面多出一条:

复制代码
{
  "type": "expense",
  "amount": "18.50",
  "category": "餐饮",
  "note": "测试晚饭"
}

那就代表:

POST → Next.js → SQL INSERT → MySQL

整条新增链路正式跑通了。

浏览器中文写入 MySQL

打开:

复制代码
app/page.tsx

原来的:

复制代码
const handleSave = () => {
  const parsedAmount = parseFloat(amount);

  if (isNaN(parsedAmount) || parsedAmount <= 0) {
    return;
  }

  const newTransaction: Transaction = {
    id: Date.now(),
    title: note.trim() !== "" ? note.trim() : category,
    amount: type === "income" ? parsedAmount : -parsedAmount,
    date: "今天",
    category,
    note: note.trim() !== "" ? note.trim() : undefined,
  };

  setTransactions((prev) => [newTransaction, ...prev]);
  setIsModalOpen(false);
  resetForm();
};

目前只是:

点击保存 → 改 React 内存里的 transactions

所以刷新网页以后数据就没了。

我们要把它改成:

点击保存 → POST /api/transactions → MySQL 真正保存

可以改成这样:

复制代码
const handleSave = async () => {
  const parsedAmount = parseFloat(amount);

  if (isNaN(parsedAmount) || parsedAmount <= 0) {
    return;
  }

  const today = new Date().toISOString().slice(0, 10);

  const response = await fetch("/api/transactions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      type,
      amount: parsedAmount,
      category,
      note,
      transaction_date: today,
    }),
  });

  const data = await response.json();

  if (!response.ok) {
    console.error("新增账单失败:", data);
    return;
  }

  const newTransaction: Transaction = {
    id: Date.now(),
    title: note.trim() !== "" ? note.trim() : category,
    amount: type === "income" ? parsedAmount : -parsedAmount,
    date: "今天",
    category,
    note: note.trim() !== "" ? note.trim() : undefined,
  };

  setTransactions((prev) => [newTransaction, ...prev]);

  setIsModalOpen(false);
  resetForm();
};

这样就同时做两件事:

复制代码
点击保存
   ↓
POST 到后端
   ↓
MySQL 真正新增一条账单
   ↓
前端也立即显示这条账单

点一次「+ 记一笔」,输入一笔中文账单测试。成功后再去 /api/transactions 看它是不是真的进入 MySQL。

首页 GET MySQL 真实账单,而不是 mock 数据。

前端 Transaction 类型还没完全对齐,先对齐

真实数据库是:

复制代码
id               bigint(20)
type             enum('income','expense')
amount           decimal(10,2)
category         varchar(50)
note             varchar(255) / 可为空
transaction_date date
created_at       timestamp

所以app/page.tsx现在这段:

复制代码
type Transaction = {
  id: number;
  title: string;
  amount: number;
  date: string;
  category: string;
  note?: string;
};

需要逐步改成更接近数据库真实结构的版本。

你先把它替换成:

复制代码
type Transaction = {
  id: number;
  type: "income" | "expense";
  amount: number;
  category: string;
  note: string | null;
  transaction_date: string;
  created_at: string;
};

这样对应关系就是:

复制代码
数据库                前端
id             ↔      id
type           ↔      type
amount         ↔      amount
category       ↔      category
note           ↔      note
transaction_date ↔    transaction_date
created_at     ↔      created_at

把模拟数据 mockTransactions 去掉。

在同一个 app/page.tsx 里,找到这一段:

复制代码
const mockTransactions: Transaction[] = [
  { id: 1, title: "工资", amount: 300, date: "今天", category: "工资" },
  { id: 2, title: "吃饭", amount: -25, date: "今天", category: "餐饮" },
  { id: 3, title: "日用品", amount: -48, date: "昨天", category: "日用品" },
  { id: 4, title: "交通", amount: -20, date: "昨天", category: "交通" },
];

整段删掉。

然后再找到:

复制代码
const [transactions, setTransactions] =
  useState<Transaction[]>(mockTransactions);

改成:

复制代码
const [transactions, setTransactions] =
  useState<Transaction[]>([]);

意思就是:

页面刚打开时先没有账单,后面我们再从 MySQL 真实读取。

让页面打开时去 GET /api/transactions,把 MySQL 里的真实账单读进来。

还是改 app/page.tsx

先把最上面的:

复制代码
import { useState } from "react";

改成:

复制代码
import { useEffect, useState } from "react";

然后在 Home() 里面,放在这段后面:

复制代码
const [transactions, setTransactions] =
  useState<Transaction[]>([]);

加上:

复制代码
useEffect(() => {
  async function loadTransactions() {
    const response = await fetch("/api/transactions");
    const data = await response.json();

    const formatted = data.map((item: Transaction) => ({
      ...item,
      amount: Number(item.amount),
    }));

    setTransactions(formatted);
  }

  loadTransactions();
}, []);

因为 Transaction 类型已经改成数据库真实字段了,下面还有几处旧逻辑没跟着改,所以接下来要修这几块。

第一处,handleSave() 里这个 newTransaction 现在还是旧结构:

复制代码
const newTransaction: Transaction = {
  id: Date.now(),
  title: note.trim() !== "" ? note.trim() : category,
  amount: type === "income" ? parsedAmount : -parsedAmount,
  date: "今天",
  category,
  note: note.trim() !== "" ? note.trim() : undefined,
};

这里要改成数据库字段对应的结构:

复制代码
const newTransaction: Transaction = {
  id: Date.now(),
  type,
  amount: parsedAmount,
  category,
  note: note.trim() !== "" ? note.trim() : null,
  transaction_date: today,
  created_at: new Date().toISOString(),
};

第二处,你现在收入支出统计还在根据正负金额判断:

复制代码
const monthlyIncome = transactions
  .filter((t) => t.amount > 0)
  .reduce((sum, t) => sum + t.amount, 0);

const monthlyExpense = transactions
  .filter((t) => t.amount < 0)
  .reduce((sum, t) => sum + Math.abs(t.amount), 0);

但我们数据库的规则是:

金额永远存正数,收入支出靠 type 区分。

所以改成:

复制代码
const monthlyIncome = transactions
  .filter((t) => t.type === "income")
  .reduce((sum, t) => sum + t.amount, 0);

const monthlyExpense = transactions
  .filter((t) => t.type === "expense")
  .reduce((sum, t) => sum + t.amount, 0);

第三处,最近账单这里还在使用数据库里根本不存在的:

复制代码
t.title
t.date

把:

复制代码
<p className="font-medium text-gray-800">{t.title}</p>
<p className="text-xs text-gray-400">
  {t.category} · {t.date}
</p>

改成:

复制代码
<p className="font-medium text-gray-800">
  {t.note || t.category}
</p>

<p className="text-xs text-gray-400">
  {t.category} · {t.transaction_date}
</p>

第四处,金额颜色和正负号也要改。

现在是:

复制代码
className={`font-semibold ${
  t.amount > 0 ? "text-emerald-600" : "text-rose-600"
}`}

改成:

复制代码
className={`font-semibold ${
  t.type === "income" ? "text-emerald-600" : "text-rose-600"
}`}

然后:

复制代码
{t.amount > 0 ? "+" : "-"}¥{Math.abs(t.amount).toFixed(2)}

改成:

复制代码
{t.type === "income" ? "+" : "-"}¥{t.amount.toFixed(2)}

这样整个前端就真正和数据库统一了:

一个小问题修改

handleSave() 成功之后现在是自己构造:

复制代码
const newTransaction: Transaction = {
  id: Date.now(),
  ...
};

这里的 id: Date.now() 只是前端临时造的,不是数据库真正的自增 id。短期能显示,但以后做删除/修改时会出问题。

第一步:把读取账单单独变成一个函数

你现在有:

复制代码
useEffect(() => {
  async function loadTransactions() {
    const response = await fetch("/api/transactions");
    const data = await response.json();

    const formatted = data.map((item: Transaction) => ({
      ...item,
      amount: Number(item.amount),
    }));

    setTransactions(formatted);
  }

  loadTransactions();
}, []);

把它整段替换成

复制代码
const loadTransactions = async () => {
  const response = await fetch("/api/transactions");
  const data = await response.json();

  const formatted = data.map((item: Transaction) => ({
    ...item,
    amount: Number(item.amount),
  }));

  setTransactions(formatted);
};

useEffect(() => {
  loadTransactions();
}, []);

为什么这么改?

以前 loadTransactions() 被关在 useEffect 里面:

复制代码
useEffect
└── loadTransactions

外面的 handleSave() 用不了它。

现在把它拿出来:

复制代码
Home
├── loadTransactions()
├── useEffect()
└── handleSave()

这样:

  • 页面第一次打开 → 可以调用它
  • 新增成功 → 也可以调用它
  • 以后删除成功 → 还可以调用它

相当于我们把"刷新账单列表"做成了一个公共小工具。


第二步:修改 handleSave

你现在 handleSave 里面 POST 成功之后,有这一大段:

复制代码
const newTransaction: Transaction = {
  id: Date.now(),
  type,
  amount: parsedAmount,
  category,
  note: note.trim() !== "" ? note.trim() : null,
  transaction_date: today,
  created_at: new Date().toISOString(),
};

setTransactions((prev) => [newTransaction, ...prev]);

setIsModalOpen(false);
resetForm();

把这里面的:

复制代码
const newTransaction...

和:

复制代码
setTransactions(...)

全部删掉。

改成:

复制代码
await loadTransactions();

setIsModalOpen(false);
resetForm();

所以完整的 handleSave() 最后应该变成:

复制代码
const handleSave = async () => {
  const parsedAmount = parseFloat(amount);

  if (isNaN(parsedAmount) || parsedAmount <= 0) {
    return;
  }

  const today = new Date().toISOString().slice(0, 10);

  const response = await fetch("/api/transactions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      type,
      amount: parsedAmount,
      category,
      note,
      transaction_date: today,
    }),
  });

  const data = await response.json();

  if (!response.ok) {
    console.error("新增账单失败:", data);
    return;
  }

  await loadTransactions();

  setIsModalOpen(false);
  resetForm();
};

测试

如果开发服务器没开:

复制代码
npm run dev

然后打开:

复制代码
http://localhost:3000

先观察首页。

正常情况下,你应该直接看到数据库里的真实账单,而不是之前的「工资300、吃饭25、日用品48、交通20」那四条 mock 数据。

目前数据库里我们知道至少有:

复制代码
25.00  餐饮  午饭
18.50  ??    ????
3.00   餐饮

所以如果看到这些,说明 GET → MySQL → 首页 成功。

然后再点:

「+ 记一笔」

这次建议故意测试一条收入,因为之前基本都在测试支出:

复制代码
类型:收入
金额:100
分类:兼职
备注:测试兼职收入

点「保存」。

正常应该马上出现:

复制代码
测试兼职收入
兼职 · 日期

+¥100.00

而且应该是绿色。

同时顶部:

复制代码
本月收入
+¥100.00

然后按 F5 刷新网页

如果刷新之后这条 100 元兼职收入 还在 ,这就非常关键了------说明它不是 React 临时显示,而是真的进了 MySQL。

相关推荐
鬼手点金1 小时前
Scrapy + Playwright 完整示例(JS 动态渲染网页)
开发语言·javascript·爬虫·python·scrapy·html·json
逝水无殇2 小时前
HTML 文本格式化详解:掌握 <strong>、<em>、<mark>、<del> 等常用标签
前端·javascript·html
光影少年2 小时前
react离线缓存、图片缓存方案
开发语言·前端·javascript·react native·react.js·缓存·前端框架
我命由我1234510 小时前
CesiumJS 笔记 - 获取容器中心点、Cartesian3 clone 方法、修改 Cartesian3 对象的高度
前端·javascript·css·前端框架·html·html5·js
抱抱宝11 小时前
Agent-study项目教程(03):手写 Mini-ReAct Agent(不依赖框架)
javascript·人工智能·gpt·react.js·prompt·agent
用户9385156350712 小时前
Next.js 笔记系统(二):Redis 数据服务与侧边栏组件拆分实战
javascript·全栈
Kendra91914 小时前
从 0 到上线:单文件网页 App 部署到阿里云 ECS 全流程(Nginx+Node+PM2+PostgreSQL)
nginx·阿里云·云计算·部署·个人开发·今天吃什么
Heo16 小时前
大厂前端调试不能只会debugger
前端·javascript·面试
windliang17 小时前
Claude Code 源码分析(十二):错误处理与自动恢复:让 Agent 稳定运行
前端·javascript·面试