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