初始提交:实习工作日志系统(设计方案 + Vue3 应用 + Electron 打包)

This commit is contained in:
root
2026-10-15 23:55:27 +08:00
commit cfb984627d
50 changed files with 9223 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
import Dexie, { type Table } from 'dexie'
import { bindDatabase, db, unbindDatabase, WorklogDB } from './schema'
import { lockVault } from './vault'
import { pbkdf2Hash, randomSaltB64 } from '../lib/crypto'
interface UserRecord {
id: string
username: string
pwd_hash: string
pwd_salt: string
legacy_imported_at?: string
created_at: string
}
// 用户注册表:独立小库,仅存用户名与密码哈希,不存任何业务数据
class UserRegistry extends Dexie {
user!: Table<UserRecord, string>
constructor() {
super('intern_worklog_users')
this.version(1).stores({ user: 'id, &username' })
}
}
const registry = new UserRegistry()
const LEGACY_DB_NAME = 'intern_worklog'
const SESSION_KEY = 'dsh_session_user'
export interface Session {
userId: string
username: string
}
// 会话存 sessionStorage:关闭标签页即退出,刷新页面保持登录
export function getSession(): Session | null {
try {
const raw = sessionStorage.getItem(SESSION_KEY)
if (!raw) return null
const s = JSON.parse(raw)
if (s && s.userId && s.username) return s as Session
} catch { /* 忽略损坏会话 */ }
return null
}
function setSession(s: Session): void {
sessionStorage.setItem(SESSION_KEY, JSON.stringify(s))
}
function clearSession(): void {
sessionStorage.removeItem(SESSION_KEY)
}
export function openUserDatabase(userId: string): void {
bindDatabase(new WorklogDB(LEGACY_DB_NAME + '_' + userId))
}
export async function createUser(username: string, password: string, migrateLegacy: boolean): Promise<Session> {
const name = username.trim()
if (name.length < 2 || name.length > 20) throw new Error('用户名需 2 到 20 个字符')
if (password.length < 6) throw new Error('登录密码至少 6 位')
const exists = await registry.user.where('username').equals(name).first()
if (exists) throw new Error('用户名已存在')
const salt = randomSaltB64()
const hash = await pbkdf2Hash(password, salt)
const id = 'u_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 6)
await registry.user.add({ id, username: name, pwd_hash: hash, pwd_salt: salt, created_at: new Date().toISOString() })
openUserDatabase(id)
let copied = 0
if (migrateLegacy && await legacyDataExists()) {
copied = await migrateLegacyInto()
await registry.user.update(id, { legacy_imported_at: new Date().toISOString() })
}
const session: Session = { userId: id, username: name }
setSession(session)
if (copied) showToastCopied(copied)
return session
}
export async function login(username: string, password: string): Promise<Session> {
const name = username.trim()
const rec = await registry.user.where('username').equals(name).first()
if (!rec) throw new Error('用户不存在')
const hash = await pbkdf2Hash(password, rec.pwd_salt)
if (hash !== rec.pwd_hash) throw new Error('密码不正确')
openUserDatabase(rec.id)
const session: Session = { userId: rec.id, username: rec.username }
setSession(session)
return session
}
export async function logoutUser(): Promise<void> {
lockVault()
unbindDatabase()
clearSession()
}
export async function changeLoginPassword(oldPwd: string, newPwd: string): Promise<void> {
const s = getSession()
if (!s) throw new Error('未登录')
if (newPwd.length < 6) throw new Error('新密码至少 6 位')
const rec = await registry.user.get(s.userId)
if (!rec) throw new Error('用户不存在')
const oldHash = await pbkdf2Hash(oldPwd, rec.pwd_salt)
if (oldHash !== rec.pwd_hash) throw new Error('旧密码不正确')
const salt = randomSaltB64()
const hash = await pbkdf2Hash(newPwd, salt)
await registry.user.update(rec.id, { pwd_hash: hash, pwd_salt: salt })
}
export async function deleteCurrentUser(): Promise<void> {
const s = getSession()
if (!s) throw new Error('未登录')
lockVault()
unbindDatabase()
await Dexie.delete(LEGACY_DB_NAME + '_' + s.userId)
await registry.user.delete(s.userId)
clearSession()
}
// 旧单机数据检测与迁移(迁移只复制,不删除原库,作为安全兜底)
export async function legacyDataExists(): Promise<boolean> {
return Dexie.exists(LEGACY_DB_NAME)
}
async function migrateLegacyInto(): Promise<number> {
if (!db) throw new Error('用户数据库未就绪')
const legacy = new WorklogDB(LEGACY_DB_NAME)
const legacyData = await Promise.all([
legacy.project.toArray(),
legacy.work_log.toArray(),
legacy.log_item.toArray(),
legacy.attendance.toArray(),
legacy.weekly_note.toArray(),
legacy.setting.toArray(),
legacy.memo.toArray(),
legacy.credential.toArray()
])
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 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()
])
if (legacyData[0].length) await db.project.bulkAdd(legacyData[0] as never[])
if (legacyData[1].length) await db.work_log.bulkAdd(legacyData[1] as never[])
if (legacyData[2].length) await db.log_item.bulkAdd(legacyData[2] as never[])
if (legacyData[3].length) await db.attendance.bulkAdd(legacyData[3] as never[])
if (legacyData[4].length) await db.weekly_note.bulkAdd(legacyData[4] as never[])
if (legacyData[5].length) await db.setting.bulkAdd(legacyData[5] as never[])
if (legacyData[6].length) await db.memo.bulkAdd(legacyData[6] as never[])
if (legacyData[7].length) await db.credential.bulkAdd(legacyData[7] as never[])
})
const count = legacyData[0].length + legacyData[1].length + legacyData[2].length +
legacyData[3].length + legacyData[4].length + legacyData[6].length + legacyData[7].length
legacy.close()
return count
}
function showToastCopied(count: number): void {
// 迁移完成的反馈由登录后的轻提示呈现(此处在会话建立前,暂存标记)
try { sessionStorage.setItem('dsh_migrate_toast', String(count)) } catch { /* 忽略 */ }
}