feat: 下掉用户端 AI,首页悬浮键改成快速记录
## 只砍入口,后端一行不动
10 个 AI 路由、service/ai_*.go、后台配额配置页、ai_usages / ai_messages
两张表全部保留。api.js 里那 5 个方法注释掉而不是删——想开回来解注释、
把入口接上就行,不用重写。
## 5 处露出
pages/ai 聊天页 删页面 + 从 app.json 移除
首页「AI 今日建议」卡 整张删(含「问问 AI 养宠助手」按钮)
计划页「AI 计划」tab 删 tab + wxml 分支 + ?tab 深链;只剩「路线图」一项后
整条 seg-tabs 也藏了——单选的 tab 条是个没用的控件
异常观察的 AI 风险评估 见下
引导页「AI 日历」文案 改成「养护日历」
## 异常观察这一处差点做错
原链路是:symptom 只收集表单 → AI 风险评估 → 用户在 risk 页再点保存 →
buildRecord 的 case 'risk' 才真正落库。**symptom 自己从来不落库。**
拆掉中间环节如果只删 risk,结果就是「记了异常但没存下来」,而且不报错。
补了 case 'symptom' 让它自己存。连带发现第二个问题:category 存的是 AI 给的
风险等级,而 report.go:46 按 category='高' 统计周报的高风险数——不写这个字段
周报会永远是 0。改成让用户自己选严重程度(轻微/需留意/严重 → 低/中/高):
谁看着它谁最清楚,比规则化猜一个准,还顺手保住了周报。
## FAB
首页 → 打开 24 项分组选择器(弹层新增 quickRecord 分支,
点某一项在同一个弹层内 setType 切过去,不关不跳)
社区 → 改成发帖(它本来就不该是记录入口)
记录/报告/我的/计划/学习 → 直接去掉,底部留白从 pad-b-fab 换成 pad-b-plain,
不然白留 330rpx
fab 组件原来图标写死成 ai,加了 icon 属性——按钮干什么事图标就得是什么。
顺手删了 settings.js 里一个死的 onFab(Phase A 拆页时漏的,页面上根本没有 fab)。
.tg 分组样式从 record.wxss 提到 app.wxss:记录页和快速记录弹层都在用,
页面级 wxss 跨不了页(这个项目已经栽过三次)。
## 顺手修了周报两个先前就有的 bug
验证时撞上的,和 AI 无关,但 AI 建议卡拆掉后周报权重变高了:
1. 摘要把「无高风险异常记录」写死,和它自己刚算出来的 highRisk 自相矛盾——
记了 2 条高风险,摘要还说没有
2. next_week_focus 是一整句静态文案「第 2 针疫苗提醒、继续观察体重趋势、
避免频繁更换食物」,不管谁的宠物多大年纪都是这句,而「第 2 针疫苗」
对成年猫狗根本不适用。改成按真实数据拼:未来 7 天到期的提醒 +
有高风险就提就医 + 一周没称体重就提醒补记
## 验证(预生产库实跑)
异常观察三档 低/中/高 各存一条,category 正确落库
周报 summary「有 2 条高风险异常记录」,不再自相矛盾
next_week_focus 有高风险的宠物 → 「继续观察上周记录的异常,必要时就医」
新建幼犬(提醒都在 7 天外、没称过体重)→ 「本周还没称体重,补记一次」
症状聚类洞察 仍然工作(insight.go 按 symptom 过滤,没受影响)
全站 grep 无 pages/ai / onFab / riskData / onGenRisk 残留
FAB 只剩首页(plus)和社区(edit)两处
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,6 @@
|
||||
"pages/mine/mine",
|
||||
"pages/decorate/decorate",
|
||||
"pages/settings/settings",
|
||||
"pages/ai/ai",
|
||||
"pages/user/user",
|
||||
"pages/relations/relations"
|
||||
],
|
||||
|
||||
@@ -427,3 +427,20 @@ page{scrollbar-width:none;-ms-overflow-style:none}
|
||||
color:var(--primary-dark);background:var(--primary-soft);border:1rpx solid var(--primary-line);
|
||||
}
|
||||
.follow-btn.on{color:var(--muted);background:#F4F1EC;border-color:var(--line)}
|
||||
|
||||
/* ===== 记录类型分组宫格 =====
|
||||
记录页和「快速记录」弹层都在用,页面级 wxss 跨不了页
|
||||
四组各一个色系,色值全走 token 的 soft/ink 二件套,没有新色。
|
||||
.quick 的配色由 .tone-N 决定(app.wxss 里已有),这里只管组标题和留白 */
|
||||
.tg{margin-bottom:var(--sp-4)}
|
||||
.tg:last-of-type{margin-bottom:0}
|
||||
.tg-head{
|
||||
display:flex;align-items:center;gap:var(--sp-2);
|
||||
font-size:var(--fs-md);font-weight:var(--fw-b);margin-bottom:var(--sp-3);
|
||||
}
|
||||
.tg-bar{width:6rpx;height:26rpx;border-radius:3rpx;background:currentColor}
|
||||
.tg-n{margin-left:auto;font-size:var(--fs-cap);font-weight:var(--fw);color:var(--muted2)}
|
||||
.tg.tone-1 .tg-head{color:var(--primary-ink)}
|
||||
.tg.tone-2 .tg-head{color:var(--green-ink)}
|
||||
.tg.tone-3 .tg-head{color:var(--blue-ink)}
|
||||
.tg.tone-4 .tg-head{color:var(--purple-ink)}
|
||||
|
||||
@@ -98,6 +98,13 @@ const SIMPLE_HINT = {
|
||||
supplement: '吃的什么、多大剂量、吃多久',
|
||||
};
|
||||
|
||||
// 异常观察的四组选项。原来散在 AI 风险评估那个方法里当局部变量,
|
||||
// 现在 buildRecord 也要用,提到模块级
|
||||
const SYMPTOMS = ['呕吐', '拉稀', '精神差', '皮肤红'];
|
||||
const DURATIONS = ['刚刚', '半天', '1天以上'];
|
||||
const SPIRITS = ['正常', '一般', '明显变差'];
|
||||
const SEVERITY = ['低', '中', '高'];
|
||||
|
||||
const POOP = ['正常', '软便', '拉稀'];
|
||||
const FOOD = ['正常', '偏少', '不吃'];
|
||||
const COST = ['食品', '医疗', '用品'];
|
||||
@@ -119,6 +126,7 @@ Component({
|
||||
optSel: {},
|
||||
dateVals: { vaccine: '', deworm: '' },
|
||||
// 通用简易记录(洗护/清洁那 15 种共用)
|
||||
typeGroups: [],
|
||||
isSimple: false,
|
||||
simpleLabel: '',
|
||||
simpleDate: '',
|
||||
@@ -151,7 +159,6 @@ Component({
|
||||
addColor: '',
|
||||
addBreed: '',
|
||||
commentText: '',
|
||||
riskData: null,
|
||||
comments: [],
|
||||
reminders: [],
|
||||
poster: null,
|
||||
@@ -213,13 +220,13 @@ Component({
|
||||
patch.replyTo = {};
|
||||
patch.cmFocus = false;
|
||||
patch.kbHeight = 0;
|
||||
patch.riskData = null;
|
||||
patch.saving = false;
|
||||
patch.simpleNote = '';
|
||||
patch.simpleImages = [];
|
||||
}
|
||||
// 是否走通用简易表单。缓存热的时候是同步的;万一没热就按「不是」处理,
|
||||
// 那 9 种的分支写死在 wxml 里不依赖这份缓存,不会因此打不开
|
||||
if (type === 'quickRecord') patch.typeGroups = recordTypes.groups() || [];
|
||||
const simple = recordTypes.isSimple(type);
|
||||
patch.isSimple = simple;
|
||||
if (simple) {
|
||||
@@ -754,37 +761,6 @@ Component({
|
||||
},
|
||||
|
||||
// 异常观察 → 生成风险评估(真调后端 AI,失败回退)
|
||||
onGenRisk() {
|
||||
const id = store.currentPetId();
|
||||
if (!this.optPicked('symp')) {
|
||||
wx.showToast({ title: '先选一下发生了什么', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const symptoms = ['呕吐', '拉稀', '精神差', '皮肤红'];
|
||||
const durations = ['刚刚', '半天', '1天以上'];
|
||||
const spirits = ['正常', '一般', '明显变差'];
|
||||
const body = {
|
||||
symptoms: [symptoms[this.optIdx('symp')]],
|
||||
duration: durations[this.segIdx('dur')],
|
||||
spirit: spirits[this.segIdx('spirit')],
|
||||
};
|
||||
if (!id) {
|
||||
this.setData({ innerType: 'risk', riskData: null });
|
||||
return;
|
||||
}
|
||||
wx.showLoading({ title: '分析中...' });
|
||||
api
|
||||
.assessSymptom(id, body)
|
||||
.then((res) => {
|
||||
wx.hideLoading();
|
||||
this.setData({ innerType: 'risk', riskData: res });
|
||||
})
|
||||
.catch(() => {
|
||||
wx.hideLoading();
|
||||
this.setData({ innerType: 'risk', riskData: null });
|
||||
});
|
||||
},
|
||||
|
||||
// 依据当前弹层类型组装一条健康记录
|
||||
buildRecord() {
|
||||
const t = this.data.innerType;
|
||||
@@ -841,12 +817,17 @@ Component({
|
||||
return { type: 'vaccine', icon: '💉', title: '疫苗提醒:' + this.data.dateVals.vaccine };
|
||||
case 'deworm':
|
||||
return { type: 'deworm', icon: '🛡️', title: '驱虫提醒:' + this.data.dateVals.deworm };
|
||||
case 'risk': {
|
||||
// 用 AI 返回的真实风险等级,而不是写死「中」
|
||||
const r = this.data.riskData || {};
|
||||
const lv = r.risk_level || '中';
|
||||
const desc = [r.causes, r.suggestion].filter(Boolean).join(';');
|
||||
return { type: 'symptom', icon: '🤒', title: '异常观察:风险' + lv, category: lv, description: desc };
|
||||
case 'symptom': {
|
||||
// 原来这一支不落库:symptom 只是收集表单,真正建记录是在 AI 风险评估
|
||||
// 之后的 risk 那一步。AI 下掉后必须自己存,否则「记了异常但没存下来」。
|
||||
//
|
||||
// category 存严重程度(高/中/低)——这是周报 high_risk_count 的依据
|
||||
// (report.go:46 按 category='高' 过滤)。原来这个值是 AI 给的,
|
||||
// 现在改成用户自己选:谁看着它谁最清楚,比规则化猜一个准。
|
||||
const sy = SYMPTOMS[this.optIdx('symp')];
|
||||
const lv = SEVERITY[this.segIdx('sev')] || '中';
|
||||
const desc = ['持续' + DURATIONS[this.segIdx('dur')], '精神' + SPIRITS[this.segIdx('spirit')]].join(',');
|
||||
return { type: 'symptom', icon: '🤒', title: '异常观察:' + sy, category: lv, description: desc };
|
||||
}
|
||||
case 'vetSummary':
|
||||
return { type: 'note', icon: '📄', title: '生成就医前摘要' };
|
||||
|
||||
@@ -133,7 +133,7 @@ module.exports.sel = function (map, key, index, def) {
|
||||
<!-- 异常观察 -->
|
||||
<block wx:elif="{{innerType === 'symptom'}}">
|
||||
<view class="sheet-h3">异常观察</view>
|
||||
<view class="sheet-p">如实描述异常,AI 会给出观察建议与就医提示(非诊断)。</view>
|
||||
<view class="sheet-p">记下来,趋势和就医摘要都会用到。持续变差或伴随便血请尽快就医。</view>
|
||||
<view class="field"><label>发生了什么?</label><view class="mini-options">
|
||||
<view class="option {{u.sel(optSel,'symp',0,0)?'selected':''}}" data-group="symp" data-index="0" bindtap="onOpt">呕吐</view>
|
||||
<view class="option {{u.sel(optSel,'symp',1,0)?'selected':''}}" data-group="symp" data-index="1" bindtap="onOpt">拉稀</view>
|
||||
@@ -150,19 +150,14 @@ module.exports.sel = function (map, key, index, def) {
|
||||
<view class="seg-btn {{u.sel(segSel,'spirit',1,0)?'selected':''}}" data-group="spirit" data-index="1" bindtap="onSeg">一般</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'spirit',2,0)?'selected':''}}" data-group="spirit" data-index="2" bindtap="onSeg">明显变差</view>
|
||||
</view></view>
|
||||
<button class="btn btn-primary btn-block" bindtap="onGenRisk">生成观察建议</button>
|
||||
</block>
|
||||
|
||||
<!-- AI 观察建议 -->
|
||||
<block wx:elif="{{innerType === 'risk'}}">
|
||||
<view class="sheet-h3">AI 观察建议</view>
|
||||
<view class="risk-box"><pt-icon name="warn" size="{{32}}"></pt-icon> 风险等级:{{riskData ? riskData.risk_level : '中'}}</view>
|
||||
<view class="sheet-p"><text class="bold">可能原因:</text>{{riskData ? riskData.causes : '可能与换粮、应激或消化不适有关。不要把这里写成诊断结论。'}}</view>
|
||||
<view class="sheet-p"><text class="bold">建议:</text>{{riskData ? riskData.suggestion : '继续观察精神、食欲和排便;暂停新食物;记录呕吐或腹泻次数。'}}</view>
|
||||
<view class="sheet-p"><text class="bold">建议就医:</text>{{riskData ? riskData.seek_care : '如果持续超过 24 小时,或伴随便血、精神明显变差、频繁呕吐,建议尽快就医。'}}</view>
|
||||
<view class="field"><label>严重程度</label><view class="seg">
|
||||
<view class="seg-btn {{u.sel(segSel,'sev',0,1)?'selected':''}}" data-group="sev" data-index="0" bindtap="onSeg">轻微</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'sev',1,1)?'selected':''}}" data-group="sev" data-index="1" bindtap="onSeg">需留意</view>
|
||||
<view class="seg-btn {{u.sel(segSel,'sev',2,1)?'selected':''}}" data-group="sev" data-index="2" bindtap="onSeg">严重</view>
|
||||
</view></view>
|
||||
<view class="sheet-actions">
|
||||
<button class="btn btn-ghost" bindtap="onSave">保存记录</button>
|
||||
<button class="btn btn-primary" data-type="vetSummary" bindtap="goSheet">就医摘要</button>
|
||||
<button class="btn btn-ghost" data-type="vetSummary" bindtap="goSheet">就医摘要</button>
|
||||
<button class="btn btn-primary" bindtap="onSave">保存记录</button>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
@@ -258,6 +253,23 @@ module.exports.sel = function (map, key, index, def) {
|
||||
<button class="btn btn-primary btn-block" bindtap="onSave">保存记录</button>
|
||||
</block>
|
||||
|
||||
<!-- 快速记录:首页 FAB 打开的 24 项选择器。
|
||||
点某一项后在同一个弹层里 setType 切到那个表单,不关不跳 -->
|
||||
<block wx:elif="{{innerType === 'quickRecord'}}">
|
||||
<view class="sheet-h3">记一笔</view>
|
||||
<view class="sheet-p">选一件刚做完的事,几秒记完。</view>
|
||||
<view wx:for="{{typeGroups}}" wx:key="key" class="tg {{item.tone}}">
|
||||
<view class="tg-head"><view class="tg-bar"></view>{{item.label}}<text class="tg-n">{{item.items.length}}</text></view>
|
||||
<view class="quick-grid cols-3">
|
||||
<view wx:for="{{item.items}}" wx:for-item="t" wx:key="code" class="quick {{item.tone}}"
|
||||
data-type="{{t.code}}" bindtap="goSheet">
|
||||
<pt-icon name="{{t.icon}}" size="{{44}}" fallback="note"></pt-icon>{{t.label}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{!typeGroups.length}}" class="empty">还没有配置可记事项</view>
|
||||
</block>
|
||||
|
||||
<!-- 提醒中心 -->
|
||||
<block wx:elif="{{innerType === 'reminders'}}">
|
||||
<view class="sheet-h3">提醒中心</view>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<view class="fab" bindtap="onTap"><pt-icon name="ai" size="{{48}}"></pt-icon></view>
|
||||
<view class="fab" bindtap="onTap"><pt-icon name="{{icon}}" size="{{48}}"></pt-icon></view>
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
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) => {
|
||||
const msg = (e && e.message) || '网络异常,请稍后再试。';
|
||||
// 额度用完不是故障,别让用户以为是网络问题反复重试
|
||||
this.typewrite(msg.indexOf('次数') >= 0 ? msg + '\n\n每天的次数是为了控制成本,明天 0 点恢复。' : msg);
|
||||
});
|
||||
},
|
||||
|
||||
// 逐字把回复填进最后一个气泡
|
||||
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' });
|
||||
},
|
||||
|
||||
goAIPlan() {
|
||||
wx.navigateTo({ url: '/pages/plan/plan?tab=aiPlan' });
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return { title: '肉垫计划 · AI 养宠助手', path: '/pages/ai/ai' };
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"nav-bar": "/components/nav-bar/nav-bar",
|
||||
"pt-icon": "/components/pt-icon/index"
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<nav-bar title="AI 养宠助手" show-back="{{true}}"></nav-bar>
|
||||
|
||||
<scroll-view class="chat-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}"
|
||||
scroll-into-view="{{scrollInto}}" scroll-with-animation="{{true}}" bindtap="onSkip">
|
||||
<view class="chat-body">
|
||||
<view wx:for="{{messages}}" wx:key="index" class="row row-{{item.role}}">
|
||||
<view wx:if="{{item.role === 'ai'}}" class="ai-avatar"><pt-icon name="ai" size="{{34}}"></pt-icon></view>
|
||||
<view class="bubble bubble-{{item.role}}">
|
||||
<text>{{item.text}}</text><text wx:if="{{item.pending}}" class="caret">▌</text>
|
||||
</view>
|
||||
</view>
|
||||
<view id="chat-bottom" class="chat-bottom"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="dock">
|
||||
<scroll-view wx:if="{{!sending}}" class="presets" scroll-x="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<!-- 结构化养护计划走计划页(它要渲染 extracted + 可勾选节点,塞进聊天流里会变成两套渲染),
|
||||
这里只保证从 AI 页找得到它 -->
|
||||
<view class="preset preset-go" bindtap="goAIPlan"><pt-icon name="plan" size="{{24}}"></pt-icon>生成养护计划</view>
|
||||
<view wx:for="{{presets}}" wx:key="*this" class="preset" data-text="{{item}}" bindtap="onSend">{{item}}</view>
|
||||
</scroll-view>
|
||||
<view class="input-row">
|
||||
<input class="input" placeholder="描述一下 {{pet.name || '毛孩子'}} 的情况…" placeholder-class="placeholder"
|
||||
value="{{input}}" bindinput="onInput" confirm-type="send" bindconfirm="onSend"
|
||||
adjust-position="{{true}}" cursor-spacing="20"/>
|
||||
<view class="send {{input ? 'on' : ''}}" bindtap="onSend">
|
||||
<pt-icon name="{{sending ? 'clock' : 'next'}}" size="{{34}}"></pt-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -1,47 +0,0 @@
|
||||
/* 整页聊天:上面滚动区自适应,下面输入区固定,中间不留空 */
|
||||
.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)}
|
||||
.preset-go{
|
||||
display:inline-flex;align-items:center;gap:6rpx;
|
||||
background:var(--primary-soft);color:var(--primary-dark);border-color:var(--primary-line);
|
||||
}
|
||||
@@ -128,8 +128,9 @@ Page({
|
||||
goLearn() {
|
||||
wx.navigateTo({ url: '/pages/learn/learn' });
|
||||
},
|
||||
onFab() {
|
||||
wx.navigateTo({ url: '/pages/ai/ai' });
|
||||
// 社区的悬浮键本来就该是发帖,不是记录(原来它跳 AI 聊天页)
|
||||
onCreatePost() {
|
||||
this.setData({ sheetType: 'createPost', sheetShow: true });
|
||||
},
|
||||
// 长按自己的帖子可删除
|
||||
onLongPressPost(e) {
|
||||
|
||||
@@ -57,6 +57,6 @@ module.exports.isUrl = function (s) { return s && s.indexOf('http') === 0; }
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<fab bind:tap="onFab"></fab>
|
||||
<fab icon="edit" bind:tap="onCreatePost"></fab>
|
||||
<bottom-sheet show="{{sheetShow}}" type="{{sheetType}}" post-id="{{sheetPostId}}"
|
||||
bind:close="closeSheet" bind:posted="onPosted" bind:commented="onCommented"></bottom-sheet>
|
||||
|
||||
@@ -298,8 +298,9 @@ Page({
|
||||
onEditPet() {
|
||||
this.openSheetType('editPet');
|
||||
},
|
||||
onFab() {
|
||||
wx.navigateTo({ url: '/pages/ai/ai' });
|
||||
// 右下角悬浮键:打开 24 项记录选择器(原来是跳 AI 聊天页)
|
||||
onQuickRecord() {
|
||||
this.openSheetType('quickRecord');
|
||||
},
|
||||
goSettings() {
|
||||
wx.navigateTo({ url: '/pages/settings/settings' });
|
||||
|
||||
@@ -117,16 +117,6 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="section-head">
|
||||
<view class="sh-title">AI 今日建议</view>
|
||||
</view>
|
||||
<view class="ai-text">{{summary.advice || '正在生成今日建议…'}}</view>
|
||||
<view class="btn btn-dark btn-block ai-cta" bindtap="onFab">
|
||||
<pt-icon name="ai" size="{{32}}"></pt-icon>问问 AI 养宠助手
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="section-head">
|
||||
<view class="sh-title">新手内容</view>
|
||||
@@ -140,5 +130,5 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<fab bind:tap="onFab"></fab>
|
||||
<fab icon="plus" bind:tap="onQuickRecord"></fab>
|
||||
<bottom-sheet show="{{sheetShow}}" type="{{sheetType}}" bind:close="closeSheet" bind:saved="onSheetSave"></bottom-sheet>
|
||||
|
||||
@@ -34,9 +34,6 @@ Page({
|
||||
closeSheet() {
|
||||
this.setData({ sheetShow: false });
|
||||
},
|
||||
onFab() {
|
||||
wx.navigateTo({ url: '/pages/ai/ai' });
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return { title: '新手养宠知识合集', path: '/pages/learn/learn' };
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<nav-bar title="新手知识" show-back="{{true}}"></nav-bar>
|
||||
|
||||
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view class="page-body">
|
||||
<view class="page-body no-fab">
|
||||
<view class="page-title">
|
||||
<view class="pt-p">看完顺手生成你的计划。</view>
|
||||
</view>
|
||||
@@ -14,5 +14,4 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<fab bind:tap="onFab"></fab>
|
||||
<bottom-sheet show="{{sheetShow}}" type="{{sheetType}}" bind:close="closeSheet"></bottom-sheet>
|
||||
|
||||
@@ -69,9 +69,6 @@ Page({
|
||||
onAddPet() {
|
||||
this.setData({ sheetType: 'addPet', sheetShow: true });
|
||||
},
|
||||
onFab() {
|
||||
wx.navigateTo({ url: '/pages/ai/ai' });
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return { title: '肉垫计划 · 每天照顾好毛孩子', path: '/pages/home/home' };
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<nav-bar title="我的"></nav-bar>
|
||||
|
||||
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view class="page-body">
|
||||
<view class="page-body no-fab">
|
||||
<!-- 和「宠友主页」同一个组件。自己看到的就是别人看到的,装扮才有意义 -->
|
||||
<profile-head card="{{card}}" bind:relations="goRelations"
|
||||
bind:decorate="goDecorate" bind:pet="onPickPet"></profile-head>
|
||||
@@ -50,5 +50,4 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<fab bind:tap="onFab"></fab>
|
||||
<bottom-sheet show="{{sheetShow}}" type="{{sheetType}}" bind:close="closeSheet"></bottom-sheet>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<view class="hero">
|
||||
<view class="pet-logo"><pt-icon name="community" size="{{88}}" color="#fff"></pt-icon></view>
|
||||
<view class="hero-h1">肉垫计划</view>
|
||||
<view class="hero-sub">新手养宠的 AI 日历、健康记录和成长报告。先生成计划,再每天照着做。</view>
|
||||
<view class="hero-sub">新手养宠的养护日历、健康记录和成长报告。先生成计划,再每天照着做。</view>
|
||||
</view>
|
||||
<view class="proof-row">
|
||||
<view class="proof"><text class="proof-b">30天</text><text class="proof-s">新手计划</text></view>
|
||||
|
||||
@@ -14,19 +14,11 @@ Page({
|
||||
data: {
|
||||
pet: {},
|
||||
planView: 'timeline',
|
||||
planTabs: [
|
||||
{ key: 'timeline', label: '路线图' },
|
||||
{ key: 'aiPlan', label: 'AI 计划' },
|
||||
],
|
||||
plan: null,
|
||||
aiInput: '',
|
||||
aiPlan: null,
|
||||
aiBusy: false,
|
||||
sheetShow: false,
|
||||
sheetType: '',
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options && options.tab === 'aiPlan') this.setData({ planView: 'aiPlan' });
|
||||
onLoad() {
|
||||
this._unsub = store.subscribe((pet) => {
|
||||
this.setData({ pet });
|
||||
if (!this._inited) return; // 首次由 onShow 加载
|
||||
@@ -59,48 +51,10 @@ Page({
|
||||
})
|
||||
.catch(() => this.setData({ plan: null }));
|
||||
},
|
||||
// 来自 seg-tabs 的 change 事件,key 即 planView
|
||||
switchPlanView(e) {
|
||||
this.setData({ planView: e.detail.key });
|
||||
},
|
||||
// 计划明细勾选
|
||||
toggleTimelineTask(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
api.togglePlanTask(id).then(() => this.loadTimeline()).catch((e) => toastErr(e, '操作失败'));
|
||||
},
|
||||
// AI 计划
|
||||
onAiInput(e) {
|
||||
this.setData({ aiInput: e.detail.value });
|
||||
},
|
||||
genAIPlan() {
|
||||
const id = store.currentPetId();
|
||||
const input = (this.data.aiInput || '').trim();
|
||||
if (!id || !input) {
|
||||
wx.showToast({ title: '先描述一下情况', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.setData({ aiBusy: true });
|
||||
wx.showLoading({ title: 'AI 生成中...' });
|
||||
api
|
||||
.createAIPlan(id, input)
|
||||
.then((plan) => {
|
||||
wx.hideLoading();
|
||||
this.setData({ aiPlan: plan, aiBusy: false });
|
||||
})
|
||||
.catch((err) => {
|
||||
wx.hideLoading();
|
||||
this.setData({ aiBusy: false });
|
||||
wx.showToast({ title: err.message || '生成失败', icon: 'none' });
|
||||
});
|
||||
},
|
||||
applyAIPlan() {
|
||||
const p = this.data.aiPlan;
|
||||
if (!p) return;
|
||||
api
|
||||
.applyAIPlan(p.id)
|
||||
.then(() => wx.showToast({ title: '已加入我的计划', icon: 'success' }))
|
||||
.catch((err) => wx.showToast({ title: err.message || '加入失败', icon: 'none' }));
|
||||
},
|
||||
openSheet(e) {
|
||||
const type = e.currentTarget.dataset.type;
|
||||
if (!type) return; // 无关联动作的计划项只可勾选,不弹层
|
||||
@@ -112,9 +66,6 @@ Page({
|
||||
onAddPet() {
|
||||
this.setData({ sheetType: 'addPet', sheetShow: true });
|
||||
},
|
||||
onFab() {
|
||||
wx.navigateTo({ url: '/pages/ai/ai' });
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return { title: '我给毛孩子做了份养护计划', path: '/pages/plan/plan' };
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<view class="top-bar"><pet-switch bind:add="onAddPet"></pet-switch></view>
|
||||
|
||||
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view class="page-body">
|
||||
<view class="page-body no-fab">
|
||||
<view wx:if="{{!pet.id}}" class="no-pet">
|
||||
<view class="no-pet-ic"><pt-icon name="community" size="{{52}}"></pt-icon></view>
|
||||
<view class="no-pet-b">还没有毛孩子的档案</view>
|
||||
@@ -11,8 +11,6 @@
|
||||
<view class="btn btn-dark" bindtap="onAddPet"><pt-icon name="plus" size="{{30}}"></pt-icon>建一份档案</view>
|
||||
</view>
|
||||
|
||||
<seg-tabs variant="solid" items="{{planTabs}}" current="{{planView}}" bind:change="switchPlanView"></seg-tabs>
|
||||
|
||||
<!-- 路线图 -->
|
||||
<view wx:if="{{planView === 'timeline'}}">
|
||||
<view class="card">
|
||||
@@ -37,34 +35,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{planView === 'aiPlan'}}">
|
||||
<view class="card">
|
||||
<view class="section-head"><view class="sh-title">AI 计划模式</view><view class="tag purple">Plan</view></view>
|
||||
<view class="ai-plan-tip">描述 {{pet.name}} 的近况(阶段、近期变化、担心的问题),AI 会提取关键信息并生成可执行计划。</view>
|
||||
<textarea class="textarea" placeholder="例如:4 个月,刚换粮有点软便,也快打第二针疫苗" placeholder-class="placeholder" value="{{aiInput}}" bindinput="onAiInput"></textarea>
|
||||
<button class="btn btn-primary btn-block" style="margin-top:16rpx" bindtap="genAIPlan" disabled="{{aiBusy}}">{{aiBusy ? '生成中…' : '生成计划'}}</button>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{aiPlan}}" class="card">
|
||||
<view class="section-head"><view class="sh-title">已提取信息</view></view>
|
||||
<view class="extracted">
|
||||
<view class="extract"><text class="ex-b">阶段</text>{{aiPlan.extracted.stage}}</view>
|
||||
<view class="extract"><text class="ex-b">风险</text>{{aiPlan.extracted.risk}}</view>
|
||||
<view class="extract"><text class="ex-b">重点</text>{{aiPlan.extracted.priority}}</view>
|
||||
<view class="extract"><text class="ex-b">提醒</text>{{aiPlan.extracted.reminder}}</view>
|
||||
</view>
|
||||
<view class="section-head" style="margin-top:20rpx"><view class="sh-title">生成的计划</view></view>
|
||||
<view class="task-list">
|
||||
<view wx:for="{{aiPlan.tasks}}" wx:key="id" class="task">
|
||||
<view class="circle"></view>
|
||||
<view class="task-text"><text class="tt-b">{{item.day_label}}· {{item.title}}</text><view class="tt-p">{{item.description}}</view></view>
|
||||
</view>
|
||||
</view>
|
||||
<button class="btn btn-primary btn-block" style="margin-top:16rpx" bindtap="applyAIPlan">加入我的计划</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<fab bind:tap="onFab"></fab>
|
||||
<bottom-sheet show="{{sheetShow}}" type="{{sheetType}}" bind:close="closeSheet"></bottom-sheet>
|
||||
|
||||
@@ -265,9 +265,6 @@ Page({
|
||||
onAddPet() {
|
||||
this.openSheetType('addPet');
|
||||
},
|
||||
onFab() {
|
||||
wx.navigateTo({ url: '/pages/ai/ai' });
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return { title: '用肉垫计划记录毛孩子的成长', path: '/pages/record/record' };
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<view class="top-bar"><pet-switch bind:add="onAddPet"></pet-switch></view>
|
||||
|
||||
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view class="page-body">
|
||||
<view class="page-body no-fab">
|
||||
<view wx:if="{{!pet.id}}" class="no-pet">
|
||||
<view class="no-pet-ic"><pt-icon name="community" size="{{52}}"></pt-icon></view>
|
||||
<view class="no-pet-b">还没有毛孩子的档案</view>
|
||||
@@ -88,5 +88,4 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<fab bind:tap="onFab"></fab>
|
||||
<bottom-sheet show="{{sheetShow}}" type="{{sheetType}}" bind:close="closeSheet" bind:saved="onSheetSaved"></bottom-sheet>
|
||||
|
||||
@@ -57,19 +57,3 @@
|
||||
.ins-title{flex:1;font-size:var(--fs-md);font-weight:var(--fw-b);line-height:1.4}
|
||||
.ins-detail{color:var(--text-2);font-size:var(--fs-sm);line-height:1.6;margin-top:var(--sp-2)}
|
||||
.ins-ev{color:var(--muted);font-size:var(--fs-cap);margin-top:var(--sp-2)}
|
||||
|
||||
/* ===== 记一笔的分组 =====
|
||||
四组各一个色系,色值全走 token 的 soft/ink 二件套,没有新色。
|
||||
.quick 的配色由 .tone-N 决定(app.wxss 里已有),这里只管组标题和留白 */
|
||||
.tg{margin-bottom:var(--sp-4)}
|
||||
.tg:last-of-type{margin-bottom:0}
|
||||
.tg-head{
|
||||
display:flex;align-items:center;gap:var(--sp-2);
|
||||
font-size:var(--fs-md);font-weight:var(--fw-b);margin-bottom:var(--sp-3);
|
||||
}
|
||||
.tg-bar{width:6rpx;height:26rpx;border-radius:3rpx;background:currentColor}
|
||||
.tg-n{margin-left:auto;font-size:var(--fs-cap);font-weight:var(--fw);color:var(--muted2)}
|
||||
.tg.tone-1 .tg-head{color:var(--primary-ink)}
|
||||
.tg.tone-2 .tg-head{color:var(--green-ink)}
|
||||
.tg.tone-3 .tg-head{color:var(--blue-ink)}
|
||||
.tg.tone-4 .tg-head{color:var(--purple-ink)}
|
||||
|
||||
@@ -57,9 +57,6 @@ Page({
|
||||
onAddPet() {
|
||||
this.setData({ sheetType: 'addPet', sheetShow: true });
|
||||
},
|
||||
onFab() {
|
||||
wx.navigateTo({ url: '/pages/ai/ai' });
|
||||
},
|
||||
onShareAppMessage() {
|
||||
// 带上宠物名,转发出去别人一眼知道是谁家的
|
||||
const n = (this.data.pet && this.data.pet.name) || '我家毛孩子';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<view class="top-bar"><pet-switch bind:add="onAddPet"></pet-switch></view>
|
||||
|
||||
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view class="page-body">
|
||||
<view class="page-body no-fab">
|
||||
<view wx:if="{{!pet.id}}" class="no-pet">
|
||||
<view class="no-pet-ic"><pt-icon name="community" size="{{52}}"></pt-icon></view>
|
||||
<view class="no-pet-b">还没有毛孩子的档案</view>
|
||||
@@ -57,5 +57,4 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<fab bind:tap="onFab"></fab>
|
||||
<bottom-sheet show="{{sheetShow}}" type="{{sheetType}}" bind:close="closeSheet"></bottom-sheet>
|
||||
|
||||
@@ -73,9 +73,6 @@ Page({
|
||||
this.setData({ sheetShow: false });
|
||||
this.loadSummary();
|
||||
},
|
||||
onFab() {
|
||||
wx.navigateTo({ url: '/pages/ai/ai' });
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return { title: '肉垫计划 · 新手养宠助手', path: '/pages/home/home' };
|
||||
},
|
||||
|
||||
@@ -85,8 +85,10 @@ const api = {
|
||||
planCalendar: (id, month) =>
|
||||
request({ url: `/api/pets/${id}/plan/calendar${month ? `?month=${month}` : ''}` }),
|
||||
dayPlan: (id, date) => request({ url: `/api/pets/${id}/day-plan?date=${date}` }),
|
||||
createAIPlan: (id, input) => request({ url: `/api/pets/${id}/ai-plan`, method: 'POST', data: { input } }),
|
||||
applyAIPlan: (planId) => request({ url: `/api/ai-plan/${planId}/apply`, method: 'POST' }),
|
||||
// AI 相关:用户端入口已全部下掉,后端 10 个路由和配额配置都还在。
|
||||
// 想开回来把这几行解注释、再把入口接上就行,不用重写。
|
||||
// createAIPlan: (id, input) => request({ url: `/api/pets/${id}/ai-plan`, method: 'POST', data: { input } }),
|
||||
// applyAIPlan: (planId) => request({ url: `/api/ai-plan/${planId}/apply`, method: 'POST' }),
|
||||
togglePlanTask: (taskId) => request({ url: `/api/plan-tasks/${taskId}/toggle`, method: 'POST' }),
|
||||
|
||||
// 提醒
|
||||
@@ -133,11 +135,11 @@ const api = {
|
||||
activatePro: () => request({ url: '/api/pro/activate', method: 'POST' }),
|
||||
|
||||
// AI
|
||||
aiMessages: (session, limit) =>
|
||||
request({ url: `/api/ai/messages?session=${session || ''}&limit=${limit || 30}` }),
|
||||
aiChat: (body) => request({ url: '/api/ai/chat', method: 'POST', data: body }),
|
||||
assessSymptom: (id, body) =>
|
||||
request({ url: `/api/pets/${id}/ai/assess-symptom`, method: 'POST', data: body }),
|
||||
// aiMessages: (session, limit) =>
|
||||
// request({ url: `/api/ai/messages?session=${session || ''}&limit=${limit || 30}` }),
|
||||
// aiChat: (body) => request({ url: '/api/ai/chat', method: 'POST', data: body }),
|
||||
// assessSymptom: (id, body) =>
|
||||
// request({ url: `/api/pets/${id}/ai/assess-symptom`, method: 'POST', data: body }),
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
|
||||
Reference in New Issue
Block a user