init: 毛孩子计划 小程序 + Go 后端 + 内嵌后台
- pets-fe: 微信原生小程序(首页/计划/记录/报告/社区/引导), 服务端驱动、无假数据;弹层改用 scroll-view,打开时隐藏自定义 tabBar - pets-be: Gin + GORM(MySQL, sundynix_ 前缀) + MinIO,统一响应/分页, 微信 code2session 登录,provider-neutral AI(DeepSeek),go:embed React 后台 - 修复:分段选择类型不匹配(字符串 vs 数字)导致选不中 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
const store = require('../../utils/store.js');
|
||||
const api = require('../../utils/api.js');
|
||||
|
||||
const GENDERS = ['男孩', '女孩', '不确定'];
|
||||
|
||||
function ageFromBirthday(bdayStr) {
|
||||
if (!bdayStr) return '';
|
||||
const b = new Date(bdayStr);
|
||||
const now = new Date();
|
||||
let months = (now.getFullYear() - b.getFullYear()) * 12 + (now.getMonth() - b.getMonth());
|
||||
if (months < 0) months = 0;
|
||||
return months < 24 ? months + '个月' : Math.floor(months / 12) + '岁';
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
step: 0,
|
||||
petType: '猫猫',
|
||||
petEmoji: '🐱',
|
||||
petName: '',
|
||||
birthday: '',
|
||||
genderIdx: 0,
|
||||
weight: '',
|
||||
stage: '幼年期',
|
||||
goals: [
|
||||
{ label: '不知道每天该做什么', on: true },
|
||||
{ label: '疫苗驱虫怕忘记', on: true },
|
||||
{ label: '想记录体重和健康', on: false },
|
||||
{ label: '担心生病不会判断', on: false },
|
||||
{ label: '想控制养宠开销', on: false },
|
||||
{ label: '想生成成长报告', on: false },
|
||||
],
|
||||
submitting: false,
|
||||
},
|
||||
onLoad() {
|
||||
// 已有宠物则直接进首页
|
||||
store
|
||||
.ready()
|
||||
.then(() => {
|
||||
if (store.getPets().length > 0) {
|
||||
wx.switchTab({ url: '/pages/home/home' });
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
nextStep() {
|
||||
this.setData({ step: Math.min(this.data.step + 1, 5) });
|
||||
},
|
||||
choosePet(e) {
|
||||
const { type, emoji } = e.currentTarget.dataset;
|
||||
this.setData({ petType: type, petEmoji: emoji });
|
||||
},
|
||||
onName(e) { this.setData({ petName: e.detail.value }); },
|
||||
onWeight(e) { this.setData({ weight: e.detail.value }); },
|
||||
onBirthday(e) { this.setData({ birthday: e.detail.value }); },
|
||||
selectGender(e) { this.setData({ genderIdx: Number(e.currentTarget.dataset.index) }); },
|
||||
// 先随便看看:不建数据,直接进首页浏览(无宠物时首页显示空态)
|
||||
skipBrowse() { wx.switchTab({ url: '/pages/home/home' }); },
|
||||
savePetNext() {
|
||||
if (!(this.data.petName || '').trim()) {
|
||||
wx.showToast({ title: '给它起个名字吧', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.nextStep();
|
||||
},
|
||||
chooseStage(e) {
|
||||
this.setData({ stage: e.currentTarget.dataset.stage });
|
||||
},
|
||||
toggleGoal(e) {
|
||||
const i = e.currentTarget.dataset.index;
|
||||
this.setData({ [`goals[${i}].on`]: !this.data.goals[i].on });
|
||||
},
|
||||
async submit() {
|
||||
if (this.data.submitting) return;
|
||||
this.setData({ submitting: true });
|
||||
wx.showLoading({ title: '生成计划中...' });
|
||||
const body = {
|
||||
name: (this.data.petName || '').trim(),
|
||||
emoji: this.data.petEmoji,
|
||||
type: this.data.petType,
|
||||
gender: GENDERS[this.data.genderIdx],
|
||||
birthday: this.data.birthday,
|
||||
weight: this.data.weight,
|
||||
stage: this.data.stage,
|
||||
age: ageFromBirthday(this.data.birthday),
|
||||
goals: this.data.goals.filter((g) => g.on).map((g) => g.label),
|
||||
};
|
||||
try {
|
||||
await store.ready();
|
||||
await api.onboarding(body);
|
||||
await store.loadPets();
|
||||
wx.hideLoading();
|
||||
wx.switchTab({ url: '/pages/home/home' });
|
||||
} catch (e) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: e.message || '创建失败', icon: 'none' });
|
||||
this.setData({ submitting: false });
|
||||
}
|
||||
},
|
||||
finishOnboard() { this.submit(); },
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"nav-bar": "/components/nav-bar/nav-bar"
|
||||
},
|
||||
"navigationBarTitleText": "毛孩子计划"
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<nav-bar title="毛孩子计划"></nav-bar>
|
||||
|
||||
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view class="onboard-page">
|
||||
<!-- step 0 欢迎 -->
|
||||
<view wx:if="{{step === 0}}" class="step">
|
||||
<view class="hero">
|
||||
<view class="pet-logo">🐾</view>
|
||||
<view class="hero-h1">毛孩子计划</view>
|
||||
<view class="hero-sub">新手养宠的 AI 日历、健康记录和成长报告。先生成计划,再每天照着做。</view>
|
||||
</view>
|
||||
<view class="proof-row">
|
||||
<view class="proof"><text class="proof-b">30天</text><text class="proof-s">新手计划</text></view>
|
||||
<view class="proof"><text class="proof-b">8类</text><text class="proof-s">快速记录</text></view>
|
||||
<view class="proof"><text class="proof-b">AI</text><text class="proof-s">观察建议</text></view>
|
||||
</view>
|
||||
<button class="btn btn-primary btn-block" bindtap="nextStep">开始建档,生成计划</button>
|
||||
<view class="skip-link" bindtap="skipBrowse">先随便看看</view>
|
||||
</view>
|
||||
|
||||
<!-- step 1 选类型 -->
|
||||
<view wx:elif="{{step === 1}}" class="step">
|
||||
<view class="step-title">你的毛孩子是?</view>
|
||||
<view class="step-sub">目前支持猫和狗,选一个开始。</view>
|
||||
<view class="choice-grid">
|
||||
<view class="choice-card {{petType === '猫猫' ? 'selected' : ''}}" data-type="猫猫" data-emoji="🐱" bindtap="choosePet">
|
||||
<view class="animal">🐱</view><view class="choice-b">猫猫</view><view class="choice-s">幼猫、成年猫、老年猫</view>
|
||||
</view>
|
||||
<view class="choice-card {{petType === '狗狗' ? 'selected' : ''}}" data-type="狗狗" data-emoji="🐶" bindtap="choosePet">
|
||||
<view class="animal">🐶</view><view class="choice-b">狗狗</view><view class="choice-s">幼犬、成年犬、老年犬</view>
|
||||
</view>
|
||||
</view>
|
||||
<button class="btn btn-primary btn-block" bindtap="nextStep">下一步</button>
|
||||
</view>
|
||||
|
||||
<!-- step 2 信息 -->
|
||||
<view wx:elif="{{step === 2}}" class="step">
|
||||
<view class="step-title">告诉我一点它的信息</view>
|
||||
<view class="step-sub">先填基础信息,之后可随时补充。</view>
|
||||
<view class="form-card">
|
||||
<view class="field"><label>宠物名字</label>
|
||||
<input class="input" value="{{petName}}" placeholder="例如:奶球" placeholder-class="placeholder" bindinput="onName"/></view>
|
||||
<view class="field"><label>生日 / 到家时间</label>
|
||||
<picker mode="date" value="{{birthday}}" bindchange="onBirthday">
|
||||
<view class="picker-box">{{birthday || '选择日期'}}</view>
|
||||
</picker></view>
|
||||
<view class="field"><label>性别</label><view class="seg">
|
||||
<view class="seg-btn {{genderIdx === 0 ? 'selected' : ''}}" data-index="0" bindtap="selectGender">男孩</view>
|
||||
<view class="seg-btn {{genderIdx === 1 ? 'selected' : ''}}" data-index="1" bindtap="selectGender">女孩</view>
|
||||
<view class="seg-btn {{genderIdx === 2 ? 'selected' : ''}}" data-index="2" bindtap="selectGender">不确定</view>
|
||||
</view></view>
|
||||
<view class="field"><label>当前体重(kg)</label>
|
||||
<input class="input" value="{{weight}}" placeholder="例如:2.8" placeholder-class="placeholder" bindinput="onWeight" type="digit"/></view>
|
||||
</view>
|
||||
<button class="btn btn-primary btn-block" bindtap="savePetNext">下一步</button>
|
||||
</view>
|
||||
|
||||
<!-- step 3 阶段 -->
|
||||
<view wx:elif="{{step === 3}}" class="step">
|
||||
<view class="step-title">它现在处于哪个阶段?</view>
|
||||
<view class="step-sub">阶段会决定今日任务、提醒和 AI 建议。</view>
|
||||
<view class="stage-list">
|
||||
<view class="stage-card {{stage === '幼年期' ? 'selected' : ''}}" data-stage="幼年期" bindtap="chooseStage">
|
||||
<view class="stage-b">幼年期</view><view class="stage-p">疫苗、驱虫、体重增长、行为训练</view></view>
|
||||
<view class="stage-card {{stage === '刚到家 0-30 天' ? 'selected' : ''}}" data-stage="刚到家 0-30 天" bindtap="chooseStage">
|
||||
<view class="stage-b">刚到家 0-30 天</view><view class="stage-p">适应环境、饮食稳定、基础照护</view></view>
|
||||
<view class="stage-card {{stage === '成年期' ? 'selected' : ''}}" data-stage="成年期" bindtap="chooseStage">
|
||||
<view class="stage-b">成年期</view><view class="stage-p">体重、饮食、日常健康管理</view></view>
|
||||
<view class="stage-card {{stage === '老年期' ? 'selected' : ''}}" data-stage="老年期" bindtap="chooseStage">
|
||||
<view class="stage-b">老年期</view><view class="stage-p">慢病、体检、用药和护理</view></view>
|
||||
</view>
|
||||
<button class="btn btn-primary btn-block" bindtap="nextStep">下一步</button>
|
||||
</view>
|
||||
|
||||
<!-- step 4 目标 -->
|
||||
<view wx:elif="{{step === 4}}" class="step">
|
||||
<view class="step-title">你最想解决什么?</view>
|
||||
<view class="step-sub">多选,我们会据此为你定制计划。</view>
|
||||
<view class="check-list">
|
||||
<view wx:for="{{goals}}" wx:key="label" class="check {{item.on ? 'selected' : ''}}" data-index="{{index}}" bindtap="toggleGoal">
|
||||
<view class="box">{{item.on ? '✓' : ''}}</view>{{item.label}}
|
||||
</view>
|
||||
</view>
|
||||
<button class="btn btn-primary btn-block" bindtap="nextStep">生成专属计划</button>
|
||||
</view>
|
||||
|
||||
<!-- step 5 生成 -->
|
||||
<view wx:elif="{{step === 5}}" class="step">
|
||||
<view class="step-title">正在生成专属养宠计划</view>
|
||||
<view class="step-sub">正在根据它的信息生成专属计划…</view>
|
||||
<view class="gen-card">
|
||||
<view class="gen-line"><view class="tick">✓</view> 已识别:{{stage}}</view>
|
||||
<view class="gen-line"><view class="tick">✓</view> 已生成:30 天新手计划</view>
|
||||
<view class="gen-line"><view class="tick">✓</view> 已安排:疫苗 / 驱虫提醒</view>
|
||||
<view class="gen-line"><view class="tick">✓</view> 已开启:体重、便便、饮食记录</view>
|
||||
<view class="gen-line"><view class="tick">✓</view> 已准备:首张成长报告卡片</view>
|
||||
</view>
|
||||
<button class="btn btn-primary btn-block" style="margin-top:36rpx" bindtap="finishOnboard">查看今日任务</button>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -0,0 +1,55 @@
|
||||
page{height:100vh;overflow:hidden;display:flex;flex-direction:column}
|
||||
.page-scroll{flex:1;min-height:0}
|
||||
.onboard-page{
|
||||
padding:0 40rpx 56rpx;
|
||||
min-height:100%;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(245,169,71,.22), transparent 45%),
|
||||
var(--bg);
|
||||
}
|
||||
.step{animation:fade .24s ease}
|
||||
.skip-link{text-align:center;margin-top:28rpx;color:var(--muted);font-size:28rpx;padding:12rpx}
|
||||
@keyframes fade{from{opacity:0;transform:translateY(16rpx)}to{opacity:1;transform:none}}
|
||||
|
||||
.hero{text-align:center;padding:60rpx 0 32rpx}
|
||||
.pet-logo{
|
||||
width:192rpx;height:192rpx;margin:0 auto 36rpx;border-radius:60rpx;
|
||||
background:linear-gradient(135deg,#FFE0AB,#FDB35C);
|
||||
display:flex;align-items:center;justify-content:center;font-size:96rpx;
|
||||
box-shadow:0 32rpx 72rpx rgba(245,169,71,.26);
|
||||
}
|
||||
.hero-h1{font-size:64rpx;letter-spacing:-1.2rpx;font-weight:800}
|
||||
.hero-sub{margin-top:20rpx;line-height:1.65;color:var(--muted);font-size:30rpx}
|
||||
|
||||
.proof-row{display:grid;grid-template-columns:repeat(3,1fr);gap:16rpx;margin:40rpx 0}
|
||||
.proof{background:#fff;border-radius:36rpx;padding:24rpx 16rpx;text-align:center;box-shadow:var(--shadow)}
|
||||
.proof-b{display:block;font-size:32rpx;font-weight:800}
|
||||
.proof-s{color:var(--muted);font-size:22rpx}
|
||||
|
||||
.step-title{font-size:48rpx;letter-spacing:-.8rpx;line-height:1.24;margin:16rpx 0;font-weight:800}
|
||||
.step-sub{color:var(--muted);line-height:1.6;font-size:28rpx;margin-bottom:32rpx}
|
||||
|
||||
.choice-grid{display:grid;grid-template-columns:1fr 1fr;gap:24rpx;margin-bottom:36rpx}
|
||||
.choice-card{
|
||||
background:#fff;border:4rpx solid transparent;border-radius:48rpx;min-height:264rpx;
|
||||
padding:36rpx 28rpx;box-shadow:var(--shadow);transition:.18s ease;
|
||||
}
|
||||
.choice-card.selected{border-color:var(--primary);background:#FFF8EC}
|
||||
.choice-card .animal{font-size:88rpx;margin-bottom:20rpx}
|
||||
.choice-b{font-size:34rpx;font-weight:900}
|
||||
.choice-s{display:block;color:var(--muted);font-size:24rpx;margin-top:12rpx}
|
||||
|
||||
.form-card{background:#fff;border-radius:48rpx;padding:32rpx;box-shadow:var(--shadow);margin-bottom:36rpx}
|
||||
|
||||
.stage-list{display:flex;flex-direction:column;gap:22rpx;margin-bottom:36rpx}
|
||||
.stage-card{background:#fff;border-radius:40rpx;border:4rpx solid transparent;padding:30rpx;box-shadow:var(--shadow)}
|
||||
.stage-card.selected{border-color:var(--green);background:var(--green-soft)}
|
||||
.stage-b{font-size:32rpx;font-weight:900}
|
||||
.stage-p{color:var(--muted);font-size:24rpx;margin-top:10rpx}
|
||||
|
||||
.gen-card{margin-top:32rpx;background:#fff;border-radius:56rpx;padding:48rpx;box-shadow:var(--shadow2)}
|
||||
.gen-line{display:flex;align-items:center;gap:20rpx;padding:20rpx 0;color:#5F554D;font-size:28rpx}
|
||||
.tick{
|
||||
width:48rpx;height:48rpx;border-radius:50%;background:var(--green);color:#fff;
|
||||
display:flex;align-items:center;justify-content:center;font-size:28rpx;font-weight:900;flex:none;
|
||||
}
|
||||
Reference in New Issue
Block a user