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

在这里插入图片描述

在这里插入图片描述

用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/,如果相关下载没有跳转,请查看这个地址,相关链接没有跳转,皆是抄袭本文,转载请备注本文原地址。


在这里插入图片描述

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

posted @ 2025-11-20 12:28  yangykaifa  阅读(11)  评论(0)    收藏  举报