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,28 @@
|
||||
const store = require('./utils/store.js');
|
||||
|
||||
App({
|
||||
globalData: {
|
||||
statusBarHeight: 20,
|
||||
navBarHeight: 44,
|
||||
menuRight: 10
|
||||
},
|
||||
onLaunch() {
|
||||
// 启动即登录并加载宠物(失败不阻塞 UI,页面会各自重试)
|
||||
store.ready().catch((e) => console.warn('[app] 启动登录失败:', e && e.message));
|
||||
// 计算自定义导航栏所需的状态栏 / 胶囊尺寸
|
||||
try {
|
||||
const win = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync();
|
||||
const menu = wx.getMenuButtonBoundingClientRect();
|
||||
const statusBarHeight = win.statusBarHeight || 20;
|
||||
// 导航栏高度 = 胶囊高度 + 上下等距间隙
|
||||
const navBarHeight = (menu.top - statusBarHeight) * 2 + menu.height;
|
||||
this.globalData.statusBarHeight = statusBarHeight;
|
||||
this.globalData.navBarHeight = navBarHeight;
|
||||
this.globalData.menuRight = win.windowWidth - menu.right;
|
||||
this.globalData.menuHeight = menu.height;
|
||||
this.globalData.menuTop = menu.top;
|
||||
} catch (e) {
|
||||
// 兜底
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/onboarding/onboarding",
|
||||
"pages/home/home",
|
||||
"pages/plan/plan",
|
||||
"pages/record/record",
|
||||
"pages/report/report",
|
||||
"pages/community/community",
|
||||
"pages/learn/learn",
|
||||
"pages/profile/profile"
|
||||
],
|
||||
"window": {
|
||||
"navigationStyle": "custom",
|
||||
"backgroundColor": "#F8F6F2",
|
||||
"backgroundTextStyle": "dark"
|
||||
},
|
||||
"tabBar": {
|
||||
"custom": true,
|
||||
"color": "#8D8277",
|
||||
"selectedColor": "#DD8420",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"list": [
|
||||
{ "pagePath": "pages/home/home", "text": "首页" },
|
||||
{ "pagePath": "pages/plan/plan", "text": "计划" },
|
||||
{ "pagePath": "pages/record/record", "text": "记录" },
|
||||
{ "pagePath": "pages/report/report", "text": "报告" },
|
||||
{ "pagePath": "pages/community/community", "text": "社区" }
|
||||
]
|
||||
},
|
||||
"sitemapLocation": "sitemap.json",
|
||||
"style": "v2"
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/* ===== 全局设计令牌 + 通用样式(由 preview.html 迁移,px→rpx ×2) ===== */
|
||||
page{
|
||||
--bg:#F8F6F2;
|
||||
--bg2:#FBF8F3;
|
||||
--card:#FFFFFF;
|
||||
--text:#2D2925;
|
||||
--muted:#8D8277;
|
||||
--muted2:#B5A99D;
|
||||
--line:#EEE6DC;
|
||||
--primary:#F5A947;
|
||||
--primary2:#FFBF68;
|
||||
--primary-dark:#DD8420;
|
||||
--green:#73BE9D;
|
||||
--green-soft:#EAF7F1;
|
||||
--red:#EE7D73;
|
||||
--red-soft:#FFF0EE;
|
||||
--blue:#78A6E8;
|
||||
--blue-soft:#EEF5FF;
|
||||
--purple:#A993E8;
|
||||
--purple-soft:#F4F0FF;
|
||||
--yellow-soft:#FFF6E4;
|
||||
--shadow:0 24rpx 60rpx rgba(70,48,25,.08);
|
||||
--shadow2:0 36rpx 88rpx rgba(70,48,25,.13);
|
||||
--radius:44rpx;
|
||||
|
||||
color:var(--text);
|
||||
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","PingFang SC","Microsoft YaHei",sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at 10% 8%, rgba(245,169,71,.26), transparent 30%),
|
||||
radial-gradient(circle at 88% 10%, rgba(115,190,157,.20), transparent 26%),
|
||||
radial-gradient(circle at 72% 88%, rgba(120,166,232,.14), transparent 28%),
|
||||
linear-gradient(135deg,#FBF5EA,#EEF6F1 55%,#F8F1E7);
|
||||
min-height:100vh;
|
||||
}
|
||||
view,text{box-sizing:border-box}
|
||||
|
||||
/* 全局隐藏滚动条(页面级 + 组件级),保留正常滚动 */
|
||||
::-webkit-scrollbar{
|
||||
width:0 !important;
|
||||
height:0 !important;
|
||||
display:none;
|
||||
-webkit-appearance:none;
|
||||
background:transparent;
|
||||
color:transparent;
|
||||
}
|
||||
page{scrollbar-width:none;-ms-overflow-style:none}
|
||||
|
||||
/* 文本 */
|
||||
.muted{color:var(--muted)}
|
||||
.tiny{font-size:24rpx;color:var(--muted)}
|
||||
|
||||
/* 区块标题 */
|
||||
.section-head{
|
||||
display:flex;align-items:center;justify-content:space-between;margin:8rpx 0 24rpx;
|
||||
}
|
||||
.section-head .sh-title{font-size:36rpx;letter-spacing:-.4rpx;font-weight:700}
|
||||
.link{
|
||||
color:var(--primary-dark);font-weight:900;font-size:26rpx;
|
||||
}
|
||||
|
||||
/* 页面标题 */
|
||||
.page-title{margin:4rpx 0 28rpx}
|
||||
.page-title h2{font-size:46rpx;letter-spacing:-.6rpx;font-weight:800}
|
||||
.page-title p{color:var(--muted);font-size:26rpx;margin-top:10rpx}
|
||||
|
||||
/* 按钮 */
|
||||
.btn{
|
||||
border:0;height:96rpx;border-radius:34rpx;padding:0 32rpx;
|
||||
display:inline-flex;align-items:center;justify-content:center;gap:16rpx;
|
||||
font-weight:900;font-size:30rpx;line-height:96rpx;transition:.16s ease;
|
||||
}
|
||||
.btn::after{border:none}
|
||||
.btn:active{transform:scale(.98)}
|
||||
.btn-primary{
|
||||
color:#fff;background:linear-gradient(135deg,var(--primary),var(--primary2));
|
||||
box-shadow:0 24rpx 48rpx rgba(245,169,71,.28);
|
||||
}
|
||||
.btn-ghost{color:var(--text);background:#fff;border:1rpx solid var(--line)}
|
||||
.btn-soft{color:#9E6200;background:var(--yellow-soft)}
|
||||
.btn-green{color:#fff;background:linear-gradient(135deg,#69B991,#86CEB0)}
|
||||
.btn-block{width:100%;display:flex}
|
||||
.btn-small{height:68rpx;border-radius:24rpx;padding:0 22rpx;font-size:24rpx;line-height:68rpx}
|
||||
|
||||
/* 卡片 */
|
||||
.card{
|
||||
background:var(--card);border-radius:var(--radius);box-shadow:var(--shadow);
|
||||
padding:32rpx;margin-bottom:28rpx;position:relative;overflow:hidden;
|
||||
}
|
||||
.card.no-shadow{box-shadow:none;border:1rpx solid var(--line)}
|
||||
|
||||
/* 标签 */
|
||||
.tag{
|
||||
display:inline-flex;align-items:center;height:48rpx;border-radius:999rpx;padding:0 18rpx;
|
||||
font-size:24rpx;font-weight:900;background:var(--green-soft);color:#377A61;white-space:nowrap;
|
||||
}
|
||||
.tag.warn{background:#FFF0D7;color:#A96500}
|
||||
.tag.red{background:var(--red-soft);color:#C95348}
|
||||
.tag.blue{background:var(--blue-soft);color:#3C6EBA}
|
||||
.tag.purple{background:var(--purple-soft);color:#765AC5}
|
||||
|
||||
/* 表单 */
|
||||
.field{margin-bottom:28rpx}
|
||||
.field:last-child{margin-bottom:0}
|
||||
.field label{display:block;font-size:26rpx;color:var(--muted);font-weight:800;margin-bottom:16rpx}
|
||||
.input,input,textarea,.picker-box{
|
||||
width:100%;border:1rpx solid var(--line);background:#FBFAF8;height:88rpx;border-radius:30rpx;
|
||||
padding:0 26rpx;color:var(--text);font-size:30rpx;outline:none;box-sizing:border-box;
|
||||
display:flex;align-items:center;
|
||||
}
|
||||
.textarea{
|
||||
width:100%;border:1rpx solid var(--line);background:#FBFAF8;height:184rpx;border-radius:30rpx;
|
||||
padding:24rpx 26rpx;color:var(--text);font-size:30rpx;box-sizing:border-box;
|
||||
}
|
||||
.placeholder{color:var(--muted2)}
|
||||
|
||||
/* 分段选择 */
|
||||
.seg{display:flex;gap:16rpx}
|
||||
.seg .seg-btn{
|
||||
flex:1;height:80rpx;line-height:80rpx;text-align:center;border-radius:26rpx;
|
||||
border:1rpx solid var(--line);background:#fff;color:var(--muted);font-weight:800;font-size:28rpx;
|
||||
}
|
||||
.seg .seg-btn.selected{color:var(--primary-dark);background:#FFF2DC;border-color:#FFD39A}
|
||||
|
||||
/* 多选 check */
|
||||
.check-list{display:flex;flex-direction:column;gap:20rpx;margin:32rpx 0}
|
||||
.check{
|
||||
display:flex;align-items:center;gap:20rpx;background:#fff;box-shadow:var(--shadow);
|
||||
border-radius:32rpx;padding:26rpx;border:1rpx solid transparent;font-size:28rpx;font-weight:700;
|
||||
}
|
||||
.box{
|
||||
width:44rpx;height:44rpx;border-radius:16rpx;border:4rpx solid #DDCEC1;
|
||||
display:flex;align-items:center;justify-content:center;color:#fff;flex:none;
|
||||
font-size:26rpx;font-weight:900;
|
||||
}
|
||||
.check.selected{background:#FFF8EC;border-color:#FFD29A}
|
||||
.check.selected .box{background:var(--primary);border-color:var(--primary)}
|
||||
|
||||
/* 圆形/条形选项 */
|
||||
.mini-options{display:flex;flex-wrap:wrap;gap:16rpx;margin:20rpx 0 28rpx}
|
||||
.option{
|
||||
border:1rpx solid var(--line);background:#fff;border-radius:999rpx;height:68rpx;line-height:68rpx;
|
||||
padding:0 24rpx;color:#5F554D;font-weight:800;font-size:24rpx;
|
||||
}
|
||||
.option.selected{background:#FFF2DC;color:var(--primary-dark);border-color:#FFD39A}
|
||||
|
||||
/* 进度条 */
|
||||
.progress-row{
|
||||
display:flex;align-items:center;justify-content:space-between;margin-bottom:16rpx;
|
||||
font-size:26rpx;color:#6A5D52;font-weight:900;
|
||||
}
|
||||
.bar{height:20rpx;border-radius:999rpx;background:#F0E7DD;overflow:hidden}
|
||||
.bar .bar-fill{
|
||||
height:100%;border-radius:999rpx;
|
||||
background:linear-gradient(90deg,var(--primary),#FFD57F);display:block;transition:.25s ease;
|
||||
}
|
||||
|
||||
/* 头像 */
|
||||
.avatar{
|
||||
width:120rpx;height:120rpx;border-radius:44rpx;
|
||||
background:linear-gradient(135deg,#FFE5B7,#FFC06B);
|
||||
display:flex;align-items:center;justify-content:center;font-size:64rpx;
|
||||
box-shadow:0 24rpx 52rpx rgba(245,169,71,.18);flex:none;
|
||||
}
|
||||
|
||||
/* 多宠切换 */
|
||||
.pet-switch{display:flex;gap:20rpx;overflow-x:auto;padding:0 0 24rpx;white-space:nowrap}
|
||||
.pet-switch::-webkit-scrollbar{display:none}
|
||||
.pet-chip{
|
||||
flex:none;display:inline-flex;border:1rpx solid var(--line);background:#fff;border-radius:40rpx;
|
||||
padding:18rpx 24rpx;align-items:center;gap:16rpx;color:var(--muted);font-weight:900;font-size:28rpx;
|
||||
box-shadow:0 12rpx 36rpx rgba(70,48,25,.05);
|
||||
}
|
||||
.pet-chip.active{color:var(--primary-dark);background:#FFF4E4;border-color:#FFD6A1}
|
||||
.pet-chip .mini-avatar{
|
||||
width:56rpx;height:56rpx;border-radius:20rpx;display:flex;align-items:center;
|
||||
justify-content:center;background:#FFE4B7;font-size:30rpx;
|
||||
}
|
||||
|
||||
/* 任务列表 */
|
||||
.task-list{display:flex;flex-direction:column;gap:20rpx}
|
||||
.task{display:flex;gap:20rpx;align-items:flex-start;background:#FBFAF8;border-radius:32rpx;padding:24rpx;transition:.15s ease}
|
||||
.circle{
|
||||
width:46rpx;height:46rpx;border-radius:50%;border:4rpx solid #DED1C4;display:flex;
|
||||
align-items:center;justify-content:center;color:#fff;font-size:24rpx;flex:none;margin-top:2rpx;font-weight:900;
|
||||
}
|
||||
.task.done{opacity:.55}
|
||||
.task.done .circle{background:var(--green);border-color:var(--green)}
|
||||
.task-text{flex:1}
|
||||
.task-text b{font-size:28rpx}
|
||||
.task-text p{font-size:24rpx;color:var(--muted);margin-top:8rpx;line-height:1.45}
|
||||
.priority{
|
||||
font-size:22rpx;color:#A96500;background:#FFF0D7;border-radius:999rpx;padding:8rpx 14rpx;font-weight:900;flex:none;
|
||||
}
|
||||
|
||||
/* 记录行 */
|
||||
.record-row{
|
||||
display:flex;justify-content:space-between;align-items:center;padding:24rpx 0;
|
||||
border-bottom:1rpx solid var(--line);font-size:26rpx;
|
||||
}
|
||||
.record-row:last-child{border-bottom:0}
|
||||
.record-row b{font-size:28rpx}
|
||||
.record-row span{color:var(--muted);font-size:24rpx}
|
||||
|
||||
/* 统计格 */
|
||||
.stats-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16rpx;margin:28rpx 0}
|
||||
.stat{background:#fff;border:1rpx solid var(--line);border-radius:32rpx;padding:24rpx 14rpx;text-align:center}
|
||||
.stat b{display:block;font-size:36rpx;margin-bottom:8rpx}
|
||||
.stat span{font-size:22rpx;color:var(--muted)}
|
||||
|
||||
/* 文章卡 */
|
||||
.article-card{
|
||||
display:flex;gap:24rpx;background:#fff;border-radius:40rpx;padding:26rpx;box-shadow:var(--shadow);margin-bottom:24rpx;
|
||||
}
|
||||
.article-icon{
|
||||
width:116rpx;height:116rpx;border-radius:36rpx;flex:none;display:flex;align-items:center;
|
||||
justify-content:center;background:#FFF1D8;font-size:56rpx;
|
||||
}
|
||||
.article-card b{font-size:30rpx}
|
||||
.article-card p{color:var(--muted);font-size:24rpx;line-height:1.45;margin-top:10rpx}
|
||||
|
||||
/* 海报 */
|
||||
.poster{
|
||||
background:
|
||||
radial-gradient(circle at 100% 0%, rgba(245,169,71,.24), transparent 35%),
|
||||
linear-gradient(160deg,#FFF7E8,#FFFFFF);
|
||||
border:1rpx solid #F2DFC3;border-radius:52rpx;padding:36rpx;text-align:center;overflow:hidden;position:relative;
|
||||
}
|
||||
.poster h3{font-size:40rpx;margin-bottom:20rpx;font-weight:800}
|
||||
.poster-avatar{
|
||||
width:152rpx;height:152rpx;margin:16rpx auto 20rpx;border-radius:54rpx;
|
||||
background:linear-gradient(135deg,#FFE5B7,#FFC06B);display:flex;align-items:center;justify-content:center;
|
||||
font-size:80rpx;box-shadow:0 24rpx 52rpx rgba(245,169,71,.20);
|
||||
}
|
||||
.poster-list{
|
||||
background:rgba(255,255,255,.78);border-radius:32rpx;padding:24rpx;margin:28rpx 0;text-align:left;line-height:1.8;font-size:26rpx;
|
||||
}
|
||||
|
||||
/* 风险盒 */
|
||||
.risk-box{
|
||||
background:#FFF0D8;color:#A96500;padding:24rpx;border-radius:32rpx;font-weight:900;
|
||||
display:flex;gap:18rpx;margin-bottom:24rpx;align-items:center;
|
||||
}
|
||||
|
||||
/* 弹层内动作行 */
|
||||
.sheet-actions{display:flex;gap:20rpx;margin-top:24rpx}
|
||||
.sheet-actions .btn{flex:1}
|
||||
|
||||
/* 聊天气泡 */
|
||||
.plan-chat{display:flex;flex-direction:column;gap:20rpx;margin-bottom:24rpx}
|
||||
.msg{max-width:88%;border-radius:36rpx;padding:22rpx 26rpx;font-size:26rpx;line-height:1.55}
|
||||
.msg.ai{background:#F3F8F5;align-self:flex-start}
|
||||
.msg.user{background:#FFF1D8;align-self:flex-end}
|
||||
@@ -0,0 +1,462 @@
|
||||
const store = require('../../utils/store.js');
|
||||
const api = require('../../utils/api.js');
|
||||
|
||||
function fmtDate(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
return d.getMonth() + 1 + '月' + d.getDate() + '日';
|
||||
}
|
||||
|
||||
function ageFromBirthday(bday) {
|
||||
if (!bday) return '';
|
||||
const b = new Date(bday);
|
||||
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) + '岁';
|
||||
}
|
||||
|
||||
const GENDERS = ['男孩', '女孩', '不确定'];
|
||||
const STAGES = ['刚到家 0-30 天', '幼年期', '成年期', '老年期'];
|
||||
|
||||
const POOP = ['正常', '软便', '拉稀'];
|
||||
const FOOD = ['正常', '偏少', '不吃'];
|
||||
const COST = ['食品', '医疗', '用品'];
|
||||
const IDENTITY = ['petName', 'anonymous', 'official'];
|
||||
const PTAG = ['晒宠', '求助', '经验', '避坑'];
|
||||
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
show: { type: Boolean, value: false },
|
||||
type: { type: String, value: '' },
|
||||
postId: { type: Number, value: 0 },
|
||||
},
|
||||
data: {
|
||||
innerType: '',
|
||||
pet: {},
|
||||
segSel: {},
|
||||
optSel: {},
|
||||
dateVals: { vaccine: '2026-07-21', deworm: '2026-07-15' },
|
||||
wInput: '',
|
||||
costAmount: '',
|
||||
postContent: '',
|
||||
addName: '',
|
||||
addBirthday: '',
|
||||
addWeight: '',
|
||||
addColor: '',
|
||||
addBreed: '',
|
||||
commentText: '',
|
||||
aiInput: '',
|
||||
aiMessages: [],
|
||||
riskData: null,
|
||||
comments: [],
|
||||
reminders: [],
|
||||
poster: null,
|
||||
reportDetail: null,
|
||||
proInfo: null,
|
||||
saving: false,
|
||||
},
|
||||
observers: {
|
||||
show: function (show) {
|
||||
// 自定义 tabBar 会盖住弹层底部按钮;打开弹层时把它藏起来,关闭时恢复
|
||||
// 用 getTabBar().hidden 开关,不用 wx.showTabBar(真机上会额外冒出原生 tabBar)
|
||||
this.toggleTabBar(!!show);
|
||||
},
|
||||
'show, type': function (show, type) {
|
||||
if (show && type) this.setType(type, true);
|
||||
},
|
||||
},
|
||||
lifetimes: {
|
||||
detached() {
|
||||
this.toggleTabBar(false);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setType(type, fresh) {
|
||||
const pet = store.getPet();
|
||||
const patch = { innerType: type, pet };
|
||||
if (fresh) {
|
||||
patch.segSel = {};
|
||||
patch.optSel = {};
|
||||
patch.costAmount = '';
|
||||
patch.postContent = '';
|
||||
patch.addName = '';
|
||||
patch.addBirthday = '';
|
||||
patch.addWeight = '';
|
||||
patch.addColor = '';
|
||||
patch.addBreed = '';
|
||||
patch.commentText = '';
|
||||
patch.riskData = null;
|
||||
patch.saving = false;
|
||||
}
|
||||
if (type === 'weight') patch.wInput = (pet.weight || '').replace('kg', '');
|
||||
if (type === 'editPet') {
|
||||
// 预填当前宠物档案
|
||||
patch.addName = pet.name || '';
|
||||
patch.addColor = pet.color || '';
|
||||
patch.addBreed = pet.breed || '';
|
||||
patch.addWeight = (pet.weight || '').replace('kg', '');
|
||||
patch.addBirthday = pet.birthday ? String(pet.birthday).slice(0, 10) : '';
|
||||
patch.segSel = {
|
||||
addType: pet.type === '狗狗' ? 1 : 0,
|
||||
addGender: Math.max(0, GENDERS.indexOf(pet.gender)),
|
||||
};
|
||||
patch.optSel = { addStage: Math.max(0, STAGES.indexOf(pet.stage)) };
|
||||
}
|
||||
if (type === 'ai') {
|
||||
patch.aiMessages = [
|
||||
{
|
||||
role: 'ai',
|
||||
text: `我是 ${pet.name || '毛孩子'} 的养宠助手。我会优先基于档案、计划和历史记录回答,而不是泛泛聊天。`,
|
||||
},
|
||||
];
|
||||
patch.aiInput = '';
|
||||
}
|
||||
this.setData(patch);
|
||||
this.loadSheetData(type);
|
||||
},
|
||||
|
||||
// 按弹层类型拉取真实数据
|
||||
loadSheetData(type) {
|
||||
const id = store.currentPetId();
|
||||
if (type === 'comments' && this.data.postId) {
|
||||
api.listComments(this.data.postId).then((page) => this.setData({ comments: page.list || [] })).catch(() => {});
|
||||
} else if (type === 'reminders' && id) {
|
||||
api
|
||||
.getReminders(id)
|
||||
.then((list) =>
|
||||
this.setData({
|
||||
reminders: (list || []).map((r) => ({
|
||||
title: r.title,
|
||||
when: r.next_due_date ? fmtDate(r.next_due_date) : r.frequency || '',
|
||||
})),
|
||||
}),
|
||||
)
|
||||
.catch(() => {});
|
||||
} else if (type === 'poster' && id) {
|
||||
api.getPoster(id).then((p) => this.setData({ poster: p })).catch(() => {});
|
||||
} else if (type === 'reportDetail' && id) {
|
||||
api.weeklyReport(id).then((r) => this.setData({ reportDetail: r })).catch(() => {});
|
||||
} else if (type === 'pro') {
|
||||
api.getPro().then((p) => this.setData({ proInfo: p })).catch(() => {});
|
||||
}
|
||||
},
|
||||
|
||||
onActivatePro() {
|
||||
api
|
||||
.activatePro()
|
||||
.then((p) => {
|
||||
this.setData({ proInfo: p });
|
||||
wx.showToast({ title: '已开通 Pro', icon: 'success' });
|
||||
setTimeout(() => this.close(), 800);
|
||||
})
|
||||
.catch((e) => wx.showToast({ title: e.message || '开通失败', icon: 'none' }));
|
||||
},
|
||||
goSheet(e) {
|
||||
this.setType(e.currentTarget.dataset.type, true);
|
||||
},
|
||||
close() {
|
||||
this.triggerEvent('close');
|
||||
},
|
||||
onMaskTap() {
|
||||
this.close();
|
||||
},
|
||||
noop() {},
|
||||
// 藏/显当前 tab 页的自定义 tabBar(非 tab 页无 getTabBar,安全跳过)
|
||||
toggleTabBar(hidden) {
|
||||
const pages = getCurrentPages();
|
||||
const page = pages[pages.length - 1];
|
||||
if (page && typeof page.getTabBar === 'function' && page.getTabBar()) {
|
||||
page.getTabBar().setData({ hidden });
|
||||
}
|
||||
},
|
||||
onSeg(e) {
|
||||
const { group, index } = e.currentTarget.dataset;
|
||||
const segSel = Object.assign({}, this.data.segSel, { [group]: Number(index) });
|
||||
this.setData({ segSel });
|
||||
},
|
||||
onOpt(e) {
|
||||
const { group, index } = e.currentTarget.dataset;
|
||||
const optSel = Object.assign({}, this.data.optSel, { [group]: Number(index) });
|
||||
this.setData({ optSel });
|
||||
},
|
||||
onDate(e) {
|
||||
this.setData({ [`dateVals.${e.currentTarget.dataset.key}`]: e.detail.value });
|
||||
},
|
||||
onWInput(e) { this.setData({ wInput: e.detail.value }); },
|
||||
onCostInput(e) { this.setData({ costAmount: e.detail.value }); },
|
||||
onPostInput(e) { this.setData({ postContent: e.detail.value }); },
|
||||
onAddName(e) { this.setData({ addName: e.detail.value }); },
|
||||
onAddBirthday(e) { this.setData({ addBirthday: e.detail.value }); },
|
||||
onAddWeight(e) { this.setData({ addWeight: e.detail.value }); },
|
||||
onAddColor(e) { this.setData({ addColor: e.detail.value }); },
|
||||
onAddBreed(e) { this.setData({ addBreed: e.detail.value }); },
|
||||
onCommentInput(e) { this.setData({ commentText: e.detail.value }); },
|
||||
|
||||
segIdx(group) {
|
||||
const v = this.data.segSel[group];
|
||||
return v === undefined ? 0 : v;
|
||||
},
|
||||
optIdx(group) {
|
||||
const v = this.data.optSel[group];
|
||||
return v === undefined ? 0 : v;
|
||||
},
|
||||
|
||||
// 异常观察 → 生成风险评估(真调后端 AI,失败回退)
|
||||
onGenRisk() {
|
||||
const id = store.currentPetId();
|
||||
const symptoms = ['呕吐', '拉稀', '精神差', '皮肤红'];
|
||||
const durations = ['刚刚', '半天', '1天以上'];
|
||||
const spirits = ['正常', '一般', '明显变差'];
|
||||
const body = {
|
||||
symptoms: [symptoms[this.optIdx('symp')]],
|
||||
duration: durations[this.segIdx('dur')],
|
||||
spirit: spirits[this.segIdx('spirit')],
|
||||
};
|
||||
if (!id) {
|
||||
this.setData({ innerType: 'risk', riskData: null });
|
||||
return;
|
||||
}
|
||||
wx.showLoading({ title: '分析中...' });
|
||||
api
|
||||
.assessSymptom(id, body)
|
||||
.then((res) => {
|
||||
wx.hideLoading();
|
||||
this.setData({ innerType: 'risk', riskData: res });
|
||||
})
|
||||
.catch(() => {
|
||||
wx.hideLoading();
|
||||
this.setData({ innerType: 'risk', riskData: null });
|
||||
});
|
||||
},
|
||||
|
||||
// 依据当前弹层类型组装一条健康记录
|
||||
buildRecord() {
|
||||
const t = this.data.innerType;
|
||||
switch (t) {
|
||||
case 'poop': {
|
||||
const s = POOP[this.segIdx('poopState')];
|
||||
return { type: 'poop', icon: '💩', title: '排便记录:' + s, category: s };
|
||||
}
|
||||
case 'food': {
|
||||
const a = FOOD[this.segIdx('food')];
|
||||
return { type: 'food', icon: '🍽️', title: '饮食记录:食欲' + a };
|
||||
}
|
||||
case 'medicine':
|
||||
return { type: 'medicine', icon: '💊', title: '用药记录' };
|
||||
case 'cost': {
|
||||
const c = COST[this.segIdx('cost')];
|
||||
const amt = parseFloat(this.data.costAmount) || 0;
|
||||
return { type: 'cost', icon: '💰', title: '消费记录:¥' + amt, num_value: amt, category: c };
|
||||
}
|
||||
case 'vaccine':
|
||||
return { type: 'vaccine', icon: '💉', title: '疫苗提醒:' + this.data.dateVals.vaccine };
|
||||
case 'deworm':
|
||||
return { type: 'deworm', icon: '🛡️', title: '驱虫提醒:' + this.data.dateVals.deworm };
|
||||
case 'risk':
|
||||
return { type: 'symptom', icon: '🤒', title: '异常观察:风险中', category: '中' };
|
||||
case 'vetSummary':
|
||||
return { type: 'note', icon: '📄', title: '生成就医前摘要' };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async createAndClose(rec) {
|
||||
const id = store.currentPetId();
|
||||
if (!rec || !id) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
if (this.data.saving) return;
|
||||
this.setData({ saving: true });
|
||||
try {
|
||||
const saved = await api.createRecord(id, rec);
|
||||
if (rec.type === 'weight') store.patchCurrent({ weight: rec.title.replace('体重 ', '') });
|
||||
this.triggerEvent('saved', saved);
|
||||
this.close();
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '保存失败', icon: 'none' });
|
||||
this.setData({ saving: false });
|
||||
}
|
||||
},
|
||||
|
||||
onSave() {
|
||||
this.createAndClose(this.buildRecord());
|
||||
},
|
||||
onSaveWeight() {
|
||||
let w = (this.data.wInput || '').toString().replace(/\s+/g, '');
|
||||
if (!w) w = (this.data.pet.weight || '').replace('kg', '');
|
||||
const num = parseFloat(w) || 0;
|
||||
const weight = w.includes('kg') ? w : w + 'kg';
|
||||
this.createAndClose({ type: 'weight', icon: '⚖️', title: '体重 ' + weight, num_value: num });
|
||||
},
|
||||
onSavePhoto() {
|
||||
const id = store.currentPetId();
|
||||
if (!id) return this.close();
|
||||
wx.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
success: (res) => {
|
||||
const path = res.tempFiles[0].tempFilePath;
|
||||
wx.showLoading({ title: '上传中...' });
|
||||
api
|
||||
.uploadFile(path)
|
||||
.then((up) =>
|
||||
api.createRecord(id, { type: 'photo', icon: '📷', title: '成长照片', image_url: up.url }),
|
||||
)
|
||||
.then((saved) => {
|
||||
wx.hideLoading();
|
||||
this.triggerEvent('saved', saved);
|
||||
this.close();
|
||||
})
|
||||
.catch((e) => {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: e.message || '上传失败', icon: 'none' });
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// 组装宠物档案表单
|
||||
petFormBody() {
|
||||
const isDog = this.segIdx('addType') === 1;
|
||||
return {
|
||||
name: (this.data.addName || '').trim(),
|
||||
type: isDog ? '狗狗' : '猫猫',
|
||||
emoji: isDog ? '🐶' : '🐱',
|
||||
gender: GENDERS[this.segIdx('addGender')],
|
||||
birthday: this.data.addBirthday,
|
||||
weight: this.data.addWeight,
|
||||
color: (this.data.addColor || '').trim(),
|
||||
breed: (this.data.addBreed || '').trim(),
|
||||
stage: STAGES[this.optIdx('addStage')],
|
||||
age: ageFromBirthday(this.data.addBirthday),
|
||||
};
|
||||
},
|
||||
|
||||
// 添加/编辑 共用提交入口
|
||||
onPetSubmit() {
|
||||
if (this.data.innerType === 'editPet') return this.onEditPetSubmit();
|
||||
return this.onAddPetSubmit();
|
||||
},
|
||||
|
||||
async onAddPetSubmit() {
|
||||
const body = this.petFormBody();
|
||||
if (!body.name) return wx.showToast({ title: '请输入名字', icon: 'none' });
|
||||
if (this.data.saving) return;
|
||||
this.setData({ saving: true });
|
||||
try {
|
||||
await api.createPet(body);
|
||||
await store.loadPets();
|
||||
this.triggerEvent('petadded');
|
||||
this.close();
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '添加失败', icon: 'none' });
|
||||
this.setData({ saving: false });
|
||||
}
|
||||
},
|
||||
|
||||
async onEditPetSubmit() {
|
||||
const body = this.petFormBody();
|
||||
if (!body.name) return wx.showToast({ title: '请输入名字', icon: 'none' });
|
||||
const id = store.currentPetId();
|
||||
if (!id) return this.close();
|
||||
if (this.data.saving) return;
|
||||
this.setData({ saving: true });
|
||||
try {
|
||||
await api.updatePet(id, body);
|
||||
await store.loadPets();
|
||||
this.triggerEvent('petadded');
|
||||
this.close();
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '保存失败', icon: 'none' });
|
||||
this.setData({ saving: false });
|
||||
}
|
||||
},
|
||||
|
||||
onDeletePet() {
|
||||
const id = store.currentPetId();
|
||||
const name = this.data.pet.name || '这只宠物';
|
||||
if (!id) return;
|
||||
wx.showModal({
|
||||
title: '删除宠物',
|
||||
content: `删除「${name}」后,它的记录、计划、提醒都会一并移除,且不可恢复。确定删除吗?`,
|
||||
confirmText: '删除',
|
||||
confirmColor: '#EE7D73',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return;
|
||||
api
|
||||
.deletePet(id)
|
||||
.then(async () => {
|
||||
await store.loadPets();
|
||||
this.triggerEvent('petadded');
|
||||
this.close();
|
||||
if (store.getPets().length === 0) {
|
||||
wx.reLaunch({ url: '/pages/onboarding/onboarding' });
|
||||
}
|
||||
})
|
||||
.catch((e) => wx.showToast({ title: e.message || '删除失败', icon: 'none' }));
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// 发帖
|
||||
async onCreatePost() {
|
||||
const content = (this.data.postContent || '').trim();
|
||||
if (!content) return wx.showToast({ title: '写点什么吧', icon: 'none' });
|
||||
const identity = IDENTITY[this.segIdx('ident')];
|
||||
const tag = PTAG[this.data.optSel.ptag === undefined ? 0 : this.data.optSel.ptag];
|
||||
if (this.data.saving) return;
|
||||
this.setData({ saving: true });
|
||||
try {
|
||||
await api.createPost({
|
||||
pet_id: identity === 'petName' ? store.currentPetId() : null,
|
||||
identity,
|
||||
content,
|
||||
tags: [tag],
|
||||
images: [],
|
||||
});
|
||||
this.triggerEvent('posted');
|
||||
this.close();
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '发布失败', icon: 'none' });
|
||||
this.setData({ saving: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 评论
|
||||
async onSendComment() {
|
||||
const text = (this.data.commentText || '').trim();
|
||||
if (!text || !this.data.postId) return this.close();
|
||||
try {
|
||||
await api.createComment(this.data.postId, text);
|
||||
this.triggerEvent('commented');
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '评论失败', icon: 'none' });
|
||||
}
|
||||
this.close();
|
||||
},
|
||||
|
||||
// AI 聊天
|
||||
onAiInput(e) { this.setData({ aiInput: e.detail.value }); },
|
||||
sendAI(e) {
|
||||
const preset = e.currentTarget.dataset.text;
|
||||
const text = (preset || this.data.aiInput || '').trim();
|
||||
if (!text) return;
|
||||
const msgs = this.data.aiMessages.concat([{ role: 'user', text }]);
|
||||
this.setData({ aiMessages: msgs, aiInput: '' });
|
||||
api
|
||||
.aiChat({ pet_id: store.currentPetId() || null, session: 'sheet', text })
|
||||
.then((res) => {
|
||||
this.setData({ aiMessages: this.data.aiMessages.concat([{ role: 'ai', text: res.reply }]) });
|
||||
})
|
||||
.catch(() => {
|
||||
this.setData({
|
||||
aiMessages: this.data.aiMessages.concat([{ role: 'ai', text: '网络异常,请稍后再试。' }]),
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
<wxs module="u">
|
||||
module.exports.sel = function (map, key, index, def) {
|
||||
var v = map[key];
|
||||
if (v === undefined || v === null) v = def;
|
||||
return v === index;
|
||||
};
|
||||
</wxs>
|
||||
|
||||
<view class="overlay {{show ? 'show' : ''}}" bindtap="onMaskTap">
|
||||
<view class="sheet" catchtap="noop">
|
||||
<view class="sheetbar"></view>
|
||||
|
||||
<scroll-view class="sheet-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<!-- 记录体重 -->
|
||||
<block wx:if="{{innerType === 'weight'}}">
|
||||
<view class="sheet-h3">记录体重</view>
|
||||
<view class="sheet-p">趋势比单次数值更重要。幼年期建议每周记录 1-2 次。</view>
|
||||
<view class="field"><label>{{pet.name}} 当前体重</label>
|
||||
<input class="input" value="{{wInput}}" bindinput="onWInput" type="digit"/></view>
|
||||
<view class="field"><label>备注</label>
|
||||
<textarea class="textarea" placeholder="例如:最近食欲不错,活动量正常" placeholder-class="placeholder"></textarea></view>
|
||||
<button class="btn btn-primary btn-block" bindtap="onSaveWeight">保存记录</button>
|
||||
</block>
|
||||
|
||||
<!-- 记录便便 -->
|
||||
<block wx:elif="{{innerType === 'poop'}}">
|
||||
<view class="sheet-h3">记录便便</view>
|
||||
<view class="sheet-p">选择状态并补充备注,几秒完成。</view>
|
||||
<view class="field"><label>状态</label><view class="seg">
|
||||
<view class="seg-btn {{u.sel(segSel,'poopState',0,0)?'selected':''}}" data-group="poopState" data-index="0" bindtap="onSeg">正常</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'poopState',1,0)?'selected':''}}" data-group="poopState" data-index="1" bindtap="onSeg">软便</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'poopState',2,0)?'selected':''}}" data-group="poopState" data-index="2" bindtap="onSeg">拉稀</view>
|
||||
</view></view>
|
||||
<view class="field"><label>颜色/备注</label>
|
||||
<textarea class="textarea" placeholder="颜色、次数、是否带血、是否有异味等" placeholder-class="placeholder"></textarea></view>
|
||||
<button class="btn btn-primary btn-block" data-text="排便记录:正常" data-icon="💩" bindtap="onSave">保存记录</button>
|
||||
</block>
|
||||
|
||||
<!-- 记录饮食 -->
|
||||
<block wx:elif="{{innerType === 'food'}}">
|
||||
<view class="sheet-h3">记录饮食</view>
|
||||
<view class="field"><label>今天吃了什么?</label>
|
||||
<input class="input" placeholder="例如:幼猫粮 45g" placeholder-class="placeholder"/></view>
|
||||
<view class="field"><label>食欲</label><view class="seg">
|
||||
<view class="seg-btn {{u.sel(segSel,'food',0,0)?'selected':''}}" data-group="food" data-index="0" bindtap="onSeg">正常</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'food',1,0)?'selected':''}}" data-group="food" data-index="1" bindtap="onSeg">偏少</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'food',2,0)?'selected':''}}" data-group="food" data-index="2" bindtap="onSeg">不吃</view>
|
||||
</view></view>
|
||||
<button class="btn btn-primary btn-block" data-text="饮食记录:食欲正常" data-icon="🍽️" bindtap="onSave">保存记录</button>
|
||||
</block>
|
||||
|
||||
<!-- 异常观察 -->
|
||||
<block wx:elif="{{innerType === 'symptom'}}">
|
||||
<view class="sheet-h3">异常观察</view>
|
||||
<view class="sheet-p">如实描述异常,AI 会给出观察建议与就医提示(非诊断)。</view>
|
||||
<view class="field"><label>发生了什么?</label><view class="mini-options">
|
||||
<view class="option {{u.sel(optSel,'symp',0,0)?'selected':''}}" data-group="symp" data-index="0" bindtap="onOpt">呕吐</view>
|
||||
<view class="option {{u.sel(optSel,'symp',1,0)?'selected':''}}" data-group="symp" data-index="1" bindtap="onOpt">拉稀</view>
|
||||
<view class="option {{u.sel(optSel,'symp',2,0)?'selected':''}}" data-group="symp" data-index="2" bindtap="onOpt">精神差</view>
|
||||
<view class="option {{u.sel(optSel,'symp',3,0)?'selected':''}}" data-group="symp" data-index="3" bindtap="onOpt">皮肤红</view>
|
||||
</view></view>
|
||||
<view class="field"><label>持续多久?</label><view class="seg">
|
||||
<view class="seg-btn {{u.sel(segSel,'dur',0,0)?'selected':''}}" data-group="dur" data-index="0" bindtap="onSeg">刚刚</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'dur',1,0)?'selected':''}}" data-group="dur" data-index="1" bindtap="onSeg">半天</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'dur',2,0)?'selected':''}}" data-group="dur" data-index="2" bindtap="onSeg">1天以上</view>
|
||||
</view></view>
|
||||
<view class="field"><label>精神状态</label><view class="seg">
|
||||
<view class="seg-btn {{u.sel(segSel,'spirit',0,0)?'selected':''}}" data-group="spirit" data-index="0" bindtap="onSeg">正常</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'spirit',1,0)?'selected':''}}" data-group="spirit" data-index="1" bindtap="onSeg">一般</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'spirit',2,0)?'selected':''}}" data-group="spirit" data-index="2" bindtap="onSeg">明显变差</view>
|
||||
</view></view>
|
||||
<button class="btn btn-primary btn-block" bindtap="onGenRisk">生成观察建议</button>
|
||||
</block>
|
||||
|
||||
<!-- AI 观察建议 -->
|
||||
<block wx:elif="{{innerType === 'risk'}}">
|
||||
<view class="sheet-h3">AI 观察建议</view>
|
||||
<view class="risk-box">⚠️ 风险等级:{{riskData ? riskData.risk_level : '中'}}</view>
|
||||
<view class="sheet-p"><text class="bold">可能原因:</text>{{riskData ? riskData.causes : '可能与换粮、应激或消化不适有关。不要把这里写成诊断结论。'}}</view>
|
||||
<view class="sheet-p"><text class="bold">建议:</text>{{riskData ? riskData.suggestion : '继续观察精神、食欲和排便;暂停新食物;记录呕吐或腹泻次数。'}}</view>
|
||||
<view class="sheet-p"><text class="bold">建议就医:</text>{{riskData ? riskData.seek_care : '如果持续超过 24 小时,或伴随便血、精神明显变差、频繁呕吐,建议尽快就医。'}}</view>
|
||||
<view class="sheet-actions">
|
||||
<button class="btn btn-ghost" data-text="异常观察:风险中" data-icon="🤒" bindtap="onSave">保存记录</button>
|
||||
<button class="btn btn-primary" data-type="vetSummary" bindtap="goSheet">就医摘要</button>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 就医前摘要 -->
|
||||
<block wx:elif="{{innerType === 'vetSummary'}}">
|
||||
<view class="sheet-h3">就医前摘要</view>
|
||||
<view class="sheet-p pre">宠物:{{pet.name}}
|
||||
阶段:{{pet.stage}}
|
||||
当前体重:{{pet.weight}}
|
||||
近期重点:疫苗期、换粮观察、排便记录
|
||||
建议携带:排便/呕吐照片、饮食变化、疫苗驱虫记录。</view>
|
||||
<button class="btn btn-primary btn-block" data-text="生成就医前摘要" data-icon="📄" bindtap="onSave">保存摘要</button>
|
||||
</block>
|
||||
|
||||
<!-- 记录用药 -->
|
||||
<block wx:elif="{{innerType === 'medicine'}}">
|
||||
<view class="sheet-h3">记录用药</view>
|
||||
<view class="field"><label>药品名称</label><input class="input" placeholder="例如:体内驱虫药" placeholder-class="placeholder"/></view>
|
||||
<view class="field"><label>剂量/备注</label><input class="input" placeholder="例如:按 2.8kg 剂量" placeholder-class="placeholder"/></view>
|
||||
<button class="btn btn-primary btn-block" data-text="用药记录" data-icon="💊" bindtap="onSave">保存记录</button>
|
||||
</block>
|
||||
|
||||
<!-- 记一笔消费 -->
|
||||
<block wx:elif="{{innerType === 'cost'}}">
|
||||
<view class="sheet-h3">记一笔消费</view>
|
||||
<view class="field"><label>金额</label><input class="input" placeholder="例如:168" placeholder-class="placeholder" type="digit" value="{{costAmount}}" bindinput="onCostInput"/></view>
|
||||
<view class="field"><label>类别</label><view class="seg">
|
||||
<view class="seg-btn {{u.sel(segSel,'cost',0,0)?'selected':''}}" data-group="cost" data-index="0" bindtap="onSeg">食品</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'cost',1,0)?'selected':''}}" data-group="cost" data-index="1" bindtap="onSeg">医疗</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'cost',2,0)?'selected':''}}" data-group="cost" data-index="2" bindtap="onSeg">用品</view>
|
||||
</view></view>
|
||||
<view class="field"><label>备注</label><input class="input" placeholder="例如:幼猫粮" placeholder-class="placeholder"/></view>
|
||||
<button class="btn btn-primary btn-block" data-text="消费记录:¥168" data-icon="💰" bindtap="onSave">保存记录</button>
|
||||
</block>
|
||||
|
||||
<!-- 疫苗提醒 -->
|
||||
<block wx:elif="{{innerType === 'vaccine'}}">
|
||||
<view class="sheet-h3">疫苗提醒</view>
|
||||
<view class="sheet-p">为 {{pet.name}} 设置疫苗提醒。接种前后避免洗澡和长途出行。</view>
|
||||
<view class="field"><label>提醒日期</label>
|
||||
<picker mode="date" value="{{dateVals.vaccine}}" data-key="vaccine" bindchange="onDate">
|
||||
<view class="picker-box">{{dateVals.vaccine}}</view>
|
||||
</picker></view>
|
||||
<button class="btn btn-primary btn-block" data-text="疫苗提醒:7月21日" data-icon="💉" bindtap="onSave">保存提醒</button>
|
||||
</block>
|
||||
|
||||
<!-- 驱虫提醒 -->
|
||||
<block wx:elif="{{innerType === 'deworm'}}">
|
||||
<view class="sheet-h3">驱虫提醒</view>
|
||||
<view class="sheet-p">设置下次驱虫日期,到点提醒你。</view>
|
||||
<view class="field"><label>下次驱虫日期</label>
|
||||
<picker mode="date" value="{{dateVals.deworm}}" data-key="deworm" bindchange="onDate">
|
||||
<view class="picker-box">{{dateVals.deworm}}</view>
|
||||
</picker></view>
|
||||
<button class="btn btn-primary btn-block" data-text="驱虫提醒:7月15日" data-icon="🛡️" bindtap="onSave">保存提醒</button>
|
||||
</block>
|
||||
|
||||
<!-- 成长照片 -->
|
||||
<block wx:elif="{{innerType === 'photo'}}">
|
||||
<view class="sheet-h3">成长照片</view>
|
||||
<view class="sheet-p">上传成长照片,用于宠物档案与成长报告。</view>
|
||||
<view class="poster" style="margin-bottom:28rpx">
|
||||
<view class="poster-avatar">{{pet.emoji}}</view>
|
||||
<view class="bold" style="font-size:30rpx">{{pet.name}} 的成长照片</view>
|
||||
<view class="muted" style="font-size:24rpx;margin-top:12rpx">模拟上传预览</view>
|
||||
</view>
|
||||
<button class="btn btn-primary btn-block" bindtap="onSavePhoto">选择图片上传</button>
|
||||
</block>
|
||||
|
||||
<!-- 提醒中心 -->
|
||||
<block wx:elif="{{innerType === 'reminders'}}">
|
||||
<view class="sheet-h3">提醒中心</view>
|
||||
<view wx:for="{{reminders}}" wx:key="title" class="record-row"><text class="bold">{{item.title}}</text><text class="muted">{{item.when}}</text></view>
|
||||
<view wx:if="{{!reminders.length}}" class="sheet-p">暂无提醒</view>
|
||||
<button class="btn btn-primary btn-block" style="margin-top:24rpx" bindtap="close">完成</button>
|
||||
</block>
|
||||
|
||||
<!-- 任务详情 -->
|
||||
<!-- 问问 AI -->
|
||||
<block wx:elif="{{innerType === 'ai'}}">
|
||||
<view class="sheet-h3">问问 AI</view>
|
||||
<scroll-view class="plan-chat" scroll-y="true" enhanced="{{true}}" show-scrollbar="{{false}}" style="max-height:660rpx" scroll-into-view="ai-bottom">
|
||||
<view wx:for="{{aiMessages}}" wx:key="index" class="msg {{item.role}}">{{item.text}}</view>
|
||||
<view id="ai-bottom"></view>
|
||||
</scroll-view>
|
||||
<view class="mini-options">
|
||||
<view class="option" data-text="猫咪今天吐了一次怎么办?" bindtap="sendAI">猫咪吐了一次</view>
|
||||
<view class="option" data-text="幼猫多久驱虫一次?" bindtap="sendAI">多久驱虫</view>
|
||||
<view class="option" data-text="换粮后软便怎么办?" bindtap="sendAI">换粮软便</view>
|
||||
</view>
|
||||
<view class="ai-input-row">
|
||||
<input class="input" placeholder="描述一下问题..." placeholder-class="placeholder"
|
||||
value="{{aiInput}}" bindinput="onAiInput" confirm-type="send" bindconfirm="sendAI"/>
|
||||
<button class="btn btn-primary ai-send" bindtap="sendAI">发</button>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 成长海报 -->
|
||||
<block wx:elif="{{innerType === 'poster'}}">
|
||||
<view class="poster">
|
||||
<view class="poster-h3">{{pet.name}} 的成长报告</view>
|
||||
<view class="poster-avatar">{{pet.emoji}}</view>
|
||||
<view class="muted">{{pet.age}}|{{pet.weight}}|{{pet.stage}}</view>
|
||||
<view class="poster-list pre">✓ 完成任务 {{poster ? poster.tasks_completed : 0}} 项
|
||||
✓ 体重记录 {{poster ? poster.weight_records : 0}} 次
|
||||
✓ 疫苗记录 {{poster ? poster.vaccine_records : 0}} 次
|
||||
✓ 高风险异常 {{poster ? poster.high_risk_count : 0}} 次</view>
|
||||
<view class="bold">健康状态:{{poster ? poster.headline : '稳定成长'}}</view>
|
||||
<view class="muted" style="font-size:24rpx;margin-top:20rpx">生成自:毛孩子计划</view>
|
||||
</view>
|
||||
<button class="btn btn-primary btn-block" style="margin-top:28rpx" bindtap="close">保存 / 分享卡片</button>
|
||||
</block>
|
||||
|
||||
<!-- 报告详情 -->
|
||||
<block wx:elif="{{innerType === 'reportDetail'}}">
|
||||
<view class="sheet-h3">本周成长报告</view>
|
||||
<view class="sheet-p">{{reportDetail ? reportDetail.summary : '正在汇总本周数据…'}}</view>
|
||||
<view wx:if="{{reportDetail}}" class="sheet-p"><text class="bold">下周重点:</text>{{reportDetail.next_week_focus}}</view>
|
||||
<button class="btn btn-primary btn-block" data-type="poster" bindtap="goSheet">生成分享卡片</button>
|
||||
</block>
|
||||
|
||||
<!-- Pro 会员 -->
|
||||
<block wx:elif="{{innerType === 'pro'}}">
|
||||
<view class="sheet-h3">毛孩子计划 Pro</view>
|
||||
<view class="sheet-p">解锁 365 天计划、PDF 健康档案、月度报告、多宠物管理与年度账单。</view>
|
||||
<view class="check-list">
|
||||
<view wx:for="{{proInfo.features}}" wx:key="*this" class="check selected"><view class="box">✓</view>{{item}}</view>
|
||||
</view>
|
||||
<button wx:if="{{proInfo.status === 'active'}}" class="btn btn-soft btn-block" bindtap="close">已开通 Pro ✓</button>
|
||||
<button wx:else class="btn btn-primary btn-block" bindtap="onActivatePro">¥29.9 / 年 开通 Pro</button>
|
||||
</block>
|
||||
|
||||
<!-- 添加宠物 -->
|
||||
<block wx:elif="{{innerType === 'addPet' || innerType === 'editPet'}}">
|
||||
<view class="sheet-h3">{{innerType === 'editPet' ? '编辑档案' : '添加毛孩子'}}</view>
|
||||
<view class="field"><label>名字</label><input class="input" placeholder="例如:布丁" placeholder-class="placeholder" value="{{addName}}" bindinput="onAddName"/></view>
|
||||
<view class="field"><label>类型</label><view class="seg">
|
||||
<view class="seg-btn {{u.sel(segSel,'addType',0,0)?'selected':''}}" data-group="addType" data-index="0" bindtap="onSeg">猫猫</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'addType',1,0)?'selected':''}}" data-group="addType" data-index="1" bindtap="onSeg">狗狗</view>
|
||||
</view></view>
|
||||
<view class="field"><label>性别</label><view class="seg">
|
||||
<view class="seg-btn {{u.sel(segSel,'addGender',0,0)?'selected':''}}" data-group="addGender" data-index="0" bindtap="onSeg">男孩</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'addGender',1,0)?'selected':''}}" data-group="addGender" data-index="1" bindtap="onSeg">女孩</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'addGender',2,0)?'selected':''}}" data-group="addGender" data-index="2" bindtap="onSeg">不确定</view>
|
||||
</view></view>
|
||||
<view class="field"><label>生日 / 到家时间</label>
|
||||
<picker mode="date" value="{{addBirthday}}" bindchange="onAddBirthday">
|
||||
<view class="picker-box">{{addBirthday || '选择日期'}}</view>
|
||||
</picker></view>
|
||||
<view class="field"><label>毛色</label><input class="input" placeholder="例如:橘白 / 奶牛 / 黑" placeholder-class="placeholder" value="{{addColor}}" bindinput="onAddColor"/></view>
|
||||
<view class="field"><label>品种</label><input class="input" placeholder="例如:中华田园猫 / 柯基" placeholder-class="placeholder" value="{{addBreed}}" bindinput="onAddBreed"/></view>
|
||||
<view class="field"><label>当前体重</label><input class="input" placeholder="例如:2.8" placeholder-class="placeholder" type="digit" value="{{addWeight}}" bindinput="onAddWeight"/></view>
|
||||
<view class="field"><label>当前阶段</label><view class="mini-options">
|
||||
<view class="option {{u.sel(optSel,'addStage',0,0)?'selected':''}}" data-group="addStage" data-index="0" bindtap="onOpt">刚到家</view>
|
||||
<view class="option {{u.sel(optSel,'addStage',1,0)?'selected':''}}" data-group="addStage" data-index="1" bindtap="onOpt">幼年期</view>
|
||||
<view class="option {{u.sel(optSel,'addStage',2,0)?'selected':''}}" data-group="addStage" data-index="2" bindtap="onOpt">成年期</view>
|
||||
<view class="option {{u.sel(optSel,'addStage',3,0)?'selected':''}}" data-group="addStage" data-index="3" bindtap="onOpt">老年期</view>
|
||||
</view></view>
|
||||
<button class="btn btn-primary btn-block" bindtap="onPetSubmit">{{innerType === 'editPet' ? '保存修改' : '添加宠物'}}</button>
|
||||
<button wx:if="{{innerType === 'editPet'}}" class="btn btn-block del-btn" style="margin-top:16rpx" bindtap="onDeletePet">删除该宠物</button>
|
||||
</block>
|
||||
|
||||
<!-- 发布图文 -->
|
||||
<block wx:elif="{{innerType === 'createPost'}}">
|
||||
<view class="sheet-h3">发布图文</view>
|
||||
<view class="sheet-p">选择发布身份,分享你的养宠日常。</view>
|
||||
<view class="field"><label>发布身份</label><view class="seg">
|
||||
<view class="seg-btn {{u.sel(segSel,'ident',0,0)?'selected':''}}" data-group="ident" data-index="0" bindtap="onSeg">{{pet.name}}</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'ident',1,0)?'selected':''}}" data-group="ident" data-index="1" bindtap="onSeg">匿名宠友</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'ident',2,0)?'selected':''}}" data-group="ident" data-index="2" bindtap="onSeg">官方笔记</view>
|
||||
</view></view>
|
||||
<view class="field"><label>内容</label>
|
||||
<textarea class="textarea" placeholder="分享养宠日常、求助问题、经验笔记..." placeholder-class="placeholder" value="{{postContent}}" bindinput="onPostInput"></textarea></view>
|
||||
<view class="mini-options">
|
||||
<view class="option {{u.sel(optSel,'ptag',0,0)?'selected':''}}" data-group="ptag" data-index="0" bindtap="onOpt">晒宠</view>
|
||||
<view class="option {{u.sel(optSel,'ptag',1,0)?'selected':''}}" data-group="ptag" data-index="1" bindtap="onOpt">求助</view>
|
||||
<view class="option {{u.sel(optSel,'ptag',2,0)?'selected':''}}" data-group="ptag" data-index="2" bindtap="onOpt">经验</view>
|
||||
<view class="option {{u.sel(optSel,'ptag',3,0)?'selected':''}}" data-group="ptag" data-index="3" bindtap="onOpt">避坑</view>
|
||||
</view>
|
||||
<button class="btn btn-primary btn-block" bindtap="onCreatePost">发布到宠友圈</button>
|
||||
</block>
|
||||
|
||||
<!-- 评论 -->
|
||||
<block wx:elif="{{innerType === 'comments'}}">
|
||||
<view class="sheet-h3">评论</view>
|
||||
<view wx:for="{{comments}}" wx:key="id" class="record-row"><text class="bold">{{item.author_name}}</text><text class="muted">{{item.content}}</text></view>
|
||||
<view wx:if="{{!comments.length}}" class="sheet-p">还没有评论,来抢沙发~</view>
|
||||
<view class="ai-input-row" style="margin-top:24rpx">
|
||||
<input class="input" placeholder="友善交流,分享经验..." placeholder-class="placeholder" value="{{commentText}}" bindinput="onCommentInput"/>
|
||||
<button class="btn btn-primary ai-send" bindtap="onSendComment">发</button>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 分享帖子 -->
|
||||
<block wx:elif="{{innerType === 'sharePost'}}">
|
||||
<view class="sheet-h3">分享帖子</view>
|
||||
<view class="sheet-p">转发给微信好友、微信群,或生成一张分享卡片。</view>
|
||||
<button class="btn btn-primary btn-block" bindtap="close">生成分享卡片</button>
|
||||
</block>
|
||||
</scroll-view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,36 @@
|
||||
.overlay{
|
||||
position:fixed;inset:0;z-index:200;
|
||||
background:rgba(26,18,12,.30);
|
||||
opacity:0;pointer-events:none;transition:.2s ease;
|
||||
backdrop-filter:blur(6rpx);-webkit-backdrop-filter:blur(6rpx);
|
||||
}
|
||||
.overlay.show{opacity:1;pointer-events:auto}
|
||||
.sheet{
|
||||
position:absolute;left:0;right:0;bottom:0;
|
||||
background:#fff;border-radius:60rpx 60rpx 0 0;
|
||||
padding:32rpx 0 0;
|
||||
transform:translateY(106%);transition:.24s ease;
|
||||
box-shadow:0 -40rpx 100rpx rgba(0,0,0,.16);
|
||||
}
|
||||
.overlay.show .sheet{transform:translateY(0)}
|
||||
.sheet-scroll{
|
||||
box-sizing:border-box;
|
||||
max-height:78vh;
|
||||
padding:0 36rpx calc(48rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.sheet-scroll::-webkit-scrollbar,.plan-chat::-webkit-scrollbar{width:0;height:0;display:none}
|
||||
.sheetbar{width:88rpx;height:10rpx;border-radius:999rpx;background:#E1D8CF;margin:0 auto 24rpx}
|
||||
|
||||
.sheet-h3{font-size:40rpx;font-weight:800;margin-bottom:20rpx}
|
||||
.sheet-p{font-size:28rpx;color:#62584F;line-height:1.65;margin-bottom:24rpx}
|
||||
.sheet-p.pre,.poster-list.pre{white-space:pre-line}
|
||||
.poster-h3{font-size:40rpx;font-weight:800;margin-bottom:20rpx}
|
||||
.bold{font-weight:900}
|
||||
|
||||
.ai-input-row{display:flex;gap:16rpx;align-items:center}
|
||||
.ai-input-row .input{flex:1}
|
||||
.ai-send{height:88rpx;line-height:88rpx;padding:0 28rpx}
|
||||
|
||||
/* 弹层内的日期选择器展示 */
|
||||
.picker-box{color:var(--text)}
|
||||
.del-btn{background:var(--red-soft);color:var(--red)}
|
||||
@@ -0,0 +1,8 @@
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
methods: {
|
||||
onTap() {
|
||||
this.triggerEvent('tap');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<view class="fab" bindtap="onTap">AI</view>
|
||||
@@ -0,0 +1,10 @@
|
||||
.fab{
|
||||
position:fixed;right:36rpx;bottom:calc(190rpx + env(safe-area-inset-bottom));
|
||||
width:116rpx;height:116rpx;border-radius:44rpx;
|
||||
background:linear-gradient(135deg,#78A6E8,#9FC0F5);
|
||||
color:#fff;font-weight:900;font-size:40rpx;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
box-shadow:0 28rpx 60rpx rgba(120,166,232,.32);
|
||||
z-index:60;
|
||||
}
|
||||
.fab:active{transform:scale(.96)}
|
||||
@@ -0,0 +1,31 @@
|
||||
const app = getApp();
|
||||
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
title: { type: String, value: '毛孩子计划' },
|
||||
showBack: { type: Boolean, value: false }
|
||||
},
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
navBarHeight: 44
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight,
|
||||
navBarHeight: app.globalData.navBarHeight
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onBack() {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
wx.navigateBack();
|
||||
} else {
|
||||
wx.switchTab({ url: '/pages/home/home' });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<view class="nav-wrap" style="padding-top:{{statusBarHeight}}px;height:{{statusBarHeight + navBarHeight}}px;">
|
||||
<view class="nav-inner" style="height:{{navBarHeight}}px;">
|
||||
<view wx:if="{{showBack}}" class="nav-back" bindtap="onBack">‹</view>
|
||||
<view class="nav-title">{{title}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 占位,避免内容被固定导航栏遮挡 -->
|
||||
<view style="height:{{statusBarHeight + navBarHeight}}px;"></view>
|
||||
@@ -0,0 +1,19 @@
|
||||
:host{display:block}
|
||||
.nav-wrap{
|
||||
position:fixed;top:0;left:0;right:0;z-index:100;
|
||||
background:rgba(248,246,242,.82);
|
||||
backdrop-filter:blur(28rpx);
|
||||
-webkit-backdrop-filter:blur(28rpx);
|
||||
}
|
||||
.nav-inner{
|
||||
position:relative;display:flex;align-items:center;justify-content:center;
|
||||
}
|
||||
.nav-title{
|
||||
font-size:32rpx;font-weight:900;color:var(--text);
|
||||
max-width:60%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
||||
}
|
||||
.nav-back{
|
||||
position:absolute;left:20rpx;top:50%;transform:translateY(-50%);
|
||||
width:64rpx;height:64rpx;display:flex;align-items:center;justify-content:center;
|
||||
font-size:52rpx;line-height:1;color:var(--text);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
const store = require('../../utils/store.js');
|
||||
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
data: {
|
||||
pets: [],
|
||||
currentId: 0,
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.refresh();
|
||||
this._unsub = store.subscribe(() => this.refresh());
|
||||
},
|
||||
detached() {
|
||||
if (this._unsub) this._unsub();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
refresh() {
|
||||
this.setData({
|
||||
pets: store.getPets(),
|
||||
currentId: store.currentPetId() || 0,
|
||||
});
|
||||
},
|
||||
onPick(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
store.switchPet(id);
|
||||
this.triggerEvent('change', { id });
|
||||
},
|
||||
onAdd() {
|
||||
this.triggerEvent('add');
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<scroll-view class="pet-switch" scroll-x="true" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view wx:for="{{pets}}" wx:key="id"
|
||||
class="pet-chip {{item.id === currentId ? 'active' : ''}}"
|
||||
data-id="{{item.id}}" bindtap="onPick">
|
||||
<view class="mini-avatar">{{item.emoji}}</view>{{item.name}}
|
||||
</view>
|
||||
<view class="pet-chip" bindtap="onAdd">
|
||||
<view class="mini-avatar">+</view>添加
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -0,0 +1,5 @@
|
||||
.pet-switch{
|
||||
display:block;white-space:nowrap;width:100%;
|
||||
}
|
||||
.pet-switch .pet-chip{margin-right:20rpx}
|
||||
.pet-switch .pet-chip:last-child{margin-right:0}
|
||||
@@ -0,0 +1,20 @@
|
||||
Component({
|
||||
data: {
|
||||
selected: 0,
|
||||
hidden: false,
|
||||
list: [
|
||||
{ pagePath: '/pages/home/home', text: '首页', icon: '🏠' },
|
||||
{ pagePath: '/pages/plan/plan', text: '计划', icon: '📅' },
|
||||
{ pagePath: '/pages/record/record', text: '记录', icon: '✍️' },
|
||||
{ pagePath: '/pages/report/report', text: '报告', icon: '📊' },
|
||||
{ pagePath: '/pages/community/community', text: '社区', icon: '🐾' }
|
||||
]
|
||||
},
|
||||
methods: {
|
||||
switchTab(e) {
|
||||
const idx = e.currentTarget.dataset.index;
|
||||
const path = this.data.list[idx].pagePath;
|
||||
wx.switchTab({ url: path });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<view class="tabbar" hidden="{{hidden}}">
|
||||
<view wx:for="{{list}}" wx:key="text" class="tab {{selected === index ? 'active' : ''}}"
|
||||
data-index="{{index}}" bindtap="switchTab">
|
||||
<text class="tab-icon">{{item.icon}}</text>
|
||||
<text class="tab-text">{{item.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,16 @@
|
||||
.tabbar{
|
||||
position:fixed;left:0;right:0;bottom:0;z-index:50;
|
||||
height:calc(156rpx + env(safe-area-inset-bottom));
|
||||
padding-bottom:calc(16rpx + env(safe-area-inset-bottom));
|
||||
background:rgba(255,255,255,.94);
|
||||
border-top:1rpx solid #EEE6DC;
|
||||
display:flex;
|
||||
backdrop-filter:blur(32rpx);
|
||||
-webkit-backdrop-filter:blur(32rpx);
|
||||
}
|
||||
.tab{
|
||||
flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8rpx;
|
||||
color:#8D8277;font-size:24rpx;
|
||||
}
|
||||
.tab-icon{font-size:42rpx;line-height:1}
|
||||
.tab.active{color:#DD8420;font-weight:900}
|
||||
@@ -0,0 +1,94 @@
|
||||
const store = require('../../utils/store.js');
|
||||
const api = require('../../utils/api.js');
|
||||
|
||||
const TAG_CLASS = { 求助: 'warn', 经验: 'blue', 精选: 'purple', 避坑: 'red', 晒宠: '' };
|
||||
|
||||
function mapPost(p) {
|
||||
const tags = p.tags || [];
|
||||
const tag = tags[0] || '';
|
||||
return {
|
||||
id: p.id,
|
||||
author_name: p.author_name,
|
||||
author_emoji: p.author_emoji || '🐾',
|
||||
content: p.content,
|
||||
images: p.images || [],
|
||||
like_count: p.like_count,
|
||||
comment_count: p.comment_count,
|
||||
liked: false,
|
||||
tag,
|
||||
tagClass: TAG_CLASS[tag] || '',
|
||||
};
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pet: {},
|
||||
feedTabs: ['推荐', '关注', '新手求助', '晒宠', '经验'],
|
||||
feedIdx: 0,
|
||||
posts: [],
|
||||
sheetShow: false,
|
||||
sheetType: '',
|
||||
sheetPostId: 0,
|
||||
},
|
||||
onLoad() {
|
||||
this._unsub = store.subscribe((pet) => this.setData({ pet }));
|
||||
},
|
||||
onUnload() {
|
||||
if (this._unsub) this._unsub();
|
||||
},
|
||||
onShow() {
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
this.getTabBar().setData({ selected: 4 });
|
||||
}
|
||||
store
|
||||
.ready()
|
||||
.then(() => {
|
||||
this.setData({ pet: store.getPet() });
|
||||
this.loadPosts();
|
||||
})
|
||||
.catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' }));
|
||||
},
|
||||
loadPosts() {
|
||||
const tab = this.data.feedTabs[this.data.feedIdx];
|
||||
api
|
||||
.listPosts(tab, 1)
|
||||
.then((page) => this.setData({ posts: (page.list || []).map(mapPost) }))
|
||||
.catch(() => {});
|
||||
},
|
||||
switchFeedTab(e) {
|
||||
this.setData({ feedIdx: e.currentTarget.dataset.index });
|
||||
this.loadPosts();
|
||||
},
|
||||
likePost(e) {
|
||||
const i = e.currentTarget.dataset.index;
|
||||
const post = this.data.posts[i];
|
||||
api
|
||||
.likePost(post.id)
|
||||
.then((res) => {
|
||||
this.setData({ [`posts[${i}].like_count`]: res.like_count, [`posts[${i}].liked`]: true });
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
openComments(e) {
|
||||
this.setData({ sheetPostId: e.currentTarget.dataset.id, sheetType: 'comments', sheetShow: true });
|
||||
},
|
||||
openSheet(e) {
|
||||
this.setData({ sheetType: e.currentTarget.dataset.type, sheetPostId: 0, sheetShow: true });
|
||||
},
|
||||
closeSheet() {
|
||||
this.setData({ sheetShow: false });
|
||||
},
|
||||
onPosted() {
|
||||
this.setData({ sheetShow: false });
|
||||
this.loadPosts();
|
||||
},
|
||||
onCommented() {
|
||||
this.loadPosts();
|
||||
},
|
||||
goLearn() {
|
||||
wx.navigateTo({ url: '/pages/learn/learn' });
|
||||
},
|
||||
onFab() {
|
||||
this.setData({ sheetType: 'ai', sheetPostId: 0, sheetShow: true });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"nav-bar": "/components/nav-bar/nav-bar",
|
||||
"bottom-sheet": "/components/bottom-sheet/bottom-sheet",
|
||||
"fab": "/components/fab/fab"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<nav-bar title="宠友圈"></nav-bar>
|
||||
|
||||
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view class="page-body">
|
||||
<view class="page-title">
|
||||
<view class="pt-h2">宠友圈</view>
|
||||
<view class="pt-p">和宠友交流、晒宠、求助与经验分享。</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="feed-tabs" scroll-x="true" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view wx:for="{{feedTabs}}" wx:key="*this"
|
||||
class="feed-tab {{feedIdx === index ? 'active' : ''}}" data-index="{{index}}" bindtap="switchFeedTab">{{item}}</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="compose-card" data-type="createPost" bindtap="openSheet">
|
||||
<view class="post-avatar">{{pet.emoji}}</view>
|
||||
<view class="compose-input">分享一下今天和 {{pet.name}} 的日常...</view>
|
||||
<button class="btn btn-primary btn-small" catchtap="openSheet" data-type="createPost">发布</button>
|
||||
</view>
|
||||
|
||||
<view wx:for="{{posts}}" wx:key="id" class="post-card">
|
||||
<view class="post-head">
|
||||
<view class="post-avatar">{{item.author_emoji}}</view>
|
||||
<view class="post-user"><text class="pu-b">{{item.author_name}}</text><text class="pu-s">宠友</text></view>
|
||||
<view wx:if="{{item.tag}}" class="tag {{item.tagClass}}">{{item.tag}}</view>
|
||||
</view>
|
||||
<view class="post-content">{{item.content}}</view>
|
||||
<view wx:if="{{item.images.length}}" class="photo-grid">
|
||||
<view wx:for="{{item.images}}" wx:for-item="img" wx:key="*this" class="photo-tile">{{img}}</view>
|
||||
</view>
|
||||
<view class="post-actions">
|
||||
<view class="pa-btn {{item.liked ? 'liked' : ''}}" data-index="{{index}}" bindtap="likePost">{{item.liked ? '♥' : '♡'}} {{item.like_count}}</view>
|
||||
<view class="pa-btn" data-id="{{item.id}}" bindtap="openComments">💬 {{item.comment_count}}</view>
|
||||
<view class="pa-btn" data-type="sharePost" bindtap="openSheet">↗ 分享</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{posts.length === 0}}" class="empty-hint">还没有帖子,来发第一条吧 🐾</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<fab bind:tap="onFab"></fab>
|
||||
<bottom-sheet show="{{sheetShow}}" type="{{sheetType}}" post-id="{{sheetPostId}}"
|
||||
bind:close="closeSheet" bind:posted="onPosted" bind:commented="onCommented"></bottom-sheet>
|
||||
@@ -0,0 +1,48 @@
|
||||
.page-body{padding:8rpx 40rpx calc(210rpx + env(safe-area-inset-bottom))}
|
||||
.pt-h2{font-size:46rpx;letter-spacing:-.6rpx;font-weight:800}
|
||||
.pt-p{color:var(--muted);font-size:26rpx;margin-top:10rpx}
|
||||
|
||||
.feed-tabs{white-space:nowrap;padding:0 0 24rpx}
|
||||
.feed-tab{
|
||||
display:inline-block;border:1rpx solid var(--line);background:#fff;border-radius:999rpx;
|
||||
height:68rpx;line-height:68rpx;padding:0 26rpx;color:var(--muted);font-size:26rpx;font-weight:900;
|
||||
box-shadow:0 12rpx 32rpx rgba(70,48,25,.05);margin-right:16rpx;
|
||||
}
|
||||
.feed-tab.active{color:var(--primary-dark);background:#FFF4E4;border-color:#FFD39A}
|
||||
|
||||
.compose-card{
|
||||
display:flex;gap:20rpx;align-items:center;background:#fff;border-radius:44rpx;
|
||||
box-shadow:var(--shadow);padding:24rpx;margin-bottom:28rpx;
|
||||
}
|
||||
.compose-input{
|
||||
flex:1;height:76rpx;line-height:76rpx;border-radius:999rpx;background:#FBFAF8;
|
||||
border:1rpx solid var(--line);color:var(--muted);padding:0 26rpx;font-size:26rpx;
|
||||
overflow:hidden;white-space:nowrap;text-overflow:ellipsis;
|
||||
}
|
||||
|
||||
.post-card{background:#fff;border-radius:48rpx;box-shadow:var(--shadow);padding:30rpx;margin-bottom:28rpx}
|
||||
.post-head{display:flex;align-items:center;gap:20rpx;margin-bottom:22rpx}
|
||||
.post-avatar{
|
||||
width:84rpx;height:84rpx;border-radius:32rpx;background:#FFF0D8;
|
||||
display:flex;align-items:center;justify-content:center;font-size:44rpx;flex:none;
|
||||
}
|
||||
.post-user{flex:1}
|
||||
.pu-b{font-size:28rpx;display:block;font-weight:700}
|
||||
.pu-s{font-size:24rpx;color:var(--muted)}
|
||||
.post-content{font-size:28rpx;line-height:1.62;color:#443D37;margin-bottom:24rpx}
|
||||
|
||||
.photo-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12rpx;margin-bottom:24rpx}
|
||||
.photo-tile{
|
||||
aspect-ratio:1;border-radius:32rpx;background:linear-gradient(135deg,#FFE6BA,#FFF8ED);
|
||||
display:flex;align-items:center;justify-content:center;font-size:60rpx;
|
||||
}
|
||||
|
||||
.post-actions{display:flex;justify-content:space-around;border-top:1rpx solid var(--line);padding-top:20rpx}
|
||||
.pa-btn{color:var(--muted);font-weight:900;font-size:26rpx}
|
||||
.pa-btn.liked{color:var(--red)}
|
||||
|
||||
.empty-hint{text-align:center;color:var(--muted);font-size:26rpx;padding:80rpx 0}
|
||||
|
||||
/* 页面不自身滚动,改由 page-scroll 承载滚动(隐藏滚动条)*/
|
||||
page{height:100vh;overflow:hidden;display:flex;flex-direction:column}
|
||||
.page-scroll{flex:1;min-height:0}
|
||||
@@ -0,0 +1,130 @@
|
||||
const store = require('../../utils/store.js');
|
||||
const api = require('../../utils/api.js');
|
||||
|
||||
function mapTask(t) {
|
||||
return {
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
sub: t.description,
|
||||
priority: t.priority,
|
||||
done: t.done,
|
||||
sheet: t.sheet_type,
|
||||
};
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pet: {},
|
||||
summary: { greeting: '', insights: [], week: [], advice: '', health_pct: 0, health_status: '正常' },
|
||||
tasks: [],
|
||||
firstArticle: null,
|
||||
sheetShow: false,
|
||||
sheetType: '',
|
||||
},
|
||||
onLoad() {
|
||||
this._unsub = store.subscribe((pet) => {
|
||||
this.setData({ pet });
|
||||
this.loadAll();
|
||||
});
|
||||
},
|
||||
onUnload() {
|
||||
if (this._unsub) this._unsub();
|
||||
},
|
||||
onShow() {
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
this.getTabBar().setData({ selected: 0 });
|
||||
}
|
||||
store
|
||||
.ready()
|
||||
.then(() => {
|
||||
this.setData({ pet: store.getPet() });
|
||||
this.loadAll();
|
||||
})
|
||||
.catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' }));
|
||||
},
|
||||
loadAll() {
|
||||
this.loadSummary();
|
||||
this.loadTasks();
|
||||
if (!this.data.firstArticle) {
|
||||
api
|
||||
.listArticles()
|
||||
.then((list) => {
|
||||
if (list && list[0]) {
|
||||
this.setData({ firstArticle: { icon: list[0].icon, title: list[0].title, desc: list[0].description } });
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
},
|
||||
loadSummary() {
|
||||
const id = store.currentPetId();
|
||||
if (!id) return;
|
||||
api
|
||||
.homeSummary(id)
|
||||
.then((summary) => this.setData({ summary }))
|
||||
.catch(() => {});
|
||||
},
|
||||
loadTasks() {
|
||||
const id = store.currentPetId();
|
||||
if (!id) return;
|
||||
api
|
||||
.getTasks(id)
|
||||
.then((tasks) => this.setData({ tasks: (tasks || []).map(mapTask) }))
|
||||
.catch(() => {});
|
||||
},
|
||||
onTapTask(e) {
|
||||
const i = e.currentTarget.dataset.index;
|
||||
const t = this.data.tasks[i];
|
||||
if (t.sheet) {
|
||||
this.openSheetType(t.sheet);
|
||||
return;
|
||||
}
|
||||
api
|
||||
.toggleTask(t.id)
|
||||
.then((res) => {
|
||||
this.setData({ [`tasks[${i}].done`]: res.done });
|
||||
this.loadSummary();
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
completeTasks() {
|
||||
const id = store.currentPetId();
|
||||
if (!id) return;
|
||||
api
|
||||
.completeAllTasks(id)
|
||||
.then((tasks) => {
|
||||
this.setData({ tasks: (tasks || []).map(mapTask) });
|
||||
this.loadSummary();
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
openSheet(e) {
|
||||
this.openSheetType(e.currentTarget.dataset.type);
|
||||
},
|
||||
openSheetType(type) {
|
||||
this.setData({ sheetType: type, sheetShow: true });
|
||||
},
|
||||
closeSheet() {
|
||||
this.setData({ sheetShow: false });
|
||||
this.loadAll();
|
||||
},
|
||||
onSheetSave() {},
|
||||
onAddPet() {
|
||||
this.openSheetType('addPet');
|
||||
},
|
||||
onEditPet() {
|
||||
this.openSheetType('editPet');
|
||||
},
|
||||
onFab() {
|
||||
this.openSheetType('ai');
|
||||
},
|
||||
goProfile() {
|
||||
wx.navigateTo({ url: '/pages/profile/profile' });
|
||||
},
|
||||
goLearn() {
|
||||
wx.navigateTo({ url: '/pages/learn/learn' });
|
||||
},
|
||||
goRecord() {
|
||||
wx.switchTab({ url: '/pages/record/record' });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"nav-bar": "/components/nav-bar/nav-bar",
|
||||
"pet-switch": "/components/pet-switch/pet-switch",
|
||||
"bottom-sheet": "/components/bottom-sheet/bottom-sheet",
|
||||
"fab": "/components/fab/fab"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<nav-bar title="毛孩子计划"></nav-bar>
|
||||
|
||||
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view class="page-body">
|
||||
<view class="top-row">
|
||||
<view class="greeting">
|
||||
<view class="greet-h2">{{summary.greeting || '你好'}}</view>
|
||||
<view class="greet-p">今天也要照顾好 {{pet.name}} 🐾</view>
|
||||
</view>
|
||||
<view class="icon-stack">
|
||||
<view class="icon-btn" data-type="reminders" bindtap="openSheet">🔔</view>
|
||||
<view class="icon-btn" bindtap="goProfile">⚙</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<pet-switch bind:add="onAddPet"></pet-switch>
|
||||
|
||||
<view class="card hero-card">
|
||||
<view class="pet-row" bindtap="onEditPet">
|
||||
<view class="avatar">{{pet.emoji}}</view>
|
||||
<view class="pet-main">
|
||||
<view class="pet-name-b">{{pet.name}}</view>
|
||||
<view class="pet-meta">{{pet.age}}|{{pet.weight}}|{{pet.stage}}</view>
|
||||
</view>
|
||||
<view class="tag">{{summary.health_status || '正常'}}</view>
|
||||
</view>
|
||||
<view class="progress-row"><text>本周健康完成度</text><text>{{summary.health_pct}}%</text></view>
|
||||
<view class="bar"><view class="bar-fill" style="width:{{summary.health_pct}}%"></view></view>
|
||||
<scroll-view wx:if="{{summary.insights.length}}" class="insight-row" scroll-x="true" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view wx:for="{{summary.insights}}" wx:key="text" class="insight"><text class="insight-b">{{item.bold}}</text>{{item.text}}</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="date-strip">
|
||||
<view wx:for="{{summary.week}}" wx:key="day" class="day {{item.active ? 'active' : ''}} {{item.has_dot ? 'has-dot' : ''}}">
|
||||
<text>{{item.weekday}}</text><text class="day-b">{{item.day}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="section-head">
|
||||
<view class="sh-title">今日任务</view>
|
||||
<view class="link" bindtap="completeTasks">全部完成</view>
|
||||
</view>
|
||||
<view class="task-list">
|
||||
<view wx:for="{{tasks}}" wx:key="title" class="task {{item.done ? 'done' : ''}}" data-index="{{index}}" bindtap="onTapTask">
|
||||
<view class="circle">✓</view>
|
||||
<view class="task-text"><text class="tt-b">{{item.title}}</text><view class="tt-p">{{item.sub}}</view></view>
|
||||
<view wx:if="{{item.priority}}" class="priority">{{item.priority}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="section-head">
|
||||
<view class="sh-title">快速记录</view>
|
||||
<view class="link" bindtap="goRecord">更多</view>
|
||||
</view>
|
||||
<view class="quick-grid">
|
||||
<view class="quick" data-type="weight" bindtap="openSheet"><text class="q-ic">⚖️</text>体重</view>
|
||||
<view class="quick" data-type="poop" bindtap="openSheet"><text class="q-ic">💩</text>便便</view>
|
||||
<view class="quick" data-type="food" bindtap="openSheet"><text class="q-ic">🍽️</text>饮食</view>
|
||||
<view class="quick" data-type="symptom" bindtap="openSheet"><text class="q-ic">🤒</text>异常</view>
|
||||
<view class="quick" data-type="cost" bindtap="openSheet"><text class="q-ic">💰</text>消费</view>
|
||||
<view class="quick" data-type="medicine" bindtap="openSheet"><text class="q-ic">💊</text>用药</view>
|
||||
<view class="quick" data-type="photo" bindtap="openSheet"><text class="q-ic">📷</text>照片</view>
|
||||
<view class="quick" data-type="vaccine" bindtap="openSheet"><text class="q-ic">💉</text>疫苗</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card ai-banner">
|
||||
<view class="section-head">
|
||||
<view class="sh-title">AI 今日建议</view>
|
||||
<view class="link" data-type="ai" bindtap="openSheet">问问 AI</view>
|
||||
</view>
|
||||
<view class="ai-text">{{summary.advice || '正在生成今日建议…'}}</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="section-head">
|
||||
<view class="sh-title">新手内容</view>
|
||||
<view class="link" bindtap="goLearn">查看</view>
|
||||
</view>
|
||||
<view wx:if="{{firstArticle}}" class="article-card no-shadow" style="margin-bottom:0" bindtap="goLearn">
|
||||
<view class="article-icon">{{firstArticle.icon}}</view>
|
||||
<view><text class="ac-b">{{firstArticle.title}}</text><view class="ac-p">{{firstArticle.desc}}</view></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<fab bind:tap="onFab"></fab>
|
||||
<bottom-sheet show="{{sheetShow}}" type="{{sheetType}}" bind:close="closeSheet" bind:save="onSheetSave"></bottom-sheet>
|
||||
@@ -0,0 +1,59 @@
|
||||
.page-body{padding:8rpx 36rpx calc(210rpx + env(safe-area-inset-bottom))}
|
||||
|
||||
.top-row{display:flex;align-items:center;justify-content:space-between;margin:4rpx 0 28rpx}
|
||||
.greet-h2{font-size:44rpx;line-height:1.15;letter-spacing:-.8rpx;font-weight:800}
|
||||
.greet-p{font-size:26rpx;color:var(--muted);margin-top:10rpx}
|
||||
.icon-stack{display:flex;gap:18rpx}
|
||||
.icon-btn{
|
||||
width:80rpx;height:80rpx;border-radius:50%;background:#fff;box-shadow:var(--shadow);
|
||||
font-size:36rpx;display:flex;align-items:center;justify-content:center;
|
||||
}
|
||||
|
||||
.hero-card{
|
||||
background:
|
||||
radial-gradient(circle at 95% 16%, rgba(245,169,71,.25), transparent 30%),
|
||||
linear-gradient(135deg,#FFFFFF,#FFF5E6);
|
||||
min-height:340rpx;
|
||||
}
|
||||
.pet-row{display:flex;align-items:center;gap:26rpx;margin-bottom:28rpx}
|
||||
.pet-main{flex:1}
|
||||
.pet-name-b{font-size:42rpx;letter-spacing:-.4rpx;font-weight:800}
|
||||
.pet-meta{color:var(--muted);font-size:26rpx;margin-top:8rpx}
|
||||
|
||||
.insight-row{white-space:nowrap;margin-top:24rpx}
|
||||
.insight{
|
||||
display:inline-block;min-width:224rpx;border-radius:32rpx;
|
||||
background:rgba(255,255,255,.72);border:1rpx solid rgba(255,255,255,.9);
|
||||
padding:20rpx;font-size:24rpx;margin-right:16rpx;vertical-align:top;
|
||||
}
|
||||
.insight-b{display:block;font-size:28rpx;margin-bottom:8rpx;font-weight:900}
|
||||
|
||||
.date-strip{display:grid;grid-template-columns:repeat(7,1fr);gap:14rpx;margin-bottom:28rpx}
|
||||
.day{
|
||||
background:#fff;border-radius:32rpx;min-height:116rpx;text-align:center;padding:16rpx 8rpx;
|
||||
border:1rpx solid var(--line);color:var(--muted);font-size:22rpx;
|
||||
}
|
||||
.day-b{display:block;color:var(--text);font-size:30rpx;margin-top:8rpx;font-weight:700}
|
||||
.day.active{border-color:#FFD39A;background:#FFF4E4;color:var(--primary-dark)}
|
||||
.day.has-dot::after{content:"";display:block;width:10rpx;height:10rpx;border-radius:50%;background:var(--green);margin:8rpx auto 0}
|
||||
|
||||
.tt-b{font-size:28rpx;font-weight:700}
|
||||
.tt-p{font-size:24rpx;color:var(--muted);margin-top:8rpx;line-height:1.45}
|
||||
|
||||
.quick-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:18rpx}
|
||||
.quick{
|
||||
background:#FBFAF8;border-radius:32rpx;min-height:132rpx;
|
||||
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12rpx;
|
||||
font-weight:900;color:var(--text);font-size:24rpx;
|
||||
}
|
||||
.q-ic{font-size:44rpx}
|
||||
|
||||
.ai-banner{background:linear-gradient(135deg,#F2FAF6,#FFFFFF)}
|
||||
.ai-text{color:#59635D;font-size:28rpx;line-height:1.6}
|
||||
|
||||
.ac-b{font-size:30rpx;font-weight:700}
|
||||
.ac-p{color:var(--muted);font-size:24rpx;line-height:1.45;margin-top:10rpx}
|
||||
|
||||
/* 页面不自身滚动,改由 page-scroll 承载滚动(隐藏滚动条)*/
|
||||
page{height:100vh;overflow:hidden;display:flex;flex-direction:column}
|
||||
.page-scroll{flex:1;min-height:0}
|
||||
@@ -0,0 +1,35 @@
|
||||
const store = require('../../utils/store.js');
|
||||
const api = require('../../utils/api.js');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
articles: [],
|
||||
sheetShow: false,
|
||||
sheetType: '',
|
||||
},
|
||||
onLoad() {
|
||||
store
|
||||
.ready()
|
||||
.then(() => api.listArticles())
|
||||
.then((list) => {
|
||||
this.setData({
|
||||
articles: (list || []).map((a) => ({
|
||||
icon: a.icon,
|
||||
title: a.title,
|
||||
desc: a.description,
|
||||
type: a.related_sheet_type || 'vaccine',
|
||||
})),
|
||||
});
|
||||
})
|
||||
.catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' }));
|
||||
},
|
||||
openSheet(e) {
|
||||
this.setData({ sheetType: e.currentTarget.dataset.type, sheetShow: true });
|
||||
},
|
||||
closeSheet() {
|
||||
this.setData({ sheetShow: false });
|
||||
},
|
||||
onFab() {
|
||||
this.setData({ sheetType: 'ai', sheetShow: true });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"nav-bar": "/components/nav-bar/nav-bar",
|
||||
"bottom-sheet": "/components/bottom-sheet/bottom-sheet",
|
||||
"fab": "/components/fab/fab"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<nav-bar title="新手知识" show-back="{{true}}"></nav-bar>
|
||||
|
||||
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view class="page-body">
|
||||
<view class="page-title">
|
||||
<view class="pt-h2">新手养宠知识</view>
|
||||
<view class="pt-p">新手养宠知识,看完顺手生成你的计划。</view>
|
||||
</view>
|
||||
|
||||
<view wx:for="{{articles}}" wx:key="type" class="article-card" data-type="{{item.type}}" bindtap="openSheet">
|
||||
<view class="article-icon">{{item.icon}}</view>
|
||||
<view><text class="ac-b">{{item.title}}</text><view class="ac-p">{{item.desc}}</view></view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<fab bind:tap="onFab"></fab>
|
||||
<bottom-sheet show="{{sheetShow}}" type="{{sheetType}}" bind:close="closeSheet"></bottom-sheet>
|
||||
@@ -0,0 +1,9 @@
|
||||
.page-body{padding:8rpx 40rpx calc(60rpx + env(safe-area-inset-bottom))}
|
||||
.pt-h2{font-size:46rpx;letter-spacing:-.6rpx;font-weight:800}
|
||||
.pt-p{color:var(--muted);font-size:26rpx;margin-top:10rpx}
|
||||
.ac-b{font-size:30rpx;font-weight:700}
|
||||
.ac-p{color:var(--muted);font-size:24rpx;line-height:1.45;margin-top:10rpx}
|
||||
|
||||
/* 页面不自身滚动,改由 page-scroll 承载滚动(隐藏滚动条)*/
|
||||
page{height:100vh;overflow:hidden;display:flex;flex-direction:column}
|
||||
.page-scroll{flex:1;min-height:0}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
const store = require('../../utils/store.js');
|
||||
const api = require('../../utils/api.js');
|
||||
|
||||
function buildCells(year, month, taskedDays, today) {
|
||||
const first = new Date(year, month - 1, 1);
|
||||
const lead = (first.getDay() + 6) % 7; // 周一为一周起点
|
||||
const daysInMonth = new Date(year, month, 0).getDate();
|
||||
const set = {};
|
||||
(taskedDays || []).forEach((d) => (set[d] = true));
|
||||
const cells = [];
|
||||
for (let i = 0; i < lead; i++) cells.push({ day: 0 });
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
cells.push({ day: d, tasked: !!set[d], today: d === today });
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pet: {},
|
||||
planView: 'timeline',
|
||||
plan: null,
|
||||
calCells: [],
|
||||
calLabel: '',
|
||||
calLoaded: false,
|
||||
aiInput: '',
|
||||
aiPlan: null,
|
||||
aiBusy: false,
|
||||
sheetShow: false,
|
||||
sheetType: '',
|
||||
},
|
||||
onLoad() {
|
||||
this._unsub = store.subscribe((pet) => {
|
||||
this.setData({ pet, calLoaded: false });
|
||||
this.loadTimeline();
|
||||
if (this.data.planView === 'calendar') this.loadCalendar();
|
||||
});
|
||||
},
|
||||
onUnload() {
|
||||
if (this._unsub) this._unsub();
|
||||
},
|
||||
onShow() {
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
this.getTabBar().setData({ selected: 1 });
|
||||
}
|
||||
store
|
||||
.ready()
|
||||
.then(() => {
|
||||
this.setData({ pet: store.getPet() });
|
||||
this.loadTimeline();
|
||||
})
|
||||
.catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' }));
|
||||
},
|
||||
loadTimeline() {
|
||||
const id = store.currentPetId();
|
||||
if (!id) return;
|
||||
api
|
||||
.getPlan(id)
|
||||
.then((plan) => this.setData({ plan }))
|
||||
.catch(() => this.setData({ plan: null }));
|
||||
},
|
||||
loadCalendar() {
|
||||
const id = store.currentPetId();
|
||||
if (!id) return;
|
||||
api
|
||||
.planCalendar(id)
|
||||
.then((res) => {
|
||||
this.setData({
|
||||
calCells: buildCells(res.year, res.month, res.tasked_days, res.today),
|
||||
calLabel: `${res.year} 年 ${res.month} 月`,
|
||||
calLoaded: true,
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
switchPlanView(e) {
|
||||
const view = e.currentTarget.dataset.view;
|
||||
this.setData({ planView: view });
|
||||
if (view === 'calendar' && !this.data.calLoaded) this.loadCalendar();
|
||||
},
|
||||
// 计划明细勾选
|
||||
toggleTimelineTask(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
api.togglePlanTask(id).then(() => this.loadTimeline()).catch(() => {});
|
||||
},
|
||||
// AI 计划
|
||||
onAiInput(e) {
|
||||
this.setData({ aiInput: e.detail.value });
|
||||
},
|
||||
genAIPlan() {
|
||||
const id = store.currentPetId();
|
||||
const input = (this.data.aiInput || '').trim();
|
||||
if (!id || !input) {
|
||||
wx.showToast({ title: '先描述一下情况', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.setData({ aiBusy: true });
|
||||
wx.showLoading({ title: 'AI 生成中...' });
|
||||
api
|
||||
.createAIPlan(id, input)
|
||||
.then((plan) => {
|
||||
wx.hideLoading();
|
||||
this.setData({ aiPlan: plan, aiBusy: false });
|
||||
})
|
||||
.catch((err) => {
|
||||
wx.hideLoading();
|
||||
this.setData({ aiBusy: false });
|
||||
wx.showToast({ title: err.message || '生成失败', icon: 'none' });
|
||||
});
|
||||
},
|
||||
applyAIPlan() {
|
||||
const p = this.data.aiPlan;
|
||||
if (!p) return;
|
||||
api
|
||||
.applyAIPlan(p.id)
|
||||
.then(() => wx.showToast({ title: '已加入我的计划', icon: 'success' }))
|
||||
.catch((err) => wx.showToast({ title: err.message || '加入失败', icon: 'none' }));
|
||||
},
|
||||
openSheet(e) {
|
||||
const type = e.currentTarget.dataset.type;
|
||||
if (!type) return; // 无关联动作的计划项只可勾选,不弹层
|
||||
this.setData({ sheetType: type, sheetShow: true });
|
||||
},
|
||||
closeSheet() {
|
||||
this.setData({ sheetShow: false });
|
||||
},
|
||||
onAddPet() {
|
||||
this.setData({ sheetType: 'addPet', sheetShow: true });
|
||||
},
|
||||
onFab() {
|
||||
this.setData({ sheetType: 'ai', sheetShow: true });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"nav-bar": "/components/nav-bar/nav-bar",
|
||||
"pet-switch": "/components/pet-switch/pet-switch",
|
||||
"bottom-sheet": "/components/bottom-sheet/bottom-sheet",
|
||||
"fab": "/components/fab/fab"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<nav-bar title="养宠计划"></nav-bar>
|
||||
|
||||
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view class="page-body">
|
||||
<view class="page-title">
|
||||
<view class="pt-h2">养宠计划</view>
|
||||
<view class="pt-p">按宠物查看路线图、日历与 AI 计划。</view>
|
||||
</view>
|
||||
|
||||
<pet-switch bind:add="onAddPet"></pet-switch>
|
||||
|
||||
<view class="inline-tabs">
|
||||
<view class="it-btn {{planView === 'timeline' ? 'active' : ''}}" data-view="timeline" bindtap="switchPlanView">路线图</view>
|
||||
<view class="it-btn {{planView === 'calendar' ? 'active' : ''}}" data-view="calendar" bindtap="switchPlanView">日历</view>
|
||||
<view class="it-btn {{planView === 'aiPlan' ? 'active' : ''}}" data-view="aiPlan" bindtap="switchPlanView">AI 计划</view>
|
||||
</view>
|
||||
|
||||
<!-- 路线图 -->
|
||||
<view wx:if="{{planView === 'timeline'}}">
|
||||
<view class="card">
|
||||
<view class="section-head">
|
||||
<view class="sh-title">{{pet.name}}的 30 天计划</view>
|
||||
<view class="tag warn">{{plan.completion_pct || 0}}%</view>
|
||||
</view>
|
||||
<view class="progress-row"><text>完成度</text><text>{{plan.completion_pct || 0}}%</text></view>
|
||||
<view class="bar"><view class="bar-fill" style="width:{{plan.completion_pct || 0}}%"></view></view>
|
||||
</view>
|
||||
<view class="timeline">
|
||||
<view wx:for="{{plan.tasks}}" wx:key="id" class="time-block">
|
||||
<view class="time-label">{{item.day_label}}</view>
|
||||
<view class="plan-task {{item.done ? 'done' : ''}}">
|
||||
<view class="pk-check" catchtap="toggleTimelineTask" data-id="{{item.id}}">{{item.done ? '✓' : ''}}</view>
|
||||
<view class="pk-body" data-type="{{item.sheet_type}}" bindtap="openSheet">
|
||||
<text class="pk-b">{{item.title}}</text><view class="pk-p">{{item.description}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{!plan}}" class="pt-p" style="padding:20rpx 0">暂无计划</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日历 -->
|
||||
<view wx:elif="{{planView === 'calendar'}}">
|
||||
<view class="calendar">
|
||||
<view class="cal-head"><text>{{calLabel}}</text><view class="link" data-type="reminders" bindtap="openSheet">提醒</view></view>
|
||||
<view class="cal-grid">
|
||||
<text class="cal-w">一</text><text class="cal-w">二</text><text class="cal-w">三</text><text class="cal-w">四</text><text class="cal-w">五</text><text class="cal-w">六</text><text class="cal-w">日</text>
|
||||
<view wx:for="{{calCells}}" wx:key="index"
|
||||
class="cal-cell {{item.today ? 'today' : ''}} {{item.tasked ? 'tasked' : ''}} {{item.day === 0 ? 'blank' : ''}}">{{item.day || ''}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI 计划 -->
|
||||
<view wx:elif="{{planView === 'aiPlan'}}">
|
||||
<view class="card">
|
||||
<view class="section-head"><view class="sh-title">AI 计划模式</view><view class="tag purple">Plan</view></view>
|
||||
<view class="ai-plan-tip">描述 {{pet.name}} 的近况(阶段、近期变化、担心的问题),AI 会提取关键信息并生成可执行计划。</view>
|
||||
<textarea class="textarea" placeholder="例如:4 个月,刚换粮有点软便,也快打第二针疫苗" placeholder-class="placeholder" value="{{aiInput}}" bindinput="onAiInput"></textarea>
|
||||
<button class="btn btn-primary btn-block" style="margin-top:16rpx" bindtap="genAIPlan" disabled="{{aiBusy}}">{{aiBusy ? '生成中…' : '生成计划'}}</button>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{aiPlan}}" class="card">
|
||||
<view class="section-head"><view class="sh-title">已提取信息</view></view>
|
||||
<view class="extracted">
|
||||
<view class="extract"><text class="ex-b">阶段</text>{{aiPlan.extracted.stage}}</view>
|
||||
<view class="extract"><text class="ex-b">风险</text>{{aiPlan.extracted.risk}}</view>
|
||||
<view class="extract"><text class="ex-b">重点</text>{{aiPlan.extracted.priority}}</view>
|
||||
<view class="extract"><text class="ex-b">提醒</text>{{aiPlan.extracted.reminder}}</view>
|
||||
</view>
|
||||
<view class="section-head" style="margin-top:20rpx"><view class="sh-title">生成的计划</view></view>
|
||||
<view class="task-list">
|
||||
<view wx:for="{{aiPlan.tasks}}" wx:key="id" class="task">
|
||||
<view class="circle"></view>
|
||||
<view class="task-text"><text class="tt-b">{{item.day_label}}· {{item.title}}</text><view class="tt-p">{{item.description}}</view></view>
|
||||
</view>
|
||||
</view>
|
||||
<button class="btn btn-primary btn-block" style="margin-top:16rpx" bindtap="applyAIPlan">加入我的计划</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<fab bind:tap="onFab"></fab>
|
||||
<bottom-sheet show="{{sheetShow}}" type="{{sheetType}}" bind:close="closeSheet"></bottom-sheet>
|
||||
@@ -0,0 +1,54 @@
|
||||
.page-body{padding:8rpx 40rpx calc(210rpx + env(safe-area-inset-bottom))}
|
||||
.pt-h2{font-size:46rpx;letter-spacing:-.6rpx;font-weight:800}
|
||||
.pt-p{color:var(--muted);font-size:26rpx;margin-top:10rpx}
|
||||
.tt-b{font-size:28rpx;font-weight:700}
|
||||
.tt-p{font-size:24rpx;color:var(--muted);margin-top:8rpx;line-height:1.45}
|
||||
.ac-b{font-size:30rpx;font-weight:700}
|
||||
.ac-p{color:var(--muted);font-size:24rpx;line-height:1.45;margin-top:10rpx}
|
||||
|
||||
.inline-tabs{display:flex;padding:8rpx;gap:8rpx;border-radius:32rpx;background:#EFE8DE;margin-bottom:28rpx}
|
||||
.it-btn{
|
||||
flex:1;height:68rpx;line-height:68rpx;text-align:center;border-radius:24rpx;
|
||||
color:var(--muted);font-weight:900;font-size:26rpx;
|
||||
}
|
||||
.it-btn.active{background:#fff;color:var(--primary-dark);box-shadow:0 12rpx 28rpx rgba(0,0,0,.06)}
|
||||
|
||||
.timeline{padding-left:36rpx;position:relative}
|
||||
.timeline::before{content:"";position:absolute;left:12rpx;top:12rpx;bottom:16rpx;width:4rpx;background:#E8DED0}
|
||||
.time-block{position:relative;margin-bottom:28rpx}
|
||||
.time-label{font-weight:900;color:var(--primary-dark);font-size:26rpx;margin-bottom:16rpx;position:relative}
|
||||
.time-label::before{
|
||||
content:"";position:absolute;left:-34rpx;top:8rpx;width:24rpx;height:24rpx;border-radius:50%;
|
||||
background:var(--primary);border:6rpx solid #fff;box-shadow:0 0 0 4rpx #F5D5A8;
|
||||
}
|
||||
.plan-task{background:#fff;border-radius:36rpx;padding:26rpx;box-shadow:var(--shadow);border:1rpx solid transparent;display:flex;gap:18rpx;align-items:flex-start}
|
||||
.plan-task.done{opacity:.55}
|
||||
.pk-check{
|
||||
width:44rpx;height:44rpx;border-radius:50%;border:4rpx solid #DED1C4;flex:none;margin-top:2rpx;
|
||||
display:flex;align-items:center;justify-content:center;color:#fff;font-size:24rpx;font-weight:900;
|
||||
}
|
||||
.plan-task.done .pk-check{background:var(--green);border-color:var(--green)}
|
||||
.pk-body{flex:1}
|
||||
.pk-b{font-size:28rpx;font-weight:700}
|
||||
.pk-p{color:var(--muted);font-size:24rpx;line-height:1.45;margin-top:10rpx}
|
||||
.ai-plan-tip{color:var(--muted);font-size:26rpx;line-height:1.6;margin-bottom:16rpx}
|
||||
|
||||
.calendar{background:#fff;border-radius:44rpx;padding:28rpx;box-shadow:var(--shadow)}
|
||||
.cal-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:20rpx;font-weight:900;font-size:30rpx}
|
||||
.cal-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:12rpx;text-align:center}
|
||||
.cal-w{font-size:22rpx;color:var(--muted);padding:8rpx 0}
|
||||
.cal-cell{
|
||||
height:76rpx;border-radius:24rpx;display:flex;align-items:center;justify-content:center;
|
||||
font-weight:800;font-size:26rpx;background:#FBFAF8;position:relative;
|
||||
}
|
||||
.cal-cell.blank{background:transparent}
|
||||
.cal-cell.today{background:#FFF3DF;color:var(--primary-dark)}
|
||||
.cal-cell.tasked::after{content:"";position:absolute;bottom:10rpx;width:10rpx;height:10rpx;border-radius:50%;background:var(--green)}
|
||||
|
||||
.extracted{display:grid;grid-template-columns:1fr 1fr;gap:16rpx;margin:24rpx 0}
|
||||
.extract{background:#FBFAF8;border-radius:30rpx;padding:20rpx;font-size:24rpx}
|
||||
.ex-b{display:block;font-size:26rpx;margin-bottom:6rpx;font-weight:700}
|
||||
|
||||
/* 页面不自身滚动,改由 page-scroll 承载滚动(隐藏滚动条)*/
|
||||
page{height:100vh;overflow:hidden;display:flex;flex-direction:column}
|
||||
.page-scroll{flex:1;min-height:0}
|
||||
@@ -0,0 +1,55 @@
|
||||
const store = require('../../utils/store.js');
|
||||
const api = require('../../utils/api.js');
|
||||
|
||||
function completion(pet) {
|
||||
if (!pet) return 0;
|
||||
const fields = ['name', 'emoji', 'type', 'gender', 'birthday', 'weight', 'stage', 'age', 'color', 'breed'];
|
||||
const filled = fields.filter((f) => pet[f]).length;
|
||||
return Math.round((filled / fields.length) * 100);
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pet: {},
|
||||
user: {},
|
||||
summary: { pets: 0, records: 0, reminders: 0, pro_status: 'none' },
|
||||
profilePct: 0,
|
||||
sheetShow: false,
|
||||
sheetType: '',
|
||||
},
|
||||
onLoad() {
|
||||
this._unsub = store.subscribe((pet) => this.setData({ pet, profilePct: completion(pet) }));
|
||||
},
|
||||
onUnload() {
|
||||
if (this._unsub) this._unsub();
|
||||
},
|
||||
onShow() {
|
||||
store
|
||||
.ready()
|
||||
.then(() => {
|
||||
const pet = store.getPet();
|
||||
this.setData({ pet, user: store.getUser() || {}, profilePct: completion(pet) });
|
||||
this.loadSummary();
|
||||
})
|
||||
.catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' }));
|
||||
},
|
||||
loadSummary() {
|
||||
api.userSummary().then((summary) => this.setData({ summary })).catch(() => {});
|
||||
},
|
||||
openSheet(e) {
|
||||
this.setData({ sheetType: e.currentTarget.dataset.type, sheetShow: true });
|
||||
},
|
||||
onAddPet() {
|
||||
this.setData({ sheetType: 'addPet', sheetShow: true });
|
||||
},
|
||||
onEditPet() {
|
||||
this.setData({ sheetType: 'editPet', sheetShow: true });
|
||||
},
|
||||
closeSheet() {
|
||||
this.setData({ sheetShow: false });
|
||||
this.loadSummary();
|
||||
},
|
||||
onFab() {
|
||||
this.setData({ sheetType: 'ai', sheetShow: true });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"nav-bar": "/components/nav-bar/nav-bar",
|
||||
"bottom-sheet": "/components/bottom-sheet/bottom-sheet",
|
||||
"fab": "/components/fab/fab"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<nav-bar title="我的" show-back="{{true}}"></nav-bar>
|
||||
|
||||
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view class="page-body">
|
||||
<view class="profile-header">
|
||||
<view class="avatar ph-avatar">{{pet.emoji}}</view>
|
||||
<view class="ph-name">{{pet.name}}</view>
|
||||
<view class="muted ph-meta">{{pet.age}}|{{pet.weight}}|{{pet.stage}}</view>
|
||||
<view wx:if="{{pet.breed || pet.color}}" class="muted ph-meta">{{pet.breed}}{{pet.breed && pet.color ? ' · ' : ''}}{{pet.color}}</view>
|
||||
</view>
|
||||
|
||||
<button class="btn btn-ghost btn-block" style="margin-bottom:28rpx" bindtap="onAddPet">+ 添加毛孩子</button>
|
||||
|
||||
<view class="profile-list">
|
||||
<view class="profile-row" bindtap="onEditPet">宠物档案(编辑)<text class="pr-val">已完善 {{profilePct}}% ›</text></view>
|
||||
<view class="profile-row">提醒设置 <text class="pr-val">{{summary.reminders}} 条</text></view>
|
||||
<view class="profile-row">健康记录 <text class="pr-val">{{summary.records}} 条</text></view>
|
||||
<view class="profile-row">多宠物管理 <text class="pr-val">{{summary.pets}} 只</text></view>
|
||||
</view>
|
||||
|
||||
<view class="profile-list">
|
||||
<view class="profile-row">会员权益 <text class="pr-val">{{summary.pro_status === 'active' ? '已开通' : '未开通'}}</text></view>
|
||||
<view class="profile-row">数据导出 <text class="pr-val">PDF</text></view>
|
||||
<view class="profile-row">意见反馈 <text class="pr-val">去填写</text></view>
|
||||
<view class="profile-row">关于我们 <text class="pr-val">v0.2</text></view>
|
||||
</view>
|
||||
|
||||
<button class="btn btn-primary btn-block" data-type="pro" bindtap="openSheet">开通 Pro</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<fab bind:tap="onFab"></fab>
|
||||
<bottom-sheet show="{{sheetShow}}" type="{{sheetType}}" bind:close="closeSheet"></bottom-sheet>
|
||||
@@ -0,0 +1,18 @@
|
||||
.page-body{padding:8rpx 40rpx calc(60rpx + env(safe-area-inset-bottom))}
|
||||
|
||||
.profile-header{text-align:center;padding:24rpx 0 32rpx}
|
||||
.ph-avatar{margin:0 auto 20rpx;width:152rpx;height:152rpx;font-size:76rpx;border-radius:56rpx}
|
||||
.ph-name{font-size:44rpx;font-weight:800}
|
||||
.ph-meta{font-size:26rpx;margin-top:10rpx}
|
||||
|
||||
.profile-list{background:#fff;border-radius:48rpx;box-shadow:var(--shadow);overflow:hidden;margin-bottom:28rpx}
|
||||
.profile-row{
|
||||
display:flex;align-items:center;justify-content:space-between;min-height:104rpx;padding:0 32rpx;
|
||||
border-bottom:1rpx solid var(--line);font-size:28rpx;font-weight:800;
|
||||
}
|
||||
.profile-row:last-child{border-bottom:0}
|
||||
.pr-val{color:var(--muted);font-weight:700;font-size:26rpx}
|
||||
|
||||
/* 页面不自身滚动,改由 page-scroll 承载滚动(隐藏滚动条)*/
|
||||
page{height:100vh;overflow:hidden;display:flex;flex-direction:column}
|
||||
.page-scroll{flex:1;min-height:0}
|
||||
@@ -0,0 +1,79 @@
|
||||
const store = require('../../utils/store.js');
|
||||
const api = require('../../utils/api.js');
|
||||
|
||||
function pad(n) {
|
||||
return n < 10 ? '0' + n : '' + n;
|
||||
}
|
||||
function fmtTime(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
const now = new Date();
|
||||
const hm = pad(d.getHours()) + ':' + pad(d.getMinutes());
|
||||
if (d.toDateString() === now.toDateString()) return '今天 ' + hm;
|
||||
const y = new Date(now.getTime() - 86400000);
|
||||
if (d.toDateString() === y.toDateString()) return '昨天 ' + hm;
|
||||
return d.getMonth() + 1 + '月' + d.getDate() + '日';
|
||||
}
|
||||
function mapRecord(r) {
|
||||
return {
|
||||
icon: r.icon || '✍️',
|
||||
title: r.title,
|
||||
desc: fmtTime(r.occurred_at) + (r.description ? '|' + r.description : ''),
|
||||
};
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pet: {},
|
||||
timeline: [],
|
||||
sheetShow: false,
|
||||
sheetType: '',
|
||||
},
|
||||
onLoad() {
|
||||
this._unsub = store.subscribe((pet) => {
|
||||
this.setData({ pet });
|
||||
this.loadRecords();
|
||||
});
|
||||
},
|
||||
onUnload() {
|
||||
if (this._unsub) this._unsub();
|
||||
},
|
||||
onShow() {
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
this.getTabBar().setData({ selected: 2 });
|
||||
}
|
||||
store
|
||||
.ready()
|
||||
.then(() => {
|
||||
this.setData({ pet: store.getPet() });
|
||||
this.loadRecords();
|
||||
})
|
||||
.catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' }));
|
||||
},
|
||||
loadRecords() {
|
||||
const id = store.currentPetId();
|
||||
if (!id) return;
|
||||
api
|
||||
.getRecords(id)
|
||||
.then((records) => this.setData({ timeline: (records || []).map(mapRecord) }))
|
||||
.catch(() => {});
|
||||
},
|
||||
openSheet(e) {
|
||||
this.openSheetType(e.currentTarget.dataset.type);
|
||||
},
|
||||
openSheetType(type) {
|
||||
this.setData({ sheetType: type, sheetShow: true });
|
||||
},
|
||||
closeSheet() {
|
||||
this.setData({ sheetShow: false });
|
||||
},
|
||||
onSheetSaved() {
|
||||
this.loadRecords();
|
||||
},
|
||||
onAddPet() {
|
||||
this.openSheetType('addPet');
|
||||
},
|
||||
onFab() {
|
||||
this.openSheetType('ai');
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"nav-bar": "/components/nav-bar/nav-bar",
|
||||
"pet-switch": "/components/pet-switch/pet-switch",
|
||||
"bottom-sheet": "/components/bottom-sheet/bottom-sheet",
|
||||
"fab": "/components/fab/fab"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<nav-bar title="快速记录"></nav-bar>
|
||||
|
||||
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view class="page-body">
|
||||
<view class="page-title">
|
||||
<view class="pt-h2">快速记录</view>
|
||||
<view class="pt-p">先选择宠物,再记录体重、饮食、异常和消费。</view>
|
||||
</view>
|
||||
|
||||
<pet-switch bind:add="onAddPet"></pet-switch>
|
||||
|
||||
<view class="record-grid">
|
||||
<view class="record-item" data-type="weight" bindtap="openSheet"><text class="ri-ic">⚖️</text>体重</view>
|
||||
<view class="record-item" data-type="food" bindtap="openSheet"><text class="ri-ic">🍽️</text>饮食</view>
|
||||
<view class="record-item" data-type="poop" bindtap="openSheet"><text class="ri-ic">💩</text>便便</view>
|
||||
<view class="record-item" data-type="symptom" bindtap="openSheet"><text class="ri-ic">🤒</text>异常</view>
|
||||
<view class="record-item" data-type="medicine" bindtap="openSheet"><text class="ri-ic">💊</text>用药</view>
|
||||
<view class="record-item" data-type="cost" bindtap="openSheet"><text class="ri-ic">💰</text>消费</view>
|
||||
<view class="record-item" data-type="vaccine" bindtap="openSheet"><text class="ri-ic">💉</text>疫苗</view>
|
||||
<view class="record-item" data-type="deworm" bindtap="openSheet"><text class="ri-ic">🛡️</text>驱虫</view>
|
||||
<view class="record-item" data-type="photo" bindtap="openSheet"><text class="ri-ic">📷</text>照片</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="section-head"><view class="sh-title">体重趋势</view><view class="link" data-type="weight" bindtap="openSheet">记录</view></view>
|
||||
<view class="mini-chart">
|
||||
<image class="chart-line" mode="scaleToFill" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMDAgMTAwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJub25lIj48cG9seWxpbmUgcG9pbnRzPSIwLDgwIDUwLDcyIDEwMCw2NiAxNTAsNTQgMjAwLDQ1IDI1MCwzMiAzMDAsMjYiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzczQkU5RCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48ZyBmaWxsPSIjNzNCRTlEIj48Y2lyY2xlIGN4PSIwIiBjeT0iODAiIHI9IjQiLz48Y2lyY2xlIGN4PSI1MCIgY3k9IjcyIiByPSI0Ii8+PGNpcmNsZSBjeD0iMTAwIiBjeT0iNjYiIHI9IjQiLz48Y2lyY2xlIGN4PSIxNTAiIGN5PSI1NCIgcj0iNCIvPjxjaXJjbGUgY3g9IjIwMCIgY3k9IjQ1IiByPSI0Ii8+PGNpcmNsZSBjeD0iMjUwIiBjeT0iMzIiIHI9IjQiLz48Y2lyY2xlIGN4PSIzMDAiIGN5PSIyNiIgcj0iNCIvPjwvZz48L3N2Zz4="></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="section-head"><view class="sh-title">健康时间轴</view><view class="link" data-type="vetSummary" bindtap="openSheet">摘要</view></view>
|
||||
<view class="health-timeline">
|
||||
<view wx:for="{{timeline}}" wx:key="index" class="health-event">
|
||||
<view class="event-dot">{{item.icon}}</view>
|
||||
<view><text class="he-b">{{item.title}}</text><view class="he-p">{{item.desc}}</view></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<fab bind:tap="onFab"></fab>
|
||||
<bottom-sheet show="{{sheetShow}}" type="{{sheetType}}" bind:close="closeSheet" bind:saved="onSheetSaved"></bottom-sheet>
|
||||
@@ -0,0 +1,32 @@
|
||||
.page-body{padding:8rpx 40rpx calc(210rpx + env(safe-area-inset-bottom))}
|
||||
.pt-h2{font-size:46rpx;letter-spacing:-.6rpx;font-weight:800}
|
||||
.pt-p{color:var(--muted);font-size:26rpx;margin-top:10rpx}
|
||||
|
||||
.record-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24rpx;margin-bottom:28rpx}
|
||||
.record-item{
|
||||
height:184rpx;background:#fff;border-radius:44rpx;box-shadow:var(--shadow);
|
||||
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16rpx;
|
||||
font-weight:900;color:var(--text);font-size:28rpx;
|
||||
}
|
||||
.ri-ic{font-size:56rpx}
|
||||
|
||||
.mini-chart{
|
||||
height:256rpx;border-radius:36rpx;margin-top:24rpx;position:relative;overflow:hidden;
|
||||
background:
|
||||
linear-gradient(180deg,rgba(115,190,157,.16),rgba(115,190,157,0)),
|
||||
repeating-linear-gradient(0deg,#fff,#fff 60rpx,#EFE8DE 62rpx);
|
||||
}
|
||||
.chart-line{position:absolute;left:36rpx;right:36rpx;top:44rpx;bottom:44rpx;width:auto;height:auto}
|
||||
|
||||
.health-timeline{display:flex;flex-direction:column;gap:20rpx}
|
||||
.health-event{display:flex;gap:20rpx;background:#FBFAF8;border-radius:32rpx;padding:24rpx}
|
||||
.event-dot{
|
||||
width:68rpx;height:68rpx;border-radius:26rpx;flex:none;
|
||||
display:flex;align-items:center;justify-content:center;background:#FFF0D7;font-size:34rpx;
|
||||
}
|
||||
.he-b{font-size:28rpx;font-weight:700}
|
||||
.he-p{color:var(--muted);font-size:24rpx;line-height:1.45;margin-top:6rpx}
|
||||
|
||||
/* 页面不自身滚动,改由 page-scroll 承载滚动(隐藏滚动条)*/
|
||||
page{height:100vh;overflow:hidden;display:flex;flex-direction:column}
|
||||
.page-scroll{flex:1;min-height:0}
|
||||
@@ -0,0 +1,62 @@
|
||||
const store = require('../../utils/store.js');
|
||||
const api = require('../../utils/api.js');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pet: {},
|
||||
report: null,
|
||||
gainText: '+0.0',
|
||||
bill: null,
|
||||
summary: null,
|
||||
sheetShow: false,
|
||||
sheetType: '',
|
||||
},
|
||||
onLoad() {
|
||||
this._unsub = store.subscribe((pet) => {
|
||||
this.setData({ pet });
|
||||
this.loadData();
|
||||
});
|
||||
},
|
||||
onUnload() {
|
||||
if (this._unsub) this._unsub();
|
||||
},
|
||||
onShow() {
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
this.getTabBar().setData({ selected: 3 });
|
||||
}
|
||||
store
|
||||
.ready()
|
||||
.then(() => {
|
||||
this.setData({ pet: store.getPet() });
|
||||
this.loadData();
|
||||
})
|
||||
.catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' }));
|
||||
},
|
||||
loadData() {
|
||||
const id = store.currentPetId();
|
||||
if (!id) return;
|
||||
Promise.all([api.weeklyReport(id), api.getBill(id, 'month'), api.healthSummary(id)])
|
||||
.then(([report, bill, summary]) => {
|
||||
const g = report.weight_gain || 0;
|
||||
this.setData({
|
||||
report,
|
||||
gainText: (g >= 0 ? '+' : '') + g.toFixed(1),
|
||||
bill,
|
||||
summary,
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
openSheet(e) {
|
||||
this.setData({ sheetType: e.currentTarget.dataset.type, sheetShow: true });
|
||||
},
|
||||
closeSheet() {
|
||||
this.setData({ sheetShow: false });
|
||||
},
|
||||
onAddPet() {
|
||||
this.setData({ sheetType: 'addPet', sheetShow: true });
|
||||
},
|
||||
onFab() {
|
||||
this.setData({ sheetType: 'ai', sheetShow: true });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"nav-bar": "/components/nav-bar/nav-bar",
|
||||
"pet-switch": "/components/pet-switch/pet-switch",
|
||||
"bottom-sheet": "/components/bottom-sheet/bottom-sheet",
|
||||
"fab": "/components/fab/fab"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<nav-bar title="成长报告"></nav-bar>
|
||||
|
||||
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view class="page-body">
|
||||
<view class="page-title">
|
||||
<view class="pt-h2">成长报告</view>
|
||||
<view class="pt-p">每只宠物的成长报告、账单与健康摘要。</view>
|
||||
</view>
|
||||
|
||||
<pet-switch bind:add="onAddPet"></pet-switch>
|
||||
|
||||
<view class="card report-card">
|
||||
<view class="section-head"><view class="sh-title">本周成长报告</view><view class="tag">{{report.health_status || '稳定成长'}}</view></view>
|
||||
<view class="rc-text">{{report.summary || '暂无本周数据'}}</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stat"><text class="stat-b">{{report.tasks_completed || 0}}</text><text class="stat-s">完成任务</text></view>
|
||||
<view class="stat"><text class="stat-b">{{gainText}}</text><text class="stat-s">kg 增长</text></view>
|
||||
<view class="stat"><text class="stat-b">{{report.high_risk_count || 0}}</text><text class="stat-s">高风险异常</text></view>
|
||||
</view>
|
||||
<view class="sheet-actions">
|
||||
<button class="btn btn-ghost" data-type="reportDetail" bindtap="openSheet">查看报告</button>
|
||||
<button class="btn btn-primary" data-type="poster" bindtap="openSheet">分享卡片</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="section-head"><view class="sh-title">健康摘要</view><view class="link" data-type="vetSummary" bindtap="openSheet">导出</view></view>
|
||||
<view class="record-row"><text class="bold">疫苗进度</text><text class="muted">{{summary.vaccine_progress || '—'}}</text></view>
|
||||
<view class="record-row"><text class="bold">驱虫状态</text><text class="muted">{{summary.deworm_status || '—'}}</text></view>
|
||||
<view class="record-row"><text class="bold">体重趋势</text><text class="muted">{{summary.weight_trend || '—'}}</text></view>
|
||||
<view class="record-row"><text class="bold">异常记录</text><text class="muted">{{summary.anomaly_count || 0}} 次</text></view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="section-head"><view class="sh-title">养宠账单</view><view class="link" data-type="cost" bindtap="openSheet">记一笔</view></view>
|
||||
<view class="stats-grid">
|
||||
<view class="stat"><text class="stat-b">¥{{bill.total || 0}}</text><text class="stat-s">本月花费</text></view>
|
||||
<view class="stat"><text class="stat-b">¥{{bill.max_single || 0}}</text><text class="stat-s">最大支出</text></view>
|
||||
<view class="stat"><text class="stat-b">{{bill.categories.length || 0}}</text><text class="stat-s">消费类别</text></view>
|
||||
</view>
|
||||
<view class="bill-bars">
|
||||
<view wx:for="{{bill.categories}}" wx:key="category" class="bill-row">
|
||||
<text class="bill-cat">{{item.category}}</text>
|
||||
<view class="mini-bar"><view class="mini-bar-fill" style="width:{{item.percent}}%"></view></view>
|
||||
<text class="bill-amt">¥{{item.amount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="section-head"><view class="sh-title">Pro 会员</view></view>
|
||||
<view class="rc-text">解锁 365 天计划、PDF 健康档案、月度报告、多宠物管理与年度账单。</view>
|
||||
<button class="btn btn-primary btn-block" style="margin-top:24rpx" data-type="pro" bindtap="openSheet">查看 Pro 权益</button>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<fab bind:tap="onFab"></fab>
|
||||
<bottom-sheet show="{{sheetShow}}" type="{{sheetType}}" bind:close="closeSheet"></bottom-sheet>
|
||||
@@ -0,0 +1,19 @@
|
||||
.page-body{padding:8rpx 40rpx calc(210rpx + env(safe-area-inset-bottom))}
|
||||
.pt-h2{font-size:46rpx;letter-spacing:-.6rpx;font-weight:800}
|
||||
.pt-p{color:var(--muted);font-size:26rpx;margin-top:10rpx}
|
||||
|
||||
.report-card{background:linear-gradient(135deg,#FFFFFF,#F2FAF6)}
|
||||
.rc-text{color:var(--muted);font-size:28rpx;line-height:1.6}
|
||||
.stat-b{display:block;font-size:36rpx;margin-bottom:8rpx;font-weight:800}
|
||||
.stat-s{font-size:22rpx;color:var(--muted)}
|
||||
|
||||
.bill-bars{display:flex;flex-direction:column;gap:20rpx;margin-top:24rpx}
|
||||
.bill-row{display:grid;grid-template-columns:88rpx 1fr 108rpx;gap:16rpx;align-items:center;font-size:24rpx}
|
||||
.bill-cat{color:var(--text)}
|
||||
.bill-amt{font-weight:900;text-align:right}
|
||||
.mini-bar{height:18rpx;border-radius:999rpx;background:#F0E7DD;overflow:hidden}
|
||||
.mini-bar-fill{display:block;height:100%;border-radius:999rpx;background:linear-gradient(90deg,var(--blue),#A9C8F6)}
|
||||
|
||||
/* 页面不自身滚动,改由 page-scroll 承载滚动(隐藏滚动条)*/
|
||||
page{height:100vh;overflow:hidden;display:flex;flex-direction:column}
|
||||
.page-scroll{flex:1;min-height:0}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"description": "毛孩子计划 - 新手养宠小程序",
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
"enhance": true,
|
||||
"postcss": true,
|
||||
"preloadBackgroundData": false,
|
||||
"minified": false,
|
||||
"newFeature": true,
|
||||
"coverView": true,
|
||||
"nodeModules": false,
|
||||
"autoAudits": false,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"scopeDataCheck": false,
|
||||
"uglifyFileName": false,
|
||||
"checkInvalidKey": true,
|
||||
"checkSiteMap": true,
|
||||
"uploadWithSourceMap": true,
|
||||
"compileHotReLoad": false,
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
},
|
||||
"compileWorklet": false,
|
||||
"packNpmManually": false,
|
||||
"packNpmRelationList": [],
|
||||
"minifyWXSS": true,
|
||||
"minifyWXML": true,
|
||||
"localPlugins": false,
|
||||
"disableUseStrict": false,
|
||||
"useCompilerPlugins": false,
|
||||
"condition": false,
|
||||
"swc": false,
|
||||
"disableSWC": true
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"libVersion": "3.5.0",
|
||||
"appid": "wx52dfc635739a9c19",
|
||||
"projectname": "pets-fe",
|
||||
"condition": {},
|
||||
"editorSetting": {
|
||||
"tabIndent": "insertSpaces",
|
||||
"tabSize": 2
|
||||
},
|
||||
"miniprogramRoot": "./",
|
||||
"simulatorPluginLibVersion": {}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"libVersion": "3.13.0",
|
||||
"projectname": "pets-fe",
|
||||
"condition": {},
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"coverView": true,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"skylineRenderEnable": false,
|
||||
"preloadBackgroundData": false,
|
||||
"autoAudits": false,
|
||||
"useApiHook": true,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"useStaticServer": false,
|
||||
"useLanDebug": false,
|
||||
"showES6CompileOption": false,
|
||||
"compileHotReLoad": true,
|
||||
"checkInvalidKey": true,
|
||||
"ignoreDevUnusedFiles": true,
|
||||
"bigPackageSizeSupport": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
|
||||
"rules": [{ "action": "allow", "page": "*" }]
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
const { request, uploadFile, setToken } = require('./request.js');
|
||||
|
||||
// wx.login 取 code
|
||||
function wxLogin() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.login({
|
||||
success: (r) => (r.code ? resolve(r.code) : reject(new Error('wx.login 未返回 code'))),
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 登录:优先微信 code2session,失败回退开发态 Mock 登录(后端 dev_login=true)
|
||||
async function login() {
|
||||
try {
|
||||
const code = await wxLogin();
|
||||
const data = await request({ url: '/api/auth/wechat', method: 'POST', data: { code } });
|
||||
setToken(data.token);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.warn('[api] 微信登录失败,回退 Mock 登录:', e && e.message);
|
||||
const data = await request({ url: '/api/auth/login', method: 'POST', data: { nickname: '开发用户' } });
|
||||
setToken(data.token);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
const api = {
|
||||
login,
|
||||
uploadFile,
|
||||
|
||||
// 用户
|
||||
getProfile: () => request({ url: '/api/user/profile' }),
|
||||
updateProfile: (body) => request({ url: '/api/user/profile', method: 'PUT', data: body }),
|
||||
|
||||
// 用户
|
||||
userSummary: () => request({ url: '/api/user/summary' }),
|
||||
|
||||
// 宠物
|
||||
getPets: () => request({ url: '/api/pets' }),
|
||||
homeSummary: (id) => request({ url: `/api/pets/${id}/home-summary` }),
|
||||
getPet: (id) => request({ url: `/api/pets/${id}` }),
|
||||
createPet: (body) => request({ url: '/api/pets', method: 'POST', data: body }),
|
||||
updatePet: (id, body) => request({ url: `/api/pets/${id}`, method: 'PUT', data: body }),
|
||||
deletePet: (id) => request({ url: `/api/pets/${id}`, method: 'DELETE' }),
|
||||
onboarding: (body) => request({ url: '/api/onboarding', method: 'POST', data: body }),
|
||||
|
||||
// 记录
|
||||
getRecords: (id, type) =>
|
||||
request({ url: `/api/pets/${id}/records${type ? `?type=${type}` : ''}` }),
|
||||
createRecord: (id, body) => request({ url: `/api/pets/${id}/records`, method: 'POST', data: body }),
|
||||
weightTrend: (id) => request({ url: `/api/pets/${id}/records/weight-trend` }),
|
||||
|
||||
// 任务
|
||||
getTasks: (id) => request({ url: `/api/pets/${id}/tasks` }),
|
||||
toggleTask: (taskId) => request({ url: `/api/tasks/${taskId}/toggle`, method: 'POST' }),
|
||||
completeAllTasks: (id) => request({ url: `/api/pets/${id}/tasks/complete-all`, method: 'POST' }),
|
||||
|
||||
// 计划
|
||||
getPlan: (id) => request({ url: `/api/pets/${id}/plan` }),
|
||||
planCalendar: (id, month) =>
|
||||
request({ url: `/api/pets/${id}/plan/calendar${month ? `?month=${month}` : ''}` }),
|
||||
createAIPlan: (id, input) => request({ url: `/api/pets/${id}/ai-plan`, method: 'POST', data: { input } }),
|
||||
applyAIPlan: (planId) => request({ url: `/api/ai-plan/${planId}/apply`, method: 'POST' }),
|
||||
togglePlanTask: (taskId) => request({ url: `/api/plan-tasks/${taskId}/toggle`, method: 'POST' }),
|
||||
|
||||
// 提醒
|
||||
getReminders: (id) => request({ url: `/api/pets/${id}/reminders` }),
|
||||
|
||||
// 报告
|
||||
weeklyReport: (id) => request({ url: `/api/pets/${id}/report/weekly` }),
|
||||
getBill: (id, period) => request({ url: `/api/pets/${id}/bill?period=${period || 'month'}` }),
|
||||
healthSummary: (id) => request({ url: `/api/pets/${id}/health-summary` }),
|
||||
getPoster: (id) => request({ url: `/api/pets/${id}/poster` }),
|
||||
|
||||
// 社区
|
||||
listPosts: (tab, page) =>
|
||||
request({ url: `/api/posts?tab=${encodeURIComponent(tab || '')}&page=${page || 1}&page_size=10` }),
|
||||
createPost: (body) => request({ url: '/api/posts', method: 'POST', data: body }),
|
||||
likePost: (id) => request({ url: `/api/posts/${id}/like`, method: 'POST' }),
|
||||
listComments: (id) => request({ url: `/api/posts/${id}/comments` }),
|
||||
createComment: (id, content) =>
|
||||
request({ url: `/api/posts/${id}/comments`, method: 'POST', data: { content } }),
|
||||
|
||||
// 文章
|
||||
listArticles: () => request({ url: '/api/articles' }),
|
||||
getArticle: (id) => request({ url: `/api/articles/${id}` }),
|
||||
|
||||
// Pro
|
||||
getPro: () => request({ url: '/api/pro' }),
|
||||
activatePro: () => request({ url: '/api/pro/activate', method: 'POST' }),
|
||||
|
||||
// AI
|
||||
aiChat: (body) => request({ url: '/api/ai/chat', method: 'POST', data: body }),
|
||||
assessSymptom: (id, body) =>
|
||||
request({ url: `/api/pets/${id}/ai/assess-symptom`, method: 'POST', data: body }),
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
@@ -0,0 +1,5 @@
|
||||
// 后端地址。本地联调:微信开发者工具需在「详情 → 本地设置」勾选「不校验合法域名」。
|
||||
// 上线时改为你的 https 域名,并在微信公众平台配置 request 合法域名。
|
||||
const BASE_URL = 'http://192.168.31.4:8080';
|
||||
|
||||
module.exports = { BASE_URL };
|
||||
@@ -0,0 +1,69 @@
|
||||
const { BASE_URL } = require('./config.js');
|
||||
|
||||
const TOKEN_KEY = 'pets_token';
|
||||
|
||||
function getToken() {
|
||||
return wx.getStorageSync(TOKEN_KEY) || '';
|
||||
}
|
||||
function setToken(t) {
|
||||
wx.setStorageSync(TOKEN_KEY, t);
|
||||
}
|
||||
function clearToken() {
|
||||
wx.removeStorageSync(TOKEN_KEY);
|
||||
}
|
||||
|
||||
// 统一请求:注入 token,解析 {code,message,data},成功返回 data,失败 reject(Error)
|
||||
function request({ url, method = 'GET', data, header = {} }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = getToken();
|
||||
wx.request({
|
||||
url: BASE_URL + url,
|
||||
method,
|
||||
data,
|
||||
header: Object.assign(
|
||||
{ 'Content-Type': 'application/json' },
|
||||
token ? { Authorization: 'Bearer ' + token } : {},
|
||||
header,
|
||||
),
|
||||
success(res) {
|
||||
const body = res.data;
|
||||
if (body && typeof body === 'object' && 'code' in body) {
|
||||
if (body.code === 0) return resolve(body.data);
|
||||
if (body.code === 40100) clearToken();
|
||||
return reject(new Error(body.message || '请求失败'));
|
||||
}
|
||||
resolve(body);
|
||||
},
|
||||
fail(err) {
|
||||
reject(new Error((err && err.errMsg) || '网络错误'));
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 文件上传(multipart)
|
||||
function uploadFile(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = getToken();
|
||||
wx.uploadFile({
|
||||
url: BASE_URL + '/api/upload',
|
||||
filePath,
|
||||
name: 'file',
|
||||
header: token ? { Authorization: 'Bearer ' + token } : {},
|
||||
success(res) {
|
||||
try {
|
||||
const body = JSON.parse(res.data);
|
||||
if (body.code === 0) return resolve(body.data);
|
||||
reject(new Error(body.message || '上传失败'));
|
||||
} catch (e) {
|
||||
reject(new Error('上传响应解析失败'));
|
||||
}
|
||||
},
|
||||
fail(err) {
|
||||
reject(new Error((err && err.errMsg) || '上传失败'));
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { request, uploadFile, getToken, setToken, clearToken };
|
||||
@@ -0,0 +1,100 @@
|
||||
// 全局状态:登录、宠物列表、当前宠物;服务端驱动 + 发布订阅
|
||||
const api = require('./api.js');
|
||||
|
||||
const state = {
|
||||
user: null,
|
||||
pets: [],
|
||||
currentId: 0,
|
||||
};
|
||||
|
||||
const listeners = new Set();
|
||||
let bootstrapPromise = null;
|
||||
|
||||
function getPets() {
|
||||
return state.pets;
|
||||
}
|
||||
function getPet() {
|
||||
return state.pets.find((p) => p.id === state.currentId) || state.pets[0] || {};
|
||||
}
|
||||
function currentPetId() {
|
||||
const p = getPet();
|
||||
return p && p.id;
|
||||
}
|
||||
|
||||
function notify() {
|
||||
const pet = getPet();
|
||||
listeners.forEach((fn) => {
|
||||
try {
|
||||
fn(pet);
|
||||
} catch (e) {
|
||||
/* noop */
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function subscribe(fn) {
|
||||
listeners.add(fn);
|
||||
return () => listeners.delete(fn);
|
||||
}
|
||||
|
||||
// 登录 + 拉宠物列表(只跑一次)
|
||||
function ready() {
|
||||
if (!bootstrapPromise) {
|
||||
bootstrapPromise = (async () => {
|
||||
const res = await api.login();
|
||||
state.user = res.user;
|
||||
await loadPets();
|
||||
})().catch((e) => {
|
||||
bootstrapPromise = null; // 允许重试
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
return bootstrapPromise;
|
||||
}
|
||||
|
||||
async function loadPets() {
|
||||
const pets = await api.getPets();
|
||||
state.pets = pets || [];
|
||||
if (!state.currentId && state.pets[0]) {
|
||||
state.currentId = state.pets[0].id;
|
||||
}
|
||||
// 当前宠物被删或不存在时回退第一只
|
||||
if (!state.pets.find((p) => p.id === state.currentId) && state.pets[0]) {
|
||||
state.currentId = state.pets[0].id;
|
||||
}
|
||||
notify();
|
||||
return state.pets;
|
||||
}
|
||||
|
||||
function switchPet(id) {
|
||||
if (state.currentId === id) return;
|
||||
state.currentId = id;
|
||||
notify();
|
||||
}
|
||||
|
||||
// 本地更新当前宠物字段(服务端已改动后同步 UI)
|
||||
function patchCurrent(patch) {
|
||||
const pet = getPet();
|
||||
Object.assign(pet, patch);
|
||||
notify();
|
||||
}
|
||||
|
||||
function setUser(u) {
|
||||
state.user = u;
|
||||
}
|
||||
function getUser() {
|
||||
return state.user;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ready,
|
||||
loadPets,
|
||||
getPets,
|
||||
getPet,
|
||||
currentPetId,
|
||||
switchPet,
|
||||
patchCurrent,
|
||||
subscribe,
|
||||
setUser,
|
||||
getUser,
|
||||
};
|
||||
Reference in New Issue
Block a user