初始提交:实习工作日志系统(设计方案 + 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
+241
View File
@@ -0,0 +1,241 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import Icon from '../components/Icon.vue'
import { buildReport, reportToMarkdown, type ReportData } from '../domain/report'
import { getWeeklyNote, saveWeeklyNote } from '../db/repo'
import { addDays, todayStr, weekStart } from '../lib/date'
import { downloadText } from '../lib/download'
import { copyText } from '../lib/clipboard'
import { polishReport } from '../domain/ai'
import { showToast } from '../lib/toast'
type RangeKey = 'week' | 'month' | 'all' | 'custom'
const RANGE_OPTIONS: { key: RangeKey; label: string }[] = [
{ key: 'week', label: '本周' },
{ key: 'month', label: '本月' },
{ key: 'all', label: '全部' },
{ key: 'custom', label: '自定义' }
]
const rangeKey = ref<RangeKey>('week')
const customStart = ref(weekStart(todayStr()))
const customEnd = ref(todayStr())
const data = ref<ReportData | null>(null)
const markdown = ref('')
const generating = ref(false)
const polishBusy = ref(false)
const prePolish = ref<string | null>(null)
const aiError = ref('')
const noteWeekStart = ref(weekStart(todayStr()))
const noteHighlights = ref('')
const noteChallenges = ref('')
const notePlan = ref('')
const noteSaved = ref(false)
async function loadNote() {
const note = await getWeeklyNote(noteWeekStart.value)
noteHighlights.value = note?.highlights || ''
noteChallenges.value = note?.challenges || ''
notePlan.value = note?.plan_next || ''
noteSaved.value = false
}
async function saveNote() {
await saveWeeklyNote(noteWeekStart.value, {
highlights: noteHighlights.value.trim() || null,
challenges: noteChallenges.value.trim() || null,
plan_next: notePlan.value.trim() || null
})
noteSaved.value = true
await generate()
}
function rangeOf(key: RangeKey): { start: string; end: string; label: string } {
const t = todayStr()
if (key === 'week') {
const ws = weekStart(t)
return { start: ws, end: addDays(ws, 6), label: '本周' }
}
if (key === 'month') {
const ym = t.slice(0, 7)
return { start: ym + '-01', end: t, label: ym + ' 本月' }
}
if (key === 'all') return { start: '2000-01-01', end: t, label: '全部实习期' }
return { start: customStart.value, end: customEnd.value, label: customStart.value + ' ~ ' + customEnd.value }
}
async function generate() {
generating.value = true
const r = rangeOf(rangeKey.value)
const d = await buildReport(r.start, r.end, r.label)
data.value = d
markdown.value = reportToMarkdown(d)
generating.value = false
}
watch(rangeKey, generate)
watch([customStart, customEnd], () => {
if (rangeKey.value === 'custom') generate()
})
onMounted(async () => {
await loadNote()
await generate()
})
function doExport() {
const t = todayStr().replace(/-/g, '')
downloadText('实习工作总结_' + t + '.md', markdown.value)
}
async function doCopy() {
await copyText(markdown.value)
showToast('总结已复制到剪贴板')
}
async function doPolish() {
if (polishBusy.value) return
aiError.value = ''
const src = markdown.value.trim()
if (!src) {
aiError.value = '当前草稿为空,无法润色'
return
}
polishBusy.value = true
try {
prePolish.value = markdown.value
markdown.value = await polishReport(src)
showToast('AI 润色完成')
} catch (e) {
prePolish.value = null
aiError.value = e instanceof Error ? e.message : '润色失败'
}
polishBusy.value = false
}
function undoPolish() {
if (prePolish.value !== null) markdown.value = prePolish.value
prePolish.value = null
}
</script>
<template>
<div class="report">
<div class="report-toolbar">
<div class="seg">
<button
v-for="o in RANGE_OPTIONS" :key="o.key"
class="seg-btn" :class="{ active: rangeKey === o.key }"
@click="rangeKey = o.key"
>{{ o.label }}</button>
</div>
<template v-if="rangeKey === 'custom'">
<input type="date" class="input w-date" v-model="customStart" />
<span class="muted">至</span>
<input type="date" class="input w-date" v-model="customEnd" />
</template>
<span class="flex1"></span>
<button class="btn btn-sm" :disabled="polishBusy" @click="doPolish">
<Icon name="edit" :size="14" />AI 润色
</button>
<button v-if="prePolish !== null" class="btn btn-sm" @click="undoPolish">还原润色前</button>
<button class="btn btn-sm" :disabled="generating" @click="generate">
<Icon name="edit" :size="14" />重新生成
</button>
<button class="btn btn-sm" @click="doCopy">复制</button>
<button class="btn btn-primary btn-sm" @click="doExport">
<Icon name="export" :size="14" />导出 Markdown
</button>
</div>
<div v-if="aiError" class="ai-error">{{ aiError }}</div>
<div class="report-grid">
<div class="report-left">
<div class="card section">
<div class="section-title">本周周记({{ noteWeekStart }} 起)</div>
<label class="note-label muted">本周亮点</label>
<textarea class="textarea" v-model="noteHighlights" placeholder="本周最有价值的一件事" @blur="saveNote"></textarea>
<label class="note-label muted">踩的坑</label>
<textarea class="textarea" v-model="noteChallenges" placeholder="遇到的问题与教训" @blur="saveNote"></textarea>
<label class="note-label muted">下周计划</label>
<textarea class="textarea" v-model="notePlan" placeholder="下周准备做什么" @blur="saveNote"></textarea>
<div class="muted note-saved">{{ noteSaved ? '已保存' : '失焦自动保存' }}</div>
</div>
<div class="card section">
<div class="section-title">总结草稿(可编辑)</div>
<textarea class="textarea md-editor" v-model="markdown" spellcheck="false"></textarea>
</div>
</div>
<div class="report-right">
<div v-if="data" class="card section">
<div class="section-title">数据摘要</div>
<div class="sum-row"><span class="muted">出勤</span><span class="num">{{ data.attendDays }} 天</span></div>
<div class="sum-row"><span class="muted">工时</span><span class="num">{{ data.totalHours }} 小时</span></div>
<div class="sum-row"><span class="muted">补卡</span><span class="num">{{ data.makeupCount }} 次</span></div>
<div class="sum-row"><span class="muted">请假</span><span class="num">{{ data.leaveCount }} 天</span></div>
<div class="sum-row"><span class="muted">记录</span><span class="num">{{ data.itemCount }} 条</span></div>
<div class="section-sub">分类分布</div>
<div v-for="c in data.categoryStats" :key="c.key" class="sum-row">
<span class="muted">{{ c.label }}</span>
<span class="num">{{ c.count }} 条 · {{ c.percent }}%</span>
</div>
<div v-if="!data.categoryStats.length" class="muted sum-empty">(无)</div>
<div class="section-sub">主要成果</div>
<div v-for="(m, i) in data.milestones" :key="'m' + i" class="sum-item">
{{ m.content }}<span class="muted">({{ m.date }})</span>
</div>
<div v-if="!data.milestones.length" class="muted sum-empty">(无)</div>
<div class="section-sub">待跟进</div>
<div v-for="(f, i) in data.followups" :key="'f' + i" class="sum-item">
{{ f.content }}<span class="muted">({{ f.date }})</span>
</div>
<div v-if="!data.followups.length" class="muted sum-empty">(无)</div>
<div class="section-sub">学习与成长</div>
<div v-for="(tk, i) in data.takeaways" :key="'t' + i" class="sum-item">
{{ tk.text }}<span class="muted">({{ tk.date }})</span>
</div>
<div v-if="!data.takeaways.length" class="muted sum-empty">(无)</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.report-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }
.ai-error { color: var(--danger); font-size: 12px; margin: 0 0 8px; }
.seg { display: inline-flex; border: 1px solid var(--border-strong); border-radius: 6px; overflow: hidden; }
.seg-btn {
height: 28px; padding: 0 12px; border: none;
background: var(--bg-page); color: var(--text-2);
cursor: pointer; font-size: 12px;
border-right: 1px solid var(--border);
}
.seg-btn:last-child { border-right: none; }
.seg-btn:hover { background: var(--bg-hover); }
.seg-btn.active { background: var(--accent-bg); color: var(--accent); }
.w-date { width: 140px; }
.report-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 12px; align-items: start; }
.section { padding: 14px 16px; }
.section-title { font-weight: 600; margin-bottom: 8px; font-size: 13px; }
.section + .section { margin-top: 12px; }
.note-label { display: block; font-size: 12px; margin: 8px 0 4px; }
.note-saved { font-size: 12px; margin-top: 6px; }
.md-editor { min-height: 420px; line-height: 1.7; }
.sum-row { display: flex; justify-content: space-between; padding: 3px 0; font-size: 12px; }
.section-sub { font-weight: 600; margin: 10px 0 4px; font-size: 12px; }
.sum-item { font-size: 12px; padding: 2px 0; word-break: break-all; }
.sum-empty { font-size: 12px; }
@media (max-width: 960px) {
.report-grid { grid-template-columns: 1fr; }
}
</style>