Files
digital-psychology/apps/user-h5/src/pages/GrowthPlanPage.vue
T
jackyu66gitandCursor 53eb4577b3 feat: 按测测功能重构 H5 首页与各子页,并修正顶栏「+」添加档案菜单
对齐测测交互:首页「+」支持邀请填档案/添加档案/邀请合盘;各 Tab 与工具页按同一视觉与功能标准落地;本地对照截图目录显式忽略。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 14:32:35 +08:00

193 lines
5.1 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.
<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>