diff --git a/pets-fe/app.json b/pets-fe/app.json
index 7f5a8cc..15629f2 100644
--- a/pets-fe/app.json
+++ b/pets-fe/app.json
@@ -8,7 +8,8 @@
"pages/community/community",
"pages/learn/learn",
"pages/article/article",
- "pages/profile/profile"
+ "pages/profile/profile",
+ "pages/ai/ai"
],
"window": {
"navigationStyle": "custom",
@@ -21,11 +22,26 @@
"selectedColor": "#DD8420",
"backgroundColor": "#FFFFFF",
"list": [
- { "pagePath": "pages/home/home", "text": "首页" },
- { "pagePath": "pages/plan/plan", "text": "计划" },
- { "pagePath": "pages/record/record", "text": "记录" },
- { "pagePath": "pages/report/report", "text": "报告" },
- { "pagePath": "pages/community/community", "text": "社区" }
+ {
+ "pagePath": "pages/home/home",
+ "text": "首页"
+ },
+ {
+ "pagePath": "pages/plan/plan",
+ "text": "计划"
+ },
+ {
+ "pagePath": "pages/record/record",
+ "text": "记录"
+ },
+ {
+ "pagePath": "pages/report/report",
+ "text": "报告"
+ },
+ {
+ "pagePath": "pages/community/community",
+ "text": "社区"
+ }
]
},
"sitemapLocation": "sitemap.json",
diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.js b/pets-fe/components/bottom-sheet/bottom-sheet.js
index 6484aa3..1be5ea5 100644
--- a/pets-fe/components/bottom-sheet/bottom-sheet.js
+++ b/pets-fe/components/bottom-sheet/bottom-sheet.js
@@ -8,6 +8,13 @@ function fmtDate(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());
+}
+
function ageFromBirthday(bday) {
if (!bday) return '';
const b = new Date(bday);
@@ -60,7 +67,7 @@ Component({
pet: {},
segSel: {},
optSel: {},
- dateVals: { vaccine: '2026-07-21', deworm: '2026-07-15' },
+ dateVals: { vaccine: '', deworm: '' },
wInput: '',
wNote: '',
poopNote: '',
@@ -87,8 +94,6 @@ Component({
addColor: '',
addBreed: '',
commentText: '',
- aiInput: '',
- aiMessages: [],
riskData: null,
comments: [],
reminders: [],
@@ -147,6 +152,9 @@ Component({
patch.wInput = (pet.weight || '').replace('kg', '');
patch.wNote = '';
}
+ // 提醒日期给个合理的默认值。原本写死成固定日期,时间一过就成了「提醒昨天」
+ if (type === 'vaccine') patch.dateVals = { ...this.data.dateVals, vaccine: daysLater(30) };
+ if (type === 'deworm') patch.dateVals = { ...this.data.dateVals, deworm: daysLater(90) };
if (type === 'editPet') {
// 预填当前宠物档案
patch.addName = pet.name || '';
@@ -160,15 +168,6 @@ Component({
};
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);
},
@@ -190,8 +189,6 @@ Component({
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 === 'ai') {
- this.loadAIHistory();
} else if (type === 'pro') {
api.getPro().then((p) => this.setData({ proInfo: p })).catch(() => {});
}
@@ -492,10 +489,18 @@ Component({
const v = this.data.optSel[group];
return v === undefined ? 0 : v;
},
+ // 与 optIdx 的区别:没选就是没选,不当成选了第 0 项
+ optPicked(group) {
+ return this.data.optSel[group] !== undefined;
+ },
// 异常观察 → 生成风险评估(真调后端 AI,失败回退)
onGenRisk() {
const id = store.currentPetId();
+ if (!this.optPicked('symp')) {
+ wx.showToast({ title: '先选一下发生了什么', icon: 'none' });
+ return;
+ }
const symptoms = ['呕吐', '拉稀', '精神差', '皮肤红'];
const durations = ['刚刚', '半天', '1天以上'];
const spirits = ['正常', '一般', '明显变差'];
@@ -597,13 +602,26 @@ Component({
},
onSave() {
+ const t = this.data.innerType;
+ if (t === 'cost' && !(parseFloat(this.data.costAmount) > 0)) {
+ wx.showToast({ title: '先填个金额', icon: 'none' });
+ return;
+ }
+ if (t === 'medicine' && !(this.data.medName || '').trim()) {
+ wx.showToast({ title: '先填药品名称', icon: 'none' });
+ return;
+ }
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';
+ const w = (this.data.wInput || '').toString().replace(/\s+/g, '').replace('kg', '');
+ const num = parseFloat(w);
+ // 原来空值会静默存成「上次的体重」,等于凭空造一条假数据
+ if (!w || !(num > 0)) {
+ wx.showToast({ title: '填一个有效体重', icon: 'none' });
+ return;
+ }
+ const weight = w + 'kg';
this.createAndClose({ type: 'weight', icon: '⚖️', title: '体重 ' + weight, num_value: num, description: (this.data.wNote || '').trim() });
},
onSavePhoto() {
@@ -745,35 +763,5 @@ Component({
this.close();
},
- // AI 聊天
- onAiInput(e) { this.setData({ aiInput: e.detail.value }); },
- // 拉取历史对话,接在开场白之后;没有历史就保持开场白
- loadAIHistory() {
- api
- .aiMessages('sheet', 30)
- .then((list) => {
- if (!list || !list.length) return;
- const history = list.map((m) => ({ role: m.role === 'user' ? 'user' : 'ai', text: m.text }));
- this.setData({ aiMessages: this.data.aiMessages.concat(history) });
- })
- .catch(() => {});
- },
- 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: '网络异常,请稍后再试。' }]),
- });
- });
- },
},
});
diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.wxml b/pets-fe/components/bottom-sheet/bottom-sheet.wxml
index c1f5ee7..49d23cb 100644
--- a/pets-fe/components/bottom-sheet/bottom-sheet.wxml
+++ b/pets-fe/components/bottom-sheet/bottom-sheet.wxml
@@ -33,7 +33,7 @@ module.exports.sel = function (map, key, index, def) {
-
+
@@ -46,7 +46,7 @@ module.exports.sel = function (map, key, index, def) {
偏少
不吃
-
+
@@ -80,7 +80,7 @@ module.exports.sel = function (map, key, index, def) {
建议:{{riskData ? riskData.suggestion : '继续观察精神、食欲和排便;暂停新食物;记录呕吐或腹泻次数。'}}
建议就医:{{riskData ? riskData.seek_care : '如果持续超过 24 小时,或伴随便血、精神明显变差、频繁呕吐,建议尽快就医。'}}
-
+
@@ -93,7 +93,7 @@ module.exports.sel = function (map, key, index, def) {
当前体重:{{pet.weight}}
近期重点:疫苗期、换粮观察、排便记录
建议携带:排便/呕吐照片、饮食变化、疫苗驱虫记录。
-
+
@@ -101,7 +101,7 @@ module.exports.sel = function (map, key, index, def) {
记录用药
-
+
@@ -114,7 +114,7 @@ module.exports.sel = function (map, key, index, def) {
用品
-
+
@@ -125,7 +125,7 @@ module.exports.sel = function (map, key, index, def) {
{{dateVals.vaccine}}
-
+
@@ -136,7 +136,7 @@ module.exports.sel = function (map, key, index, def) {
{{dateVals.deworm}}
-
+
@@ -146,7 +146,7 @@ module.exports.sel = function (map, key, index, def) {
{{pet.emoji}}
{{pet.name}} 的成长照片
- 模拟上传预览
+ 支持 jpg / png,单张不超过 10MB
@@ -184,24 +184,6 @@ module.exports.sel = function (map, key, index, def) {
-
-
- 问问 AI
-
- {{item.text}}
-
-
-
- 猫咪吐了一次
- 多久驱虫
- 换粮软便
-
-
-
-
-
-
diff --git a/pets-fe/components/nav-bar/nav-bar.wxss b/pets-fe/components/nav-bar/nav-bar.wxss
index 7ca13bc..b6fc5ec 100644
--- a/pets-fe/components/nav-bar/nav-bar.wxss
+++ b/pets-fe/components/nav-bar/nav-bar.wxss
@@ -1,19 +1,33 @@
:host{display:block}
+
+/* 原来是一整条半透明色块,底边和页面渐变硬碰硬,屏幕上横着一道接缝。
+ 改成上实下虚的渐变,并且用同一条 mask 把毛玻璃也一起淡出——
+ 只淡化背景色而不淡化 blur 的话,接缝会从「色差」变成「糊边」,一样难看。 */
.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);
+ background:linear-gradient(180deg,
+ rgba(250,247,242,.94) 0%,
+ rgba(250,247,242,.88) 58%,
+ rgba(250,247,242,0) 100%);
+ backdrop-filter:blur(20rpx);
+ -webkit-backdrop-filter:blur(20rpx);
+ -webkit-mask-image:linear-gradient(180deg,#000 0%,#000 62%,transparent 100%);
+ mask-image:linear-gradient(180deg,#000 0%,#000 62%,transparent 100%);
}
.nav-inner{
position:relative;display:flex;align-items:center;justify-content:center;
}
.nav-title{
- font-size:var(--fs-lg);font-weight:var(--fw-b);color:var(--text);
- max-width:60%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
+ font-size:var(--fs-md);font-weight:var(--fw-b);color:var(--text);
+ letter-spacing:.5rpx;
+ max-width:56%;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;
- color:var(--text);
+ 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(--text);background:rgba(255,255,255,.72);
+ box-shadow:0 4rpx 12rpx rgba(70,48,25,.06);
}
+.nav-back:active{background:rgba(255,255,255,.95)}
diff --git a/pets-fe/pages/ai/ai.js b/pets-fe/pages/ai/ai.js
new file mode 100644
index 0000000..b2a28bd
--- /dev/null
+++ b/pets-fe/pages/ai/ai.js
@@ -0,0 +1,107 @@
+const store = require('../../utils/store.js');
+const api = require('../../utils/api.js');
+const { toastErr } = require('../../utils/ui.js');
+
+const SESSION = 'chat';
+const PRESETS = ['猫咪今天吐了一次怎么办?', '幼猫多久驱虫一次?', '换粮后软便怎么办?', '疫苗打完要注意什么?'];
+
+// 打字机:每帧吐几个字。太快没效果,太慢让人等,30ms/2 字接近正常朗读速度。
+const TYPE_STEP = 2;
+const TYPE_INTERVAL = 30;
+
+Page({
+ data: {
+ pet: {},
+ messages: [],
+ input: '',
+ presets: PRESETS,
+ sending: false,
+ loaded: false,
+ },
+ onLoad() {
+ this._unsub = store.subscribe((pet) => this.setData({ pet }));
+ store
+ .ready()
+ .then(() => {
+ this.setData({ pet: store.getPet() });
+ this.loadHistory();
+ })
+ .catch((e) => toastErr(e));
+ },
+ onUnload() {
+ if (this._unsub) this._unsub();
+ this.stopTyping();
+ },
+
+ loadHistory() {
+ const opening = {
+ role: 'ai',
+ text: '我是' + (store.getPet().name || '毛孩子') + '的养宠助手。我会优先根据档案、计划和历史记录回答,而不是泛泛聊天。',
+ };
+ api
+ .aiMessages(SESSION, 50)
+ .then((list) => {
+ const history = (list || []).map((m) => ({ role: m.role === 'user' ? 'user' : 'ai', text: m.text }));
+ this.setData({ messages: [opening].concat(history), loaded: true }, () => this.scrollToBottom());
+ })
+ .catch(() => this.setData({ messages: [opening], loaded: true }));
+ },
+
+ onInput(e) {
+ this.setData({ input: e.detail.value });
+ },
+
+ onSend(e) {
+ const preset = e.currentTarget && e.currentTarget.dataset.text;
+ const text = (preset || this.data.input || '').trim();
+ if (!text || this.data.sending) return;
+
+ // 先把用户消息和一个空的 AI 气泡放上去,AI 气泡随后逐字填充
+ const messages = this.data.messages.concat([
+ { role: 'user', text },
+ { role: 'ai', text: '', pending: true },
+ ]);
+ this.setData({ messages, input: '', sending: true }, () => this.scrollToBottom());
+
+ api
+ .aiChat({ pet_id: store.currentPetId() || null, session: SESSION, text })
+ .then((res) => this.typewrite(res.reply || '(没有返回内容)'))
+ .catch((e) => this.typewrite(e.message || '网络异常,请稍后再试。'));
+ },
+
+ // 逐字把回复填进最后一个气泡
+ typewrite(full) {
+ this.stopTyping();
+ const idx = this.data.messages.length - 1;
+ let n = 0;
+ this._timer = setInterval(() => {
+ n = Math.min(full.length, n + TYPE_STEP);
+ this.setData({ [`messages[${idx}].text`]: full.slice(0, n) });
+ if (n % 20 === 0) this.scrollToBottom();
+ if (n >= full.length) {
+ this.stopTyping();
+ this.setData({ [`messages[${idx}].pending`]: false, sending: false }, () => this.scrollToBottom());
+ }
+ }, TYPE_INTERVAL);
+ },
+ stopTyping() {
+ if (this._timer) {
+ clearInterval(this._timer);
+ this._timer = null;
+ }
+ },
+ // 打字过程中点一下可以跳过,不用干等
+ onSkip() {
+ if (!this.data.sending) return;
+ this.stopTyping();
+ this.setData({ sending: false });
+ },
+
+ scrollToBottom() {
+ this.setData({ scrollInto: 'chat-bottom' });
+ },
+
+ onShareAppMessage() {
+ return { title: '肉垫计划 · AI 养宠助手', path: '/pages/ai/ai' };
+ },
+});
diff --git a/pets-fe/pages/ai/ai.json b/pets-fe/pages/ai/ai.json
new file mode 100644
index 0000000..6594cd8
--- /dev/null
+++ b/pets-fe/pages/ai/ai.json
@@ -0,0 +1,6 @@
+{
+ "usingComponents": {
+ "nav-bar": "/components/nav-bar/nav-bar",
+ "pt-icon": "/components/pt-icon/index"
+ }
+}
diff --git a/pets-fe/pages/ai/ai.wxml b/pets-fe/pages/ai/ai.wxml
new file mode 100644
index 0000000..e444230
--- /dev/null
+++ b/pets-fe/pages/ai/ai.wxml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+ {{item.text}}▌
+
+
+
+
+
+
+
+
+ {{item}}
+
+
+
+
+
+
+
+
diff --git a/pets-fe/pages/ai/ai.wxss b/pets-fe/pages/ai/ai.wxss
new file mode 100644
index 0000000..c06e288
--- /dev/null
+++ b/pets-fe/pages/ai/ai.wxss
@@ -0,0 +1,43 @@
+/* 整页聊天:上面滚动区自适应,下面输入区固定,中间不留空 */
+.chat-scroll{flex:1;min-height:0}
+.chat-body{padding:var(--sp-3) var(--pad-x) var(--sp-5)}
+.chat-bottom{height:1rpx}
+
+.row{display:flex;align-items:flex-start;gap:var(--sp-2);margin-bottom:var(--sp-4)}
+.row-user{justify-content:flex-end}
+.ai-avatar{
+ width:60rpx;height:60rpx;border-radius:var(--r-sm);flex:none;margin-top:4rpx;
+ display:flex;align-items:center;justify-content:center;
+ background:var(--cta);color:#fff;
+}
+.bubble{
+ max-width:76%;border-radius:var(--r-md);padding:var(--sp-3) var(--sp-4);
+ font-size:var(--fs-md);line-height:1.62;word-break:break-word;
+}
+.bubble-ai{background:#fff;box-shadow:var(--sd-1);border-top-left-radius:var(--r-xs)}
+.bubble-user{background:var(--primary-soft);color:var(--primary-ink);border-top-right-radius:var(--r-xs)}
+/* 打字光标 */
+.caret{color:var(--primary);animation:blink .9s steps(1) infinite}
+@keyframes blink{50%{opacity:0}}
+
+/* 底部输入区 */
+.dock{
+ flex:none;background:rgba(255,255,255,.94);
+ backdrop-filter:blur(28rpx);-webkit-backdrop-filter:blur(28rpx);
+ border-top:1rpx solid var(--line);
+ padding:var(--sp-3) var(--pad-x) calc(var(--sp-3) + env(safe-area-inset-bottom));
+}
+.presets{white-space:nowrap;margin-bottom:var(--sp-3)}
+.preset{
+ 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(--text-2);font-size:var(--fs-sm);
+}
+.input-row{display:flex;align-items:center;gap:var(--sp-3)}
+.input-row .input{flex:1;background:var(--surface-2)}
+.send{
+ width:88rpx;height:88rpx;border-radius:var(--r-md);flex:none;
+ display:flex;align-items:center;justify-content:center;
+ background:var(--line);color:#fff;transition:.16s ease;
+}
+.send.on{background:var(--cta)}
diff --git a/pets-fe/pages/community/community.js b/pets-fe/pages/community/community.js
index 65bc7b9..b2c994d 100644
--- a/pets-fe/pages/community/community.js
+++ b/pets-fe/pages/community/community.js
@@ -102,7 +102,7 @@ Page({
wx.navigateTo({ url: '/pages/learn/learn' });
},
onFab() {
- this.setData({ sheetType: 'ai', sheetPostId: '', sheetShow: true });
+ wx.navigateTo({ url: '/pages/ai/ai' });
},
// 长按自己的帖子可删除
onLongPressPost(e) {
diff --git a/pets-fe/pages/home/home.js b/pets-fe/pages/home/home.js
index 22f96a7..488a97d 100644
--- a/pets-fe/pages/home/home.js
+++ b/pets-fe/pages/home/home.js
@@ -189,7 +189,7 @@ Page({
this.openSheetType('editPet');
},
onFab() {
- this.openSheetType('ai');
+ wx.navigateTo({ url: '/pages/ai/ai' });
},
goProfile() {
wx.navigateTo({ url: '/pages/profile/profile' });
diff --git a/pets-fe/pages/home/home.wxml b/pets-fe/pages/home/home.wxml
index 4f2ff22..6aa8c07 100644
--- a/pets-fe/pages/home/home.wxml
+++ b/pets-fe/pages/home/home.wxml
@@ -79,7 +79,7 @@
AI 今日建议
{{summary.advice || '正在生成今日建议…'}}
-
+
问问 AI 养宠助手
diff --git a/pets-fe/pages/learn/learn.js b/pets-fe/pages/learn/learn.js
index fc92938..ddec272 100644
--- a/pets-fe/pages/learn/learn.js
+++ b/pets-fe/pages/learn/learn.js
@@ -35,7 +35,7 @@ Page({
this.setData({ sheetShow: false });
},
onFab() {
- this.setData({ sheetType: 'ai', sheetShow: true });
+ wx.navigateTo({ url: '/pages/ai/ai' });
},
onShareAppMessage() {
return { title: '新手养宠知识合集', path: '/pages/learn/learn' };
diff --git a/pets-fe/pages/plan/plan.js b/pets-fe/pages/plan/plan.js
index 111159b..528b853 100644
--- a/pets-fe/pages/plan/plan.js
+++ b/pets-fe/pages/plan/plan.js
@@ -178,7 +178,7 @@ Page({
this.setData({ sheetType: 'addPet', sheetShow: true });
},
onFab() {
- this.setData({ sheetType: 'ai', sheetShow: true });
+ wx.navigateTo({ url: '/pages/ai/ai' });
},
onShareAppMessage() {
return { title: '我给毛孩子做了份养护计划', path: '/pages/plan/plan' };
diff --git a/pets-fe/pages/profile/profile.js b/pets-fe/pages/profile/profile.js
index 167b9e0..7f6d451 100644
--- a/pets-fe/pages/profile/profile.js
+++ b/pets-fe/pages/profile/profile.js
@@ -68,7 +68,7 @@ Page({
this.loadSummary();
},
onFab() {
- this.setData({ sheetType: 'ai', sheetShow: true });
+ wx.navigateTo({ url: '/pages/ai/ai' });
},
onShareAppMessage() {
return { title: '肉垫计划 · 新手养宠助手', path: '/pages/home/home' };
diff --git a/pets-fe/pages/record/record.js b/pets-fe/pages/record/record.js
index 6adc7a2..7a7beef 100644
--- a/pets-fe/pages/record/record.js
+++ b/pets-fe/pages/record/record.js
@@ -245,7 +245,7 @@ Page({
this.openSheetType('addPet');
},
onFab() {
- this.openSheetType('ai');
+ wx.navigateTo({ url: '/pages/ai/ai' });
},
onShareAppMessage() {
return { title: '用肉垫计划记录毛孩子的成长', path: '/pages/record/record' };
diff --git a/pets-fe/pages/report/report.js b/pets-fe/pages/report/report.js
index c064f85..f8eeb97 100644
--- a/pets-fe/pages/report/report.js
+++ b/pets-fe/pages/report/report.js
@@ -59,7 +59,7 @@ Page({
this.setData({ sheetType: 'addPet', sheetShow: true });
},
onFab() {
- this.setData({ sheetType: 'ai', sheetShow: true });
+ wx.navigateTo({ url: '/pages/ai/ai' });
},
onShareAppMessage() {
return { title: '看看我家毛孩子的成长报告', path: '/pages/report/report' };