feat: 按测测功能重构 H5 首页与各子页,并修正顶栏「+」添加档案菜单

对齐测测交互:首页「+」支持邀请填档案/添加档案/邀请合盘;各 Tab 与工具页按同一视觉与功能标准落地;本地对照截图目录显式忽略。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-05 14:32:35 +08:00
co-authored by Cursor
parent bd22d9dddd
commit 53eb4577b3
35 changed files with 8900 additions and 1356 deletions
+115 -33
View File
@@ -1,43 +1,50 @@
<template>
<main class="yxg-page">
<div class="yxg-hero">
<PageShell>
<template #hero>
<BackButton />
<PageTitle feature="growth" title="成长计划" sub="小目标 · 每日打卡(生活成长,非运势)" />
</div>
<div class="head">
<HomeToolIcon name="growth" :size="48" />
<div>
<h1 class="title">成长计划</h1>
<p class="sub">小目标 · 每日打卡</p>
</div>
</div>
</template>
<div class="yxg-sheet">
<div class="yxg-card">
<label class="yxg-label">新计划标题</label>
<input class="yxg-input" v-model="title" maxlength="40" placeholder="例如:每晚 11 点前放下手机" />
<label class="yxg-label">焦点可选</label>
<input class="yxg-input" v-model="focus" maxlength="80" placeholder="例如:保护睡眠与情绪" />
<button class="yxg-btn" type="button" :disabled="saving || !title.trim()" @click="create">创建计划</button>
<p v-if="err" class="yxg-err">{{ err }}</p>
<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="yxg-hint pad">加载中</p>
<ul v-else class="yxg-list plans">
<li v-for="p in plans" :key="p.id" class="plan">
<strong> {{ p.title }}</strong>
<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="yxg-btn yxg-btn-ghost" :disabled="checking === p.id" @click="checkin(p.id)">
<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>
</li>
<li v-if="!plans.length" class="empty">还没有计划先创建一个小目标吧</li>
</ul>
</div>
<p v-if="!plans.length" class="hint">还没有计划先创建一个小目标吧</p>
</div>
</div>
</main>
</PageShell>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { api } from '../api/client'
import BackButton from '../components/BackButton.vue'
import PageTitle from '../components/PageTitle.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 }[]>>({})
@@ -83,7 +90,7 @@ async function checkin(id: string) {
checking.value = id
err.value = ''
try {
await api.growthCheckin(id, {})
await api.createGrowthCheckin(id, {})
const ck = await api.listGrowthCheckins(id)
checkins.value[id] = ck.items || []
} catch (e) {
@@ -97,14 +104,89 @@ onMounted(load)
</script>
<style scoped>
.yxg-card{margin-top:8px}
.yxg-card .yxg-label:first-child{margin-top:0}
.yxg-btn{margin-top:12px}
.pad{padding:0 16px}
.plans{list-style:none}
.plan{display:block}
.plan strong{font-size:15px;color:#222}
.focus{font-size:13px;color:#888;margin:6px 0}
.cks{margin-top:8px;padding-left:16px;font-size:12px;color:#999}
.empty{color:#999;font-size:13px;padding:8px 4px}
.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>