feat: 长期计划(目标→每日任务物化+进度条) / 详情页Markdown范围导出 / 记账与详情页改内容优先弹窗新增

This commit is contained in:
root
2026-10-26 12:16:14 +08:00
parent 284f8ebd46
commit 0f268a495e
20 changed files with 535 additions and 268 deletions
+149 -2
View File
@@ -1,14 +1,72 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import Icon from '../components/Icon.vue'
import ConfirmDialog from '../components/ConfirmDialog.vue'
import {
addPlan, deletePlan, listPlansByDate, setPlanStatus,
startPlanTimer, stopPlanTimer, updatePlan, type PlanItem
startPlanTimer, stopPlanTimer, updatePlan, type PlanItem,
addGoal, updateGoal, deleteGoal, listGoals, getGoalProgress, type Goal
} from '../db/repo'
import { addDays, parseDateStr, todayStr, toDateStr, formatDateCN } from '../lib/date'
import { attendanceChanged } from '../lib/bus'
import { showToast } from '../lib/toast'
// ---- 长期计划 ----
const goals = ref<Goal[]>([])
const goalProg = ref<Record<string, { done: number; total: number }>>({})
const goalOpen = ref(false)
const goalEditing = ref<Goal | null>(null)
const gTitle = ref('')
const gKind = ref('学习')
const gStart = ref(todayStr())
const gEnd = ref(addDays(todayStr(), 6))
const gNote = ref('')
const gError = ref('')
const delGoal = ref<Goal | null>(null)
function openGoalCreate() {
goalEditing.value = null
gTitle.value = ''
gKind.value = '学习'
gStart.value = todayStr()
gEnd.value = addDays(todayStr(), 6)
gNote.value = ''
gError.value = ''
goalOpen.value = true
}
function openGoalEdit(g: Goal) {
goalEditing.value = g
gTitle.value = g.title
gKind.value = g.kind
gStart.value = g.start_date
gEnd.value = g.end_date
gNote.value = g.note || ''
gError.value = ''
goalOpen.value = true
}
async function submitGoal() {
if (!gTitle.value.trim()) { gError.value = '请填写计划名称'; return }
if (gEnd.value < gStart.value) { gError.value = '结束日期不能早于开始日期'; return }
if (goalEditing.value) {
await updateGoal(goalEditing.value.id as number, {
title: gTitle.value.trim(), kind: gKind.value, note: gNote.value
})
showToast('长期计划已更新')
} else {
await addGoal({ title: gTitle.value, kind: gKind.value, start_date: gStart.value, end_date: gEnd.value, note: gNote.value })
showToast('长期计划已创建,已生成每日任务')
}
goalOpen.value = false
await load()
}
async function removeGoal() {
if (!delGoal.value) return
await deleteGoal(delGoal.value.id as number)
showToast('已删除长期计划及其每日任务')
delGoal.value = null
await load()
}
const date = ref(todayStr())
const items = ref<PlanItem[]>([])
const loading = ref(true)
@@ -20,7 +78,16 @@ const isToday = computed(() => date.value === todayStr())
async function load() {
loading.value = true
items.value = await listPlansByDate(date.value)
const [plans, gs, prog] = await Promise.all([
listPlansByDate(date.value),
listGoals(),
getGoalProgress()
])
items.value = plans
goals.value = gs
const map: Record<string, { done: number; total: number }> = {}
for (const p of prog) map[String(p.goalId)] = { done: p.done, total: p.total }
goalProg.value = map
loading.value = false
}
watch(date, load)
@@ -165,6 +232,34 @@ function weekday(s: string): string {
</div>
</div>
<div class="goals-sec">
<div class="goals-head">
<span class="goals-title">长期计划</span>
<button class="btn btn-sm" @click="openGoalCreate"><Icon name="plus" :size="14" /> 新建长期计划</button>
</div>
<div v-if="!goals.length" class="muted goals-empty">暂无长期计划。可以创建一个跨越多天的目标(如「产品文档学习」),系统会自动生成每天的打卡任务。</div>
<div v-for="g in goals" :key="g.id" class="card sec goal-card">
<div class="goal-main">
<div class="goal-line">
<span class="tag">{{ g.kind }}</span>
<span class="goal-name">{{ g.title }}</span>
<span class="muted num goal-range">{{ g.start_date.slice(5) }} ~ {{ g.end_date.slice(5) }}</span>
</div>
<div class="goal-bar">
<span class="goal-bar-fill" :style="{ width: (goalProg[String(g.id)] ? Math.round(goalProg[String(g.id)].done / Math.max(1, goalProg[String(g.id)].total) * 100) : 0) + '%' }"></span>
</div>
<div class="muted num goal-meta">
{{ goalProg[String(g.id)] ? goalProg[String(g.id)].done + '/' + goalProg[String(g.id)].total + ' 天已完成' : '0/' + (goalProg[String(g.id)] ? goalProg[String(g.id)].total : 0) + ' 天' }}
<template v-if="g.note"> · {{ g.note }}</template>
</div>
</div>
<div class="goal-ops">
<button class="icon-btn" title="编辑" @click="openGoalEdit(g)"><Icon name="edit" :size="14" /></button>
<button class="icon-btn" title="删除" @click="delGoal = g"><Icon name="close" :size="14" /></button>
</div>
</div>
</div>
<div class="plan-list">
<div v-if="!loading && !items.length" class="card sec empty muted">
今天还没有计划,点右上角「添加计划」开始规划:学习、健身、自律……
@@ -180,6 +275,7 @@ function weekday(s: string): string {
<div class="pi-main">
<div class="pi-title">
<span class="tag">{{ p.kind }}</span>
<span v-if="p.goal_id" class="tag tag-goal">长期</span>
<span class="pi-text" :class="{ strike: p.status === 'done' }">{{ p.title }}</span>
<span v-if="p.status === 'done'" class="state-done">已完成</span>
<span v-else-if="p.status === 'skip'" class="state-skip">已跳过</span>
@@ -248,6 +344,57 @@ function weekday(s: string): string {
</div>
</div>
</teleport>
<teleport to="body">
<div v-if="goalOpen" class="fixed-mask" @click.self="goalOpen = false">
<div class="dialog" role="dialog" aria-label="长期计划">
<div class="dialog-head">
<span class="dialog-title">{{ goalEditing ? '编辑长期计划' : '新建长期计划' }}</span>
<button class="icon-btn" @click="goalOpen = false"><Icon name="close" :size="16" /></button>
</div>
<div class="dialog-body">
<div class="form-row">
<label>计划名称(落实为每天一条任务)</label>
<input class="input" v-model="gTitle" placeholder="如:产品文档学习 / 健身月计划" />
</div>
<div class="form-row">
<label>类别</label>
<select class="select" v-model="gKind">
<option v-for="k in KINDS" :key="k" :value="k">{{ k }}</option>
</select>
</div>
<div class="form-row two">
<div>
<label>开始日期</label>
<input class="input" type="date" v-model="gStart" />
</div>
<div>
<label>结束日期</label>
<input class="input" type="date" v-model="gEnd" />
</div>
</div>
<div class="form-row">
<label>备注(选填)</label>
<input class="input" v-model="gNote" />
</div>
<div v-if="gError" class="form-error">{{ gError }}</div>
<div class="muted goal-hint">创建后区间内每一天都会生成一条任务,可在每日安排中逐日打卡与计时。</div>
</div>
<div class="dialog-btns">
<button class="btn btn-sm" @click="goalOpen = false">取消</button>
<button class="btn btn-primary btn-sm" @click="submitGoal">{{ goalEditing ? '保存' : '创建' }}</button>
</div>
</div>
</div>
</teleport>
<ConfirmDialog
:open="!!delGoal"
title="删除长期计划"
:message="delGoal ? '将删除「' + delGoal.title + '」及其全部每日任务(含已完成记录),不可恢复。' : ''"
confirm-text="删除" danger
@cancel="delGoal = null"
@confirm="removeGoal"
/>
</div>
</template>