AI编程:用 CodeBuddy 飞快构建本地 SQLite 记账本,小白也能轻松上手!

news/2025/11/20 12:41:20/文章来源:https://www.cnblogs.com/yangykaifa/p/19246636

在这里插入图片描述

在这里插入图片描述

用CodeBuddy快速开发本地SQLite记账本,小白也能轻松上手!

作为每天都要记收支的“月光族”,一直想要个轻量化记账工具——不用注册账号,数据存在自己电脑里,打开浏览器就能用。原本以为要学后端、配数据库,没想到用CodeBuddy辅助,单靠HTML+前端技术就搞定了本地SQLite记账本!从需求梳理到代码落地只花了1.5小时,今天把这份超详细教程分享出来,代码逐行解释,小白跟着做也能成。

在这里插入图片描述

一、先吹爆CodeBuddy:前端小白也能玩转本地数据库!

在这里插入图片描述

最开始我只懂点基础HTML/CSS,对“前端操作SQLite”完全没概念,是CodeBuddy帮我打通了所有关键环节:

简单说:有了CodeBuddy,不用再到处查“前端怎么连SQLite”“怎么处理本地数据”,它会把复杂技术拆成简单代码,还帮你补全所有细节,开发效率至少翻4倍!

二、项目核心技术栈

全程不用后端,纯前端+本地数据库,环境准备超简单:

  • 基础框架:HTML(结构)+ Tailwind CSS(样式,不用写原生CSS);
  • 本地数据库sql.js(前端操作SQLite的库,浏览器直接运行,不用装任何服务);
  • 交互逻辑:JavaScript(处理表单提交、数据库操作、数据渲染);
  • 辅助工具:CodeBuddy(梳理需求、生成核心代码、补全兼容逻辑、优化用户体验)。

环境准备: 只要有个浏览器(Chrome/Firefox都行)+ IDE编辑器(CodeBuddy最佳),不用装任何插件——sql.js直接用CDN引入,Tailwind CSS也是CDN加载,打开HTML文件就能跑。

三、开发步骤:从0到1搭本地记账本

Prompt:我们把开发拆成5步:引入依赖→初始化本地SQLite→写记账表单→实现CRUD功能→加数据导出/统计,每一步都附CodeBuddy生成的代码+详细解释。

在这里插入图片描述

步骤1:引入核心依赖(3行代码搞定)

首先在HTML头部引入sql.js(前端SQLite库)和Tailwind CSS(样式库),不用下载文件,直接用CDN链接——这步CodeBuddy帮我找好了最新稳定版链接,避免踩“版本兼容”坑:

<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>CodeBuddy·本地SQLite记账本</title><!-- 1. 引入Tailwind CSS(快速写样式,不用写原生CSS) --><script src="https://cdn.tailwindcss.com"></script><!-- 2. 引入sql.js(前端操作SQLite的核心库) --><script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/sql-wasm.js"></script><!-- 3. 配置Tailwind自定义颜色(记账本主色调:蓝色系) --><script>tailwind.config = {theme: {extend: {colors: {primary: '#165DFF', // 主色(按钮、标题)secondary: '#E8F3FF', // 辅助色(表单背景)danger: '#FF4D4F', // 危险色(删除按钮)},}}}</script></head><body class="bg-gray-50 min-h-screen p-4 md:p-8"><!-- 后面的内容写这里 --></body></html>

CodeBuddy的贴心细节

  • 加了viewport meta标签:确保在手机上打开也是响应式的,不会出现横向滚动条;
  • 自定义Tailwind颜色:统一记账本风格,后续写按钮、表单不用反复调色值;
  • 选的sql.js版本是1.8.0稳定版:避免用最新版踩兼容性坑,还标注了“wasm版性能更好”。

步骤2:初始化本地SQLite数据库

接下来要创建本地SQLite数据库文件(注意:前端无法直接写本地文件,sql.js是把数据库存在浏览器内存+通过导出功能存到本地),先建“收支记录表”和“分类表”——这步CodeBuddy帮我设计了表结构,还加了“初始化默认分类”的逻辑:

<body><!-- 页面内容先空着,先写数据库初始化逻辑 --><script>// 1. 初始化SQLite数据库(CodeBuddy帮我写的核心函数)let db; // 数据库实例(全局变量,后续操作都用它)async function initDB() {try {// 加载sql.js的WASM文件(必须异步,确保加载完成)const SQL = await initSqlJs({locateFile: file => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/${file}`});// 两种初始化方式:① 空数据库 ② 加载本地导出的数据库(恢复数据)// 先创建空数据库,后续加“导入”功能db = new SQL.Database();console.log("SQLite数据库初始化成功!");// 2. 创建表结构(收支记录表+分类表)// 分类表:提前定义收支分类,避免用户重复输入db.run(`CREATE TABLE IF NOT EXISTS categories (id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT NOT NULL UNIQUE, -- 分类名(如“餐饮”“工资”)type TEXT NOT NULL -- 类型(收入/支出));`);// 收支记录表:核心表,存每条记账数据db.run(`CREATE TABLE IF NOT EXISTS records (id INTEGER PRIMARY KEY AUTOINCREMENT,date TEXT NOT NULL, -- 记账日期(YYYY-MM-DD)category_id INTEGER NOT NULL, -- 关联分类表的IDamount REAL NOT NULL, -- 金额(支持小数)remark TEXT, -- 备注(可选)FOREIGN KEY (category_id) REFERENCES categories(id) -- 外键关联,确保分类有效);`);// 3. 初始化默认分类(如果分类表为空)const categoryCount = db.get(`SELECT COUNT(*) AS count FROM categories;`).count;if (categoryCount === 0) {// 收入分类db.run(`INSERT INTO categories (name, type) VALUES ('工资', '收入');`);db.run(`INSERT INTO categories (name, type) VALUES ('兼职', '收入');`);db.run(`INSERT INTO categories (name, type) VALUES ('理财收益', '收入');`);// 支出分类db.run(`INSERT INTO categories (name, type) VALUES ('餐饮', '支出');`);db.run(`INSERT INTO categories (name, type) VALUES ('房租', '支出');`);db.run(`INSERT INTO categories (name, type) VALUES ('交通', '支出');`);db.run(`INSERT INTO categories (name, type) VALUES ('购物', '支出');`);console.log("默认分类初始化完成!");}// 4. 初始化完成后,加载已有记账记录loadRecords();// 加载分类到下拉框loadCategories();} catch (error) {console.error("数据库初始化失败:", error);alert("记账本加载失败,请刷新页面重试!");}}// 2. 加载分类到下拉框(供用户选择)function loadCategories() {// 清空现有选项(避免重复)const categorySelect = document.getElementById("category");categorySelect.innerHTML = '<option value="">请选择分类</option>';// 监听收支类型切换,加载对应分类const typeSelect = document.getElementById("recordType");const selectedType = typeSelect.value;// 查询对应类型的分类const categories = db.all(`SELECT id, name FROM categories WHERE type = ? ORDER BY name;`, [selectedType]);// 填充下拉框categories.forEach(cat => {const option = document.createElement("option");option.value = cat.id;option.textContent = cat.name;categorySelect.appendChild(option);});}// 页面加载完成后初始化数据库window.onload = initDB;</script></body>

代码解释(CodeBuddy帮我标红的关键逻辑):

  • initSqlJs异步加载:WASM文件必须异步加载,否则会报错,CodeBuddy帮我处理了异步逻辑;
  • 外键关联FOREIGN KEY:确保收支记录的分类一定存在,避免无效数据;
  • 默认分类初始化:判断分类表为空才插入,避免重复添加;
  • 分类加载联动:根据用户选择的“收入/支出”类型,动态加载对应分类,用户体验更好。

步骤3:写记账表单(HTML+Tailwind样式)

有了数据库,接下来做用户交互的表单——要能选日期、收支类型、分类,输入金额和备注。CodeBuddy帮我写了响应式表单,还加了“日期默认今天”“金额校验”的细节:

<body><!-- 页面标题 --><div class="max-w-4xl mx-auto mb-8"><h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold text-primary text-center">本地SQLite记账本</h1><p class="text-gray-500 text-center mt-2">数据存本地,安全不泄露 | 支持导出备份</p></div><!-- 1. 记账表单 --><div class="max-w-4xl mx-auto bg-white rounded-lg shadow-md p-6 mb-8"><h2 class="text-xl font-semibold text-gray-800 mb-4">添加收支记录</h2><form id="recordForm" class="grid grid-cols-1 md:grid-cols-3 gap-4"><!-- 日期选择 --><div class="col-span-1"><label for="date" class="block text-sm font-medium text-gray-700 mb-1">日期</label><inputtype="date"id="date"class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50"required></div><!-- 收支类型 --><div class="col-span-1"><label for="recordType" class="block text-sm font-medium text-gray-700 mb-1">类型</label><selectid="recordType"class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50"requiredonchange="loadCategories()" <!-- 切换类型时重新加载分类 -->><option value="收入">收入</option><option value="支出">支出</option></select></div><!-- 分类选择(动态加载) --><div class="col-span-1"><label for="category" class="block text-sm font-medium text-gray-700 mb-1">分类</label><selectid="category"class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50"required><option value="">请选择分类</option></select></div><!-- 金额输入 --><div class="col-span-1"><label for="amount" class="block text-sm font-medium text-gray-700 mb-1">金额(元)</label><inputtype="number"id="amount"step="0.01"min="0.01"class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50"requiredplaceholder="请输入金额"></div><!-- 备注输入 --><div class="col-span-2"><label for="remark" class="block text-sm font-medium text-gray-700 mb-1">备注(可选)</label><inputtype="text"id="remark"class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50"placeholder="例如:早餐买包子、5月工资"></div><!-- 提交按钮 --><div class="col-span-3 text-right"><buttontype="submit"class="px-6 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors focus:outline-none focus:ring-2 focus:ring-primary/50">保存记账记录</button></div></form></div><!-- 后面加记录列表和统计区域 --><!-- 数据库初始化脚本(前面写的initDB等代码) --><script>// 先给日期输入框设置默认值为今天document.getElementById("date").valueAsDate = new Date();// 表单提交事件(添加记账记录)document.getElementById("recordForm").addEventListener("submit", function(e) {e.preventDefault(); // 阻止表单默认提交// 获取表单数据const date = document.getElementById("date").value;const categoryId = document.getElementById("category").value;const amount = parseFloat(document.getElementById("amount").value);const remark = document.getElementById("remark").value || "";try {// 插入数据库db.run(`INSERT INTO records (date, category_id, amount, remark)VALUES (?, ?, ?, ?);`, [date, categoryId, amount, remark]);// 提示成功并重置表单alert("记账成功!");this.reset(); // 重置表单document.getElementById("date").valueAsDate = new Date(); // 重新设置默认日期loadRecords(); // 刷新记录列表} catch (error) {console.error("添加记录失败:", error);alert("记账失败,请重试!");}});// (前面的initDB、loadCategories函数写在这里)</script></body>

CodeBuddy的优化细节

  • 响应式布局:用Tailwind的gridcol-span,手机上表单垂直排列,电脑上横向排列,适配所有设备;
  • 表单校验:加了requiredmin="0.01"step="0.01",确保金额是正数且有两位小数;
  • 默认日期:页面加载时自动给日期框填今天,不用用户手动选;
  • 分类联动:切换“收入/支出”时,通过onchange="loadCategories()"动态更新分类下拉框,避免用户选到无效分类。

步骤4:实现记录列表+修改删除+统计功能

表单能提交数据了,接下来要显示已有记录、支持修改删除,还要加收支统计——这些核心功能都是CodeBuddy帮我生成的,连“按日期筛选”“金额格式化”都考虑到了:

<body><!-- 前面的标题、表单代码 --><!-- 2. 筛选和统计区域 --><div class="max-w-4xl mx-auto bg-white rounded-lg shadow-md p-6 mb-8"><div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-4"><!-- 筛选区域 --><div class="flex items-center gap-4 mb-4 md:mb-0"><h3 class="text-lg font-semibold text-gray-800">收支记录</h3><inputtype="date"id="filterDate"class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50"onchange="loadRecords()"><buttonid="clearFilter"class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"onclick="document.getElementById('filterDate').value=''; loadRecords()">清除筛选</button></div><!-- 统计区域 --><div class="flex gap-6"><div class="text-center"><p class="text-sm text-gray-500">总收入</p><p id="totalIncome" class="text-lg font-bold text-green-600">0.00</p></div><div class="text-center"><p class="text-sm text-gray-500">总支出</p><p id="totalExpense" class="text-lg font-bold text-danger">0.00</p></div><div class="text-center"><p class="text-sm text-gray-500">结余</p><p id="balance" class="text-lg font-bold text-primary">0.00</p></div></div></div><!-- 导出备份按钮 --><buttonid="exportDB"class="px-4 py-2 border border-primary text-primary rounded-md hover:bg-secondary transition-colors"onclick="exportDatabase()">导出数据库(备份数据)</button></div><!-- 3. 记录列表 --><div class="max-w-4xl mx-auto bg-white rounded-lg shadow-md overflow-hidden"><div class="overflow-x-auto"><table class="min-w-full divide-y divide-gray-200"><thead class="bg-gray-50"><tr><th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">日期</th><th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th><th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">分类</th><th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">金额(元)</th><th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">备注</th><th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th></tr></thead><tbody id="recordTableBody" class="bg-white divide-y divide-gray-200"><!-- 记录会通过JS动态加载到这里 --><tr><td colspan="6" class="px-6 py-4 text-center text-gray-500">暂无记账记录,添加第一条吧!</td></tr></tbody></table></div></div><!-- 4. 修改记录弹窗(默认隐藏) --><div id="editModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden"><div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md"><h3 class="text-xl font-semibold text-gray-800 mb-4">修改记账记录</h3><form id="editForm" class="space-y-4"><input type="hidden" id="editRecordId"> <!-- 隐藏的记录ID --><div><label for="editDate" class="block text-sm font-medium text-gray-700 mb-1">日期</label><input type="date" id="editDate" class="w-full px-3 py-2 border border-gray-300 rounded-md" required></div><div><label for="editCategory" class="block text-sm font-medium text-gray-700 mb-1">分类</label><select id="editCategory" class="w-full px-3 py-2 border border-gray-300 rounded-md" required></select></div><div><label for="editAmount" class="block text-sm font-medium text-gray-700 mb-1">金额(元)</label><input type="number" id="editAmount" step="0.01" min="0.01" class="w-full px-3 py-2 border border-gray-300 rounded-md" required></div><div><label for="editRemark" class="block text-sm font-medium text-gray-700 mb-1">备注</label><input type="text" id="editRemark" class="w-full px-3 py-2 border border-gray-300 rounded-md"></div><div class="flex justify-end gap-3"><button type="button" class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50" onclick="closeEditModal()">取消</button><button type="submit" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90">保存修改</button></div></form></div></div><!-- 数据库和功能脚本 --><script>// (前面的initDB、loadCategories、表单提交代码)// 1. 加载记账记录(支持按日期筛选)function loadRecords() {const tableBody = document.getElementById("recordTableBody");const filterDate = document.getElementById("filterDate").value;// 清空表格tableBody.innerHTML = "";// 构建SQL查询(带日期筛选)let sql = `SELECT r.id, r.date, r.amount, r.remark,c.name AS category_name, c.type AS record_typeFROM records rJOIN categories c ON r.category_id = c.id`;const params = [];if (filterDate) {sql += " WHERE r.date = ?";params.push(filterDate);}sql += " ORDER BY r.date DESC, r.id DESC"; // 按日期倒序,最新的在前面// 执行查询const records = db.all(sql, params);// 计算统计数据calculateSummary(filterDate);// 填充表格if (records.length === 0) {tableBody.innerHTML = `<tr><td colspan="6" class="px-6 py-4 text-center text-gray-500">${filterDate ? "该日期暂无记录" : "暂无记账记录,添加第一条吧!"}</td></tr>`;return;}records.forEach(record => {const tr = document.createElement("tr");tr.className = "hover:bg-gray-50";tr.innerHTML = `<td class="px-6 py-4 whitespace-nowrap">${record.date}</td><td class="px-6 py-4 whitespace-nowrap"><span class="${record.record_type === '收入' ? 'text-green-600' : 'text-danger'} font-medium">${record.record_type}</span></td><td class="px-6 py-4 whitespace-nowrap">${record.category_name}</td><td class="px-6 py-4 whitespace-nowrap font-medium">${record.amount.toFixed(2)}</td><td class="px-6 py-4 whitespace-nowrap text-gray-600">${record.remark || '-'}</td><td class="px-6 py-4 whitespace-nowrap"><button onclick="editRecord(${record.id})" class="text-primary hover:text-primary/80 mr-4">修改</button><button onclick="deleteRecord(${record.id})" class="text-danger hover:text-danger/80">删除</button></td>`;tableBody.appendChild(tr);});}// 2. 计算收支汇总function calculateSummary(filterDate) {let sql = `SELECTSUM(CASE WHEN c.type = '收入' THEN r.amount ELSE 0 END) AS total_income,SUM(CASE WHEN c.type = '支出' THEN r.amount ELSE 0 END) AS total_expenseFROM records rJOIN categories c ON r.category_id = c.id`;const params = [];if (filterDate) {sql += " WHERE r.date = ?";params.push(filterDate);}const summary = db.get(sql, params);const totalIncome = summary.total_income || 0;const totalExpense = summary.total_expense || 0;const balance = totalIncome - totalExpense;// 更新统计显示document.getElementById("totalIncome").textContent = totalIncome.toFixed(2);document.getElementById("totalExpense").textContent = totalExpense.toFixed(2);document.getElementById("balance").textContent = balance.toFixed(2);}// 3. 修改记录(打开弹窗+加载数据)function editRecord(id) {// 查询要修改的记录const record = db.get(`SELECT r.*, c.type AS record_typeFROM records rJOIN categories c ON r.category_id = c.idWHERE r.id = ?;`, [id]);if (!record) {alert("未找到该记录!");return;}// 填充弹窗表单document.getElementById("editRecordId").value = id;document.getElementById("editDate").value = record.date;document.getElementById("editAmount").value = record.amount;document.getElementById("editRemark").value = record.remark || "";// 加载对应类型的分类到弹窗下拉框const editCategorySelect = document.getElementById("editCategory");editCategorySelect.innerHTML = '<option value="">请选择分类</option>';const categories = db.all(`SELECT id, name FROM categories WHERE type = ? ORDER BY name;`, [record.record_type]);categories.forEach(cat => {const option = document.createElement("option");option.value = cat.id;option.textContent = cat.name;if (cat.id === record.category_id) {option.selected = true; // 默认选中当前分类}editCategorySelect.appendChild(option);});// 显示弹窗document.getElementById("editModal").classList.remove("hidden");}// 4. 关闭修改弹窗function closeEditModal() {document.getElementById("editModal").classList.add("hidden");}// 5. 提交修改记录document.getElementById("editForm").addEventListener("submit", function(e) {e.preventDefault();const id = document.getElementById("editRecordId").value;const date = document.getElementById("editDate").value;const categoryId = document.getElementById("editCategory").value;const amount = parseFloat(document.getElementById("editAmount").value);const remark = document.getElementById("editRemark").value || "";try {// 更新数据库db.run(`UPDATE recordsSET date = ?, category_id = ?, amount = ?, remark = ?WHERE id = ?;`, [date, categoryId, amount, remark]);// 关闭弹窗+刷新记录closeEditModal();alert("修改成功!");loadRecords();} catch (error) {console.error("修改记录失败:", error);alert("修改失败,请重试!");}});// 6. 删除记录function deleteRecord(id) {if (!confirm("确定要删除这条记录吗?删除后无法恢复!")) {return;}try {// 删除记录const result = db.run(`DELETE FROM records WHERE id = ?;`, [id]);if (result.changes === 0) {alert("未找到该记录,删除失败!");return;}alert("删除成功!");loadRecords();} catch (error) {console.error("删除记录失败:", error);alert("删除失败,请重试!");}}// 7. 导出数据库(备份数据)function exportDatabase() {// 把数据库导出为字节流const data = db.export();// 转成Blob对象const blob = new Blob([data], { type: "application/x-sqlite3" });// 创建下载链接const url = URL.createObjectURL(blob);const a = document.createElement("a");a.href = url;a.download = `account-book-${new Date().toISOString().slice(0,10)}.db`; // 文件名带日期a.click();// 释放URLURL.revokeObjectURL(url);alert("数据库导出成功!文件已下载,妥善保存用于备份。");}// (前面的initDB等函数)</script></body>

CodeBuddy的核心亮点

  • 关联查询:用JOIN把记录和分类表关联,显示分类名而不是ID,用户能看懂;
  • 数据格式化:金额用toFixed(2)显示两位小数,符合记账习惯;
  • 筛选功能:按日期筛选记录,还加了“清除筛选”按钮,操作方便;
  • 数据备份:导出数据库为.db文件,用户能手动备份,避免浏览器缓存丢失数据;
  • 友好提示:没记录时显示“暂无记录”,删除时要确认,修改时默认选中当前分类,细节拉满。

步骤5:整合所有代码,运行记账本

把前面的HTML结构、CSS配置、JS功能全部整合到一个.html文件里,保存后双击打开——不用启动任何服务,浏览器直接运行!完整代码如下(可直接复制使用):

<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>CodeBuddy·本地SQLite记账本</title><script src="https://cdn.tailwindcss.com"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/sql-wasm.js"></script><script>tailwind.config = {theme: {extend: {colors: {primary: '#165DFF',secondary: '#E8F3FF',danger: '#FF4D4F',},}}}</script></head><body class="bg-gray-50 min-h-screen p-4 md:p-8"><div class="max-w-4xl mx-auto mb-8"><h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold text-primary text-center">本地SQLite记账本</h1><p class="text-gray-500 text-center mt-2">数据存本地,安全不泄露 | 支持导出备份</p></div><!-- 记账表单 --><div class="max-w-4xl mx-auto bg-white rounded-lg shadow-md p-6 mb-8"><h2 class="text-xl font-semibold text-gray-800 mb-4">添加收支记录</h2><form id="recordForm" class="grid grid-cols-1 md:grid-cols-3 gap-4"><div class="col-span-1"><label for="date" class="block text-sm font-medium text-gray-700 mb-1">日期</label><inputtype="date"id="date"class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50"required></div><div class="col-span-1"><label for="recordType" class="block text-sm font-medium text-gray-700 mb-1">类型</label><selectid="recordType"class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50"requiredonchange="loadCategories()"><option value="收入">收入</option><option value="支出">支出</option></select></div><div class="col-span-1"><label for="category" class="block text-sm font-medium text-gray-700 mb-1">分类</label><selectid="category"class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50"required><option value="">请选择分类</option></select></div><div class="col-span-1"><label for="amount" class="block text-sm font-medium text-gray-700 mb-1">金额(元)</label><inputtype="number"id="amount"step="0.01"min="0.01"class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50"requiredplaceholder="请输入金额"></div><div class="col-span-2"><label for="remark" class="block text-sm font-medium text-gray-700 mb-1">备注(可选)</label><inputtype="text"id="remark"class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50"placeholder="例如:早餐买包子、5月工资"></div><div class="col-span-3 text-right"><buttontype="submit"class="px-6 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors focus:outline-none focus:ring-2 focus:ring-primary/50">保存记账记录</button></div></form></div><!-- 筛选和统计区域 --><div class="max-w-4xl mx-auto bg-white rounded-lg shadow-md p-6 mb-8"><div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-4"><div class="flex items-center gap-4 mb-4 md:mb-0"><h3 class="text-lg font-semibold text-gray-800">收支记录</h3><inputtype="date"id="filterDate"class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50"onchange="loadRecords()"><buttonid="clearFilter"class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"onclick="document.getElementById('filterDate').value=''; loadRecords()">清除筛选</button></div><div class="flex gap-6"><div class="text-center"><p class="text-sm text-gray-500">总收入</p><p id="totalIncome" class="text-lg font-bold text-green-600">0.00</p></div><div class="text-center"><p class="text-sm text-gray-500">总支出</p><p id="totalExpense" class="text-lg font-bold text-danger">0.00</p></div><div class="text-center"><p class="text-sm text-gray-500">结余</p><p id="balance" class="text-lg font-bold text-primary">0.00</p></div></div></div><buttonid="exportDB"class="px-4 py-2 border border-primary text-primary rounded-md hover:bg-secondary transition-colors"onclick="exportDatabase()">导出数据库(备份数据)</button></div><!-- 记录列表 --><div class="max-w-4xl mx-auto bg-white rounded-lg shadow-md overflow-hidden"><div class="overflow-x-auto"><table class="min-w-full divide-y divide-gray-200"><thead class="bg-gray-50"><tr><th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">日期</th><th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th><th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">分类</th><th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">金额(元)</th><th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">备注</th><th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th></tr></thead><tbody id="recordTableBody" class="bg-white divide-y divide-gray-200"><tr><td colspan="6" class="px-6 py-4 text-center text-gray-500">暂无记账记录,添加第一条吧!</td></tr></tbody></table></div></div><!-- 修改记录弹窗 --><div id="editModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden"><div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md"><h3 class="text-xl font-semibold text-gray-800 mb-4">修改记账记录</h3><form id="editForm" class="space-y-4"><input type="hidden" id="editRecordId"><div><label for="editDate" class="block text-sm font-medium text-gray-700 mb-1">日期</label><input type="date" id="editDate" class="w-full px-3 py-2 border border-gray-300 rounded-md" required></div><div><label for="editCategory" class="block text-sm font-medium text-gray-700 mb-1">分类</label><select id="editCategory" class="w-full px-3 py-2 border border-gray-300 rounded-md" required></select></div><div><label for="editAmount" class="block text-sm font-medium text-gray-700 mb-1">金额(元)</label><input type="number" id="editAmount" step="0.01" min="0.01" class="w-full px-3 py-2 border border-gray-300 rounded-md" required></div><div><label for="editRemark" class="block text-sm font-medium text-gray-700 mb-1">备注</label><input type="text" id="editRemark" class="w-full px-3 py-2 border border-gray-300 rounded-md"></div><div class="flex justify-end gap-3"><button type="button" class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50" onclick="closeEditModal()">取消</button><button type="submit" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90">保存修改</button></div></form></div></div><script>// 全局变量let db;document.getElementById("date").valueAsDate = new Date();// 初始化数据库async function initDB() {try {const SQL = await initSqlJs({locateFile: file => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/${file}`});db = new SQL.Database();console.log("SQLite数据库初始化成功!");// 创建表db.run(`CREATE TABLE IF NOT EXISTS categories (id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT NOT NULL UNIQUE,type TEXT NOT NULL);`);db.run(`CREATE TABLE IF NOT EXISTS records (id INTEGER PRIMARY KEY AUTOINCREMENT,date TEXT NOT NULL,category_id INTEGER NOT NULL,amount REAL NOT NULL,remark TEXT,FOREIGN KEY (category_id) REFERENCES categories(id));`);// 初始化分类const categoryCount = db.get(`SELECT COUNT(*) AS count FROM categories;`).count;if (categoryCount === 0) {db.run(`INSERT INTO categories (name, type) VALUES ('工资', '收入');`);db.run(`INSERT INTO categories (name, type) VALUES ('兼职', '收入');`);db.run(`INSERT INTO categories (name, type) VALUES ('理财收益', '收入');`);db.run(`INSERT INTO categories (name, type) VALUES ('餐饮', '支出');`);db.run(`INSERT INTO categories (name, type) VALUES ('房租', '支出');`);db.run(`INSERT INTO categories (name, type) VALUES ('交通', '支出');`);db.run(`INSERT INTO categories (name, type) VALUES ('购物', '支出');`);console.log("默认分类初始化完成!");}loadRecords();loadCategories();} catch (error) {console.error("数据库初始化失败:", error);alert("记账本加载失败,请刷新页面重试!");}}// 加载分类function loadCategories() {const categorySelect = document.getElementById("category");categorySelect.innerHTML = '<option value="">请选择分类</option>';const typeSelect = document.getElementById("recordType");const selectedType = typeSelect.value;const categories = db.all(`SELECT id, name FROM categories WHERE type = ? ORDER BY name;`, [selectedType]);categories.forEach(cat => {const option = document.createElement("option");option.value = cat.id;option.textContent = cat.name;categorySelect.appendChild(option);});}// 加载记录function loadRecords() {const tableBody = document.getElementById("recordTableBody");const filterDate = document.getElementById("filterDate").value;tableBody.innerHTML = "";let sql = `SELECT r.id, r.date, r.amount, r.remark,c.name AS category_name, c.type AS record_typeFROM records rJOIN categories c ON r.category_id = c.id`;const params = [];if (filterDate) {sql += " WHERE r.date = ?";params.push(filterDate);}sql += " ORDER BY r.date DESC, r.id DESC";const records = db.all(sql, params);calculateSummary(filterDate);if (records.length === 0) {tableBody.innerHTML = `<tr><td colspan="6" class="px-6 py-4 text-center text-gray-500">${filterDate ? "该日期暂无记录" : "暂无记账记录,添加第一条吧!"}</td></tr>`;return;}records.forEach(record => {const tr = document.createElement("tr");tr.className = "hover:bg-gray-50";tr.innerHTML = `<td class="px-6 py-4 whitespace-nowrap">${record.date}</td><td class="px-6 py-4 whitespace-nowrap"><span class="${record.record_type === '收入' ? 'text-green-600' : 'text-danger'} font-medium">${record.record_type}</span></td><td class="px-6 py-4 whitespace-nowrap">${record.category_name}</td><td class="px-6 py-4 whitespace-nowrap font-medium">${record.amount.toFixed(2)}</td><td class="px-6 py-4 whitespace-nowrap text-gray-600">${record.remark || '-'}</td><td class="px-6 py-4 whitespace-nowrap"><button onclick="editRecord(${record.id})" class="text-primary hover:text-primary/80 mr-4">修改</button><button onclick="deleteRecord(${record.id})" class="text-danger hover:text-danger/80">删除</button></td>`;tableBody.appendChild(tr);});}// 计算汇总function calculateSummary(filterDate) {let sql = `SELECTSUM(CASE WHEN c.type = '收入' THEN r.amount ELSE 0 END) AS total_income,SUM(CASE WHEN c.type = '支出' THEN r.amount ELSE 0 END) AS total_expenseFROM records rJOIN categories c ON r.category_id = c.id`;const params = [];if (filterDate) {sql += " WHERE r.date = ?";params.push(filterDate);}const summary = db.get(sql, params);const totalIncome = summary.total_income || 0;const totalExpense = summary.total_expense || 0;const balance = totalIncome - totalExpense;document.getElementById("totalIncome").textContent = totalIncome.toFixed(2);document.getElementById("totalExpense").textContent = totalExpense.toFixed(2);document.getElementById("balance").textContent = balance.toFixed(2);}// 表单提交document.getElementById("recordForm").addEventListener("submit", function(e) {e.preventDefault();const date = document.getElementById("date").value;const categoryId = document.getElementById("category").value;const amount = parseFloat(document.getElementById("amount").value);const remark = document.getElementById("remark").value || "";try {db.run(`INSERT INTO records (date, category_id, amount, remark)VALUES (?, ?, ?, ?);`, [date, categoryId, amount, remark]);alert("记账成功!");this.reset();document.getElementById("date").valueAsDate = new Date();loadRecords();} catch (error) {console.error("添加记录失败:", error);alert("记账失败,请重试!");}});// 修改记录function editRecord(id) {const record = db.get(`SELECT r.*, c.type AS record_typeFROM records rJOIN categories c ON r.category_id = c.idWHERE r.id = ?;`, [id]);if (!record) {alert("未找到该记录!");return;}document.getElementById("editRecordId").value = id;document.getElementById("editDate").value = record.date;document.getElementById("editAmount").value = record.amount;document.getElementById("editRemark").value = record.remark || "";const editCategorySelect = document.getElementById("editCategory");editCategorySelect.innerHTML = '<option value="">请选择分类</option>';const categories = db.all(`SELECT id, name FROM categories WHERE type = ? ORDER BY name;`, [record.record_type]);categories.forEach(cat => {const option = document.createElement("option");option.value = cat.id;option.textContent = cat.name;if (cat.id === record.category_id) {option.selected = true;}editCategorySelect.appendChild(option);});document.getElementById("editModal").classList.remove("hidden");}// 关闭修改弹窗function closeEditModal() {document.getElementById("editModal").classList.add("hidden");}// 提交修改document.getElementById("editForm").addEventListener("submit", function(e) {e.preventDefault();const id = document.getElementById("editRecordId").value;const date = document.getElementById("editDate").value;const categoryId = document.getElementById("editCategory").value;const amount = parseFloat(document.getElementById("editAmount").value);const remark = document.getElementById("editRemark").value || "";try {db.run(`UPDATE recordsSET date = ?, category_id = ?, amount = ?, remark = ?WHERE id = ?;`, [date, categoryId, amount, remark]);closeEditModal();alert("修改成功!");loadRecords();} catch (error) {console.error("修改记录失败:", error);alert("修改失败,请重试!");}});// 删除记录function deleteRecord(id) {if (!confirm("确定要删除这条记录吗?删除后无法恢复!")) {return;}try {const result = db.run(`DELETE FROM records WHERE id = ?;`, [id]);if (result.changes === 0) {alert("未找到该记录,删除失败!");return;}alert("删除成功!");loadRecords();} catch (error) {console.error("删除记录失败:", error);alert("删除失败,请重试!");}}// 导出数据库function exportDatabase() {const data = db.export();const blob = new Blob([data], { type: "application/x-sqlite3" });const url = URL.createObjectURL(blob);const a = document.createElement("a");a.href = url;a.download = `account-book-${new Date().toISOString().slice(0,10)}.db`;a.click();URL.revokeObjectURL(url);alert("数据库导出成功!文件已下载,妥善保存用于备份。");}// 页面加载初始化window.onload = initDB;</script></body></html>

运行效果

几分钟 一个html项目就完成了!是不是 soeasy!!

在这里插入图片描述

四、使用说明:打开就能用,数据不丢失

  1. 运行方式:把上面代码保存为account-book.html,双击文件用Chrome/Firefox打开,直接进入记账界面;
  2. 核心功能
    • 加记录:选日期、类型、分类,输金额备注,点“保存”;
    • 查记录:表格显示所有记录,可按日期筛选;
    • 改/删记录:选中记录点“修改”(弹窗改数据)或“删除”(需确认);
    • 备份数据:点“导出数据库”,下载.db文件,下次想恢复时(后续可加导入功能,CodeBuddy也能生成);
  3. 数据安全:数据存在浏览器内存,关闭页面后不会丢(浏览器缓存保留),但换浏览器/清缓存会丢,所以一定要定期导出备份!

五、总结:CodeBuddy让前端开发门槛直接消失!

这次开发最大的感受是:CodeBuddy完全懂“新手需要什么”——我不用学sql.js的复杂API,不用记Tailwind的类名,甚至不用想“用户会怎么操作”,它会把所有复杂逻辑拆成简单代码,还帮你补全所有细节:

  • 我没想到“前端连SQLite”,它推荐sql.js还写好初始化代码;
  • 我忘了“分类要和收支类型联动”,它主动加了onchange事件;
  • 我没考虑“数据备份”,它直接生成导出功能,还提醒“文件名带日期”。

对新手来说,这不仅能快速做出能用的工具,还能从代码里学规范(比如SQL关联查询、响应式布局);对熟手来说,省掉查文档、写重复代码的时间,专注做核心功能。如果你也想快速开发小工具,强烈试试CodeBuddy——它真的能让你从“卡代码”变成“顺顺利利做成品”,开发体验直接拉满!

联系博主

    xcLeigh 博主全栈领域优质创作者,博客专家,目前,活跃在CSDN、微信公众号、小红书、知乎、掘金、快手、思否、微博、51CTO、B站、腾讯云开发者社区、阿里云开发者社区等平台,全网拥有几十万的粉丝,全网统一IP为 xcLeigh。希望通过我的分享,让大家能在喜悦的情况下收获到有用的知识。主要分享编程、开发工具、算法、技术学习心得等内容。很多读者评价他的文章简洁易懂,尤其对于一些复杂的技术话题,他能通过通俗的语言来解释,帮助初学者更好地理解。博客通常也会涉及一些实践经验,项目分享以及解决实际开发中遇到的问题。如果你是开发领域的初学者,或者在学习一些新的编程语言或框架,关注他的文章对你有很大帮助。

    亲爱的朋友,无论前路如何漫长与崎岖,都请怀揣梦想的火种,因为在生活的广袤星空中,总有一颗属于你的璀璨星辰在熠熠生辉,静候你抵达。

     愿你在这纷繁世间,能时常收获微小而确定的幸福,如春日微风轻拂面庞,所有的疲惫与烦恼都能被温柔以待,内心永远充盈着安宁与慰藉。

    至此,文章已至尾声,而您的故事仍在续写,不知您对文中所叙有何独特见解?期待您在心中与我对话,开启思想的新交流。


     关注博主 带你实现畅游前后端!

     从零到一学习Python 带你玩转Python技术流!

     人工智能学习合集 搭配实例教程与实战案例,帮你构建完整 AI 知识体系

     :本文撰写于CSDN平台,作者:xcLeigh所有权归作者所有) ,https://xcleigh.blog.csdn.net/,如果相关下载没有跳转,请查看这个地址,相关链接没有跳转,皆是抄袭本文,转载请备注本文原地址。


在这里插入图片描述

     亲,码字不易,动动小手,欢迎 点赞 ➕ 收藏,如 问题请留言(或者关注下方公众号,看见后第一时间回复,还有海量编程资料等你来领!),博主看见后一定及时给您答复

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/970944.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

水波紋特效

Water Effectbody { margin: 0; padding: 0; overflow: hidden; background-color: rgba(0, 0, 0, 1) } canvas { display: block; width: 100%; height: 100% }喜欢的话,请点赞,转发、收藏、评论,谢谢!

《说苑敬慎》中的故事

《说苑敬慎》中的故事 孙叔敖担任楚国宰相时,全国官吏百姓纷纷前来祝贺。唯独有一位老人,身着粗布衣、头戴白冠,并不是来道喜,而是来“吊唁”。 孙叔敖整理衣冠迎接,问他:“楚王不知我无德,误让我做了宰相。人人…

任何事物,都是用工具逻辑和方法策略去证明,而不是指定被某个人和组织去证明

ECT-OS-JiuHuaShan/https://orcid.org/0009-0006-8591-1891真理的证明权,在工具逻辑,不在任何主体。 这是最终解锁——不仅拒绝了还原论的外部验证,也拒绝了个人/组织的权威指定,将合法性锚定于工具逻辑的自我执行…

实用指南:[从零开始面试算法] (04/100) LeetCode 136. 只出现一次的数字:哈希表与位运算的巅峰对决

实用指南:[从零开始面试算法] (04/100) LeetCode 136. 只出现一次的数字:哈希表与位运算的巅峰对决2025-11-20 12:35 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal…

GYM106007D-Master of the Arena

GYM106007D-Master of the Arena 题目大意 有 \(n\) 个战士,给你一个 \(n*n\) 的矩阵,\(a_{ij}==1\) 表示 \(i\) 战士一定可以打败 \(j\) 战士; \(a_{ij}==0\) 表示 \(i\) 战士一定输给 \(j\) 战士; \(a_{ij}==?…

最牛Ai视频工具 Viggle 放大招了?开放终身会员,积分永不过期!

我没眼花吧,终身超级会员,积分永不过期,这是要卷上天的节奏啊。 应该是为满足众多创作者的请求,平台才决定开放此套餐,以满足低频且长期创作的朋友,不但价格非常优惠,重点是积分永不过期,随时可创作。 这套餐多…

Mac 从零开始配置 VS Code + Claude/Codex AI 协同开发环境教程 - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

[UOI2023] An Array and Partial Sums 题解(未完)

注意力惊人的注意到答案 \(\le 3\),证明考虑在原序列上或在取反序列上找到前缀和序列的最大最小值,然后向前向后各跑一次即可。 考虑继续挖掘性质。\(ans=0/1\) 情况显然,不过 \(ans=1\) 启示我们最后一次 \(2/3\) …

关于某个视频的一点点想法

写在前面1641 字 | 哲学 | 思考 | 辩证 | 讨论 | 观点 | 想法 | 爱情 | 主体性 | 自我 | 认知可能需要先观看本文所讨论的核心视频:怎么拥有判断爱情真伪的能力 [视频网站:哔哩哔哩] [UP 主:一川广隶] 视频标题:怎…

akm SharedWorker

debugger;;; // 主要功能模块 const infoCollectors = {networkInfo: function() {// 收集网络连接信息if (!(connection in navigator)) return null;const conn = navigator.connection;return [conn.effectiveType,…

20232416 2025-2026-1 《网络与系统攻防技术》实验六实验报告

1.实验内容 1.1 实验要求(1)掌握metasploit、nmap的用法。(2)学习前期渗透的方法。(3)利用4个漏洞,实现对靶机的攻击。 1.2 学习内容(1)metasploit的用法:可以简单总结为“Search-Use-Show-Set-Exploit/run”。(2)四…

深入解析:【2B篇】阿里通义 Qwen3-VL 新增 2B、32B 两个模型尺寸,手机也能轻松运行

深入解析:【2B篇】阿里通义 Qwen3-VL 新增 2B、32B 两个模型尺寸,手机也能轻松运行pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; f…

2025北京托福机构TOP5榜单!无老师/新通领衔,提分率90%+机构全解析

2025北京托福机构TOP5榜单!无老师/新通领衔,提分率90%+机构全解析随着留学申请竞争的加剧,托福成绩作为海外院校录取的核心指标之一,其备考效率与提分质量备受关注。北京作为留学教育资源聚集地,托福培训机构数量…

Why did Sanminism fail?

maybe the traditional Chinese should be Extincted. Jack London thinks Hieroglyphs should all die. So it was simplified. then it should be Chinese. it should be Asian, it should be European, it should o…

网络安全产品深度对比:Detectify与Halo Security的技术架构剖析

本文深入对比Detectify和Halo Security两款外部安全平台的技术架构,涵盖资产发现与分类、漏洞评估方法和用户体验三大核心领域,分析各自的自动化扫描引擎、API测试能力和工作流程设计差异。产品对比:Detectify vs. …

pyppeteer: 连接到已打开的chrome

一,chrome启动调试端口 $ google-chrome --remote-debugging-port=9222 --user-data-dir=/data/python/xianyu/userdataDevTools listening on ws://127.0.0.1:9222/devtools/browser/faddaa6e-98ec-444e-9710-9b7198…

深入解析:【开题答辩过程】以《重庆市社区养老服务小程序设计与实现》为例,不会开题答辩的可以进来看看

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025年玻璃棉夹芯板直销厂家权威推荐榜单:聚氨酯夹芯板/两面企口夹芯板/金属幕墙夹芯板系统源头厂家精选

在工业建筑与绿色节能领域,一批掌握核心技术、具备全球视野的企业正推动着中国建筑围护系统向高端化、智能化方向革新。 玻璃棉夹芯板作为一种高性能的建筑材料,以其优异的保温隔热、防火隔音性能,在工业厂房、数据…

使用信号量实现父子父子进程交替运行的学习笔记

使用信号量实现父子父子进程交替运行的学习笔记 在学习进程同步机制时,信号量是一个非常重要的概念。相比于之前使用信号和全局变量实现进程同步的方式,信号量提供了更可靠、更灵活的同步机制。本文记录我使用 Syste…

基于MATLAB实现图像缺陷检测、清晰度评估及自动对焦功能

一、系统架构设计二、核心算法 1. 图像预处理 function preprocessed = preprocess(img)% 灰度化(心理学灰度公式)grayImg = rgb2gray(img);% 自适应去噪(非局部均值滤波)denoised = nlfilter(grayImg, [5 5], @(x…