ui: 全站界面重设计(文字侧栏+灰底白卡层次+组件统一) fix: 添加记录弹窗不显示/计时汇总崩溃/CSV导出崩溃/记账切换类型崩溃
This commit is contained in:
@@ -108,12 +108,13 @@ const summary = computed(() => {
|
||||
const done = items.value.filter(p => p.status === 'done').length
|
||||
const plannedMin = items.value.reduce((acc, p) => acc + spanMinutes(p), 0)
|
||||
const actualMin = items.value.reduce((acc, p) => acc + (p.actual_minutes || 0), 0)
|
||||
let liveExtra = 0
|
||||
const runningId = running.value
|
||||
if (runningId) {
|
||||
const rp = items.value.find(p => p.id === runningId)
|
||||
if (rp) actualMin + elapsedMinutes(rp)
|
||||
if (rp) liveExtra = liveElapsed(rp)
|
||||
}
|
||||
return { total, done, plannedMin, actualMin }
|
||||
return { total, done, plannedMin, actualMin: actualMin + liveExtra }
|
||||
})
|
||||
function spanMinutes(p: PlanItem): number {
|
||||
const [sh, sm] = p.start_time.split(':').map(Number)
|
||||
@@ -211,62 +212,65 @@ function weekday(s: string): string {
|
||||
return wdNames[parseDateStr(s).getDay()]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="plan-page">
|
||||
<div class="card sec plan-head">
|
||||
<div class="nav-row">
|
||||
<button class="icon-btn" @click="prevDay"><Icon name="chevron-left" :size="16" /></button>
|
||||
<div class="date-box">
|
||||
<span class="date-main num">{{ date.slice(5) }} {{ weekday(date) }}</span>
|
||||
<span class="muted date-sub num">{{ date }}</span>
|
||||
<!-- 长期计划 -->
|
||||
<div class="blk-head">
|
||||
<div class="blk-head-text">
|
||||
<div class="blk-title">长期计划</div>
|
||||
<div class="muted blk-sub">跨越多天的目标,自动落实到每一天</div>
|
||||
</div>
|
||||
<button class="btn btn-sm" @click="openGoalCreate"><Icon name="plus" :size="14" /> 新建长期计划</button>
|
||||
</div>
|
||||
<div v-if="!goals.length" class="card empty muted">
|
||||
暂无长期计划。创建一个跨越多天的目标(如「产品文档学习」),系统会自动生成每天的打卡任务。
|
||||
</div>
|
||||
<div v-for="g in goals" :key="g.id" class="card 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>
|
||||
<button class="icon-btn" @click="nextDay"><Icon name="chevron-right" :size="16" /></button>
|
||||
<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 class="blk-head day-head">
|
||||
<div class="blk-head-text">
|
||||
<div class="blk-title">每日安排</div>
|
||||
<div class="muted blk-sub num">
|
||||
共 {{ summary.total }} 项 · 完成 {{ summary.done }} 项 · 计划 {{ fmtMin(summary.plannedMin) }} · 实际投入 {{ fmtMin(summary.actualMin) }}
|
||||
<template v-if="running"> · <span class="timer-live">计时中</span></template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="day-ops">
|
||||
<button class="icon-btn" title="前一天" @click="prevDay"><Icon name="chevron-left" :size="16" /></button>
|
||||
<span class="date-chip num" :class="{ today: isToday }">{{ date.slice(5) }} {{ weekday(date) }}</span>
|
||||
<button class="icon-btn" title="后一天" @click="nextDay"><Icon name="chevron-right" :size="16" /></button>
|
||||
<button class="btn btn-sm" :class="{ 'btn-primary': !isToday }" @click="goToday">今天</button>
|
||||
<span class="flex1"></span>
|
||||
<button class="btn btn-primary btn-sm" @click="openCreate"><Icon name="plus" :size="14" /> 添加计划</button>
|
||||
</div>
|
||||
<div class="muted sum-line">
|
||||
共 {{ summary.total }} 项 · 完成 {{ summary.done }} 项 · 计划 {{ fmtMin(summary.plannedMin) }} · 实际投入 {{ fmtMin(summary.actualMin) }}
|
||||
<template v-if="running"> · <span class="timer-live">计时中</span></template>
|
||||
</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">
|
||||
今天还没有计划,点右上角「添加计划」开始规划:学习、健身、自律……
|
||||
</div>
|
||||
<div class="card list-card">
|
||||
<div v-if="!loading && !items.length" class="empty muted">今天还没有计划,点「添加计划」开始规划。</div>
|
||||
<div
|
||||
v-for="p in items" :key="p.id"
|
||||
class="card sec plan-item"
|
||||
class="plan-item"
|
||||
:class="{ done: p.status === 'done', skip: p.status === 'skip', running: !!p.timer_started_at }"
|
||||
>
|
||||
<div class="pi-time num">
|
||||
@@ -304,7 +308,7 @@ function weekday(s: string): string {
|
||||
</div>
|
||||
|
||||
<teleport to="body">
|
||||
<div v-if="dialogOpen" class="fixed-mask" @click.self="dialogOpen = false">
|
||||
<div v-if="dialogOpen" class="mask" @click.self="dialogOpen = false">
|
||||
<div class="dialog" role="dialog" aria-label="计划">
|
||||
<div class="dialog-head">
|
||||
<span class="dialog-title">{{ editingId ? '编辑计划' : '添加计划' }}</span>
|
||||
@@ -345,7 +349,7 @@ function weekday(s: string): string {
|
||||
</div>
|
||||
</teleport>
|
||||
<teleport to="body">
|
||||
<div v-if="goalOpen" class="fixed-mask" @click.self="goalOpen = false">
|
||||
<div v-if="goalOpen" class="mask" @click.self="goalOpen = false">
|
||||
<div class="dialog" role="dialog" aria-label="长期计划">
|
||||
<div class="dialog-head">
|
||||
<span class="dialog-title">{{ goalEditing ? '编辑长期计划' : '新建长期计划' }}</span>
|
||||
@@ -397,52 +401,65 @@ function weekday(s: string): string {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.plan-page { display: flex; flex-direction: column; gap: 10px; max-width: 760px; }
|
||||
.plan-head { padding: 12px 16px; }
|
||||
.nav-row { display: flex; align-items: center; gap: 8px; }
|
||||
.date-box { min-width: 150px; text-align: center; }
|
||||
.date-main { font-weight: 600; font-size: 14px; }
|
||||
.date-sub { font-size: 11px; margin-left: 6px; }
|
||||
.sum-line { font-size: 12px; margin-top: 8px; }
|
||||
.timer-live { color: var(--accent); font-weight: 600; }
|
||||
.flex1 { flex: 1; }
|
||||
.plan-page { display: flex; flex-direction: column; }
|
||||
|
||||
.plan-item { display: flex; align-items: flex-start; gap: 12px; padding: 12px 14px; }
|
||||
.plan-item.done { opacity: 0.62; }
|
||||
.plan-item.skip { opacity: 0.5; }
|
||||
.plan-item.running { border-left: 3px solid var(--accent); }
|
||||
.blk-head {
|
||||
display: flex; align-items: flex-start; justify-content: space-between;
|
||||
gap: 12px; flex-wrap: wrap;
|
||||
margin: 4px 2px 10px;
|
||||
}
|
||||
.blk-head-text { min-width: 0; }
|
||||
.blk-title { font-size: 14px; font-weight: 600; }
|
||||
.blk-sub { font-size: 12px; margin-top: 1px; }
|
||||
.day-head { margin-top: 18px; }
|
||||
.day-ops { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||||
.date-chip { min-width: 74px; text-align: center; font-weight: 600; font-size: 13px; }
|
||||
.date-chip.today { color: var(--accent); }
|
||||
.timer-live { color: var(--accent); font-weight: 600; }
|
||||
|
||||
.goal-card { display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px; margin-bottom: 10px; }
|
||||
.goal-main { flex: 1; min-width: 0; }
|
||||
.goal-line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.goal-name { font-size: 14px; font-weight: 500; word-break: break-all; }
|
||||
.goal-range { font-size: 12px; }
|
||||
.goal-bar { height: 6px; border-radius: 3px; background: var(--bg-subtle); overflow: hidden; margin-top: 10px; }
|
||||
.goal-bar-fill { display: block; height: 100%; background: var(--accent); border-radius: 3px; }
|
||||
.goal-meta { font-size: 12px; margin-top: 6px; }
|
||||
.goal-ops { display: flex; gap: 4px; }
|
||||
|
||||
.list-card { overflow: hidden; }
|
||||
.plan-item {
|
||||
display: flex; align-items: flex-start; gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.plan-item:last-child { border-bottom: none; }
|
||||
.plan-item.done { opacity: 0.6; }
|
||||
.plan-item.skip { opacity: 0.48; }
|
||||
.plan-item.running { box-shadow: inset 3px 0 0 var(--accent); }
|
||||
.pi-time { font-size: 13px; font-weight: 600; min-width: 92px; padding-top: 2px; }
|
||||
.pi-main { flex: 1; min-width: 0; }
|
||||
.pi-title { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.pi-text { font-size: 14px; font-weight: 500; word-break: break-all; }
|
||||
.pi-text.strike { text-decoration: line-through; }
|
||||
.state-done { color: var(--ok, #2e9e5b); font-size: 12px; }
|
||||
.state-done { color: var(--ok); font-size: 12px; }
|
||||
.state-skip { color: var(--text-3); font-size: 12px; }
|
||||
.pi-note { font-size: 12px; margin-top: 3px; }
|
||||
.pi-meta { font-size: 12px; margin-top: 3px; }
|
||||
.pi-ops { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
|
||||
.timer-on { color: var(--accent); border-color: var(--accent); }
|
||||
.empty { text-align: center; padding: 26px 16px; }
|
||||
.empty { padding: 28px 16px; }
|
||||
|
||||
.fixed-mask {
|
||||
position: fixed; inset: 0; z-index: 80;
|
||||
background: rgba(29, 33, 41, 0.35);
|
||||
display: flex; align-items: center; justify-content: center; padding: 16px;
|
||||
}
|
||||
.dialog { background: var(--bg-page); border-radius: 12px; width: 420px; max-width: 100%; }
|
||||
.dialog-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px 0; }
|
||||
.dialog-title { font-weight: 600; font-size: 14px; }
|
||||
.dialog-body { padding: 6px 16px 4px; }
|
||||
.dialog-btns { display: flex; justify-content: flex-end; gap: 10px; padding: 12px 16px 16px; }
|
||||
.dialog { width: 420px; }
|
||||
.form-row { display: flex; flex-direction: column; gap: 5px; margin-top: 10px; }
|
||||
.form-row label { font-size: 12px; color: var(--text-2); }
|
||||
.form-row.two { flex-direction: row; gap: 10px; }
|
||||
.form-row.two > div { flex: 1; display: flex; flex-direction: column; gap: 5px; }
|
||||
.goal-hint { font-size: 12px; margin-top: 8px; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.plan-item { flex-wrap: wrap; }
|
||||
.pi-ops { width: 100%; justify-content: flex-start; }
|
||||
.day-ops { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user