From 838753a05844cefacb0ed53558440b1056162e1f Mon Sep 17 00:00:00 2001 From: root Date: Fri, 16 Oct 2026 10:40:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=AE=B0=E8=B4=A6?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=EF=BC=88=E6=94=B6=E6=94=AF=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E3=80=81=E6=8A=A5=E9=94=80=E8=B7=9F=E8=B8=AA=E3=80=81=E6=9C=88?= =?UTF-8?q?=E5=BA=A6=E6=B1=87=E6=80=BB=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- intern-log/README.md | 1 + intern-log/src/App.vue | 3 + .../src/components/TransactionDialog.vue | 193 +++++++++ intern-log/src/components/icons.ts | 3 +- intern-log/src/db/repo.ts | 47 ++- intern-log/src/db/schema.ts | 21 +- intern-log/src/views/LedgerView.vue | 388 ++++++++++++++++++ 实习工作日志系统-设计方案.md | 25 +- 8 files changed, 672 insertions(+), 9 deletions(-) create mode 100644 intern-log/src/components/TransactionDialog.vue create mode 100644 intern-log/src/views/LedgerView.vue diff --git a/intern-log/README.md b/intern-log/README.md index 6e7a8cc..f7e5eb7 100644 --- a/intern-log/README.md +++ b/intern-log/README.md @@ -23,6 +23,7 @@ - 备忘录:双栏列表与编辑,未保存修改有守卫 - 密码库:企业账号密码管理,主密码 PBKDF2 派生 AES-256-GCM 加密,支持解锁、锁定、搜索、复制、随机密码生成、修改主密码;备份文件中密码保持密文 - 用户系统:每位用户独立 IndexedDB 数据库隔离存储;登录密码仅存 PBKDF2 哈希;30 分钟无操作自动退出;创建用户时可迁移升级前数据 +- 记账系统:支出/收入记录、报销跟踪(面向出差垫付)、月度汇总与分类分布 - PWA:在 Edge/Chrome 中打开后,地址栏右侧点「安装」或菜单选择「将此站点作为应用安装」,即可获得独立窗口与桌面图标,离线可用 - Electron 桌面版:pnpm dist 产出 release/InternLog-Setup-<版本>.exe(NSIS 安装包)与 release/win-unpacked/InternLog.exe(免安装版);数据存于系统用户目录,与浏览器数据互不影响 - 全局搜索:Ctrl+K 唤起,覆盖工作记录与备忘录,点击结果跳转定位 diff --git a/intern-log/src/App.vue b/intern-log/src/App.vue index 9d354c3..9209fef 100644 --- a/intern-log/src/App.vue +++ b/intern-log/src/App.vue @@ -5,6 +5,7 @@ import CalendarView from './views/CalendarView.vue' import TodayView from './views/TodayView.vue' import StatsView from './views/StatsView.vue' import ReportView from './views/ReportView.vue' +import LedgerView from './views/LedgerView.vue' import MemoView from './views/MemoView.vue' import VaultView from './views/VaultView.vue' import SettingsView from './views/SettingsView.vue' @@ -104,6 +105,7 @@ const VIEWS = [ { key: 'today', name: '今日', icon: 'edit' }, { key: 'stats', name: '统计', icon: 'chart' }, { key: 'report', name: '总结', icon: 'export' }, + { key: 'ledger', name: '记账', icon: 'wallet' }, { key: 'memo', name: '备忘录', icon: 'memo' }, { key: 'vault', name: '密码库', icon: 'lock' }, { key: 'settings', name: '设置', icon: 'settings' } @@ -247,6 +249,7 @@ onMounted(async () => { + diff --git a/intern-log/src/components/TransactionDialog.vue b/intern-log/src/components/TransactionDialog.vue new file mode 100644 index 0000000..e434727 --- /dev/null +++ b/intern-log/src/components/TransactionDialog.vue @@ -0,0 +1,193 @@ + + + + + diff --git a/intern-log/src/components/icons.ts b/intern-log/src/components/icons.ts index 7e9a67e..4903995 100644 --- a/intern-log/src/components/icons.ts +++ b/intern-log/src/components/icons.ts @@ -21,5 +21,6 @@ export const ICONS: Record = { search: svg(''), settings: svg(''), memo: svg(''), - lock: svg('') + lock: svg(''), + wallet: svg('') } diff --git a/intern-log/src/db/repo.ts b/intern-log/src/db/repo.ts index 5a04630..35b060c 100644 --- a/intern-log/src/db/repo.ts +++ b/intern-log/src/db/repo.ts @@ -1,4 +1,4 @@ -import { db, DEFAULT_LOCATIONS, SCHEMA_VERSION, type Attendance, type Credential, type LogItem, type Memo, type Project, type WeeklyNote, type WorkLog } from './schema' +import { db, DEFAULT_LOCATIONS, SCHEMA_VERSION, type Attendance, type Credential, type LogItem, type Memo, type Project, type Transaction, type WeeklyNote, type WorkLog } from './schema' import { addDays, nowLocalISO, toDateStr } from '../lib/date' // ---------- 设置 ---------- @@ -347,6 +347,38 @@ export async function searchMemos(q: string, limit = 4): Promise { .slice(0, limit) } +// ---------- 记账 ---------- + +export interface TransactionInput { + type: string + category: string + amount: number + date: string + note: string + reimbursable: number + reimbursed: number +} + +export async function listTransactionsBetween(start: string, end: string): Promise { + return db.transaction.where('date').between(start, end, true, true).toArray() +} + +export async function addTransaction(input: TransactionInput): Promise { + await db.transaction.add({ ...input, created_at: nowLocalISO() }) +} + +export async function updateTransaction(id: number, input: TransactionInput): Promise { + await db.transaction.update(id, { ...input }) +} + +export async function deleteTransaction(id: number): Promise { + await db.transaction.delete(id) +} + +export async function setTransactionReimbursed(id: number, done: boolean): Promise { + await db.transaction.update(id, { reimbursed: done ? 1 : 0 }) +} + // ---------- 备份 ---------- export async function exportAll(): Promise { @@ -358,7 +390,8 @@ export async function exportAll(): Promise { db.weekly_note.toArray(), db.setting.toArray(), db.memo.toArray(), - db.credential.toArray() + db.credential.toArray(), + db.transaction.toArray() ]) return JSON.stringify({ schema_version: SCHEMA_VERSION, @@ -371,7 +404,8 @@ export async function exportAll(): Promise { weekly_note: tables[4], setting: tables[5], memo: tables[6], - credential: tables[7] + credential: tables[7], + transaction: tables[8] } }, null, 2) } @@ -383,15 +417,15 @@ export async function importAll(jsonText: string): Promise { } catch { throw new Error('不是有效的 JSON 文件') } - if (!parsed || (parsed.schema_version !== 1 && parsed.schema_version !== 2) || !parsed.data) { + if (!parsed || (parsed.schema_version !== 1 && parsed.schema_version !== 2 && parsed.schema_version !== 3) || !parsed.data) { throw new Error('文件格式或 schema 版本不匹配') } const d = parsed.data - await db.transaction('rw', [db.project, db.work_log, db.log_item, db.attendance, db.weekly_note, db.setting, db.memo, db.credential], async () => { + await db.transaction('rw', [db.project, db.work_log, db.log_item, db.attendance, db.weekly_note, db.setting, db.memo, db.credential, db.transaction], async () => { await Promise.all([ db.project.clear(), db.work_log.clear(), db.log_item.clear(), db.attendance.clear(), db.weekly_note.clear(), db.setting.clear(), - db.memo.clear(), db.credential.clear() + db.memo.clear(), db.credential.clear(), db.transaction.clear() ]) if (d.project && d.project.length) await db.project.bulkAdd(d.project as never[]) if (d.work_log && d.work_log.length) await db.work_log.bulkAdd(d.work_log as never[]) @@ -401,5 +435,6 @@ export async function importAll(jsonText: string): Promise { if (d.setting && d.setting.length) await db.setting.bulkAdd(d.setting as never[]) if (d.memo && d.memo.length) await db.memo.bulkAdd(d.memo as never[]) if (d.credential && d.credential.length) await db.credential.bulkAdd(d.credential as never[]) + if (d.transaction && d.transaction.length) await db.transaction.bulkAdd(d.transaction as never[]) }) } diff --git a/intern-log/src/db/schema.ts b/intern-log/src/db/schema.ts index f57c727..d45ef38 100644 --- a/intern-log/src/db/schema.ts +++ b/intern-log/src/db/schema.ts @@ -66,6 +66,18 @@ export interface Credential { updated_at: string } +export interface Transaction { + id?: number + type: string // income | expense + category: string + amount: number // 单位:分 + date: string // YYYY-MM-DD + note: string + reimbursable: number // 支出是否需要报销 + reimbursed: number // 是否已报销 + created_at: string +} + export class WorklogDB extends Dexie { project!: Table work_log!: Table @@ -75,6 +87,7 @@ export class WorklogDB extends Dexie { setting!: Table<{ key: string; value: string }, string> memo!: Table credential!: Table + transaction!: Table constructor(dbName = 'intern_worklog') { super(dbName) @@ -90,6 +103,9 @@ export class WorklogDB extends Dexie { memo: '++id, updated_at', credential: '++id, system, updated_at' }) + this.version(3).stores({ + transaction: '++id, date, type, category' + }) } } @@ -105,7 +121,10 @@ export function unbindDatabase(): void { db = undefined } -export const SCHEMA_VERSION = 2 +export const SCHEMA_VERSION = 3 + +export const EXPENSE_CATEGORIES: string[] = ['餐饮', '交通', '住房', '通讯', '学习', '娱乐', '其他'] +export const INCOME_CATEGORIES: string[] = ['工资', '报销', '其他'] export const DEFAULT_LOCATIONS: string[] = ['公司', '客户现场', '出差', '居家办公'] diff --git a/intern-log/src/views/LedgerView.vue b/intern-log/src/views/LedgerView.vue new file mode 100644 index 0000000..e7b79e7 --- /dev/null +++ b/intern-log/src/views/LedgerView.vue @@ -0,0 +1,388 @@ + + + + + diff --git a/实习工作日志系统-设计方案.md b/实习工作日志系统-设计方案.md index 868715e..793a9f4 100644 --- a/实习工作日志系统-设计方案.md +++ b/实习工作日志系统-设计方案.md @@ -1,6 +1,6 @@ # 实习工作日志系统 设计方案 -- 版本:v1.10(Electron 桌面应用) +- 版本:v1.11(记账系统) - 范围:个人版全量功能;团队版仅作扩展预留 ## 1. 产品定位 @@ -154,6 +154,15 @@ AI 润色为可选进阶:将统计、成果、周记交给大模型按四段 | 账号管理 | 设置页可修改登录密码、删除用户(连同其全部数据,二次确认) | | 旧数据迁移 | 首次创建用户时可勾选迁移升级前单机数据,迁移仅复制、原库保留兜底 | +### 3.12 记账系统 + +| 项 | 说明 | +|---|---| +| 录入 | 支出/收入两类型;金额以「分」为整数存储避免浮点误差;日期、分类(支出:餐饮/交通/住房/通讯/学习/娱乐/其他;收入:工资/报销/其他,固定枚举)、备注 | +| 报销跟踪 | 支出可标记「需要报销」并跟踪待报销/已报销状态,面向出差垫付场景;汇总卡片显示待报销金额 | +| 汇总 | 按月展示收入、支出、结余、待报销四卡片;支出分类分布条形图 | +| 明细 | 月度流水表格,类型与报销状态筛选,支持弹窗编辑、删除确认、标记已报销 | + ## 4. 数据模型(SQLite) ### 4.1 建表语句 @@ -232,6 +241,19 @@ CREATE TABLE credential ( updated_at TEXT NOT NULL ); +-- 记账流水(amount 单位为分) +CREATE TABLE transaction ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, + category TEXT NOT NULL, + amount INTEGER NOT NULL, + date TEXT NOT NULL, + note TEXT, + reimbursable INTEGER NOT NULL DEFAULT 0, + reimbursed INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL +); + -- 设置 CREATE TABLE setting ( key TEXT PRIMARY KEY, @@ -482,3 +504,4 @@ src/ - v1.8:新增用户系统——每位用户独立 IndexedDB 数据库物理隔离数据;登录密码仅存 PBKDF2 哈希;会话存 sessionStorage 并支持 30 分钟无操作自动退出;设置页支持修改登录密码与删除用户;首次创建用户可一键迁移升级前单机数据(原库保留兜底) - v1.9:PWA 应用化——新增 Web 应用清单、Service Worker 离线缓存与自绘应用图标;在 Edge/Chrome 中可一键安装为独立窗口应用(桌面图标、开始菜单入口),离线可用,数据随浏览器配置文件存储 - v1.10:Electron 桌面应用——pnpm dist 一键产出 NSIS 安装包与免安装目录;主进程加载本地构建产物,数据存于系统用户目录;窗口禁用菜单栏、外部链接交由系统浏览器打开 +- v1.11:新增记账系统——支出/收入录入(金额以分存储)、固定分类枚举、报销跟踪(待报销/已报销与待报销金额汇总)、月度汇总卡片与支出分类分布、月度流水筛选与弹窗编辑;备份格式升级至 v3 并向下兼容