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,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}
|
||||
Reference in New Issue
Block a user