对齐测测交互:首页「+」支持邀请填档案/添加档案/邀请合盘;各 Tab 与工具页按同一视觉与功能标准落地;本地对照截图目录显式忽略。 Co-authored-by: Cursor <cursoragent@cursor.com>
193 lines
5.1 KiB
Vue
193 lines
5.1 KiB
Vue
<template>
|
||
<PageShell>
|
||
<template #hero>
|
||
<BackButton />
|
||
<div class="head">
|
||
<HomeToolIcon name="growth" :size="48" />
|
||
<div>
|
||
<h1 class="title">成长计划</h1>
|
||
<p class="sub">小目标 · 每日打卡</p>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="body">
|
||
<div class="create">
|
||
<label class="lbl">新计划标题</label>
|
||
<input class="inp" v-model="title" maxlength="40" placeholder="例如:每晚 11 点前放下手机" />
|
||
<label class="lbl">焦点(可选)</label>
|
||
<input class="inp" v-model="focus" maxlength="80" placeholder="例如:保护睡眠与情绪" />
|
||
<button class="cta" type="button" :disabled="saving || !title.trim()" @click="create">创建计划</button>
|
||
<p v-if="err" class="err">{{ err }}</p>
|
||
</div>
|
||
|
||
<p v-if="loading" class="hint">加载中…</p>
|
||
<div v-else class="plans">
|
||
<div v-for="p in plans" :key="p.id" class="plan">
|
||
<strong>{{ p.title }}</strong>
|
||
<p v-if="p.focus" class="focus">{{ p.focus }}</p>
|
||
<button type="button" class="check" :disabled="checking === p.id" @click="checkin(p.id)">
|
||
{{ checking === p.id ? '记录中…' : '今日打卡' }}
|
||
</button>
|
||
<ul v-if="checkins[p.id]?.length" class="cks">
|
||
<li v-for="c in checkins[p.id]" :key="c.id">{{ c.day }}{{ c.note ? ' · ' + c.note : '' }}</li>
|
||
</ul>
|
||
</div>
|
||
<p v-if="!plans.length" class="hint">还没有计划,先创建一个小目标吧。</p>
|
||
</div>
|
||
</div>
|
||
</PageShell>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { onMounted, ref } from 'vue'
|
||
import { api } from '../api/client'
|
||
import BackButton from '../components/BackButton.vue'
|
||
import HomeToolIcon from '../components/HomeToolIcon.vue'
|
||
import PageShell from '../components/PageShell.vue'
|
||
|
||
const plans = ref<{ id: string; title: string; focus: string }[]>([])
|
||
const checkins = ref<Record<string, { id: string; day: string; note?: string }[]>>({})
|
||
const title = ref('')
|
||
const focus = ref('')
|
||
const loading = ref(true)
|
||
const saving = ref(false)
|
||
const checking = ref('')
|
||
const err = ref('')
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
const res = await api.listGrowthPlans()
|
||
plans.value = res.items || []
|
||
for (const p of plans.value) {
|
||
const ck = await api.listGrowthCheckins(p.id)
|
||
checkins.value[p.id] = ck.items || []
|
||
}
|
||
} catch (e) {
|
||
err.value = e instanceof Error ? e.message : '加载失败'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function create() {
|
||
saving.value = true
|
||
err.value = ''
|
||
try {
|
||
await api.createGrowthPlan({ title: title.value.trim(), focus: focus.value.trim() || undefined })
|
||
title.value = ''
|
||
focus.value = ''
|
||
await load()
|
||
} catch (e) {
|
||
err.value = e instanceof Error ? e.message : '创建失败'
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
async function checkin(id: string) {
|
||
checking.value = id
|
||
err.value = ''
|
||
try {
|
||
await api.createGrowthCheckin(id, {})
|
||
const ck = await api.listGrowthCheckins(id)
|
||
checkins.value[id] = ck.items || []
|
||
} catch (e) {
|
||
err.value = e instanceof Error ? e.message : '打卡失败'
|
||
} finally {
|
||
checking.value = ''
|
||
}
|
||
}
|
||
|
||
onMounted(load)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-top: 8px;
|
||
padding-bottom: 8px;
|
||
}
|
||
.title {
|
||
font-family: var(--font-display);
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.06em;
|
||
}
|
||
.sub {
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.38);
|
||
margin-top: 2px;
|
||
}
|
||
.body { padding: 8px 16px 0; }
|
||
.create {
|
||
padding: 16px;
|
||
background: #fafafa;
|
||
border-radius: var(--radius-xl);
|
||
margin-bottom: 14px;
|
||
}
|
||
.lbl {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin: 8px 0 6px;
|
||
}
|
||
.lbl:first-child { margin-top: 0; }
|
||
.inp {
|
||
width: 100%;
|
||
padding: 11px 12px;
|
||
border-radius: 12px;
|
||
border: 1.5px solid #eee;
|
||
background: #fff;
|
||
font-size: 14px;
|
||
font-family: inherit;
|
||
outline: none;
|
||
}
|
||
.cta {
|
||
margin-top: 14px;
|
||
width: 100%;
|
||
padding: 12px;
|
||
border: none;
|
||
border-radius: 22px;
|
||
color: #fff;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
background: linear-gradient(135deg, #ff7a6e, var(--color-primary));
|
||
box-shadow: 0 6px 16px rgba(229, 77, 66, 0.28);
|
||
}
|
||
.cta:disabled { opacity: 0.45; }
|
||
.plans { display: flex; flex-direction: column; gap: 10px; }
|
||
.plan {
|
||
padding: 16px;
|
||
background: #fff;
|
||
border-radius: var(--radius-lg);
|
||
box-shadow: var(--shadow-card);
|
||
border: 1px solid #f5f0ed;
|
||
}
|
||
.plan strong { font-size: 15px; color: #222; }
|
||
.focus { font-size: 12px; color: #888; margin: 6px 0 10px; }
|
||
.check {
|
||
padding: 8px 14px;
|
||
border-radius: 18px;
|
||
border: 1.5px solid #f5c4bc;
|
||
background: #fff;
|
||
color: var(--color-primary);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
.check:disabled { opacity: 0.5; }
|
||
.cks {
|
||
list-style: none;
|
||
margin: 10px 0 0;
|
||
padding: 0;
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
.cks li { padding: 4px 0; }
|
||
.hint { font-size: 13px; color: #999; text-align: center; padding: 16px; }
|
||
.err { color: var(--color-primary); font-size: 12px; margin-top: 8px; }
|
||
</style>
|