初始提交:实习工作日志系统(设计方案 + 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
+178
View File
@@ -0,0 +1,178 @@
import { getWorkRule, listAttendanceBetween, listLogItemsBetween, listProjects, listWeeklyNotesBetween } from '../db/repo'
import { CATEGORIES, type LogItem } from '../db/schema'
import { todayStr } from '../lib/date'
import { computeDay } from './attendance'
export interface ReportRange {
start: string
end: string
label: string
}
export interface CategoryStat {
key: string
label: string
count: number
percent: number
}
export interface ProjectStat {
name: string
count: number
}
export interface ReportData {
range: ReportRange
attendDays: number
totalHours: string
makeupCount: number
leaveCount: number
itemCount: number
categoryStats: CategoryStat[]
projectStats: ProjectStat[]
locationStats: { name: string; count: number }[]
milestones: { date: string; content: string }[]
followups: { date: string; content: string; remark: string }[]
takeaways: { date: string; text: string }[]
weekNotes: { weekStart: string; highlights: string; challenges: string; plan_next: string }[]
}
export async function buildReport(start: string, end: string, label: string): Promise<ReportData> {
const t = todayStr()
const results = await Promise.all([
getWorkRule(),
listAttendanceBetween(start, end),
listLogItemsBetween(start, end),
listProjects(true),
listWeeklyNotesBetween(start, end)
])
const rule = results[0]
const atts = results[1]
const items = results[2]
const projects = results[3]
const notes = results[4]
let attendDays = 0
let workedMin = 0
let makeupCount = 0
for (const a of atts) {
if (a.clock_in_at || a.kind === 'makeup') attendDays++
if (a.kind === 'makeup') makeupCount++
const info = computeDay(a, a.date, rule, a.date === t)
workedMin += info.workedMinutes ?? 0
}
const lcount = new Map<string, number>()
for (const a of atts) {
if (!(a.clock_in_at || a.kind === 'makeup')) continue
const name = a.location || '未记录'
lcount.set(name, (lcount.get(name) || 0) + 1)
}
const locationStats = Array.from(lcount.entries())
.map(pair => ({ name: pair[0], count: pair[1] }))
.sort((a, b) => b.count - a.count)
const total = items.length || 1
const categoryStats: CategoryStat[] = CATEGORIES
.map(c => {
const count = items.filter((i: LogItem) => i.category === c.key).length
return { key: c.key, label: c.label, count, percent: Math.round((count / total) * 100) }
})
.filter(c => c.count > 0)
.sort((a, b) => b.count - a.count)
const pname = new Map<number, string>(projects.map(p => [p.id as number, p.name]))
const pcount = new Map<string, number>()
for (const it of items) {
const name = it.project_id ? (pname.get(it.project_id) || '已删除项目') : '无项目'
pcount.set(name, (pcount.get(name) || 0) + 1)
}
const projectStats: ProjectStat[] = Array.from(pcount.entries())
.map(pair => ({ name: pair[0], count: pair[1] }))
.sort((a, b) => b.count - a.count)
const milestones = items.filter(i => i.is_milestone === 1).map(i => ({ date: i.date, content: i.content }))
const followups = items.filter(i => i.needs_followup === 1).map(i => ({ date: i.date, content: i.content, remark: i.remark || '' }))
const takeaways = items.filter(i => !!i.takeaway).map(i => ({ date: i.date, text: i.takeaway as string }))
const weekNotes = notes.map(n => ({
weekStart: n.week_start,
highlights: n.highlights || '',
challenges: n.challenges || '',
plan_next: n.plan_next || ''
}))
return {
range: { start, end, label },
attendDays,
totalHours: (workedMin / 60).toFixed(1),
makeupCount,
leaveCount: atts.filter(a => a.kind === 'leave').length,
itemCount: items.length,
categoryStats,
projectStats,
locationStats,
milestones,
followups,
takeaways,
weekNotes
}
}
export function reportToMarkdown(d: ReportData): string {
const out: string[] = []
out.push('# 实习工作总结(' + d.range.start + ' ~ ' + d.range.end + ')')
out.push('')
out.push('## 一、考勤概况')
out.push('')
out.push('出勤 ' + d.attendDays + ' 天,累计工时 ' + d.totalHours + ' 小时,补卡 ' + d.makeupCount + ' 次' + (d.leaveCount ? ',请假 ' + d.leaveCount + ' 天' : ''))
const locParts = d.locationStats.filter(l => l.name !== '未记录')
if (locParts.length) {
out.push('其中:' + locParts.map(l => l.name + ' ' + l.count + ' 天').join(','))
}
out.push('')
out.push('## 二、工作内容分布')
out.push('')
out.push(d.categoryStats.length
? d.categoryStats.map(c => c.label + ' ' + c.percent + '%(' + c.count + ' 条)').join(',')
: '(无记录)')
out.push('')
out.push('## 三、项目投入')
out.push('')
out.push(d.projectStats.length
? d.projectStats.map(p => p.name + ' ' + p.count + ' 条').join(',')
: '(无记录)')
out.push('')
out.push('## 四、主要成果')
out.push('')
if (d.milestones.length) {
d.milestones.forEach((m, i) => out.push((i + 1) + '. 【成果】' + m.content + '(' + m.date + ')'))
} else {
out.push('(无)')
}
out.push('')
out.push('## 五、待跟进事项')
out.push('')
if (d.followups.length) {
d.followups.forEach(f => out.push('- 【待跟进】' + f.content + (f.remark ? '(' + f.date + ' 备注:' + f.remark + ')' : '(' + f.date + ')')))
} else {
out.push('(无)')
}
out.push('')
out.push('## 六、学习与成长')
out.push('')
if (d.takeaways.length) {
d.takeaways.forEach(tk => out.push('- ' + tk.text + '(' + tk.date + ')'))
} else {
out.push('(无)')
}
for (const w of d.weekNotes) {
out.push('')
out.push('### 周记(' + w.weekStart + ' 起)')
out.push('')
out.push('- 本周亮点:' + (w.highlights || '(无)'))
out.push('- 踩的坑:' + (w.challenges || '(无)'))
out.push('- 下周计划:' + (w.plan_next || '(无)'))
}
out.push('')
return out.join('\n')
}