在这里插入图片描述

在这里插入图片描述

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

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

在这里插入图片描述

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

在这里插入图片描述

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

  • 我刚说“想用HTML做记账本,数据存在本地”,它立刻推荐用sql.js(前端SQLite库),还解释“不用装后端,浏览器直接读写字节流,数据能导出备份”;
  • 写数据库初始化代码时,我忘了“创建表后默认插入分类数据”,CodeBuddy直接补全INSERT语句,还标注“提前加好‘餐饮’‘工资’等分类,用户不用手动输”;
  • 做收支统计时,我纠结“怎么按月份分组计算”,它不光生成GROUP BYSQL,还帮我写了日期格式化函数,连“空数据时显示‘暂无记录’”的兼容逻辑都考虑到了;
  • 甚至UI布局,我只说“想要简洁点,分输入区和列表区”,它直接给了响应式代码,还提醒“加个导出按钮,防止浏览器缓存丢失数据”。

简单说:有了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, -- 关联分类表的ID
                        amount 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>
                <input 
                    type="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>
                <select 
                    id="recordType" 
                    class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50"
                    required
                    onchange="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>
                <select 
                    id="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>
                <input 
                    type="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"
                    required
                    placeholder="请输入金额"
                >
            </div>

            <!-- 备注输入 -->
            <div class="col-span-2">
                <label for="remark" class="block text-sm font-medium text-gray-700 mb-1">备注(可选)</label>
                <input 
                    type="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">
                <button 
                    type="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>
                <input 
                    type="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()"
                >
                <button 
                    id="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>

        <!-- 导出备份按钮 -->
        <button 
            id="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_type
                FROM records r
                JOIN 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 = `
                SELECT 
                    SUM(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_expense
                FROM records r
                JOIN 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_type
                FROM records r
                JOIN categories c ON r.category_id = c.id
                WHERE 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 records 
                    SET 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();
            // 释放URL
            URL.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>
                <input 
                    type="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>
                <select 
                    id="recordType" 
                    class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50"
                    required
                    onchange="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>
                <select 
                    id="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>
                <input 
                    type="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"
                    required
                    placeholder="请输入金额"
                >
            </div>
            <div class="col-span-2">
                <label for="remark" class="block text-sm font-medium text-gray-700 mb-1">备注(可选)</label>
                <input 
                    type="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">
                <button 
                    type="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>
                <input 
                    type="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()"
                >
                <button 
                    id="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>
        <button 
            id="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_type
                FROM records r
                JOIN 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 = `
                SELECT 
                    SUM(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_expense
                FROM records r
                JOIN 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_type
                FROM records r
                JOIN categories c ON r.category_id = c.id
                WHERE 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 records 
                    SET 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/,如果相关下载没有跳转,请查看这个地址,相关链接没有跳转,皆是抄袭本文,转载请备注本文原地址。


在这里插入图片描述

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

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐