Files
daywork/intern-log/src/views/TodayView.vue
T

151 lines
6.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import Icon from '../components/Icon.vue'
import DayPanel from '../components/log/DayPanel.vue'
import { listLogItemsBetween, listAttendanceBetween, listProjects, getWorkRule } from '../db/repo'
import { computeDay } from '../domain/attendance'
import { addDays, formatDateCN, hhmm, todayStr, weekStart } from '../lib/date'
import { downloadText } from '../lib/download'
import { showToast } from '../lib/toast'
import { categoryLabel } from '../db/schema'
// 详情视图:自由切换日期,对任意一天进行详细查看与修改
const date = ref(todayStr())
const isToday = computed(() => date.value === todayStr())
function prevDay() { date.value = addDays(date.value, -1) }
function nextDay() { date.value = addDays(date.value, 1) }
function goToday() { date.value = todayStr() }
// ---- 工作详情导出(Markdown) ----
const expRange = ref<'day' | 'week' | 'month' | 'custom'>('day')
const expStart = ref(todayStr())
const expEnd = ref(todayStr())
const exporting = ref(false)
function rangeStartEnd(): { start: string; end: string; label: string } {
// 「指定日」导出:导的是详情页当前浏览的那一天,而不是今天
if (expRange.value === 'day') return { start: date.value, end: date.value, label: date.value }
if (expRange.value === 'week') {
const ws = weekStart(expStart.value)
return { start: ws, end: addDays(ws, 6), label: ws + '_' + addDays(ws, 6) }
}
if (expRange.value === 'month') {
const y = expStart.value.slice(0, 4)
const m = expStart.value.slice(5, 7)
const last = new Date(Number(y), Number(m), 0).getDate()
return { start: y + '-' + m + '-01', end: y + '-' + m + '-' + last, label: y + '-' + m }
}
return { start: expStart.value, end: expEnd.value, label: expStart.value + '_' + expEnd.value }
}
async function doExportMd() {
if (exporting.value) return
exporting.value = true
try {
const { start, end, label } = rangeStartEnd()
const [rule, atts, items, projects] = await Promise.all([
getWorkRule(),
listAttendanceBetween(start, end),
listLogItemsBetween(start, end),
listProjects(true)
])
const pname = new Map<number, string>(projects.map(p => [p.id as number, p.name]))
const lines: string[] = ['# 工作详情 ' + label, '']
const wd = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
let cur = start
let guard = 0
while (cur <= end && guard < 400) {
const dayItems = items.filter(i => i.date === cur)
const att = atts.find(a => a.date === cur)
if (att || dayItems.length) {
lines.push('## ' + cur + ' ' + wd[new Date(cur + 'T00:00:00').getDay()], '')
if (att && (att.clock_in_at || att.kind === 'makeup')) {
const info = computeDay(att, cur, rule, cur === todayStr())
const seg = [hhmm(att.clock_in_at), '–', hhmm(att.clock_out_at), '·', info.label]
if (att.location) seg.push('· 地点:' + att.location)
lines.push('考勤:' + seg.join(' '), '')
}
for (const it of dayItems) {
const proj = it.project_id ? (pname.get(it.project_id) || '') : ''
const head = '- 【' + categoryLabel(it.category) + '】' + it.content + (it.minutes ? '(' + it.minutes + ' 分钟)' : '') + (proj ? ' @' + proj : '')
lines.push(head)
if (it.takeaway) lines.push(' - 收获:' + it.takeaway)
if (it.needs_followup === 1 && it.remark) lines.push(' - 待跟进:' + it.remark)
else if (it.remark) lines.push(' - 备注:' + it.remark)
}
lines.push('')
}
cur = addDays(cur, 1)
guard++
}
await downloadText('工作详情_' + label + '.md', lines.join('\n'))
showToast('已导出 Markdown')
} finally {
exporting.value = false
}
}
function onDateInput(e: Event) {
const v = (e.target as HTMLInputElement).value
if (v) date.value = v
}
// 浏览日期变化时,导出区间锚点跟随(周/月/自定义默认围绕当前指定日)
watch(date, d => { expStart.value = d; if (expRange.value === 'custom') expEnd.value = d })
</script>
<template>
<div class="detail-page">
<div class="card toolbar">
<div class="nav-row">
<button class="icon-btn" title="前一天" @click="prevDay"><Icon name="chevron-left" :size="16" /></button>
<span class="date-title num" :class="{ 'is-today': isToday }">{{ formatDateCN(date) }}</span>
<button class="icon-btn" title="后一天" @click="nextDay"><Icon name="chevron-right" :size="16" /></button>
<input type="date" class="input w-date" :value="date" @change="onDateInput" />
<button class="btn btn-sm" :disabled="isToday" @click="goToday">回到今天</button>
</div>
<div class="tool-sep"></div>
<div class="exp-row">
<span class="muted exp-label">导出</span>
<div class="seg">
<button class="seg-btn" :class="{ active: expRange === 'day' }" @click="expRange = 'day'">指定日</button>
<button class="seg-btn" :class="{ active: expRange === 'week' }" @click="expRange = 'week'">当周</button>
<button class="seg-btn" :class="{ active: expRange === 'month' }" @click="expRange = 'month'">当月</button>
<button class="seg-btn" :class="{ active: expRange === 'custom' }" @click="expRange = 'custom'">自定义</button>
</div>
<input v-if="expRange !== 'day'" class="input w-exp" type="date" v-model="expStart" />
<input v-if="expRange === 'custom'" class="input w-exp" type="date" v-model="expEnd" />
<button class="btn btn-sm" :disabled="exporting" @click="doExportMd">导出 Markdown</button>
</div>
</div>
<DayPanel :date="date" />
</div>
</template>
<style scoped>
.detail-page { display: flex; flex-direction: column; }
.toolbar {
padding: 10px 16px;
display: flex; align-items: center; gap: 14px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.nav-row { display: flex; align-items: center; gap: 6px; }
.date-title { font-size: 14px; font-weight: 600; min-width: 170px; text-align: center; }
.date-title.is-today { color: var(--accent); }
.w-date { width: 150px; flex: none; }
.tool-sep { width: 1px; align-self: stretch; background: var(--border); }
.exp-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.exp-label { font-size: 12px; }
.w-exp { width: 140px; flex: none; }
@media (max-width: 720px) {
.tool-sep { display: none; }
.toolbar { padding: 10px 12px; gap: 10px; }
.nav-row { flex: 1 1 100%; }
.date-title { min-width: 0; flex: 1; text-align: left; }
.w-date { flex: 1; min-width: 120px; width: auto; }
.exp-row { width: 100%; }
.exp-row .seg { flex: 1; display: flex; }
.exp-row .seg-btn { flex: 1; padding: 0 6px; }
.w-exp { flex: 1; min-width: 0; width: auto; }
}
</style>