feat: rebuild pet app and backend around core care flows
This commit is contained in:
@@ -1,747 +0,0 @@
|
||||
const store = require('../../utils/store.js');
|
||||
const api = require('../../utils/api.js');
|
||||
const upload = require('../../utils/upload.js');
|
||||
|
||||
function fmtDate(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
return d.getMonth() + 1 + '月' + d.getDate() + '日';
|
||||
}
|
||||
|
||||
// n 天后的 YYYY-MM-DD,给提醒类弹层做默认日期
|
||||
function daysLater(n) {
|
||||
const d = new Date(Date.now() + n * 86400000);
|
||||
const p = (x) => (x < 10 ? '0' + x : '' + x);
|
||||
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
|
||||
}
|
||||
|
||||
// "2026-07-30" → "2026-07-30T12:00:00+08:00"
|
||||
//
|
||||
// 相对时间。社区里关心的是「多久以前发的」,精确到秒没意义
|
||||
function fmtAgo(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
const min = Math.floor((Date.now() - d.getTime()) / 60000);
|
||||
if (min < 1) return '刚刚';
|
||||
if (min < 60) return min + ' 分钟前';
|
||||
if (min < 60 * 24) return Math.floor(min / 60) + ' 小时前';
|
||||
if (min < 60 * 24 * 7) return Math.floor(min / 1440) + ' 天前';
|
||||
const p = (n) => (n < 10 ? '0' + n : '' + n);
|
||||
const sameYear = d.getFullYear() === new Date().getFullYear();
|
||||
return (sameYear ? '' : d.getFullYear() + '-') + p(d.getMonth() + 1) + '-' + p(d.getDate());
|
||||
}
|
||||
|
||||
|
||||
// 提醒类型(与后端 model.Reminder* 常量对应)
|
||||
const REMINDER_TYPES = [
|
||||
{ key: 'vaccine', label: '疫苗' },
|
||||
{ key: 'deworm', label: '驱虫' },
|
||||
{ key: 'weight', label: '体重' },
|
||||
{ key: 'monthlyReport', label: '月度报告' },
|
||||
];
|
||||
const REMINDER_LABELS = REMINDER_TYPES.map((t) => t.label);
|
||||
|
||||
|
||||
// 标签和社区顶部筛选保持一致(晒宠 / 新手求助 / 经验)
|
||||
const PTAG = ['晒宠', '新手求助', '经验'];
|
||||
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
show: { type: Boolean, value: false },
|
||||
type: { type: String, value: '' },
|
||||
// 雪花 ID 是 18 位字符串,声明成 Number 会超出 JS 安全整数范围而丢精度
|
||||
postId: { type: String, value: '' },
|
||||
},
|
||||
data: {
|
||||
innerType: '',
|
||||
pet: {},
|
||||
segSel: {},
|
||||
optSel: {},
|
||||
// 通用简易记录(洗护/清洁那 15 种共用)
|
||||
postContent: '',
|
||||
pfFocus: false, // 发帖内容框聚焦标记(点整块 field 聚焦用)
|
||||
postImages: [],
|
||||
remForm: { id: '', typeIdx: 0, title: '', date: '', freq: '' },
|
||||
remTypeLabels: REMINDER_LABELS,
|
||||
myPets: [],
|
||||
exportText: '',
|
||||
fbContent: '',
|
||||
fbContact: '',
|
||||
fbDone: false,
|
||||
commentText: '',
|
||||
comments: [],
|
||||
reminders: [],
|
||||
poster: null,
|
||||
reportDetail: null,
|
||||
proInfo: null,
|
||||
saving: false,
|
||||
posterSaving: false,
|
||||
commentImages: [],
|
||||
replyTo: {},
|
||||
cmFocus: false,
|
||||
composing: false, // 评论输入面板是否展开(收起时只是一条细条)
|
||||
kbHeight: 0,
|
||||
total: 0,
|
||||
},
|
||||
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.postContent = '';
|
||||
patch.postImages = [];
|
||||
patch.medDose = '';
|
||||
patch.remForm = { id: '', typeIdx: 0, title: '', date: '', freq: '' };
|
||||
patch.exportText = '';
|
||||
patch.fbContent = '';
|
||||
patch.fbContact = '';
|
||||
patch.fbDone = false;
|
||||
patch.commentText = '';
|
||||
patch.commentImages = [];
|
||||
patch.replyTo = {};
|
||||
patch.cmFocus = false;
|
||||
patch.composing = false;
|
||||
patch.kbHeight = 0;
|
||||
patch.saving = false;
|
||||
}
|
||||
this.setData(patch);
|
||||
this.loadSheetData(type);
|
||||
},
|
||||
|
||||
// 按弹层类型拉取真实数据
|
||||
loadSheetData(type) {
|
||||
const id = store.currentPetId();
|
||||
if (type === 'manageTasks') {
|
||||
this.loadManageTasks();
|
||||
} else if (type === 'managePets') {
|
||||
this.loadMyPets();
|
||||
} else if (type === 'exportData') {
|
||||
this.buildExport();
|
||||
} else if (type === 'comments' && this.data.postId) {
|
||||
this.loadComments();
|
||||
} else if (type === 'reminders' && id) {
|
||||
this.loadReminders();
|
||||
} 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(() => {});
|
||||
}
|
||||
},
|
||||
|
||||
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 });
|
||||
}
|
||||
},
|
||||
// ---- 成长海报:画进 canvas 才能存进相册 ----
|
||||
|
||||
onSavePoster() {
|
||||
if (this.data.posterSaving) return;
|
||||
this.setData({ posterSaving: true });
|
||||
this.drawPoster()
|
||||
.then((tempPath) => this.saveToAlbum(tempPath))
|
||||
.then(() => wx.showToast({ title: '已保存到相册', icon: 'success' }))
|
||||
.catch((e) => {
|
||||
if (e && e.canceled) return;
|
||||
wx.showToast({ title: (e && e.message) || '保存失败', icon: 'none' });
|
||||
})
|
||||
.then(() => this.setData({ posterSaving: false }));
|
||||
},
|
||||
|
||||
// 把海报画到离屏 canvas 上,返回临时图片路径
|
||||
drawPoster() {
|
||||
const W = 600;
|
||||
const H = 840;
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.createSelectorQuery()
|
||||
.in(this)
|
||||
.select('#posterCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res) => {
|
||||
const node = res && res[0] && res[0].node;
|
||||
if (!node) return reject(new Error('画布还没准备好,稍后再试'));
|
||||
|
||||
// 按设备像素比放大,否则在高分屏上导出的图是糊的
|
||||
let dpr = 2;
|
||||
try {
|
||||
dpr = (wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync()).pixelRatio || 2;
|
||||
} catch (err) {
|
||||
dpr = 2;
|
||||
}
|
||||
node.width = W * dpr;
|
||||
node.height = H * dpr;
|
||||
const ctx = node.getContext('2d');
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
const pet = this.data.pet || {};
|
||||
const p = this.data.poster || {};
|
||||
// 优先用后端海报数据里的头像(和身份卡同一路径,可靠),store 兜底
|
||||
const avatarUrl = p.pet_avatar_url || pet.avatar_url || '';
|
||||
|
||||
// 头像是远程图,异步加载。加载完(或没有头像)再往下画,
|
||||
// 否则头像会画不上。加载失败当没有头像处理
|
||||
const loadAvatar = () =>
|
||||
new Promise((res) => {
|
||||
if (!avatarUrl) return res(null);
|
||||
const img = node.createImage();
|
||||
img.onload = () => res(img);
|
||||
img.onerror = () => res(null);
|
||||
img.src = avatarUrl;
|
||||
});
|
||||
|
||||
loadAvatar().then((avatar) => {
|
||||
const bg = ctx.createLinearGradient(0, 0, W, H);
|
||||
bg.addColorStop(0, '#FFF7E8');
|
||||
bg.addColorStop(1, '#FFFFFF');
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
ctx.fillStyle = '#F2DFC3';
|
||||
ctx.fillRect(0, 0, W, 8);
|
||||
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = '#2D2925';
|
||||
ctx.font = '600 40px sans-serif';
|
||||
ctx.fillText((pet.name || '毛孩子') + ' 的成长报告', W / 2, 96);
|
||||
|
||||
// 上传过照片就画圆形头像,没有才退回 emoji(默认头像太丑)
|
||||
const avD = 132, avX = (W - avD) / 2, avY = 132;
|
||||
if (avatar) {
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(W / 2, avY + avD / 2, avD / 2, 0, Math.PI * 2);
|
||||
ctx.closePath();
|
||||
ctx.clip();
|
||||
// 居中裁剪成正方形再画,竖图/横图都不压扁
|
||||
const iw = avatar.width || avD;
|
||||
const ih = avatar.height || avD;
|
||||
const side = Math.min(iw, ih);
|
||||
const sx = (iw - side) / 2;
|
||||
const sy = (ih - side) / 2;
|
||||
ctx.drawImage(avatar, sx, sy, side, side, avX, avY, avD, avD);
|
||||
ctx.restore();
|
||||
} else {
|
||||
ctx.font = '96px sans-serif';
|
||||
ctx.fillText(pet.emoji || '🐾', W / 2, 216);
|
||||
}
|
||||
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = '#8D8277';
|
||||
ctx.font = '24px sans-serif';
|
||||
ctx.fillText([pet.age, pet.weight, pet.stage].filter(Boolean).join(' | '), W / 2, 262);
|
||||
|
||||
// 周报告:显示统计周期,让人知道这四个数是近 7 天的
|
||||
if (p.period) {
|
||||
ctx.fillStyle = '#C7A36A';
|
||||
ctx.font = '22px sans-serif';
|
||||
ctx.fillText('近 7 天 · ' + p.period, W / 2, 298);
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,.8)';
|
||||
this.roundRect(ctx, 60, 316, W - 120, 232, 24);
|
||||
ctx.fill();
|
||||
const lines = [
|
||||
'完成任务 ' + (p.tasks_completed || 0) + ' 项',
|
||||
'体重记录 ' + (p.weight_records || 0) + ' 次',
|
||||
'疫苗记录 ' + (p.vaccine_records || 0) + ' 次',
|
||||
'高风险异常 ' + (p.high_risk_count || 0) + ' 次',
|
||||
];
|
||||
ctx.textAlign = 'left';
|
||||
ctx.font = '28px sans-serif';
|
||||
lines.forEach((t, i) => {
|
||||
const y = 360 + i * 52;
|
||||
ctx.fillStyle = '#73BE9D';
|
||||
ctx.fillText('✓', 92, y);
|
||||
ctx.fillStyle = '#443D37';
|
||||
ctx.fillText(t, 132, y);
|
||||
});
|
||||
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = '#2D2925';
|
||||
ctx.font = '600 30px sans-serif';
|
||||
ctx.fillText('健康状态:' + (p.headline || '稳定成长'), W / 2, 620);
|
||||
|
||||
ctx.fillStyle = '#B5A99D';
|
||||
ctx.font = '22px sans-serif';
|
||||
ctx.fillText('生成自 · 肉垫计划', W / 2, 780);
|
||||
|
||||
// 头像加载完(或没有)才走到这里,此刻画布已完整
|
||||
wx.canvasToTempFilePath(
|
||||
{
|
||||
canvas: node,
|
||||
fileType: 'png',
|
||||
success: (r) => resolve(r.tempFilePath),
|
||||
fail: () => reject(new Error('生成图片失败')),
|
||||
},
|
||||
this,
|
||||
);
|
||||
}); // loadAvatar().then
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
roundRect(ctx, x, y, w, h, r) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, r);
|
||||
ctx.arcTo(x + w, y + h, x, y + h, r);
|
||||
ctx.arcTo(x, y + h, x, y, r);
|
||||
ctx.arcTo(x, y, x + w, y, r);
|
||||
ctx.closePath();
|
||||
},
|
||||
|
||||
// 存相册要相册写权限。用户拒过一次之后 wx.authorize 不会再弹,
|
||||
// 必须引导他去设置页打开,否则这里会永远静默失败。
|
||||
saveToAlbum(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath,
|
||||
success: resolve,
|
||||
fail: (err) => {
|
||||
const msg = (err && err.errMsg) || '';
|
||||
if (msg.indexOf('cancel') >= 0) return reject({ canceled: true });
|
||||
if (msg.indexOf('auth deny') >= 0 || msg.indexOf('authorize') >= 0) {
|
||||
wx.showModal({
|
||||
title: '需要相册权限',
|
||||
content: '保存卡片需要允许访问相册,去设置里打开一下?',
|
||||
confirmText: '去设置',
|
||||
success: (r) => {
|
||||
if (r.confirm) wx.openSetting({});
|
||||
},
|
||||
});
|
||||
return reject({ canceled: true });
|
||||
}
|
||||
reject(new Error('保存失败'));
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
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 });
|
||||
},
|
||||
onPostInput(e) { this.setData({ postContent: e.detail.value }); },
|
||||
// 点内容 field 任意处都聚焦(原生 textarea 在 scroll-view 里命中区域会偏)
|
||||
onPostFieldTap() { this.setData({ pfFocus: true }); },
|
||||
onPostBlur() { this.setData({ pfFocus: false }); },
|
||||
loadComments() {
|
||||
if (!this.data.postId) return;
|
||||
api.listComments(this.data.postId)
|
||||
.then((page) =>
|
||||
this.setData({
|
||||
total: page.total || 0,
|
||||
comments: (page.list || []).map((c) => ({
|
||||
...c,
|
||||
timeText: fmtAgo(c.created_at),
|
||||
// 没有头像字段,用昵称首字做个色块,比一律显示同一个 emoji 强
|
||||
initial: (c.author_name || '?').slice(0, 1),
|
||||
imgs: Array.isArray(c.images) ? c.images : [],
|
||||
replies: (c.replies || []).map((r) => ({
|
||||
...r,
|
||||
timeText: fmtAgo(r.created_at),
|
||||
initial: (r.author_name || '?').slice(0, 1),
|
||||
})),
|
||||
})),
|
||||
}),
|
||||
)
|
||||
.catch(() => {});
|
||||
},
|
||||
// 长按删除自己的评论
|
||||
onDeleteComment(e) {
|
||||
const { id, self } = e.currentTarget.dataset;
|
||||
if (!id || !self) return;
|
||||
const c = { id };
|
||||
wx.showModal({
|
||||
title: '删除评论',
|
||||
content: '确定删除这条评论?',
|
||||
confirmColor: '#D9534F',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return;
|
||||
api.deleteComment(c.id)
|
||||
.then(() => { this.loadComments(); this.triggerEvent('commented'); })
|
||||
.catch((err) => wx.showToast({ title: err.message || '删除失败', icon: 'none' }));
|
||||
},
|
||||
});
|
||||
},
|
||||
// ---- 多宠物管理 ----
|
||||
loadMyPets() {
|
||||
const cur = store.currentPetId();
|
||||
this.setData({
|
||||
myPets: (store.getPets() || []).map((p) => ({
|
||||
id: p.id, name: p.name, emoji: p.emoji,
|
||||
meta: [p.type, p.stage, p.weight].filter(Boolean).join(' · '),
|
||||
current: p.id === cur,
|
||||
})),
|
||||
});
|
||||
},
|
||||
onSwitchPet(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
store.switchPet(id);
|
||||
this.loadMyPets();
|
||||
wx.showToast({ title: '已切换', icon: 'success' });
|
||||
setTimeout(() => this.close(), 500);
|
||||
},
|
||||
// 多宠管理里点某只去编辑。editPet 那支已经搬到 pages/petform,
|
||||
// 这里必须关掉弹层再跳页 —— 留着 setType('editPet') 会开一个空白弹层
|
||||
onEditPetFromList(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
store.switchPet(id);
|
||||
this.close();
|
||||
wx.navigateTo({ url: '/pages/petform/petform?id=' + id });
|
||||
},
|
||||
|
||||
// ---- 导出健康档案 ----
|
||||
buildExport() {
|
||||
const id = store.currentPetId();
|
||||
const pet = store.getPet() || {};
|
||||
if (!id) {
|
||||
this.setData({ exportText: '还没有建档,先添加一只毛孩子吧。' });
|
||||
return;
|
||||
}
|
||||
Promise.all([
|
||||
api.getRecords(id, { page: 1, pageSize: 50 }).catch(() => ({ list: [], total: 0 })),
|
||||
api.getReminders(id).catch(() => []),
|
||||
]).then(([recPage, rems]) => {
|
||||
const lines = [];
|
||||
lines.push('【肉垫计划 · 健康档案】');
|
||||
lines.push(`宠物:${pet.name || ''}(${pet.type || ''}|${pet.gender || ''})`);
|
||||
lines.push(`阶段:${pet.stage || '-'} 年龄:${pet.age || '-'} 体重:${pet.weight || '-'}`);
|
||||
if (pet.breed || pet.color) lines.push(`品种/毛色:${pet.breed || '-'} / ${pet.color || '-'}`);
|
||||
lines.push('');
|
||||
lines.push(`— 健康记录(共 ${recPage.total || 0} 条,导出最近 ${(recPage.list || []).length} 条)—`);
|
||||
(recPage.list || []).forEach((r) => {
|
||||
lines.push(`· ${fmtDate(r.occurred_at)} ${r.title}${r.description ? '|' + r.description : ''}`);
|
||||
});
|
||||
lines.push('');
|
||||
lines.push('— 提醒 —');
|
||||
(rems || []).forEach((r) => {
|
||||
lines.push(`· ${r.title}:${r.next_due_date ? fmtDate(r.next_due_date) : r.frequency || '未设置'}`);
|
||||
});
|
||||
lines.push('');
|
||||
lines.push('(本档案仅供日常照护与就医参考,不构成诊断意见)');
|
||||
this.setData({ exportText: lines.join('\n') });
|
||||
});
|
||||
},
|
||||
onCopyExport() {
|
||||
const t = this.data.exportText;
|
||||
if (!t) return;
|
||||
wx.setClipboardData({
|
||||
data: t,
|
||||
success: () => wx.showToast({ title: '已复制到剪贴板', icon: 'success' }),
|
||||
});
|
||||
},
|
||||
|
||||
// ---- 意见反馈 ----
|
||||
onFbContent(e) { this.setData({ fbContent: e.detail.value }); },
|
||||
onFbContact(e) { this.setData({ fbContact: e.detail.value }); },
|
||||
onSubmitFeedback() {
|
||||
const content = (this.data.fbContent || '').trim();
|
||||
if (!content) return wx.showToast({ title: '写点什么再提交', icon: 'none' });
|
||||
if (this.data.saving) return;
|
||||
this.setData({ saving: true });
|
||||
api
|
||||
.submitFeedback({ content, contact: (this.data.fbContact || '').trim() })
|
||||
.then(() => this.setData({ fbDone: true, saving: false }))
|
||||
.catch((e) => {
|
||||
wx.showToast({ title: e.message || '提交失败', icon: 'none' });
|
||||
this.setData({ saving: false });
|
||||
});
|
||||
},
|
||||
|
||||
// ---- 提醒增删改 ----
|
||||
loadReminders() {
|
||||
const id = store.currentPetId();
|
||||
if (!id) return;
|
||||
api
|
||||
.getReminders(id)
|
||||
.then((list) =>
|
||||
this.setData({
|
||||
reminders: (list || []).map((r) => ({
|
||||
id: r.id,
|
||||
type: r.type,
|
||||
title: r.title,
|
||||
date: r.next_due_date ? String(r.next_due_date).slice(0, 10) : '',
|
||||
freq: r.frequency || '',
|
||||
when: r.next_due_date ? fmtDate(r.next_due_date) : r.frequency || '未设置',
|
||||
})),
|
||||
}),
|
||||
)
|
||||
.catch(() => {});
|
||||
},
|
||||
onRemTitle(e) { this.setData({ 'remForm.title': e.detail.value }); },
|
||||
onRemFreq(e) { this.setData({ 'remForm.freq': e.detail.value }); },
|
||||
onRemType(e) { this.setData({ 'remForm.typeIdx': Number(e.detail.value) }); },
|
||||
onRemDate(e) { this.setData({ 'remForm.date': e.detail.value }); },
|
||||
onEditReminder(e) {
|
||||
const r = this.data.reminders[e.currentTarget.dataset.index];
|
||||
let idx = REMINDER_TYPES.findIndex((t) => t.key === r.type);
|
||||
if (idx < 0) idx = 0;
|
||||
this.setData({ remForm: { id: r.id, typeIdx: idx, title: r.title, date: r.date, freq: r.freq } });
|
||||
},
|
||||
onSaveReminder() {
|
||||
const f = this.data.remForm;
|
||||
const title = (f.title || '').trim();
|
||||
if (!title) return wx.showToast({ title: '填个提醒名', icon: 'none' });
|
||||
const body = {
|
||||
type: REMINDER_TYPES[f.typeIdx].key,
|
||||
title,
|
||||
next_due_date: f.date || '',
|
||||
frequency: (f.freq || '').trim(),
|
||||
};
|
||||
const petId = store.currentPetId();
|
||||
const p = f.id ? api.updateReminder(f.id, body) : api.createReminder(petId, body);
|
||||
p.then(() => {
|
||||
this.setData({ remForm: { id: '', typeIdx: 0, title: '', date: '', freq: '' } });
|
||||
this.loadReminders();
|
||||
this.triggerEvent('saved');
|
||||
}).catch((e) => wx.showToast({ title: e.message || '保存失败', icon: 'none' }));
|
||||
},
|
||||
onDeleteReminder(e) {
|
||||
const r = this.data.reminders[e.currentTarget.dataset.index];
|
||||
wx.showModal({
|
||||
title: '删除提醒',
|
||||
content: '确定删除「' + r.title + '」?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return;
|
||||
api.deleteReminder(r.id).then(() => {
|
||||
this.loadReminders();
|
||||
this.triggerEvent('saved');
|
||||
}).catch((err) => wx.showToast({ title: err.message || '删除失败', icon: 'none' }));
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// ---- 管理任务 ----
|
||||
loadManageTasks() {
|
||||
const id = store.currentPetId();
|
||||
if (!id) return;
|
||||
api.getTasks(id).then((tasks) => this.setData({ manageTasks: tasks || [] })).catch(() => {});
|
||||
},
|
||||
onPickPostImages() {
|
||||
const left = 2 - this.data.postImages.length;
|
||||
if (left <= 0) return wx.showToast({ title: '最多 2 张', icon: 'none' });
|
||||
upload
|
||||
.chooseAndUploadImages(left)
|
||||
.then((list) => this.setData({ postImages: this.data.postImages.concat(list) }))
|
||||
.catch((e) => {
|
||||
if (e && e.canceled) return;
|
||||
wx.showToast({ title: (e && e.message) || '上传失败', icon: 'none' });
|
||||
});
|
||||
},
|
||||
onRemovePostImage(e) {
|
||||
const i = e.currentTarget.dataset.index;
|
||||
const arr = this.data.postImages.slice();
|
||||
arr.splice(i, 1);
|
||||
this.setData({ postImages: arr });
|
||||
},
|
||||
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;
|
||||
},
|
||||
// 记录表单整体搬去 pages/addrecord 之后,这里只剩「生成就医前摘要」
|
||||
// 会往 health_records 写一条 note —— 它是个动作留痕,不是用户填的表单
|
||||
buildRecord() {
|
||||
if (this.data.innerType === 'vetSummary') {
|
||||
return { type: 'note', icon: '📄', title: '生成就医前摘要' };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
async createAndClose(rec) {
|
||||
const id = store.currentPetId();
|
||||
if (!rec) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
// 没建档时点「保存」原来是静默关掉弹层,用户以为存上了,其实什么都没发生
|
||||
if (!id) {
|
||||
wx.showToast({ title: '先建一份宠物档案再记录', icon: 'none' });
|
||||
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());
|
||||
},
|
||||
async onCreatePost() {
|
||||
const content = (this.data.postContent || '').trim();
|
||||
if (!content) return wx.showToast({ title: '写点什么吧', icon: 'none' });
|
||||
const tag = PTAG[this.data.optSel.ptag === undefined ? 0 : this.data.optSel.ptag];
|
||||
if (this.data.saving) return;
|
||||
this.setData({ saving: true });
|
||||
try {
|
||||
// 去掉发布身份,统一用发布者昵称(后端按 user 取昵称)
|
||||
const post = await api.createPost({
|
||||
identity: 'user',
|
||||
content,
|
||||
tags: [tag],
|
||||
image_file_ids: this.data.postImages.map((i) => i.id),
|
||||
});
|
||||
this.triggerEvent('posted');
|
||||
this.close();
|
||||
// 内容安全审核:没直接过审的,明确告诉用户在审核,别以为发失败/是 bug
|
||||
if (post && post.status && post.status !== 'published') {
|
||||
wx.showModal({
|
||||
title: '发布成功,审核中',
|
||||
content: '内容需要通过安全审核后才会出现在社区。可在「我的 → 我的帖子」查看审核状态。',
|
||||
showCancel: false,
|
||||
confirmText: '知道了',
|
||||
});
|
||||
} else {
|
||||
wx.showToast({ title: '已发布', icon: 'success' });
|
||||
}
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '发布失败', icon: 'none' });
|
||||
this.setData({ saving: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 评论
|
||||
async onSendComment() {
|
||||
const text = (this.data.commentText || '').trim();
|
||||
// 原来空评论会直接把弹层关掉,看起来像发成功了。改成明确提示
|
||||
if (!text) return wx.showToast({ title: '写点什么再发', icon: 'none' });
|
||||
if (!this.data.postId) return this.close();
|
||||
try {
|
||||
await api.createComment(
|
||||
this.data.postId,
|
||||
text,
|
||||
this.data.commentImages.map((i) => i.url),
|
||||
this.data.replyTo.id || '',
|
||||
);
|
||||
this.setData({ commentText: '', commentImages: [], replyTo: {}, composing: false, cmFocus: false });
|
||||
this.loadComments();
|
||||
this.triggerEvent('commented');
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '评论失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
// 评论配图,最多 3 张
|
||||
onPickCommentImages() {
|
||||
const left = 3 - this.data.commentImages.length;
|
||||
if (left <= 0) return wx.showToast({ title: '最多 3 张', icon: 'none' });
|
||||
upload
|
||||
.chooseAndUploadImages(left)
|
||||
.then((files) => this.setData({ commentImages: this.data.commentImages.concat(files) }))
|
||||
.catch((e) => {
|
||||
if (e && e.canceled) return;
|
||||
wx.showToast({ title: (e && e.message) || '上传失败', icon: 'none' });
|
||||
});
|
||||
},
|
||||
// 回复某条评论:输入区顶部显示「回复 xxx」,发送时带上 parent_id
|
||||
// 小红书那套:点任意一条评论/回复,直接把输入框激活并填上「回复 @xxx」,
|
||||
// 不用再去底部找输入框、也不用单独点一个「回复」按钮
|
||||
onReplyTo(e) {
|
||||
const { id, name } = e.currentTarget.dataset;
|
||||
if (!id) return;
|
||||
// 点任意评论直接展开面板并预填回复对象
|
||||
this.setData({ replyTo: { id, name }, composing: true, cmFocus: true });
|
||||
},
|
||||
// 点细条:展开大输入面板并聚焦(拉起键盘)
|
||||
onStartCompose() {
|
||||
this.setData({ composing: true, cmFocus: true });
|
||||
},
|
||||
// 收起面板,草稿(文字/图片)留着,下次点开接着写
|
||||
onCloseCompose() {
|
||||
this.setData({ composing: false, cmFocus: false, kbHeight: 0 });
|
||||
},
|
||||
onCommentFocus(e) {
|
||||
// adjust-position=false,键盘高度自己接管:弹层是 fixed 定位,
|
||||
// 交给系统顶会把整块推出屏幕
|
||||
this.setData({ kbHeight: (e.detail && e.detail.height) || 0 });
|
||||
},
|
||||
onCommentBlur() {
|
||||
// 失焦先放下键盘。有草稿就保持展开(否则点「发送」会因面板收起而丢掉这一下点击);
|
||||
// 空内容才收回细条
|
||||
const empty = !(this.data.commentText || '').trim() && !this.data.commentImages.length;
|
||||
this.setData({ kbHeight: 0, cmFocus: false, composing: empty ? false : this.data.composing });
|
||||
},
|
||||
onCancelReply() {
|
||||
this.setData({ replyTo: {} });
|
||||
},
|
||||
// 一级评论默认只带 3 条回复,点开拉全量
|
||||
onExpandReplies(e) {
|
||||
const { id, index } = e.currentTarget.dataset;
|
||||
api
|
||||
.listReplies(id)
|
||||
.then((list) =>
|
||||
this.setData({
|
||||
[`comments[${index}].replies`]: (list || []).map((r) => ({
|
||||
...r,
|
||||
timeText: fmtAgo(r.created_at),
|
||||
initial: (r.author_name || '?').slice(0, 1),
|
||||
})),
|
||||
}),
|
||||
)
|
||||
.catch((err) => wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' }));
|
||||
},
|
||||
onPreviewCommentImage(e) {
|
||||
const { urls, cur } = e.currentTarget.dataset;
|
||||
if (urls && urls.length) wx.previewImage({ urls, current: cur });
|
||||
},
|
||||
onRemoveCommentImage(e) {
|
||||
const list = this.data.commentImages.slice();
|
||||
list.splice(e.currentTarget.dataset.index, 1);
|
||||
this.setData({ commentImages: list });
|
||||
},
|
||||
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"pt-icon": "/components/pt-icon/index"
|
||||
}
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
<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 {{innerType === 'comments' ? 'sheet-cmp' : ''}}" catchtap="noop">
|
||||
<view class="sheetbar"></view>
|
||||
|
||||
<!-- 评论:头 + 滚动列表 + 常驻输入栏。输入栏不能放进滚动区,
|
||||
否则列表一滚它就跟着走,点评论回复时还得再找回来 -->
|
||||
<block wx:if="{{innerType === 'comments'}}">
|
||||
<view class="cmp-head">共 {{total}} 条评论</view>
|
||||
|
||||
<scroll-view class="cmp-list" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view wx:for="{{comments}}" wx:key="id" class="cm"
|
||||
bindtap="onReplyTo" data-id="{{item.id}}" data-name="{{item.author_name}}">
|
||||
<view class="cm-av">{{item.initial}}</view>
|
||||
<view class="cm-body">
|
||||
<view class="cm-name">{{item.author_name}}<text wx:if="{{item.is_self}}" class="mine-tag">我</text></view>
|
||||
<view class="cm-text">{{item.content}}</view>
|
||||
<view wx:if="{{item.imgs.length}}" class="cm-imgs">
|
||||
<image wx:for="{{item.imgs}}" wx:for-item="u" wx:key="*this" class="cm-img"
|
||||
src="{{u}}" mode="aspectFill" catchtap="onPreviewCommentImage"
|
||||
data-urls="{{item.imgs}}" data-cur="{{u}}"></image>
|
||||
</view>
|
||||
<view class="cm-meta">
|
||||
<text>{{item.timeText}}</text>
|
||||
<text class="cm-reply-btn">回复</text>
|
||||
<text wx:if="{{item.is_self}}" class="cm-del" catchtap="onDeleteComment"
|
||||
data-id="{{item.id}}" data-self="{{true}}">删除</text>
|
||||
</view>
|
||||
|
||||
<!-- 回复只缩进一层,再深手机上没法读 -->
|
||||
<view wx:if="{{item.replies.length}}" class="cm-replies">
|
||||
<view wx:for="{{item.replies}}" wx:for-item="r" wx:key="id" class="cm-reply"
|
||||
catchtap="onReplyTo" data-id="{{r.id}}" data-name="{{r.author_name}}">
|
||||
<view class="cm-av cm-av-sm">{{r.initial}}</view>
|
||||
<view class="cm-body">
|
||||
<view class="cm-name">
|
||||
{{r.author_name}}<text wx:if="{{r.is_self}}" class="mine-tag">我</text>
|
||||
<text wx:if="{{r.reply_to_name}}" class="cm-at"> 回复 {{r.reply_to_name}}</text>
|
||||
</view>
|
||||
<view class="cm-text">{{r.content}}</view>
|
||||
<view class="cm-meta">
|
||||
<text>{{r.timeText}}</text>
|
||||
<text class="cm-reply-btn">回复</text>
|
||||
<text wx:if="{{r.is_self}}" class="cm-del" catchtap="onDeleteComment"
|
||||
data-id="{{r.id}}" data-self="{{true}}">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{item.reply_n > item.replies.length}}" class="cm-more"
|
||||
catchtap="onExpandReplies" data-id="{{item.id}}" data-index="{{index}}">
|
||||
— 展开全部 {{item.reply_n}} 条回复
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!comments.length}}" class="cmp-empty">
|
||||
<view class="cmp-empty-ic"><pt-icon name="comment" size="{{72}}"></pt-icon></view>
|
||||
<view class="cmp-empty-t">还没有评论</view>
|
||||
<view class="cmp-empty-p">来抢个沙发,说点什么吧~</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 收起态:一条细条,点了才展开成大写评论面板(小红书那套) -->
|
||||
<view wx:if="{{!composing}}" class="cm-collapsed" catchtap="onStartCompose">
|
||||
<view class="cm-collapsed-pill {{commentText ? 'has' : ''}}">{{commentText || '说点什么…'}}</view>
|
||||
<view class="cm-collapsed-ic"><pt-icon name="photo" size="{{40}}"></pt-icon></view>
|
||||
</view>
|
||||
|
||||
<!-- 展开态:大输入面板,浮在键盘上方 -->
|
||||
<view wx:else class="cmp-dock" style="padding-bottom:{{kbHeight ? kbHeight + 'px' : 'calc(20rpx + env(safe-area-inset-bottom))'}}">
|
||||
<view class="cm-dock-head">
|
||||
<text class="cm-dock-title">{{replyTo.name ? '回复 ' + replyTo.name : '写评论'}}</text>
|
||||
<view class="cm-dock-close" catchtap="onCloseCompose"><pt-icon name="close" size="{{30}}"></pt-icon></view>
|
||||
</view>
|
||||
<textarea class="cm-ta" placeholder="{{replyTo.name ? '回复 @' + replyTo.name + '…' : '有爱评论,说点好听的~'}}"
|
||||
placeholder-class="placeholder" value="{{commentText}}" bindinput="onCommentInput"
|
||||
focus="{{cmFocus}}" bindfocus="onCommentFocus" bindblur="onCommentBlur"
|
||||
maxlength="500" auto-height="{{true}}" show-confirm-bar="{{false}}"
|
||||
adjust-position="{{false}}" cursor-spacing="12"></textarea>
|
||||
<view wx:if="{{commentImages.length}}" class="img-picker" style="margin-top:12rpx">
|
||||
<view wx:for="{{commentImages}}" wx:key="id" class="img-thumb">
|
||||
<image src="{{item.url}}" mode="aspectFill"></image>
|
||||
<view class="img-del" catchtap="onRemoveCommentImage" data-index="{{index}}"><pt-icon name="close" size="{{24}}"></pt-icon></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="cm-toolbar">
|
||||
<view class="cm-pic" catchtap="onPickCommentImages"><pt-icon name="photo" size="{{44}}"></pt-icon></view>
|
||||
<text class="cm-count">{{commentText.length}}/500</text>
|
||||
<view class="cm-send {{commentText ? 'on' : ''}}" catchtap="onSendComment">发送</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
|
||||
<scroll-view wx:if="{{innerType !== 'comments'}}" class="sheet-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<!-- 那 9 种记录表单(体重/排便/饮食/异常/给药/记账/疫苗/驱虫/照片)已经整体
|
||||
搬到 pages/addrecord —— 字段由后端 record_types.fields 驱动,一个页面吃掉
|
||||
全部 24 种,不再每种一个 wx:elif。弹层现在只管「不是记一笔」的那些事:
|
||||
提醒、海报、档案、发帖、评论…… -->
|
||||
<!-- 就医前摘要。这是链上第一支,必须是 wx:if -->
|
||||
<block wx:if="{{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" bindtap="onSave">保存摘要</button>
|
||||
</block>
|
||||
|
||||
<!-- 记录用药 -->
|
||||
<!-- 提醒中心 -->
|
||||
<block wx:elif="{{innerType === 'reminders'}}">
|
||||
<view class="sheet-h3">提醒中心</view>
|
||||
<view class="sheet-p">疫苗、驱虫这些别靠记,设好日期到时候提醒你。</view>
|
||||
|
||||
<view wx:for="{{reminders}}" wx:key="id" class="mtask-row">
|
||||
<view class="mtask-body">
|
||||
<text class="mtask-b">{{item.title}}</text>
|
||||
<view class="mtask-p">{{item.when}}</view>
|
||||
</view>
|
||||
<view class="mtask-op" catchtap="onEditReminder" data-index="{{index}}">改</view>
|
||||
<view class="mtask-op del" catchtap="onDeleteReminder" data-index="{{index}}">删</view>
|
||||
</view>
|
||||
<view wx:if="{{!reminders.length}}" class="sheet-p">还没有提醒,下面加一条吧。</view>
|
||||
|
||||
<view class="field" style="margin-top:12rpx"><label>{{remForm.id ? '修改提醒' : '新增提醒'}}</label>
|
||||
<input class="input" placeholder="提醒名,例如:第 3 针疫苗" placeholder-class="placeholder" value="{{remForm.title}}" bindinput="onRemTitle"/></view>
|
||||
<view class="field"><label>类型 / 到期日</label>
|
||||
<view class="mtask-form-row">
|
||||
<picker mode="selector" range="{{remTypeLabels}}" value="{{remForm.typeIdx}}" bindchange="onRemType">
|
||||
<view class="picker-box">{{remTypeLabels[remForm.typeIdx]}}</view>
|
||||
</picker>
|
||||
<picker mode="date" value="{{remForm.date}}" bindchange="onRemDate">
|
||||
<view class="picker-box">{{remForm.date || '选择到期日'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
<view class="field"><label>重复频率(可选)</label>
|
||||
<input class="input" placeholder="例如:每月一次;填了可不选到期日" placeholder-class="placeholder" value="{{remForm.freq}}" bindinput="onRemFreq"/></view>
|
||||
<button class="btn btn-primary btn-block" bindtap="onSaveReminder">{{remForm.id ? '保存修改' : '添加提醒'}}</button>
|
||||
</block>
|
||||
|
||||
<!-- 任务详情 -->
|
||||
|
||||
<!-- 成长海报 -->
|
||||
<block wx:elif="{{innerType === 'poster'}}">
|
||||
<view class="poster">
|
||||
<view class="poster-h3">{{pet.name}} 的成长报告</view>
|
||||
<view class="poster-avatar">
|
||||
<image wx:if="{{poster.pet_avatar_url || pet.avatar_url}}" class="poster-avatar-img" src="{{poster.pet_avatar_url || pet.avatar_url}}" mode="aspectFill"></image>
|
||||
<block wx:else>{{pet.emoji}}</block>
|
||||
</view>
|
||||
<view class="poster-meta">{{pet.age}}|{{pet.weight}}|{{pet.stage}}</view>
|
||||
<view wx:if="{{poster.period}}" class="poster-period">近 7 天 · {{poster.period}}</view>
|
||||
<view class="poster-list">
|
||||
<view class="poster-row"><view class="poster-tick"><pt-icon name="check" size="{{22}}"></pt-icon></view><text class="poster-rt">完成任务</text><text class="poster-rn">{{poster ? poster.tasks_completed : 0}} 项</text></view>
|
||||
<view class="poster-row"><view class="poster-tick"><pt-icon name="check" size="{{22}}"></pt-icon></view><text class="poster-rt">体重记录</text><text class="poster-rn">{{poster ? poster.weight_records : 0}} 次</text></view>
|
||||
<view class="poster-row"><view class="poster-tick"><pt-icon name="check" size="{{22}}"></pt-icon></view><text class="poster-rt">疫苗记录</text><text class="poster-rn">{{poster ? poster.vaccine_records : 0}} 次</text></view>
|
||||
<view class="poster-row"><view class="poster-tick"><pt-icon name="check" size="{{22}}"></pt-icon></view><text class="poster-rt">高风险异常</text><text class="poster-rn">{{poster ? poster.high_risk_count : 0}} 次</text></view>
|
||||
</view>
|
||||
<view class="poster-status">健康状态<text class="poster-badge">{{poster ? poster.headline : '稳定成长'}}</text></view>
|
||||
<view class="poster-from">生成自 · 肉垫计划</view>
|
||||
</view>
|
||||
<view class="sheet-actions" style="margin-top:28rpx">
|
||||
<button class="btn btn-ghost" open-type="share">分享给朋友</button>
|
||||
<button class="btn btn-primary" bindtap="onSavePoster" disabled="{{posterSaving}}">
|
||||
{{posterSaving ? '生成中…' : '保存到相册'}}
|
||||
</button>
|
||||
</view>
|
||||
<!-- 离屏画布:海报要存成图片才能进相册。不能用 display:none,否则取不到节点 -->
|
||||
<canvas type="2d" id="posterCanvas" class="poster-canvas"></canvas>
|
||||
</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"><pt-icon name="check" size="{{26}}"></pt-icon></view>{{item}}</view>
|
||||
</view>
|
||||
<button wx:if="{{proInfo.status === 'active'}}" class="btn btn-soft btn-block" bindtap="close"><pt-icon name="check" size="{{30}}"></pt-icon>已开通 Pro</button>
|
||||
<button wx:else class="btn btn-soft btn-block" disabled="true">即将开放</button>
|
||||
<view wx:if="{{proInfo.status !== 'active'}}" class="sheet-p" style="text-align:center;margin-top:16rpx;font-size:24rpx">
|
||||
会员功能还在准备中,暂未开放购买。
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 添加宠物 -->
|
||||
<!-- 添加/编辑宠物已经整体搬到 pages/petform:三步向导(名字+头像 → 猫狗+品种
|
||||
→ 其他资料),品种从后端 breeds 表来、按首字母索引。弹层塞不下这些 -->
|
||||
<block wx:elif="{{innerType === 'createPost'}}">
|
||||
<view class="sheet-h3">发布图文</view>
|
||||
<view class="sheet-p">用你的昵称分享养宠日常。</view>
|
||||
<!-- 点整块 field 都能聚焦:原生 textarea 在 scroll-view 里命中区域会偏 -->
|
||||
<view class="field" bindtap="onPostFieldTap"><label>内容</label>
|
||||
<textarea class="textarea" focus="{{pfFocus}}" placeholder="分享养宠日常、求助问题、经验笔记..." placeholder-class="placeholder" value="{{postContent}}" bindinput="onPostInput" bindblur="onPostBlur"></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>
|
||||
<view class="field"><label>图片(可选,最多 2 张)</label>
|
||||
<view class="img-picker">
|
||||
<view wx:for="{{postImages}}" wx:key="id" class="img-thumb">
|
||||
<image src="{{item.url}}" mode="aspectFill"></image>
|
||||
<view class="img-del" catchtap="onRemovePostImage" data-index="{{index}}"><pt-icon name="close" size="{{24}}"></pt-icon></view>
|
||||
</view>
|
||||
<view wx:if="{{postImages.length < 2}}" class="img-add" bindtap="onPickPostImages"><pt-icon name="plus" size="{{48}}"></pt-icon></view>
|
||||
</view>
|
||||
</view>
|
||||
<button class="btn btn-primary btn-block" bindtap="onCreatePost">发布到宠友圈</button>
|
||||
</block>
|
||||
|
||||
<!-- 任务管理那一支删了:它管的是 DailyTask,而「今日任务」已经并进计划节点,
|
||||
增删改都在计划页做。留着会出现「这里加的任务」和「计划页加的节点」进不同的表 -->
|
||||
<block wx:elif="{{innerType === 'managePets'}}">
|
||||
<view class="sheet-h3">多宠物管理</view>
|
||||
<view class="sheet-p">点一只切换为当前宠物,或编辑它的档案。</view>
|
||||
<view wx:for="{{myPets}}" wx:key="id" class="mtask-row">
|
||||
<view class="mtask-body" catchtap="onSwitchPet" data-id="{{item.id}}">
|
||||
<text class="mtask-b">{{item.emoji}} {{item.name}}<text wx:if="{{item.current}}" class="mtask-pri">当前</text></text>
|
||||
<view class="mtask-p">{{item.meta}}</view>
|
||||
</view>
|
||||
<view class="mtask-op" catchtap="onEditPetFromList" data-id="{{item.id}}">编辑</view>
|
||||
</view>
|
||||
<view wx:if="{{!myPets.length}}" class="sheet-p">还没有毛孩子。</view>
|
||||
<button class="btn btn-primary btn-block" style="margin-top:16rpx" data-type="addPet" bindtap="goSheet">+ 添加毛孩子</button>
|
||||
</block>
|
||||
|
||||
<!-- 导出健康档案 -->
|
||||
<block wx:elif="{{innerType === 'exportData'}}">
|
||||
<view class="sheet-h3">导出健康档案</view>
|
||||
<view class="sheet-p">整理成一段文字,可复制后发给医生或存到备忘录。</view>
|
||||
<view class="export-box">{{exportText || '正在整理…'}}</view>
|
||||
<button class="btn btn-primary btn-block" bindtap="onCopyExport">复制全部内容</button>
|
||||
</block>
|
||||
|
||||
<!-- 意见反馈 -->
|
||||
<block wx:elif="{{innerType === 'feedback'}}">
|
||||
<view class="sheet-h3">意见反馈</view>
|
||||
<block wx:if="{{!fbDone}}">
|
||||
<view class="sheet-p">用着哪儿别扭、想要什么功能,都可以告诉我们。</view>
|
||||
<view class="field"><label>你的反馈</label>
|
||||
<textarea class="textarea" placeholder="例如:希望能记录洗澡时间" placeholder-class="placeholder" value="{{fbContent}}" bindinput="onFbContent"></textarea></view>
|
||||
<view class="field"><label>联系方式(可选)</label>
|
||||
<input class="input" placeholder="微信号或邮箱,方便回复你" placeholder-class="placeholder" value="{{fbContact}}" bindinput="onFbContact"/></view>
|
||||
<button class="btn btn-primary btn-block" bindtap="onSubmitFeedback" disabled="{{saving}}">{{saving ? '提交中…' : '提交反馈'}}</button>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="sheet-p">已收到,谢谢你的反馈 🐾</view>
|
||||
<button class="btn btn-primary btn-block" bindtap="close">好的</button>
|
||||
</block>
|
||||
</block>
|
||||
|
||||
<!-- 关于我们 -->
|
||||
<block wx:elif="{{innerType === 'about'}}">
|
||||
<view class="sheet-h3">关于肉垫计划</view>
|
||||
<view class="sheet-p pre">肉垫计划是给新手养宠人做的照护记录工具。
|
||||
|
||||
按猫狗的品类和成长阶段生成每日照护任务和养护计划,随手记录体重、饮食、排便、用药、疫苗和开支,疫苗驱虫到期前提醒你,并把这些整理成成长报告。
|
||||
|
||||
版本:v0.2
|
||||
反馈:我的 → 意见反馈</view>
|
||||
<view class="sheet-p" style="color:var(--muted);font-size:24rpx">本小程序提供的是记录与观察建议,不能替代医生诊断。宠物出现持续异常请及时就医。</view>
|
||||
<button class="btn btn-primary btn-block" bindtap="close">知道了</button>
|
||||
</block>
|
||||
|
||||
</scroll-view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
@@ -1,170 +0,0 @@
|
||||
.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:48rpx 48rpx 0 0;
|
||||
padding:var(--sp-5) 0 0;
|
||||
transform:translateY(106%);transition:.24s ease;
|
||||
box-shadow:var(--sd-3);
|
||||
}
|
||||
.overlay.show .sheet{transform:translateY(0)}
|
||||
.sheet-scroll{
|
||||
box-sizing:border-box;
|
||||
/* exportData / manageTasks / comments 是最长的三型,圆角间距变化后靠这个上限内滚 */
|
||||
max-height:80vh;
|
||||
padding:0 var(--sp-5) calc(120rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.sheet-scroll::-webkit-scrollbar,.plan-chat::-webkit-scrollbar{width:0;height:0;display:none}
|
||||
.sheetbar{width:80rpx;height:8rpx;border-radius:var(--r-full);background:#E1D8CF;margin:0 auto var(--sp-4)}
|
||||
|
||||
.sheet-h3{font-size:var(--fs-xl);font-weight:var(--fw-b);margin-bottom:var(--sp-3)}
|
||||
.sheet-p{font-size:var(--fs-md);color:var(--text-2);line-height:1.65;margin-bottom:var(--sp-4)}
|
||||
.sheet-p.pre,.poster-list.pre{white-space:pre-line}
|
||||
.poster-h3{font-size:var(--fs-xl);font-weight:var(--fw-b);margin-bottom:var(--sp-3)}
|
||||
|
||||
.ai-input-row{display:flex;gap:var(--sp-3);align-items:center}
|
||||
.ai-input-row .input{flex:1}
|
||||
.ai-send{height:88rpx;line-height:88rpx;padding:0 var(--sp-4)}
|
||||
|
||||
/* 弹层内的日期选择器展示 */
|
||||
.picker-box{color:var(--text)}
|
||||
.del-btn{background:var(--red-soft);color:var(--red-ink)}
|
||||
|
||||
/* 管理任务 / 管理宠物 */
|
||||
.mtask-row{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-3) 0;border-bottom:1rpx solid var(--line)}
|
||||
.mtask-body{flex:1;min-width:0}
|
||||
.mtask-b{font-size:var(--fs-md);font-weight:var(--fw-b)}
|
||||
.mtask-pri{
|
||||
margin-left:var(--sp-2);font-size:var(--fs-cap);font-weight:var(--fw-b);
|
||||
color:var(--primary-ink);background:var(--primary-soft);padding:2rpx var(--sp-2);border-radius:var(--r-full);
|
||||
}
|
||||
.mtask-p{margin-top:6rpx;font-size:var(--fs-sm);color:var(--muted)}
|
||||
.mtask-op{
|
||||
flex:none;font-size:var(--fs-sm);font-weight:var(--fw-b);color:var(--primary-dark);
|
||||
padding:var(--sp-1) var(--sp-3);background:var(--surface-2);border-radius:var(--r-xs);
|
||||
}
|
||||
.mtask-op.del{color:var(--red-ink)}
|
||||
.mtask-form-row{display:flex;align-items:center;gap:var(--sp-3)}
|
||||
/* 这条覆写了全局 .seg-btn 的高度,全局值变化时要一起看 */
|
||||
.mtask-form-row .seg-btn{flex:none;padding:0 var(--sp-4);height:var(--h-sm);line-height:var(--h-sm)}
|
||||
.mtask-form-row .picker-box{flex:1}
|
||||
|
||||
/* 图片选择器 */
|
||||
.img-picker{display:flex;flex-wrap:wrap;gap:var(--sp-3)}
|
||||
.img-thumb{position:relative;width:160rpx;height:160rpx;border-radius:var(--r-sm);overflow:hidden}
|
||||
.img-thumb image{width:100%;height:100%}
|
||||
.img-del{
|
||||
position:absolute;top:0;right:0;width:44rpx;height:44rpx;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
background:rgba(0,0,0,.55);color:#fff;border-radius:0 var(--r-sm) 0 var(--r-sm);
|
||||
}
|
||||
.img-add{
|
||||
width:160rpx;height:160rpx;border-radius:var(--r-sm);border:2rpx dashed #D8CFC4;
|
||||
display:flex;align-items:center;justify-content:center;color:var(--muted2);background:var(--surface-2);
|
||||
}
|
||||
|
||||
/* 导出健康档案 */
|
||||
.export-box{
|
||||
max-height:560rpx;overflow-y:auto;background:var(--surface-2);border-radius:var(--r-md);
|
||||
padding:var(--sp-4);font-size:var(--fs-sm);line-height:1.7;color:var(--text-2);white-space:pre-line;
|
||||
border:1rpx solid var(--line);margin-bottom:var(--sp-4);
|
||||
}
|
||||
.export-box::-webkit-scrollbar{width:0;height:0;display:none}
|
||||
.mine-tag{
|
||||
margin-left:var(--sp-1);font-size:var(--fs-cap);font-weight:var(--fw-b);
|
||||
color:var(--primary-ink);background:var(--primary-soft);padding:2rpx var(--sp-1);border-radius:var(--r-full);
|
||||
}
|
||||
|
||||
/* 离屏画布:挪到可视区外而不是 display:none,否则 selectorQuery 取不到节点 */
|
||||
.poster-canvas{position:fixed;left:-9999rpx;top:0;width:600rpx;height:840rpx}
|
||||
|
||||
/* ── 评论:头 + 滚动列表 + 常驻输入栏 ── */
|
||||
.sheet.sheet-cmp{display:flex;flex-direction:column;height:82vh;padding-bottom:0}
|
||||
.cmp-head{
|
||||
flex:none;text-align:center;padding:0 0 var(--sp-3);
|
||||
font-size:var(--fs-md);font-weight:var(--fw-b);color:var(--text);border-bottom:1rpx solid var(--line);
|
||||
}
|
||||
/* 空态:图标 + 两行字,居中在列表区,不再是一行字飘在大片留白里 */
|
||||
.cmp-empty{
|
||||
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
padding:120rpx 0;color:var(--muted);
|
||||
}
|
||||
.cmp-empty-ic{
|
||||
width:140rpx;height:140rpx;border-radius:50%;margin-bottom:var(--sp-4);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
background:var(--primary-soft);color:var(--primary);
|
||||
}
|
||||
.cmp-empty-t{font-size:var(--fs-lg);font-weight:var(--fw-b);color:var(--text-2)}
|
||||
.cmp-empty-p{margin-top:var(--sp-1);font-size:var(--fs-sm);color:var(--muted2)}
|
||||
.cmp-list{flex:1;min-height:0;padding:var(--sp-3) var(--sp-5) 0}
|
||||
.cmp-dock{
|
||||
flex:none;background:#fff;border-top:1rpx solid var(--line);
|
||||
padding:var(--sp-3) var(--sp-5) calc(20rpx + env(safe-area-inset-bottom));
|
||||
transition:padding-bottom .2s ease;
|
||||
}
|
||||
|
||||
.cm{display:flex;gap:var(--sp-3);padding:var(--sp-3) 0}
|
||||
.cm-av{
|
||||
width:64rpx;height:64rpx;border-radius:50%;flex:none;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
background:var(--primary-soft);color:var(--primary-ink);
|
||||
font-size:var(--fs-md);font-weight:var(--fw-b);
|
||||
}
|
||||
.cm-av-sm{width:48rpx;height:48rpx;font-size:var(--fs-sm)}
|
||||
.cm-body{flex:1;min-width:0}
|
||||
.cm-name{font-size:var(--fs-sm);color:var(--muted)}
|
||||
.cm-at{color:var(--muted2)}
|
||||
.cm-text{margin-top:6rpx;font-size:var(--fs-md);line-height:1.55;word-break:break-word}
|
||||
.cm-imgs{display:flex;flex-wrap:wrap;gap:var(--sp-1);margin-top:var(--sp-2)}
|
||||
.cm-img{width:150rpx;height:150rpx;border-radius:var(--r-sm)}
|
||||
.cm-meta{display:flex;align-items:center;gap:var(--sp-4);margin-top:var(--sp-2);font-size:var(--fs-cap);color:var(--muted2)}
|
||||
.cm-reply-btn{color:var(--muted)}
|
||||
.cm-del{color:var(--red-ink)}
|
||||
.cm-replies{margin-top:var(--sp-2)}
|
||||
.cm-reply{display:flex;gap:var(--sp-2);padding:var(--sp-2) 0}
|
||||
.cm-more{margin-top:4rpx;font-size:var(--fs-cap);color:var(--muted)}
|
||||
|
||||
/* 「回复 @xxx」的提示条 */
|
||||
.cm-replying{
|
||||
display:inline-flex;align-items:center;gap:var(--sp-2);margin-bottom:var(--sp-2);
|
||||
padding:4rpx var(--sp-3);border-radius:var(--r-full);
|
||||
background:var(--primary-soft);color:var(--primary-ink);font-size:var(--fs-cap);
|
||||
}
|
||||
.cm-cancel{display:flex;align-items:center}
|
||||
/* 收起态:底部一条细条,点了才展开成大面板(小红书那套) */
|
||||
.cm-collapsed{
|
||||
flex:none;display:flex;align-items:center;gap:var(--sp-3);
|
||||
background:#fff;border-top:1rpx solid var(--line);
|
||||
padding:var(--sp-3) var(--sp-5) calc(20rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.cm-collapsed-pill{
|
||||
flex:1;min-width:0;height:76rpx;line-height:76rpx;padding:0 var(--sp-5);
|
||||
border-radius:var(--r-full);background:var(--surface-2);color:var(--muted2);
|
||||
font-size:var(--fs-md);overflow:hidden;white-space:nowrap;text-overflow:ellipsis;
|
||||
}
|
||||
.cm-collapsed-pill.has{color:var(--text)}
|
||||
.cm-collapsed-ic{flex:none;display:flex;align-items:center;color:var(--muted)}
|
||||
|
||||
/* 展开态:大写评论面板 */
|
||||
.cm-dock-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--sp-2)}
|
||||
.cm-dock-title{font-size:var(--fs-md);font-weight:var(--fw-b);color:var(--text)}
|
||||
.cm-dock-close{display:flex;align-items:center;color:var(--muted);padding:var(--sp-1)}
|
||||
.cm-ta{
|
||||
width:100%;box-sizing:border-box;min-height:200rpx;max-height:520rpx;height:auto;
|
||||
padding:var(--sp-4);background:var(--surface-2);border-radius:var(--r-md);
|
||||
font-size:var(--fs-lg);line-height:1.5;
|
||||
}
|
||||
.cm-toolbar{display:flex;align-items:center;gap:var(--sp-3);margin-top:var(--sp-3)}
|
||||
.cm-pic{flex:none;display:flex;align-items:center;color:var(--muted)}
|
||||
.cm-count{margin-left:auto;color:var(--muted2);font-size:var(--fs-cap)}
|
||||
.cm-send{
|
||||
flex:none;height:76rpx;padding:0 var(--sp-6);border-radius:var(--r-full);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
background:var(--line);color:#fff;font-size:var(--fs-md);font-weight:var(--fw-b);transition:.16s ease;
|
||||
}
|
||||
.cm-send.on{background:var(--cta)}
|
||||
@@ -1,15 +0,0 @@
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
// 之前漏声明,icon 一直没传进来,pt-icon 收到空值退到 note 兜底字形,
|
||||
// 首页那个「文档」图标就是这么来的
|
||||
icon: { type: String, value: 'plus' },
|
||||
// 首页快速记录用肉垫造型(呼应「肉垫计划」),社区发帖仍用普通图标
|
||||
paw: { type: Boolean, value: false },
|
||||
},
|
||||
methods: {
|
||||
onTap() {
|
||||
this.triggerEvent('tap');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"pt-icon": "/components/pt-icon/index"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<view class="fab {{paw ? 'fab-paw' : ''}}" bindtap="onTap">
|
||||
<!-- 肉垫:一个大掌垫 + 四个脚趾垫,纯 CSS 画,呼应 app 名 -->
|
||||
<view wx:if="{{paw}}" class="paw">
|
||||
<view class="bean bean-1"></view>
|
||||
<view class="bean bean-2"></view>
|
||||
<view class="bean bean-3"></view>
|
||||
<view class="bean bean-4"></view>
|
||||
<view class="pad"></view>
|
||||
</view>
|
||||
<pt-icon wx:else name="{{icon}}" size="{{48}}"></pt-icon>
|
||||
</view>
|
||||
@@ -1,28 +0,0 @@
|
||||
/* 统一暖橘圆钮:首页肉垫和社区发帖用同一套主题色 */
|
||||
.fab{
|
||||
position:fixed;right:36rpx;bottom:calc(190rpx + env(safe-area-inset-bottom));
|
||||
width:112rpx;height:112rpx;border-radius:50%;
|
||||
background:linear-gradient(150deg,#FFB865,#F5943E);
|
||||
color:#fff;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
box-shadow:0 12rpx 28rpx rgba(214,138,52,.42),0 2rpx 6rpx rgba(214,138,52,.3);
|
||||
z-index:60;
|
||||
}
|
||||
.fab:active{transform:scale(.94)}
|
||||
|
||||
/* 肉垫造型:和基础一致的暖橘圆钮,中间一只奶白肉垫 */
|
||||
.fab-paw{width:116rpx;height:116rpx}
|
||||
|
||||
.paw{position:relative;width:66rpx;height:60rpx}
|
||||
/* 掌垫:上窄下宽的圆角心形块,坐在下方 */
|
||||
.paw .pad{
|
||||
position:absolute;left:50%;bottom:0;transform:translateX(-50%);
|
||||
width:44rpx;height:36rpx;background:#FFF7EC;
|
||||
border-radius:48% 48% 44% 44%/62% 62% 40% 40%;
|
||||
}
|
||||
/* 四个脚趾垫,沿上方弧线排开,外侧两个更低、内侧两个更高 */
|
||||
.paw .bean{position:absolute;background:#FFF7EC;border-radius:50%}
|
||||
.paw .bean-1{width:15rpx;height:20rpx;left:0;top:20rpx;transform:rotate(-20deg)}
|
||||
.paw .bean-2{width:16rpx;height:22rpx;left:18rpx;top:5rpx;transform:rotate(-7deg)}
|
||||
.paw .bean-3{width:16rpx;height:22rpx;right:18rpx;top:5rpx;transform:rotate(7deg)}
|
||||
.paw .bean-4{width:15rpx;height:20rpx;right:0;top:20rpx;transform:rotate(20deg)}
|
||||
@@ -1,31 +0,0 @@
|
||||
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' });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"pt-icon": "/components/pt-icon/index"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<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"><pt-icon name="back" size="{{40}}"></pt-icon></view>
|
||||
<view class="nav-title">{{title}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 占位,避免内容被固定导航栏遮挡 -->
|
||||
<view style="height:{{statusBarHeight + navBarHeight}}px;"></view>
|
||||
@@ -1,26 +0,0 @@
|
||||
:host{display:block}
|
||||
|
||||
/* 整页暖橘渐变后,顶部本身就是暖色,nav 不再铺奶白/毛玻璃,透明即可,
|
||||
文字用深棕压在暖底上。内容在 .page-scroll 里滚,不会跑到 nav 背后,
|
||||
所以不需要毛玻璃遮挡。 */
|
||||
.nav-wrap{
|
||||
position:fixed;top:0;left:0;right:0;z-index:100;
|
||||
background:none;
|
||||
}
|
||||
.nav-inner{
|
||||
position:relative;display:flex;align-items:center;justify-content:center;
|
||||
}
|
||||
.nav-title{
|
||||
font-size:var(--fs-md);font-weight:var(--fw-b);color:var(--band-ink);
|
||||
letter-spacing:.5rpx;
|
||||
max-width:56%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
||||
}
|
||||
/* 返回键做成有边界的圆形,和右上角胶囊在视觉上对称 */
|
||||
.nav-back{
|
||||
position:absolute;left:var(--sp-3);top:50%;transform:translateY(-50%);
|
||||
width:60rpx;height:60rpx;border-radius:50%;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
color:var(--band-ink);background:rgba(255,255,255,.6);
|
||||
box-shadow:0 4rpx 12rpx rgba(70,48,25,.06);
|
||||
}
|
||||
.nav-back:active{background:rgba(255,255,255,.92)}
|
||||
@@ -0,0 +1,26 @@
|
||||
Component({
|
||||
properties: {
|
||||
pets: { type: Array, value: [] },
|
||||
selected: { type: Array, value: [] },
|
||||
},
|
||||
data: { viewPets: [] },
|
||||
observers: {
|
||||
"pets,selected": function (pets, selected) {
|
||||
this.setData({
|
||||
viewPets: (pets || []).map((p) => ({
|
||||
...p,
|
||||
chosen: (selected || []).includes(p.id),
|
||||
})),
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggle(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
const selected = this.data.selected.includes(id)
|
||||
? this.data.selected.filter((x) => x !== id)
|
||||
: [...this.data.selected, id];
|
||||
this.triggerEvent("change", { selected });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{ "component": true }
|
||||
@@ -0,0 +1,14 @@
|
||||
<view class="picker">
|
||||
<view
|
||||
wx:for="{{viewPets}}"
|
||||
wx:key="id"
|
||||
class="chip {{item.chosen?'chosen':''}}"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="toggle"
|
||||
>
|
||||
<image wx:if="{{item.avatar_url}}" src="{{item.avatar_url}}" />
|
||||
<text wx:else>🐕</text>
|
||||
<text>{{item.name}}</text>
|
||||
<text class="check">{{item.chosen?'✓':'+'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,31 @@
|
||||
.picker {
|
||||
display: flex;
|
||||
gap: 15rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
background: white;
|
||||
border: 2rpx solid #e0eae2;
|
||||
color: #527164;
|
||||
padding: 11rpx 18rpx;
|
||||
border-radius: 99rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.chip.chosen {
|
||||
background: #dff0e1;
|
||||
border-color: #55a273;
|
||||
color: #145441;
|
||||
font-weight: 700;
|
||||
}
|
||||
.chip image {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.check {
|
||||
margin-left: 3rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
const store = require('../../utils/store.js');
|
||||
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
// 「添加」这一格不是每个页面都该有:计划页/记录页只是在既有宠物之间切,
|
||||
// 建档入口在首页和「我的」就够了,到处摆一个会让这条 chip 变得很长
|
||||
showAdd: { type: Boolean, value: 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');
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"pt-icon": "/components/pt-icon/index"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<scroll-view class="pet-switch" scroll-x="true" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<!-- 纯文字筛选:带头像时不同宠物 chip 高度对不齐,这里只作切换用,文字最稳 -->
|
||||
<view wx:for="{{pets}}" wx:key="id"
|
||||
class="pet-chip {{item.id === currentId ? 'active' : ''}}"
|
||||
data-id="{{item.id}}" bindtap="onPick">{{item.name}}</view>
|
||||
<view wx:if="{{showAdd}}" class="pet-chip pet-chip-add" bindtap="onAdd">+ 添加</view>
|
||||
</scroll-view>
|
||||
@@ -1,11 +0,0 @@
|
||||
.pet-switch{
|
||||
display:block;white-space:nowrap;width:100%;font-size:0;
|
||||
}
|
||||
/* chip 是 inline-flex,行内盒默认按 baseline 对齐,不同内容会高低不齐;
|
||||
统一固定高度 + vertical-align:middle,彻底对齐。父级 font-size:0 消掉 chip 间的空白基线 */
|
||||
.pet-switch .pet-chip{
|
||||
margin-right:20rpx;vertical-align:middle;
|
||||
height:64rpx;padding-top:0;padding-bottom:0;font-size:var(--fs-md);
|
||||
}
|
||||
.pet-switch .pet-chip:last-child{margin-right:0}
|
||||
.pet-chip-add{color:var(--muted)}
|
||||
@@ -1,37 +0,0 @@
|
||||
// 个人主页头部。「我的」和「宠友主页」共用同一份渲染——自己看和别人看
|
||||
// 长得不一样的话,两边会慢慢长歪,装扮功能也就失去意义了。
|
||||
// 差别只有一处:自己看是「装扮主页」,别人看是「关注 TA」。
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
card: { type: Object, value: null },
|
||||
// 装扮页的实时预览:只看效果,不要底部那排按钮
|
||||
preview: { type: Boolean, value: false },
|
||||
hideWall: { type: Boolean, value: false }, // 「我的」页藏掉社交宠物墙,避免和「我的毛孩子」卡重复
|
||||
},
|
||||
data: { initial: '', theme: 'warm' },
|
||||
observers: {
|
||||
card(c) {
|
||||
if (!c) return;
|
||||
this.setData({
|
||||
// WXML 取不了字符串下标,首字得在 JS 里算
|
||||
initial: (c.nickname || '?').slice(0, 1),
|
||||
theme: c.theme || 'warm',
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onFollow() {
|
||||
this.triggerEvent('follow');
|
||||
},
|
||||
onDecorate() {
|
||||
this.triggerEvent('decorate');
|
||||
},
|
||||
onRelations(e) {
|
||||
this.triggerEvent('relations', { kind: e.currentTarget.dataset.kind });
|
||||
},
|
||||
onPet(e) {
|
||||
this.triggerEvent('pet', { id: e.currentTarget.dataset.id });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"pt-icon": "/components/pt-icon/index"
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<view class="pf th-{{theme}}">
|
||||
<!-- 头图:只有设了自定义头图才显示,没设就是干净白卡(不再留一块空色块) -->
|
||||
<image wx:if="{{card.bg_url}}" class="pf-bg-img" src="{{card.bg_url}}" mode="aspectFill"></image>
|
||||
|
||||
<view class="pf-body">
|
||||
<!-- 身份行:头像 + 昵称/签名,左对齐 -->
|
||||
<view class="pf-top">
|
||||
<view class="pf-av">
|
||||
<image wx:if="{{card.avatar_url}}" class="pf-av-img" src="{{card.avatar_url}}" mode="aspectFill"></image>
|
||||
<block wx:else>{{initial}}</block>
|
||||
</view>
|
||||
<view class="pf-id">
|
||||
<view class="pf-name">{{card.nickname || '毛孩子家长'}}<text wx:if="{{card.is_bot}}" class="ai-tag">AI</text></view>
|
||||
<view class="pf-bio">{{card.bio || '这个人很懒,还没写签名'}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 社交数:小一行,数字+标签内联 -->
|
||||
<view class="pf-social">
|
||||
<view class="pf-stat"><text class="pf-n">{{card.post_count || 0}}</text><text class="pf-l">帖子</text></view>
|
||||
<view class="pf-stat" data-kind="following" bindtap="onRelations"><text class="pf-n">{{card.following_count || 0}}</text><text class="pf-l">关注</text></view>
|
||||
<view class="pf-stat" data-kind="followers" bindtap="onRelations"><text class="pf-n">{{card.follower_count || 0}}</text><text class="pf-l">粉丝</text></view>
|
||||
</view>
|
||||
|
||||
<!-- 养宠数据:这个 App 独有,做成高亮带,比帖子数更能说明是个认真的铲屎官 -->
|
||||
<view wx:if="{{card.stats}}" class="pf-keep">
|
||||
<view class="pf-keep-i"><text class="pf-kn">{{card.stats.days || 0}}</text><text class="pf-kl">养宠天数</text></view>
|
||||
<view class="pf-keep-i"><text class="pf-kn">{{card.stats.records || 0}}</text><text class="pf-kl">累计记录</text></view>
|
||||
<view class="pf-keep-i"><text class="pf-kn">{{card.stats.streak || 0}}</text><text class="pf-kl">连续打卡</text></view>
|
||||
</view>
|
||||
|
||||
<!-- 宠物名片墙:给别人看的(受开关控制)。「我的」页 hideWall 藏掉 -->
|
||||
<scroll-view wx:if="{{card.show_pets && card.pets.length && !hideWall}}" class="pf-wall"
|
||||
scroll-x="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view wx:for="{{card.pets}}" wx:key="id" class="pf-pet" data-id="{{item.id}}" bindtap="onPet">
|
||||
<view class="pf-pet-av">
|
||||
<image wx:if="{{item.avatar_url}}" class="pf-pet-img" src="{{item.avatar_url}}" mode="aspectFill"></image>
|
||||
<block wx:else>{{item.emoji}}</block>
|
||||
</view>
|
||||
<view class="pf-pet-n">{{item.name}}</view>
|
||||
<view class="pf-pet-s">{{item.stage}}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 别人主页的关注键(装扮按钮已挪到「我的」页) -->
|
||||
<view wx:if="{{!preview && !card.is_self}}" class="btn {{card.followed ? 'btn-ghost' : 'btn-dark'}} btn-block pf-follow" bindtap="onFollow">
|
||||
<pt-icon wx:if="{{!card.followed}}" name="plus" size="{{28}}"></pt-icon>{{card.followed ? '已关注' : '关注 TA'}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -1,65 +0,0 @@
|
||||
/* 主题只在这里定义两个变量,下面所有着色都走变量。
|
||||
后端存的是 theme key 不是色值,就是为了这套配色能随时在前端改。 */
|
||||
.pf{--pf-a:#F5A947;--pf-b:#FFE6BA;--pf-ink:#8A5A18}
|
||||
.th-warm{--pf-a:#F5A947;--pf-b:#FFE6BA;--pf-ink:#8A5A18}
|
||||
.th-mint{--pf-a:#3FB98A;--pf-b:#D7F3E7;--pf-ink:#1F6B4E}
|
||||
.th-sky{--pf-a:#4A9BE0;--pf-b:#D8ECFB;--pf-ink:#245F94}
|
||||
.th-violet{--pf-a:#9B7BE0;--pf-b:#E8DFFA;--pf-ink:#5B3F9B}
|
||||
.th-ink{--pf-a:#5A5550;--pf-b:#E4DED7;--pf-ink:#2D2925}
|
||||
|
||||
.pf{
|
||||
background:#fff;border-radius:var(--r-lg);overflow:hidden;
|
||||
box-shadow:var(--sd-1);margin-bottom:var(--sp-5);
|
||||
}
|
||||
/* 头图:设了才显示 */
|
||||
.pf-bg-img{width:100%;height:200rpx;display:block}
|
||||
|
||||
.pf-body{padding:var(--sp-5)}
|
||||
|
||||
/* 身份行:头像 + 昵称/签名 左对齐 */
|
||||
.pf-top{display:flex;align-items:center;gap:var(--sp-4)}
|
||||
.pf-av{
|
||||
flex:none;width:120rpx;height:120rpx;border-radius:50%;
|
||||
background:var(--pf-b);color:var(--pf-ink);overflow:hidden;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:var(--fs-xl);font-weight:var(--fw-b);
|
||||
}
|
||||
.pf-av-img{width:100%;height:100%;display:block}
|
||||
.pf-id{flex:1;min-width:0}
|
||||
.pf-name{font-size:var(--fs-xl);font-weight:var(--fw-b);color:var(--text)}
|
||||
.pf-bio{
|
||||
font-size:var(--fs-sm);color:var(--muted);margin-top:8rpx;
|
||||
line-height:1.5;word-break:break-all;
|
||||
}
|
||||
|
||||
/* 社交数:小一行,数字+标签内联,左对齐 */
|
||||
.pf-social{display:flex;gap:var(--sp-6);margin-top:var(--sp-4)}
|
||||
.pf-stat{display:flex;align-items:baseline;gap:6rpx}
|
||||
.pf-n{font-size:var(--fs-md);font-weight:var(--fw-b);color:var(--text)}
|
||||
.pf-l{font-size:var(--fs-cap);color:var(--muted)}
|
||||
|
||||
/* 养宠数据高亮带 */
|
||||
.pf-keep{
|
||||
display:flex;background:var(--pf-b);border-radius:var(--r-md);
|
||||
padding:var(--sp-4) 0;margin-top:var(--sp-4);
|
||||
}
|
||||
.pf-keep-i{flex:1;display:flex;flex-direction:column;align-items:center;gap:2rpx}
|
||||
.pf-kn{font-size:var(--fs-lg);font-weight:var(--fw-b);color:var(--pf-ink)}
|
||||
.pf-kl{font-size:var(--fs-cap);color:var(--pf-ink);opacity:.75}
|
||||
|
||||
.pf-wall{white-space:nowrap;margin-top:var(--sp-4)}
|
||||
.pf-pet{
|
||||
display:inline-block;width:150rpx;vertical-align:top;
|
||||
margin-right:var(--sp-3);white-space:normal;
|
||||
}
|
||||
.pf-pet:last-child{margin-right:0}
|
||||
.pf-pet-av{
|
||||
width:112rpx;height:112rpx;margin:0 auto 8rpx;border-radius:var(--r-md);
|
||||
background:var(--surface-2);display:flex;align-items:center;justify-content:center;
|
||||
font-size:52rpx;overflow:hidden;
|
||||
}
|
||||
.pf-pet-img{width:100%;height:100%;display:block}
|
||||
.pf-pet-n{font-size:var(--fs-cap);font-weight:var(--fw-b);color:var(--text);text-align:center}
|
||||
.pf-pet-s{font-size:var(--fs-cap);color:var(--muted);text-align:center}
|
||||
|
||||
.pf-follow{margin-top:var(--sp-4)}
|
||||
@@ -1,59 +0,0 @@
|
||||
// 线性图标。图标资源在 styles/iconfont.wxss(mask-image + currentColor)。
|
||||
//
|
||||
// name 可以直接传图标名,也可以传服务端下发的值:
|
||||
// - 记录类型(HealthRecord.Type,9 值枚举,可靠)
|
||||
// - 文章分类(Article.Category)
|
||||
// - 历史 emoji(老数据里 icon 字段存的就是 emoji,只能兜底映射)
|
||||
// 三层都命不中时退到 fallback(默认 note),绝不留空白。
|
||||
|
||||
// 记录类型 / 文章分类 → 图标名
|
||||
const BY_TYPE = {
|
||||
weight: 'weight',
|
||||
poop: 'poop',
|
||||
food: 'food',
|
||||
symptom: 'symptom',
|
||||
medicine: 'medicine',
|
||||
vaccine: 'vaccine',
|
||||
deworm: 'deworm',
|
||||
cost: 'cost',
|
||||
photo: 'photo',
|
||||
note: 'doc',
|
||||
};
|
||||
|
||||
// 老数据里存的 emoji → 图标名。新数据不再写 emoji,但历史记录得能正常显示
|
||||
const BY_EMOJI = {
|
||||
'⚖️': 'weight', '⚖': 'weight',
|
||||
'💩': 'poop',
|
||||
'🍽️': 'food', '🍽': 'food', '🍚': 'food',
|
||||
'🤒': 'symptom', '🌡️': 'symptom',
|
||||
'💊': 'medicine',
|
||||
'💰': 'cost', '💸': 'cost',
|
||||
'📷': 'photo', '📸': 'photo',
|
||||
'💉': 'vaccine',
|
||||
'🛡️': 'deworm', '🛡': 'deworm',
|
||||
'📄': 'doc', '📋': 'doc',
|
||||
'📝': 'note', '✍️': 'note', '✍': 'note',
|
||||
'🐱': 'cat', '🐶': 'dog', '🐾': 'community',
|
||||
'🔔': 'bell', '⚙️': 'settings', '⚙': 'settings',
|
||||
'📅': 'plan', '📊': 'report', '🏠': 'home',
|
||||
'⚠️': 'warn', '📖': 'book', '📚': 'book',
|
||||
};
|
||||
|
||||
Component({
|
||||
options: { addGlobalClass: true, virtualHost: true },
|
||||
properties: {
|
||||
name: { type: String, value: '' },
|
||||
size: { type: Number, value: 32 }, // rpx
|
||||
color: { type: String, value: '' }, // 留空则跟随父级文字色
|
||||
fallback: { type: String, value: 'note' },
|
||||
},
|
||||
data: { cls: '', style: '' },
|
||||
observers: {
|
||||
'name,size,color,fallback': function (name, size, color, fallback) {
|
||||
const n = BY_TYPE[name] || BY_EMOJI[name] || name || fallback;
|
||||
const style =
|
||||
'width:' + size + 'rpx;height:' + size + 'rpx' + (color ? ';background-color:' + color : '');
|
||||
this.setData({ cls: 'pt-i pt-i-' + n, style });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<view class="{{cls}}" style="{{style}}"></view>
|
||||
@@ -1,17 +0,0 @@
|
||||
/* 图标资源在组件内 import:这样 mask 定义和用它的节点在同一个样式作用域,
|
||||
不受组件样式隔离影响,custom-tab-bar 这种没开 addGlobalClass 的地方也能直接用。 */
|
||||
@import "../../styles/iconfont.wxss";
|
||||
|
||||
.pt-i {
|
||||
display: inline-block;
|
||||
flex: none;
|
||||
background-color: currentColor;
|
||||
-webkit-mask-image: var(--m);
|
||||
mask-image: var(--m);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
mask-position: center;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// 横向切换控件。此前有三套长得像但尺寸各不同的实现:
|
||||
// plan 的 .inline-tabs(灰槽 + 白块)、community 的 .feed-tab(白药丸 + 橙选中 + 阴影)、
|
||||
// record 的 .rf-chip(白药丸 + 橙选中 + 无阴影,高度还差 10rpx)
|
||||
// 这里合成一个:variant="solid" 对应灰槽白块,variant="pill" 对应横滑药丸。
|
||||
//
|
||||
// items 支持两种写法:
|
||||
// ['推荐','关注','经验'] —— current 传下标
|
||||
// [{key:'',label:'全部'},{key:'weight',label:'体重'}] —— current 传 key
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
items: { type: Array, value: [] },
|
||||
current: { type: null, value: 0 },
|
||||
variant: { type: String, value: 'pill' },
|
||||
},
|
||||
data: { list: [] },
|
||||
observers: {
|
||||
'items,current': function (items, current) {
|
||||
const byKey = typeof current === 'string';
|
||||
const list = (items || []).map((it, i) => {
|
||||
const obj = it && typeof it === 'object';
|
||||
const key = obj ? it.key : i;
|
||||
return {
|
||||
key,
|
||||
label: obj ? it.label : it,
|
||||
active: byKey ? key === current : i === Number(current),
|
||||
};
|
||||
});
|
||||
this.setData({ list });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onTap(e) {
|
||||
const { index, key } = e.currentTarget.dataset;
|
||||
this.triggerEvent('change', { index: Number(index), key });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<scroll-view wx:if="{{variant === 'pill'}}" class="st st-pill" scroll-x="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view wx:for="{{list}}" wx:key="key" class="st-item {{item.active ? 'on' : ''}}"
|
||||
data-index="{{index}}" data-key="{{item.key}}" bindtap="onTap">{{item.label}}</view>
|
||||
</scroll-view>
|
||||
|
||||
<view wx:else class="st st-solid">
|
||||
<view wx:for="{{list}}" wx:key="key" class="st-item {{item.active ? 'on' : ''}}"
|
||||
data-index="{{index}}" data-key="{{item.key}}" bindtap="onTap">{{item.label}}</view>
|
||||
</view>
|
||||
@@ -1,21 +0,0 @@
|
||||
.st{white-space:nowrap}
|
||||
|
||||
/* 横滑药丸:数量不定、可滚动(社区分类、记录类型筛选) */
|
||||
.st-pill{padding:0 0 var(--sp-4)}
|
||||
.st-pill .st-item{
|
||||
display:inline-block;height:var(--h-sm);line-height:var(--h-sm);padding:0 var(--sp-4);
|
||||
margin-right:var(--sp-2);border-radius:var(--r-full);border:1rpx solid var(--line);
|
||||
background:#fff;color:var(--muted);font-size:var(--fs-sm);font-weight:var(--fw-b);
|
||||
}
|
||||
.st-pill .st-item.on{color:var(--primary-dark);background:var(--primary-soft);border-color:var(--primary-line)}
|
||||
|
||||
/* 灰槽白块:数量固定的互斥视图(计划页三段) */
|
||||
.st-solid{
|
||||
display:flex;gap:var(--sp-1);padding:var(--sp-1);border-radius:var(--r-md);
|
||||
background:#EFE8DE;margin-bottom:var(--sp-4);
|
||||
}
|
||||
.st-solid .st-item{
|
||||
flex:1;height:var(--h-sm);line-height:var(--h-sm);text-align:center;border-radius:var(--r-sm);
|
||||
color:var(--muted);font-size:var(--fs-md);font-weight:var(--fw-b);
|
||||
}
|
||||
.st-solid .st-item.on{background:#fff;color:var(--primary-dark)}
|
||||
@@ -1,41 +0,0 @@
|
||||
// 完成度环。用 SVG data-uri 画(和 record 页的体重折线图同一套路),不用 canvas:
|
||||
// canvas 在 scroll-view 里的层级、滚动跟随和截图行为全是坑。
|
||||
const TAU = 2 * Math.PI;
|
||||
const R = 42; // viewBox 100 下的半径,留出描边宽度
|
||||
|
||||
function ringSvg(percent, stroke, track, fill) {
|
||||
const c = TAU * R;
|
||||
const p = Math.max(0, Math.min(100, Number(percent) || 0));
|
||||
const off = c * (1 - p / 100);
|
||||
const circle = (color, dash) =>
|
||||
'<circle cx="50" cy="50" r="' + R + '" fill="none" stroke="' + color +
|
||||
'" stroke-width="' + stroke + '" stroke-linecap="round"' + dash + '/>';
|
||||
return (
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">' +
|
||||
circle(track, '') +
|
||||
'<g transform="rotate(-90 50 50)">' +
|
||||
circle(fill, ' stroke-dasharray="' + c.toFixed(1) + '" stroke-dashoffset="' + off.toFixed(1) + '"') +
|
||||
'</g></svg>'
|
||||
);
|
||||
}
|
||||
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
percent: { type: Number, value: 0 },
|
||||
size: { type: Number, value: 180 }, // rpx
|
||||
stroke: { type: Number, value: 8 }, // viewBox 单位,即直径的百分比
|
||||
track: { type: String, value: '#FFE6BA' },
|
||||
fill: { type: String, value: '#F5A947' },
|
||||
},
|
||||
data: { src: '', boxStyle: '' },
|
||||
observers: {
|
||||
'percent,size,stroke,track,fill': function (percent, size, stroke, track, fill) {
|
||||
const svg = ringSvg(percent, stroke, track, fill);
|
||||
this.setData({
|
||||
src: 'data:image/svg+xml,' + encodeURIComponent(svg),
|
||||
boxStyle: 'width:' + size + 'rpx;height:' + size + 'rpx',
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<view class="ring" style="{{boxStyle}}">
|
||||
<image class="ring-img" src="{{src}}" mode="widthFix"></image>
|
||||
<view class="ring-center"><slot></slot></view>
|
||||
</view>
|
||||
@@ -1,6 +0,0 @@
|
||||
.ring{position:relative;flex:none}
|
||||
.ring-img{width:100%;height:100%;display:block}
|
||||
.ring-center{
|
||||
position:absolute;left:0;top:0;right:0;bottom:0;
|
||||
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
const tabs = [
|
||||
{ path: "/pages/home/home", label: "首页", icon: "⌂" },
|
||||
{ path: "/pages/meetups/meetups", label: "约遛", icon: "♡" },
|
||||
{ path: "/pages/walk/walk", label: "遛狗", icon: "🐾" },
|
||||
{ path: "/pages/tasks/tasks", label: "任务", icon: "☑" },
|
||||
{ path: "/pages/pets/pets", label: "宠物", icon: "♙" },
|
||||
];
|
||||
Component({
|
||||
properties: { current: String },
|
||||
data: { tabs },
|
||||
methods: {
|
||||
go(e) {
|
||||
const path = e.currentTarget.dataset.path;
|
||||
if (path !== this.data.current) wx.redirectTo({ url: path });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{ "component": true }
|
||||
@@ -0,0 +1,12 @@
|
||||
<view class="bar">
|
||||
<view
|
||||
wx:for="{{tabs}}"
|
||||
wx:key="path"
|
||||
class="item {{current===item.path?'active':''}} {{item.label==='遛狗'?'walk':''}}"
|
||||
data-path="{{item.path}}"
|
||||
bindtap="go"
|
||||
>
|
||||
<view class="icon">{{item.icon}}</view>
|
||||
<text>{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,47 @@
|
||||
.bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: calc(124rpx + env(safe-area-inset-bottom));
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background: #fff;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
box-shadow: 0 -8rpx 35rpx rgba(32, 68, 50, 0.06);
|
||||
z-index: 20;
|
||||
}
|
||||
.item {
|
||||
width: 20%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
color: #9eafa4;
|
||||
font-size: 21rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.item.active {
|
||||
color: #174d40;
|
||||
}
|
||||
.icon {
|
||||
font-size: 46rpx;
|
||||
height: 52rpx;
|
||||
line-height: 52rpx;
|
||||
}
|
||||
.walk .icon {
|
||||
width: 82rpx;
|
||||
height: 82rpx;
|
||||
line-height: 82rpx;
|
||||
text-align: center;
|
||||
background: #174d40;
|
||||
color: #fff;
|
||||
border-radius: 27rpx;
|
||||
box-shadow: 0 10rpx 23rpx rgba(24, 74, 59, 0.23);
|
||||
margin-top: -30rpx;
|
||||
font-size: 39rpx;
|
||||
}
|
||||
.walk.active .icon {
|
||||
background: #eaaa69;
|
||||
}
|
||||
Reference in New Issue
Block a user