diff --git a/pets-be/internal/handler/community.go b/pets-be/internal/handler/community.go index 54d89c3..7d448cd 100644 --- a/pets-be/internal/handler/community.go +++ b/pets-be/internal/handler/community.go @@ -104,7 +104,7 @@ func (h *Handler) ListComments(c *gin.Context) { _ = c.ShouldBindQuery(&req) req.Normalize() - comments, total, err := h.svc.ListComments(idParam(c, "id"), req.Offset(), req.Limit()) + comments, total, err := h.svc.ListComments(middleware.UserID(c), idParam(c, "id"), req.Offset(), req.Limit()) if err != nil { response.FailErr(c, err) return @@ -152,3 +152,21 @@ func (h *Handler) UnfollowUser(c *gin.Context) { } response.OK(c, gin.H{"followed": false}) } + +// DeleteOwnPost DELETE /api/posts/:id (仅限本人) +func (h *Handler) DeleteOwnPost(c *gin.Context) { + if err := h.svc.DeleteOwnPost(middleware.UserID(c), idParam(c, "id")); err != nil { + respondErr(c, err) + return + } + response.OK(c, gin.H{"deleted": true}) +} + +// DeleteOwnComment DELETE /api/comments/:id (仅限本人) +func (h *Handler) DeleteOwnComment(c *gin.Context) { + if err := h.svc.DeleteOwnComment(middleware.UserID(c), idParam(c, "id")); err != nil { + respondErr(c, err) + return + } + response.OK(c, gin.H{"deleted": true}) +} diff --git a/pets-be/internal/handler/helper.go b/pets-be/internal/handler/helper.go index 4006395..383b5b6 100644 --- a/pets-be/internal/handler/helper.go +++ b/pets-be/internal/handler/helper.go @@ -27,5 +27,9 @@ func respondErr(c *gin.Context, err error) { response.Fail(c, errcode.ErrNotFound, "") return } + if errors.Is(err, service.ErrForbidden) { + response.Fail(c, errcode.ErrForbidden, err.Error()) + return + } response.FailErr(c, err) } diff --git a/pets-be/internal/handler/misc.go b/pets-be/internal/handler/misc.go index 170241a..0110f5a 100644 --- a/pets-be/internal/handler/misc.go +++ b/pets-be/internal/handler/misc.go @@ -2,6 +2,7 @@ package handler import ( "errors" + "strconv" "github.com/gin-gonic/gin" @@ -173,3 +174,17 @@ func (h *Handler) AdminSetFeedbackHandled(c *gin.Context) { } response.OK(c, gin.H{"ok": true}) } + +// ListAIMessages GET /api/ai/messages?session=&limit= +func (h *Handler) ListAIMessages(c *gin.Context) { + limit := 0 + if v := c.Query("limit"); v != "" { + limit, _ = strconv.Atoi(v) + } + msgs, err := h.svc.ListAIMessages(middleware.UserID(c), c.Query("session"), limit) + if err != nil { + response.FailErr(c, err) + return + } + response.OK(c, msgs) +} diff --git a/pets-be/internal/model/comment.go b/pets-be/internal/model/comment.go index 405d093..27905d4 100644 --- a/pets-be/internal/model/comment.go +++ b/pets-be/internal/model/comment.go @@ -8,4 +8,6 @@ type Comment struct { AuthorName string `gorm:"size:64" json:"author_name"` Content string `gorm:"size:512" json:"content"` Status string `gorm:"size:16;default:published" json:"status"` + + IsSelf bool `gorm:"-" json:"is_self"` // 计算字段:是不是我自己发的 } diff --git a/pets-be/internal/router/router.go b/pets-be/internal/router/router.go index f8a82c2..b1a361d 100644 --- a/pets-be/internal/router/router.go +++ b/pets-be/internal/router/router.go @@ -94,6 +94,8 @@ func registerUserAPI(api *gin.RouterGroup, h *handler.Handler, jm *appjwt.Manage g.DELETE("/posts/:id/like", h.UnlikePost) g.GET("/posts/:id/comments", h.ListComments) g.POST("/posts/:id/comments", h.CreateComment) + g.DELETE("/posts/:id", h.DeleteOwnPost) + g.DELETE("/comments/:id", h.DeleteOwnComment) g.GET("/articles", h.ListArticles) g.POST("/feedback", h.CreateFeedback) @@ -103,6 +105,7 @@ func registerUserAPI(api *gin.RouterGroup, h *handler.Handler, jm *appjwt.Manage g.POST("/pro/activate", h.ActivatePro) g.POST("/ai/chat", h.AIChat) + g.GET("/ai/messages", h.ListAIMessages) g.POST("/pets/:id/ai/assess-symptom", h.AssessSymptom) g.POST("/upload", h.Upload) } diff --git a/pets-be/internal/service/ai.go b/pets-be/internal/service/ai.go index 2af1a5e..a89c194 100644 --- a/pets-be/internal/service/ai.go +++ b/pets-be/internal/service/ai.go @@ -29,3 +29,23 @@ func (s *Service) AIChat(userID string, petID *string, session, text string) (st func (s *Service) Upload(objectName string, reader io.Reader, size int64, contentType string) (string, error) { return s.storage.Upload(objectName, reader, size, contentType) } + +// ListAIMessages 取当前用户的聊天历史(按会话,默认最近 N 条,返回时间正序) +func (s *Service) ListAIMessages(userID, session string, limit int) ([]model.AIMessage, error) { + if limit <= 0 || limit > 100 { + limit = 30 + } + q := s.db.Where("user_id = ?", userID) + if session != "" { + q = q.Where("session = ?", session) + } + var msgs []model.AIMessage + if err := q.Order("id desc").Limit(limit).Find(&msgs).Error; err != nil { + return nil, err + } + // 反转成时间正序,前端直接按顺序渲染 + for i, j := 0, len(msgs)-1; i < j; i, j = i+1, j-1 { + msgs[i], msgs[j] = msgs[j], msgs[i] + } + return msgs, nil +} diff --git a/pets-be/internal/service/community.go b/pets-be/internal/service/community.go index a372d1c..077b821 100644 --- a/pets-be/internal/service/community.go +++ b/pets-be/internal/service/community.go @@ -192,7 +192,7 @@ func (s *Service) postLikeCount(postID string) (int, error) { } // ListComments 评论分页 -func (s *Service) ListComments(postID string, offset, limit int) ([]model.Comment, int64, error) { +func (s *Service) ListComments(userID, postID string, offset, limit int) ([]model.Comment, int64, error) { q := s.db.Model(&model.Comment{}).Where("post_id = ? AND status = ?", postID, "published") var total int64 if err := q.Count(&total).Error; err != nil { @@ -202,6 +202,9 @@ func (s *Service) ListComments(postID string, offset, limit int) ([]model.Commen if err := q.Order("id desc").Offset(offset).Limit(limit).Find(&comments).Error; err != nil { return nil, 0, err } + for i := range comments { + comments[i].IsSelf = comments[i].UserID == userID + } return comments, total, nil } @@ -227,3 +230,38 @@ func (s *Service) CreateComment(userID, postID string, content string) (*model.C } return &comment, nil } + +// DeleteOwnPost 用户删除自己的帖子(软删除,保留数据可追溯) +func (s *Service) DeleteOwnPost(userID, postID string) error { + var p model.Post + if err := s.db.Where("id = ?", postID).First(&p).Error; err != nil { + return ErrNotFound + } + if p.UserID != userID { + return ErrForbidden + } + return s.db.Model(&model.Post{}).Where("id = ?", postID). + Update("status", model.PostDeleted).Error +} + +// DeleteOwnComment 用户删除自己的评论,并同步帖子评论数 +func (s *Service) DeleteOwnComment(userID, commentID string) error { + var c model.Comment + if err := s.db.Where("id = ?", commentID).First(&c).Error; err != nil { + return ErrNotFound + } + if c.UserID != userID { + return ErrForbidden + } + if c.Status == "deleted" { + return nil // 幂等 + } + return s.db.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&model.Comment{}).Where("id = ?", commentID). + Update("status", "deleted").Error; err != nil { + return err + } + return tx.Model(&model.Post{}).Where("id = ? AND comment_count > 0", c.PostID). + UpdateColumn("comment_count", gorm.Expr("comment_count - 1")).Error + }) +} diff --git a/pets-be/internal/service/service.go b/pets-be/internal/service/service.go index dc6361b..44aba4d 100644 --- a/pets-be/internal/service/service.go +++ b/pets-be/internal/service/service.go @@ -18,6 +18,9 @@ var ErrNotFound = errors.New("not found") // 避免「点一下白拿会员」。 var ErrPayNotReady = errors.New("支付功能即将开放,敬请期待") +// ErrForbidden 越权操作(如删除别人的内容) +var ErrForbidden = errors.New("无权操作") + // Service 业务逻辑聚合,方法按领域分散在各文件 type Service struct { db *gorm.DB diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.js b/pets-fe/components/bottom-sheet/bottom-sheet.js index 880a9f9..6484aa3 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.js +++ b/pets-fe/components/bottom-sheet/bottom-sheet.js @@ -52,7 +52,8 @@ Component({ properties: { show: { type: Boolean, value: false }, type: { type: String, value: '' }, - postId: { type: Number, value: 0 }, + // 雪花 ID 是 18 位字符串,声明成 Number 会超出 JS 安全整数范围而丢精度 + postId: { type: String, value: '' }, }, data: { innerType: '', @@ -182,13 +183,15 @@ Component({ } else if (type === 'exportData') { this.buildExport(); } else if (type === 'comments' && this.data.postId) { - api.listComments(this.data.postId).then((page) => this.setData({ comments: page.list || [] })).catch(() => {}); + 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 === 'ai') { + this.loadAIHistory(); } else if (type === 'pro') { api.getPro().then((p) => this.setData({ proInfo: p })).catch(() => {}); } @@ -244,6 +247,28 @@ Component({ onCostNote(e) { this.setData({ costNote: e.detail.value }); }, onCostInput(e) { this.setData({ costAmount: e.detail.value }); }, onPostInput(e) { this.setData({ postContent: e.detail.value }); }, + loadComments() { + if (!this.data.postId) return; + api.listComments(this.data.postId) + .then((page) => this.setData({ comments: page.list || [] })) + .catch(() => {}); + }, + // 长按删除自己的评论 + onDeleteComment(e) { + const c = this.data.comments[e.currentTarget.dataset.index]; + if (!c || !c.is_self) return; + 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(); @@ -722,6 +747,17 @@ Component({ // 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(); diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.wxml b/pets-fe/components/bottom-sheet/bottom-sheet.wxml index 77f28a0..d57b615 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.wxml +++ b/pets-fe/components/bottom-sheet/bottom-sheet.wxml @@ -331,8 +331,12 @@ module.exports.sel = function (map, key, index, def) { 评论 - {{item.author_name}}{{item.content}} + + {{item.author_name}} + {{item.content}} + 还没有评论,来抢沙发~ + 长按自己的评论可删除 diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.wxss b/pets-fe/components/bottom-sheet/bottom-sheet.wxss index 137ff8a..2501820 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.wxss +++ b/pets-fe/components/bottom-sheet/bottom-sheet.wxss @@ -61,3 +61,4 @@ border:1rpx solid var(--line);margin-bottom:24rpx; } .export-box::-webkit-scrollbar{width:0;height:0;display:none} +.mine-tag{margin-left:10rpx;font-size:20rpx;font-weight:700;color:var(--primary-dark);background:#FFF2DC;padding:2rpx 10rpx;border-radius:999rpx} diff --git a/pets-fe/pages/community/community.js b/pets-fe/pages/community/community.js index 1760a78..537345f 100644 --- a/pets-fe/pages/community/community.js +++ b/pets-fe/pages/community/community.js @@ -1,5 +1,6 @@ const store = require('../../utils/store.js'); const api = require('../../utils/api.js'); +const { toastErr } = require('../../utils/ui.js'); const TAG_CLASS = { 求助: 'warn', 经验: 'blue', 精选: 'purple', 避坑: 'red', 晒宠: '' }; @@ -32,7 +33,9 @@ Page({ posts: [], sheetShow: false, sheetType: '', - sheetPostId: 0, + sheetPostId: '', + loaded: false, + loadErr: '', }, onLoad() { this._unsub = store.subscribe((pet) => this.setData({ pet })); @@ -54,10 +57,15 @@ Page({ }, loadPosts() { const tab = this.data.feedTabs[this.data.feedIdx]; + this.setData({ loadErr: '' }); api .listPosts(tab, 1) - .then((page) => this.setData({ posts: (page.list || []).map(mapPost) })) - .catch(() => {}); + .then((page) => this.setData({ posts: (page.list || []).map(mapPost), loaded: true })) + .catch((e) => { + // 不能让「加载失败」显示成「还没有帖子」,那是在骗用户 + this.setData({ loadErr: e.message || '加载失败', loaded: true }); + toastErr(e); + }); }, switchFeedTab(e) { this.setData({ feedIdx: e.currentTarget.dataset.index }); @@ -77,7 +85,7 @@ Page({ this.setData({ sheetPostId: e.currentTarget.dataset.id, sheetType: 'comments', sheetShow: true }); }, openSheet(e) { - this.setData({ sheetType: e.currentTarget.dataset.type, sheetPostId: 0, sheetShow: true }); + this.setData({ sheetType: e.currentTarget.dataset.type, sheetPostId: '', sheetShow: true }); }, closeSheet() { this.setData({ sheetShow: false }); @@ -93,7 +101,23 @@ Page({ wx.navigateTo({ url: '/pages/learn/learn' }); }, onFab() { - this.setData({ sheetType: 'ai', sheetPostId: 0, sheetShow: true }); + this.setData({ sheetType: 'ai', sheetPostId: '', sheetShow: true }); + }, + // 长按自己的帖子可删除 + onLongPressPost(e) { + const p = this.data.posts[e.currentTarget.dataset.index]; + if (!p || !p.is_self) return; + wx.showModal({ + title: '删除帖子', + content: '确定删除这条帖子?删除后其他人将看不到。', + confirmColor: '#D9534F', + success: (res) => { + if (!res.confirm) return; + api.deletePost(p.id) + .then(() => { wx.showToast({ title: '已删除', icon: 'success' }); this.loadPosts(); }) + .catch((err) => wx.showToast({ title: err.message || '删除失败', icon: 'none' })); + }, + }); }, // 关注 / 取关帖子作者 toggleFollow(e) { diff --git a/pets-fe/pages/community/community.wxml b/pets-fe/pages/community/community.wxml index 55d071c..06c4830 100644 --- a/pets-fe/pages/community/community.wxml +++ b/pets-fe/pages/community/community.wxml @@ -19,7 +19,7 @@ module.exports.isUrl = function (s) { return s && s.indexOf('http') === 0; } - + {{item.author_emoji}} {{item.author_name}}AI宠友 @@ -41,7 +41,13 @@ module.exports.isUrl = function (s) { return s && s.indexOf('http') === 0; } - 还没有帖子,来发第一条吧 🐾 + + {{loadErr}} + 点击重试 + + + {{feedTabs[feedIdx] === '关注' ? '还没有关注的人,去「推荐」里关注几个宠友吧 🐾' : '还没有帖子,来发第一条吧 🐾'}} + diff --git a/pets-fe/pages/community/community.wxss b/pets-fe/pages/community/community.wxss index 1997f84..e25dce0 100644 --- a/pets-fe/pages/community/community.wxss +++ b/pets-fe/pages/community/community.wxss @@ -53,3 +53,4 @@ page{height:100vh;overflow:hidden;display:flex;flex-direction:column} color:var(--primary-dark);background:#FFF2DC;border:1rpx solid #FFD39A; } .follow-btn.on{color:var(--muted);background:#F4F1EC;border-color:var(--line)} +.retry-btn{margin-top:20rpx;display:inline-block;padding:12rpx 36rpx;border-radius:999rpx;background:#FFF2DC;color:var(--primary-dark);font-weight:700;font-size:26rpx} diff --git a/pets-fe/pages/home/home.js b/pets-fe/pages/home/home.js index 22710be..819e3f3 100644 --- a/pets-fe/pages/home/home.js +++ b/pets-fe/pages/home/home.js @@ -1,5 +1,6 @@ const store = require('../../utils/store.js'); const api = require('../../utils/api.js'); +const { toastErr } = require('../../utils/ui.js'); function todayStr() { const d = new Date(); @@ -34,6 +35,8 @@ Page({ this.setData({ todayDate: todayStr() }); this._unsub = store.subscribe((pet) => { this.setData({ pet }); + // 首次由 onShow 统一加载,这里只处理之后的「切换宠物 / 数据变更」,避免重复请求 + if (!this._inited) return; this.loadAll(); }); }, @@ -47,6 +50,7 @@ Page({ store .ready() .then(() => { + this._inited = true; this.setData({ pet: store.getPet() }); this.loadAll(); }) @@ -72,7 +76,7 @@ Page({ api .homeSummary(id) .then((summary) => this.setData({ summary })) - .catch(() => {}); + .catch((e) => toastErr(e)); }, loadTasks() { const id = store.currentPetId(); @@ -81,7 +85,7 @@ Page({ api .getTasks(id, date) .then((tasks) => this.setData({ tasks: (tasks || []).map(mapTask) })) - .catch(() => {}); + .catch((e) => toastErr(e)); }, onTapDay(e) { const { date, active } = e.currentTarget.dataset; @@ -130,8 +134,9 @@ Page({ this.setData({ sheetType: type, sheetShow: true }); }, closeSheet() { + // 只关闭。数据变更由 bind:saved 或 store 的宠物变更通知触发刷新, + // 不必每次关弹层(哪怕只是看了眼 AI)都全量重拉一遍 this.setData({ sheetShow: false }); - this.loadAll(); }, onSheetSave() { this.loadTasks(); diff --git a/pets-fe/pages/home/home.wxml b/pets-fe/pages/home/home.wxml index 503e14f..558e5ac 100644 --- a/pets-fe/pages/home/home.wxml +++ b/pets-fe/pages/home/home.wxml @@ -97,4 +97,4 @@ - + diff --git a/pets-fe/pages/plan/plan.js b/pets-fe/pages/plan/plan.js index 7e6378a..f92a611 100644 --- a/pets-fe/pages/plan/plan.js +++ b/pets-fe/pages/plan/plan.js @@ -46,6 +46,7 @@ Page({ onLoad() { this._unsub = store.subscribe((pet) => { this.setData({ pet, calLoaded: false }); + if (!this._inited) return; // 首次由 onShow 加载 this.loadTimeline(); if (this.data.planView === 'calendar') this.loadCalendar(); }); @@ -60,6 +61,7 @@ Page({ store .ready() .then(() => { + this._inited = true; this.setData({ pet: store.getPet() }); this.loadTimeline(); }) diff --git a/pets-fe/pages/record/record.js b/pets-fe/pages/record/record.js index 9037356..45bae8b 100644 --- a/pets-fe/pages/record/record.js +++ b/pets-fe/pages/record/record.js @@ -1,5 +1,6 @@ const store = require('../../utils/store.js'); const api = require('../../utils/api.js'); +const { toastErr } = require('../../utils/ui.js'); function pad(n) { return n < 10 ? '0' + n : '' + n; @@ -16,6 +17,7 @@ function fmtTime(iso) { } function mapRecord(r) { return { + id: r.id, icon: r.icon || '✍️', title: r.title, desc: fmtTime(r.occurred_at) + (r.description ? '|' + r.description : ''), @@ -23,6 +25,19 @@ function mapRecord(r) { }; } +// 时间轴类型筛选 +const REC_FILTERS = [ + { key: '', label: '全部' }, + { key: 'weight', label: '体重' }, + { key: 'poop', label: '便便' }, + { key: 'food', label: '饮食' }, + { key: 'symptom', label: '异常' }, + { key: 'medicine', label: '用药' }, + { key: 'vaccine', label: '疫苗' }, + { key: 'cost', label: '消费' }, + { key: 'photo', label: '照片' }, +]; + function fmtDay(iso) { if (!iso) return ''; const d = new Date(iso); @@ -63,6 +78,8 @@ Page({ recPage: 1, recTotal: 0, recHasMore: false, + recFilters: REC_FILTERS, + recFilterIdx: 0, trendSvg: '', trendStats: null, trendDots: [], @@ -74,6 +91,7 @@ Page({ onLoad() { this._unsub = store.subscribe((pet) => { this.setData({ pet }); + if (!this._inited) return; // 首次由 onShow 加载 this.loadRecords(); }); }, @@ -87,21 +105,50 @@ Page({ store .ready() .then(() => { + this._inited = true; this.setData({ pet: store.getPet() }); this.loadRecords(); }) .catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' })); }, + curFilter() { + return this.data.recFilters[this.data.recFilterIdx].key; + }, + onFilterTap(e) { + const i = Number(e.currentTarget.dataset.index); + if (i === this.data.recFilterIdx) return; + this.setData({ recFilterIdx: i }, () => this.loadRecords()); + }, + // 长按删除一条记录 + onDeleteRecord(e) { + const r = this.data.timeline[e.currentTarget.dataset.index]; + if (!r || !r.id) return; + wx.showModal({ + title: '删除记录', + content: '确定删除「' + r.title + '」?删除后不可恢复。', + confirmColor: '#D9534F', + success: (res) => { + if (!res.confirm) return; + api + .deleteRecord(r.id) + .then(() => { + wx.showToast({ title: '已删除', icon: 'success' }); + this.loadRecords(); + }) + .catch((err) => wx.showToast({ title: err.message || '删除失败', icon: 'none' })); + }, + }); + }, loadRecords() { const id = store.currentPetId(); if (!id) return; api - .getRecords(id, { page: 1, pageSize: REC_PAGE }) + .getRecords(id, { page: 1, pageSize: REC_PAGE, type: this.curFilter() }) .then((res) => { const list = (res.list || []).map(mapRecord); this.setData({ timeline: list, recPage: 1, recTotal: res.total || 0, recHasMore: list.length < (res.total || 0) }); }) - .catch(() => {}); + .catch((e) => toastErr(e)); this.loadTrend(); }, loadMoreRecords() { @@ -109,7 +156,7 @@ Page({ if (!id) return; const next = this.data.recPage + 1; api - .getRecords(id, { page: next, pageSize: REC_PAGE }) + .getRecords(id, { page: next, pageSize: REC_PAGE, type: this.curFilter() }) .then((res) => { const merged = this.data.timeline.concat((res.list || []).map(mapRecord)); this.setData({ timeline: merged, recPage: next, recTotal: res.total || 0, recHasMore: merged.length < (res.total || 0) }); diff --git a/pets-fe/pages/record/record.wxml b/pets-fe/pages/record/record.wxml index 8818da7..554392c 100644 --- a/pets-fe/pages/record/record.wxml +++ b/pets-fe/pages/record/record.wxml @@ -50,16 +50,24 @@ 健康时间轴摘要 + + {{item.label}} + + - + {{item.icon}} {{item.title}}{{item.desc}} - + - 还没有记录,用上面的按钮记一条吧 + 这个分类下还没有记录 + 长按任意一条可删除 查看更多(共 {{recTotal}} 条) 收起 diff --git a/pets-fe/pages/record/record.wxss b/pets-fe/pages/record/record.wxss index fa393bc..331e61d 100644 --- a/pets-fe/pages/record/record.wxss +++ b/pets-fe/pages/record/record.wxss @@ -50,3 +50,12 @@ /* 页面不自身滚动,改由 page-scroll 承载滚动(隐藏滚动条)*/ page{height:100vh;overflow:hidden;display:flex;flex-direction:column} .page-scroll{flex:1;min-height:0} + +/* 时间轴类型筛选 */ +.rec-filter{white-space:nowrap;margin-bottom:20rpx} +.rf-chip{ + display:inline-block;padding:10rpx 26rpx;margin-right:14rpx;border-radius:999rpx; + background:#fff;border:1rpx solid var(--line);color:var(--muted);font-size:24rpx;font-weight:700; +} +.rf-chip.on{background:#FFF2DC;color:var(--primary-dark);border-color:#FFD39A} +.tl-tip{margin-top:16rpx;text-align:center;color:var(--muted);font-size:22rpx} diff --git a/pets-fe/pages/report/report.js b/pets-fe/pages/report/report.js index 5e3d16f..c064f85 100644 --- a/pets-fe/pages/report/report.js +++ b/pets-fe/pages/report/report.js @@ -14,6 +14,7 @@ Page({ onLoad() { this._unsub = store.subscribe((pet) => { this.setData({ pet }); + if (!this._inited) return; // 首次由 onShow 加载 this.loadData(); }); }, @@ -27,6 +28,7 @@ Page({ store .ready() .then(() => { + this._inited = true; this.setData({ pet: store.getPet() }); this.loadData(); }) diff --git a/pets-fe/utils/api.js b/pets-fe/utils/api.js index 1c90b40..f7b1ee5 100644 --- a/pets-fe/utils/api.js +++ b/pets-fe/utils/api.js @@ -54,6 +54,7 @@ const api = { return request({ url: `/api/pets/${id}/records${qs.length ? '?' + qs.join('&') : ''}` }); }, createRecord: (id, body) => request({ url: `/api/pets/${id}/records`, method: 'POST', data: body }), + deleteRecord: (recordId) => request({ url: `/api/records/${recordId}`, method: 'DELETE' }), weightTrend: (id) => request({ url: `/api/pets/${id}/records/weight-trend` }), // 任务 @@ -92,6 +93,8 @@ const api = { likePost: (id) => request({ url: `/api/posts/${id}/like`, method: 'POST' }), followUser: (userId) => request({ url: `/api/users/${userId}/follow`, method: 'POST' }), unfollowUser: (userId) => request({ url: `/api/users/${userId}/follow`, method: 'DELETE' }), + deletePost: (id) => request({ url: `/api/posts/${id}`, method: 'DELETE' }), + deleteComment: (id) => request({ url: `/api/comments/${id}`, method: 'DELETE' }), listComments: (id) => request({ url: `/api/posts/${id}/comments` }), createComment: (id, content) => request({ url: `/api/posts/${id}/comments`, method: 'POST', data: { content } }), @@ -106,6 +109,8 @@ 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 }), diff --git a/pets-fe/utils/config.js b/pets-fe/utils/config.js index b1825eb..afef976 100644 --- a/pets-fe/utils/config.js +++ b/pets-fe/utils/config.js @@ -1,5 +1,7 @@ // 后端地址。本地联调:微信开发者工具需在「详情 → 本地设置」勾选「不校验合法域名」。 // 上线时改为你的 https 域名,并在微信公众平台配置 request 合法域名。 -const BASE_URL = 'http://192.168.31.4:9090'; +//const BASE_URL = 'http://192.168.31.4:9090'; +const BASE_URL = 'https://pet.sundynix.cn'; + module.exports = { BASE_URL }; diff --git a/pets-fe/utils/request.js b/pets-fe/utils/request.js index 0c15602..609367e 100644 --- a/pets-fe/utils/request.js +++ b/pets-fe/utils/request.js @@ -12,14 +12,25 @@ function clearToken() { wx.removeStorageSync(TOKEN_KEY); } -// 统一请求:注入 token,解析 {code,message,data},成功返回 data,失败 reject(Error) -function request({ url, method = 'GET', data, header = {} }) { +// token 失效时的重新登录钩子,由 store 注册(放在这里避免 request ← api ← store 循环依赖) +let reAuth = null; +function setReAuthHandler(fn) { + reAuth = fn; +} + +const TIMEOUT = 15000; + +// 统一请求:注入 token,解析 {code,message,data},成功返回 data,失败 reject(Error)。 +// token 失效(40100)时自动重新登录并重试一次,避免 JWT 过期后小程序静默白屏。 +function request(options, _retried) { + const { url, method = 'GET', data, header = {} } = options; return new Promise((resolve, reject) => { const token = getToken(); wx.request({ url: BASE_URL + url, method, data, + timeout: TIMEOUT, header: Object.assign( { 'Content-Type': 'application/json' }, token ? { Authorization: 'Bearer ' + token } : {}, @@ -29,13 +40,22 @@ function request({ url, method = 'GET', data, header = {} }) { const body = res.data; if (body && typeof body === 'object' && 'code' in body) { if (body.code === 0) return resolve(body.data); - if (body.code === 40100) clearToken(); + if (body.code === 40100) { + clearToken(); + // 只重试一次,防止登录接口本身出问题时无限循环 + if (!_retried && reAuth) { + return reAuth() + .then(() => request(options, true).then(resolve, reject)) + .catch(() => reject(new Error(body.message || '登录已过期,请重进小程序'))); + } + } return reject(new Error(body.message || '请求失败')); } resolve(body); }, fail(err) { - reject(new Error((err && err.errMsg) || '网络错误')); + const msg = (err && err.errMsg) || '网络错误'; + reject(new Error(/timeout/i.test(msg) ? '网络超时,请稍后重试' : msg)); }, }); }); @@ -49,6 +69,7 @@ function uploadFile(filePath) { url: BASE_URL + '/api/upload', filePath, name: 'file', + timeout: 60000, header: token ? { Authorization: 'Bearer ' + token } : {}, success(res) { try { @@ -66,4 +87,4 @@ function uploadFile(filePath) { }); } -module.exports = { request, uploadFile, getToken, setToken, clearToken }; +module.exports = { request, uploadFile, getToken, setToken, clearToken, setReAuthHandler }; diff --git a/pets-fe/utils/store.js b/pets-fe/utils/store.js index a37413a..90037c1 100644 --- a/pets-fe/utils/store.js +++ b/pets-fe/utils/store.js @@ -1,5 +1,6 @@ // 全局状态:登录、宠物列表、当前宠物;服务端驱动 + 发布订阅 const api = require('./api.js'); +const { setReAuthHandler } = require('./request.js'); const state = { user: null, @@ -38,6 +39,12 @@ function subscribe(fn) { } // 登录 + 拉宠物列表(只跑一次) +// token 失效时清掉引导缓存并重新登录一次,让 request 层能自动重试 +setReAuthHandler(async () => { + bootstrapPromise = null; + await ready(); +}); + function ready() { if (!bootstrapPromise) { bootstrapPromise = (async () => { diff --git a/pets-fe/utils/ui.js b/pets-fe/utils/ui.js new file mode 100644 index 0000000..3793767 --- /dev/null +++ b/pets-fe/utils/ui.js @@ -0,0 +1,15 @@ +// 轻量 UI 提示:统一错误反馈,避免请求失败时页面一片空白、用户不知道发生了什么。 +let lastMsg = ''; +let lastAt = 0; + +// 提示错误。相同文案 3 秒内不重复弹,避免多个并发请求同时失败时刷屏。 +function toastErr(e, fallback) { + const msg = (e && e.message) || fallback || '加载失败'; + const now = Date.now(); + if (msg === lastMsg && now - lastAt < 3000) return; + lastMsg = msg; + lastAt = now; + wx.showToast({ title: msg, icon: 'none', duration: 2000 }); +} + +module.exports = { toastErr };