221 lines
5.8 KiB
TypeScript
221 lines
5.8 KiB
TypeScript
import Dexie, { type Table } from 'dexie'
|
||
|
||
export interface Project {
|
||
id?: number
|
||
name: string
|
||
archived: number
|
||
created_at: string
|
||
}
|
||
|
||
export interface WorkLog {
|
||
id?: number
|
||
date: string
|
||
created_at: string
|
||
}
|
||
|
||
export interface LogItem {
|
||
id?: number
|
||
work_log_id: number
|
||
content: string
|
||
project_id: number | null
|
||
category: string
|
||
minutes: number | null
|
||
is_milestone: number
|
||
needs_followup: number
|
||
takeaway: string | null
|
||
remark: string | null
|
||
sort_order: number
|
||
created_at: string
|
||
}
|
||
|
||
export interface Attendance {
|
||
id?: number
|
||
date: string
|
||
clock_in_at: string | null
|
||
clock_out_at: string | null
|
||
kind: string
|
||
note: string | null
|
||
location?: string | null
|
||
created_at: string
|
||
}
|
||
|
||
export interface WeeklyNote {
|
||
id?: number
|
||
week_start: string
|
||
highlights: string | null
|
||
challenges: string | null
|
||
plan_next: string | null
|
||
created_at: string
|
||
}
|
||
|
||
export interface Goal {
|
||
id?: number
|
||
title: string // 长期计划,如:学完产品文档 / 本月健身 20 次
|
||
kind: string
|
||
start_date: string // YYYY-MM-DD
|
||
end_date: string
|
||
note: string | null
|
||
uuid: string
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export interface PlanItem {
|
||
id?: number
|
||
date: string // YYYY-MM-DD
|
||
start_time: string // HH:mm
|
||
end_time: string // HH:mm
|
||
title: string // 计划内容,如:产品学习 / 健身
|
||
kind: string // 类别:学习 / 健身 / 自律 / 其他自定义
|
||
note: string | null
|
||
status: string // todo | done | skip
|
||
actual_minutes: number // 计时时钟累计的实际投入分钟
|
||
timer_started_at: string | null
|
||
goal_id: number | null // 来源长期计划(本地 id)
|
||
uuid: string
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export interface Memo {
|
||
id?: number
|
||
title: string
|
||
content: string
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export interface Credential {
|
||
id?: number
|
||
system: string
|
||
account: string
|
||
password: string
|
||
remark: string
|
||
created_at: string
|
||
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 interface SyncMirror {
|
||
id?: number
|
||
table: string
|
||
uuid: string
|
||
hash: string
|
||
}
|
||
|
||
// 删除墓碑:同步时告知其他端该记录已删除
|
||
export interface SyncTombstone {
|
||
id?: number
|
||
table: string
|
||
uuid: string
|
||
deleted_at: string
|
||
}
|
||
|
||
export class WorklogDB extends Dexie {
|
||
project!: Table<Project, number>
|
||
work_log!: Table<WorkLog, number>
|
||
log_item!: Table<LogItem, number>
|
||
attendance!: Table<Attendance, number>
|
||
weekly_note!: Table<WeeklyNote, number>
|
||
setting!: Table<{ key: string; value: string }, string>
|
||
memo!: Table<Memo, number>
|
||
credential!: Table<Credential, number>
|
||
// 注意:transaction 表通过 db.table('transaction') 访问,
|
||
// 因为 db.transaction 会与 Dexie 内置的 transaction() 方法冲突
|
||
sync_mirror!: Table<SyncMirror, number>
|
||
tombstone!: Table<SyncTombstone, number>
|
||
plan!: Table<PlanItem, number>
|
||
goal!: Table<Goal, number>
|
||
|
||
constructor(dbName = 'intern_worklog') {
|
||
super(dbName)
|
||
this.version(1).stores({
|
||
project: '++id, &name, archived',
|
||
work_log: '++id, &date',
|
||
log_item: '++id, work_log_id, project_id, category',
|
||
attendance: '++id, &date',
|
||
weekly_note: '++id, &week_start',
|
||
setting: 'key'
|
||
})
|
||
this.version(2).stores({
|
||
memo: '++id, updated_at',
|
||
credential: '++id, system, updated_at'
|
||
})
|
||
this.version(3).stores({
|
||
transaction: '++id, date, type, category'
|
||
})
|
||
this.version(4).stores({
|
||
sync_mirror: '++id, table, uuid',
|
||
tombstone: '++id, table, uuid, deleted_at'
|
||
})
|
||
this.version(5).stores({
|
||
plan: '++id, date, status'
|
||
}).upgrade(async tx => {
|
||
// 旧固定分类 key 迁移为用户自定义分类标签(v0.4.0 起分类为用户数据)
|
||
const map: Record<string, string> = {
|
||
dev: '项目实操',
|
||
test: '项目实操',
|
||
bugfix: '售后服务',
|
||
learning: '产品学习',
|
||
meeting: '打杂',
|
||
doc: '打杂',
|
||
other: '打杂'
|
||
}
|
||
await tx.table('log_item').toCollection().modify((item: { category?: string }) => {
|
||
if (item.category && map[item.category]) item.category = map[item.category]
|
||
})
|
||
})
|
||
this.version(6).stores({
|
||
plan: '++id, date, status, goal_id',
|
||
goal: '++id, start_date'
|
||
})
|
||
}
|
||
}
|
||
|
||
// 当前登录用户绑定的数据库实例:不同用户物理隔离不同库,登录后由 auth 模块注入
|
||
export let db: WorklogDB | undefined
|
||
|
||
export function bindDatabase(instance: WorklogDB): void {
|
||
db = instance
|
||
}
|
||
|
||
export function unbindDatabase(): void {
|
||
if (db) db.close()
|
||
db = undefined
|
||
}
|
||
|
||
export const SCHEMA_VERSION = 6
|
||
|
||
export const EXPENSE_CATEGORIES: string[] = ['餐饮', '交通', '住房', '通讯', '学习', '娱乐', '其他']
|
||
export const INCOME_CATEGORIES: string[] = ['工资', '报销', '其他']
|
||
|
||
export const DEFAULT_LOCATIONS: string[] = ['公司', '客户现场', '出差', '居家办公']
|
||
|
||
// 旧版固定分类 key → 默认标签的映射(仅用于历史数据迁移与展示兜底)
|
||
export const LEGACY_CATEGORY_LABELS: Record<string, string> = {
|
||
dev: '项目实操',
|
||
test: '项目实操',
|
||
bugfix: '售后服务',
|
||
learning: '产品学习',
|
||
meeting: '打杂',
|
||
doc: '打杂',
|
||
other: '打杂'
|
||
}
|
||
|
||
// v0.4.0 起分类即用户自定义标签本身;历史 key 在此直通转换
|
||
export function categoryLabel(key: string): string {
|
||
return LEGACY_CATEGORY_LABELS[key] || key
|
||
}
|