初始提交:实习工作日志系统(设计方案 + Vue3 应用 + Electron 打包)
This commit is contained in:
@@ -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')
|
||||
}
|
||||
Reference in New Issue
Block a user