feat: 长期计划(目标→每日任务物化+进度条) / 详情页Markdown范围导出 / 记账与详情页改内容优先弹窗新增
This commit is contained in:
@@ -2,7 +2,12 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import Icon from '../components/Icon.vue'
|
||||
import DayPanel from '../components/log/DayPanel.vue'
|
||||
import { addDays, formatDateCN, todayStr } from '../lib/date'
|
||||
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())
|
||||
@@ -11,6 +16,73 @@ 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: expStart.value, end: expStart.value, label: expStart.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
|
||||
@@ -26,6 +98,18 @@ function onDateInput(e: Event) {
|
||||
<input type="date" class="input w-date" :value="date" @change="onDateInput" />
|
||||
<button class="btn btn-sm" :disabled="isToday" @click="goToday">回到今天</button>
|
||||
</div>
|
||||
<div class="export-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>
|
||||
<DayPanel :date="date" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -42,6 +126,18 @@ function onDateInput(e: Event) {
|
||||
.date-title.is-today { color: var(--accent); }
|
||||
.w-date { width: 150px; }
|
||||
|
||||
.export-row {
|
||||
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||||
margin-bottom: 12px; padding: 8px 12px;
|
||||
border: 1px solid var(--border); border-radius: var(--radius-md);
|
||||
background: var(--bg-page);
|
||||
}
|
||||
.exp-label { font-size: 12px; }
|
||||
.seg { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
|
||||
.seg-btn { border: none; background: var(--bg-page); color: var(--text-2); cursor: pointer; font-size: 12px; padding: 4px 10px; }
|
||||
.seg-btn.active { background: var(--accent-bg); color: var(--accent); }
|
||||
.w-exp { width: 140px; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.detail-toolbar { gap: 8px; }
|
||||
.date-title { min-width: 0; flex: 1; text-align: left; }
|
||||
|
||||
Reference in New Issue
Block a user