feat(auth): access token 缩到 2 小时 + refresh token 机制 #3
@@ -182,3 +182,40 @@ func (h *Handler) ListReplies(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
response.OK(c, list)
|
response.OK(c, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserCard GET /api/users/:id/profile 某人的公开资料
|
||||||
|
func (h *Handler) UserCard(c *gin.Context) {
|
||||||
|
card, err := h.svc.GetUserCard(middleware.UserID(c), idParam(c, "id"))
|
||||||
|
if err != nil {
|
||||||
|
respondErr(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, card)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserPosts GET /api/users/:id/posts 某人发布的帖子
|
||||||
|
func (h *Handler) UserPosts(c *gin.Context) {
|
||||||
|
var req adminListReq
|
||||||
|
_ = c.ShouldBindQuery(&req)
|
||||||
|
req.Normalize()
|
||||||
|
posts, total, err := h.svc.ListUserPosts(middleware.UserID(c), idParam(c, "id"), req.Offset(), req.Limit())
|
||||||
|
if err != nil {
|
||||||
|
response.FailErr(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, response.NewPage(posts, total, req.PageQuery))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relations GET /api/users/:id/relations?kind=following|followers
|
||||||
|
func (h *Handler) Relations(c *gin.Context) {
|
||||||
|
var req adminListReq
|
||||||
|
_ = c.ShouldBindQuery(&req)
|
||||||
|
req.Normalize()
|
||||||
|
kind := c.Query("kind")
|
||||||
|
list, total, err := h.svc.ListRelations(middleware.UserID(c), idParam(c, "id"), kind, req.Offset(), req.Limit())
|
||||||
|
if err != nil {
|
||||||
|
response.FailErr(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, response.NewPage(list, total, req.PageQuery))
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,6 +88,9 @@ func registerUserAPI(api *gin.RouterGroup, h *handler.Handler, jm *appjwt.Manage
|
|||||||
g.GET("/pets/:id/health-summary", h.HealthSummary)
|
g.GET("/pets/:id/health-summary", h.HealthSummary)
|
||||||
g.GET("/pets/:id/poster", h.Poster)
|
g.GET("/pets/:id/poster", h.Poster)
|
||||||
|
|
||||||
|
g.GET("/users/:id/profile", h.UserCard)
|
||||||
|
g.GET("/users/:id/posts", h.UserPosts)
|
||||||
|
g.GET("/users/:id/relations", h.Relations)
|
||||||
g.POST("/users/:id/follow", h.FollowUser)
|
g.POST("/users/:id/follow", h.FollowUser)
|
||||||
g.DELETE("/users/:id/follow", h.UnfollowUser)
|
g.DELETE("/users/:id/follow", h.UnfollowUser)
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,8 @@ type UserSummary struct {
|
|||||||
Pets int64 `json:"pets"`
|
Pets int64 `json:"pets"`
|
||||||
Records int64 `json:"records"`
|
Records int64 `json:"records"`
|
||||||
Reminders int64 `json:"reminders"`
|
Reminders int64 `json:"reminders"`
|
||||||
|
Following int64 `json:"following"` // 我关注了多少人
|
||||||
|
Followers int64 `json:"followers"` // 多少人关注我
|
||||||
ProStatus string `json:"pro_status"`
|
ProStatus string `json:"pro_status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,6 +227,8 @@ func (s *Service) GetUserSummary(userID string) (*UserSummary, error) {
|
|||||||
s.db.Model(&model.Pet{}).Where("user_id = ?", userID).Count(&sum.Pets)
|
s.db.Model(&model.Pet{}).Where("user_id = ?", userID).Count(&sum.Pets)
|
||||||
s.db.Model(&model.HealthRecord{}).Where("user_id = ?", userID).Count(&sum.Records)
|
s.db.Model(&model.HealthRecord{}).Where("user_id = ?", userID).Count(&sum.Records)
|
||||||
s.db.Model(&model.Reminder{}).Where("user_id = ?", userID).Count(&sum.Reminders)
|
s.db.Model(&model.Reminder{}).Where("user_id = ?", userID).Count(&sum.Reminders)
|
||||||
|
s.db.Model(&model.Follow{}).Where("follower_id = ?", userID).Count(&sum.Following)
|
||||||
|
s.db.Model(&model.Follow{}).Where("followee_id = ?", userID).Count(&sum.Followers)
|
||||||
|
|
||||||
sum.ProStatus = model.ProNone
|
sum.ProStatus = model.ProNone
|
||||||
var m model.ProMembership
|
var m model.ProMembership
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sundynix/pets-be/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserCard 用户公开资料。关注这件事之前只做了「点一下关注」,
|
||||||
|
// 关注完既看不到对方主页、也看不到自己关注了谁——等于关注完就石沉大海。
|
||||||
|
type UserCard struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
AvatarURL string `json:"avatar_url"`
|
||||||
|
IsBot bool `json:"is_bot"`
|
||||||
|
IsSelf bool `json:"is_self"`
|
||||||
|
Followed bool `json:"followed"` // 我有没有关注 TA
|
||||||
|
Posts int64 `json:"post_count"`
|
||||||
|
Following int64 `json:"following_count"`
|
||||||
|
Followers int64 `json:"follower_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserCard 取某人的公开资料 + 三个计数 + 我是否已关注
|
||||||
|
func (s *Service) GetUserCard(viewerID, userID string) (*UserCard, error) {
|
||||||
|
var u model.User
|
||||||
|
if err := s.db.First(&u, "id = ?", userID).Error; err != nil {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
card := &UserCard{
|
||||||
|
ID: u.ID, Nickname: u.Nickname, IsBot: u.IsBot,
|
||||||
|
IsSelf: u.ID == viewerID,
|
||||||
|
AvatarURL: s.fileURL(u.AvatarFileID),
|
||||||
|
}
|
||||||
|
if card.AvatarURL == "" {
|
||||||
|
card.AvatarURL = u.Avatar
|
||||||
|
}
|
||||||
|
s.db.Model(&model.Post{}).Where("user_id = ? AND status = ?", userID, "published").Count(&card.Posts)
|
||||||
|
s.db.Model(&model.Follow{}).Where("follower_id = ?", userID).Count(&card.Following)
|
||||||
|
s.db.Model(&model.Follow{}).Where("followee_id = ?", userID).Count(&card.Followers)
|
||||||
|
|
||||||
|
if !card.IsSelf {
|
||||||
|
var n int64
|
||||||
|
s.db.Model(&model.Follow{}).
|
||||||
|
Where("follower_id = ? AND followee_id = ?", viewerID, userID).Count(&n)
|
||||||
|
card.Followed = n > 0
|
||||||
|
}
|
||||||
|
return card, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUserPosts 某人发布的帖子
|
||||||
|
func (s *Service) ListUserPosts(viewerID, userID string, offset, limit int) ([]model.Post, int64, error) {
|
||||||
|
q := s.db.Model(&model.Post{}).Where("user_id = ? AND status = ?", userID, "published")
|
||||||
|
var total int64
|
||||||
|
if err := q.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
var posts []model.Post
|
||||||
|
if err := q.Order("id desc").Offset(offset).Limit(limit).Find(&posts).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
s.attachPostImages(posts)
|
||||||
|
s.markFollowed(viewerID, posts)
|
||||||
|
return posts, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRelations 关注列表 / 粉丝列表。
|
||||||
|
// kind = "following" 我关注的人;"followers" 关注我的人
|
||||||
|
func (s *Service) ListRelations(viewerID, userID, kind string, offset, limit int) ([]UserCard, int64, error) {
|
||||||
|
q := s.db.Model(&model.Follow{})
|
||||||
|
var idCol string
|
||||||
|
if kind == "followers" {
|
||||||
|
q = q.Where("followee_id = ?", userID)
|
||||||
|
idCol = "follower_id"
|
||||||
|
} else {
|
||||||
|
q = q.Where("follower_id = ?", userID)
|
||||||
|
idCol = "followee_id"
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
if err := q.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
var ids []string
|
||||||
|
if err := q.Order("id desc").Offset(offset).Limit(limit).Pluck(idCol, &ids).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return []UserCard{}, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var users []model.User
|
||||||
|
s.db.Where("id IN ?", ids).Find(&users)
|
||||||
|
byID := map[string]model.User{}
|
||||||
|
for _, u := range users {
|
||||||
|
byID[u.ID] = u
|
||||||
|
}
|
||||||
|
|
||||||
|
// 一次查出我关注了这批人里的哪些,不要每行一次查询
|
||||||
|
var mine []string
|
||||||
|
s.db.Model(&model.Follow{}).Where("follower_id = ? AND followee_id IN ?", viewerID, ids).
|
||||||
|
Pluck("followee_id", &mine)
|
||||||
|
followed := map[string]bool{}
|
||||||
|
for _, id := range mine {
|
||||||
|
followed[id] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 ids 的顺序输出,保持「最近关注的在前」
|
||||||
|
out := make([]UserCard, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
u, ok := byID[id]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
avatar := s.fileURL(u.AvatarFileID)
|
||||||
|
if avatar == "" {
|
||||||
|
avatar = u.Avatar
|
||||||
|
}
|
||||||
|
out = append(out, UserCard{
|
||||||
|
ID: u.ID, Nickname: u.Nickname, IsBot: u.IsBot,
|
||||||
|
AvatarURL: avatar,
|
||||||
|
IsSelf: u.ID == viewerID,
|
||||||
|
Followed: followed[u.ID],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out, total, nil
|
||||||
|
}
|
||||||
+3
-1
@@ -9,7 +9,9 @@
|
|||||||
"pages/learn/learn",
|
"pages/learn/learn",
|
||||||
"pages/article/article",
|
"pages/article/article",
|
||||||
"pages/profile/profile",
|
"pages/profile/profile",
|
||||||
"pages/ai/ai"
|
"pages/ai/ai",
|
||||||
|
"pages/user/user",
|
||||||
|
"pages/relations/relations"
|
||||||
],
|
],
|
||||||
"window": {
|
"window": {
|
||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
|
|||||||
@@ -121,6 +121,11 @@ Page({
|
|||||||
onCommented() {
|
onCommented() {
|
||||||
this.loadPosts();
|
this.loadPosts();
|
||||||
},
|
},
|
||||||
|
// 点头像或昵称进 TA 的主页。关注做了却没有主页,关注完就石沉大海
|
||||||
|
goUser(e) {
|
||||||
|
const id = e.currentTarget.dataset.id;
|
||||||
|
if (id) wx.navigateTo({ url: `/pages/user/user?id=${id}` });
|
||||||
|
},
|
||||||
goLearn() {
|
goLearn() {
|
||||||
wx.navigateTo({ url: '/pages/learn/learn' });
|
wx.navigateTo({ url: '/pages/learn/learn' });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ module.exports.isUrl = function (s) { return s && s.indexOf('http') === 0; }
|
|||||||
<view id="feed-top"></view>
|
<view id="feed-top"></view>
|
||||||
<view wx:for="{{posts}}" wx:key="id" class="post-card" bindlongpress="onLongPressPost" data-index="{{index}}">
|
<view wx:for="{{posts}}" wx:key="id" class="post-card" bindlongpress="onLongPressPost" data-index="{{index}}">
|
||||||
<view class="post-head">
|
<view class="post-head">
|
||||||
<view class="post-avatar">{{item.author_emoji}}</view>
|
<view class="post-avatar" catchtap="goUser" data-id="{{item.user_id}}">{{item.author_emoji}}</view>
|
||||||
<view class="post-user">
|
<view class="post-user" catchtap="goUser" data-id="{{item.user_id}}">
|
||||||
<view class="pu-b">{{item.author_name}}<text wx:if="{{item.is_ai}}" class="ai-tag">AI</text></view>
|
<view class="pu-b">{{item.author_name}}<text wx:if="{{item.is_ai}}" class="ai-tag">AI</text></view>
|
||||||
<view class="pu-s">{{item.timeText}}</view>
|
<view class="pu-s">{{item.timeText}}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ Page({
|
|||||||
openSheet(e) {
|
openSheet(e) {
|
||||||
this.setData({ sheetType: e.currentTarget.dataset.type, sheetShow: true });
|
this.setData({ sheetType: e.currentTarget.dataset.type, sheetShow: true });
|
||||||
},
|
},
|
||||||
|
// 自己的关注/粉丝。之前只有帖子上的「+关注」按钮,关注完没有任何地方能看
|
||||||
|
goRelations(e) {
|
||||||
|
const uid = (this.data.user && this.data.user.id) || '';
|
||||||
|
if (!uid) return wx.showToast({ title: '登录信息还没就绪', icon: 'none' });
|
||||||
|
wx.navigateTo({ url: `/pages/relations/relations?id=${uid}&kind=${e.currentTarget.dataset.kind}` });
|
||||||
|
},
|
||||||
goRecord() {
|
goRecord() {
|
||||||
wx.switchTab({ url: '/pages/record/record' });
|
wx.switchTab({ url: '/pages/record/record' });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
<view class="profile-row" data-type="reminders" bindtap="openSheet"><view class="pr-ic"><pt-icon name="bell" size="{{34}}"></pt-icon></view><text class="pr-label">提醒设置</text><text class="pr-val">{{summary.reminders}} 条</text><view class="pr-arrow"><pt-icon name="next" size="{{28}}"></pt-icon></view></view>
|
<view class="profile-row" data-type="reminders" bindtap="openSheet"><view class="pr-ic"><pt-icon name="bell" size="{{34}}"></pt-icon></view><text class="pr-label">提醒设置</text><text class="pr-val">{{summary.reminders}} 条</text><view class="pr-arrow"><pt-icon name="next" size="{{28}}"></pt-icon></view></view>
|
||||||
<view class="profile-row" bindtap="goRecord"><view class="pr-ic"><pt-icon name="note" size="{{34}}"></pt-icon></view><text class="pr-label">健康记录</text><text class="pr-val">{{summary.records}} 条</text><view class="pr-arrow"><pt-icon name="next" size="{{28}}"></pt-icon></view></view>
|
<view class="profile-row" bindtap="goRecord"><view class="pr-ic"><pt-icon name="note" size="{{34}}"></pt-icon></view><text class="pr-label">健康记录</text><text class="pr-val">{{summary.records}} 条</text><view class="pr-arrow"><pt-icon name="next" size="{{28}}"></pt-icon></view></view>
|
||||||
<view class="profile-row" data-type="managePets" bindtap="openSheet"><view class="pr-ic"><pt-icon name="community" size="{{34}}"></pt-icon></view><text class="pr-label">多宠物管理</text><text class="pr-val">{{summary.pets}} 只</text><view class="pr-arrow"><pt-icon name="next" size="{{28}}"></pt-icon></view></view>
|
<view class="profile-row" data-type="managePets" bindtap="openSheet"><view class="pr-ic"><pt-icon name="community" size="{{34}}"></pt-icon></view><text class="pr-label">多宠物管理</text><text class="pr-val">{{summary.pets}} 只</text><view class="pr-arrow"><pt-icon name="next" size="{{28}}"></pt-icon></view></view>
|
||||||
|
<view class="profile-row" data-kind="following" bindtap="goRelations"><view class="pr-ic"><pt-icon name="community" size="{{34}}"></pt-icon></view><text class="pr-label">我的关注</text><text class="pr-val">{{summary.following || 0}} 人</text><view class="pr-arrow"><pt-icon name="next" size="{{28}}"></pt-icon></view></view>
|
||||||
|
<view class="profile-row" data-kind="followers" bindtap="goRelations"><view class="pr-ic"><pt-icon name="user" size="{{34}}"></pt-icon></view><text class="pr-label">我的粉丝</text><text class="pr-val">{{summary.followers || 0}} 人</text><view class="pr-arrow"><pt-icon name="next" size="{{28}}"></pt-icon></view></view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="profile-list">
|
<view class="profile-list">
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
const api = require('../../utils/api.js');
|
||||||
|
const { toastErr } = require('../../utils/ui.js');
|
||||||
|
|
||||||
|
Page({
|
||||||
|
data: { uid: '', kind: 'following', title: '关注', list: [], page: 1, hasMore: false, loaded: false },
|
||||||
|
onLoad(q) {
|
||||||
|
const kind = q && q.kind === 'followers' ? 'followers' : 'following';
|
||||||
|
this.setData({ uid: (q && q.id) || '', kind, title: kind === 'followers' ? '粉丝' : '关注' });
|
||||||
|
this.load(1);
|
||||||
|
},
|
||||||
|
load(page) {
|
||||||
|
api
|
||||||
|
.relations(this.data.uid, this.data.kind, page)
|
||||||
|
.then((res) => {
|
||||||
|
const merged = page === 1 ? res.list || [] : this.data.list.concat(res.list || []);
|
||||||
|
this.setData({ list: merged, page, loaded: true, hasMore: merged.length < (res.total || 0) });
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.setData({ loaded: true });
|
||||||
|
toastErr(e);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
loadMore() {
|
||||||
|
if (this.data.hasMore) this.load(this.data.page + 1);
|
||||||
|
},
|
||||||
|
goUser(e) {
|
||||||
|
wx.navigateTo({ url: `/pages/user/user?id=${e.currentTarget.dataset.id}` });
|
||||||
|
},
|
||||||
|
// 同样先本地翻转再发请求,失败回滚
|
||||||
|
toggleFollow(e) {
|
||||||
|
const i = e.currentTarget.dataset.index;
|
||||||
|
const u = this.data.list[i];
|
||||||
|
if (!u || u.is_self) return;
|
||||||
|
const next = !u.followed;
|
||||||
|
this.setData({ [`list[${i}].followed`]: next });
|
||||||
|
(next ? api.followUser(u.id) : api.unfollowUser(u.id)).catch((err) => {
|
||||||
|
this.setData({ [`list[${i}].followed`]: !next });
|
||||||
|
toastErr(err, '操作失败');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"usingComponents": {
|
||||||
|
"nav-bar": "/components/nav-bar/nav-bar",
|
||||||
|
"pt-icon": "/components/pt-icon/index"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<nav-bar title="{{title}}" show-back="{{true}}"></nav-bar>
|
||||||
|
|
||||||
|
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}"
|
||||||
|
bindscrolltolower="loadMore">
|
||||||
|
<view class="page-body no-fab">
|
||||||
|
<view wx:for="{{list}}" wx:key="id" class="rel" bindtap="goUser" data-id="{{item.id}}">
|
||||||
|
<view class="rel-av">
|
||||||
|
<image wx:if="{{item.avatar_url}}" class="rel-av-img" src="{{item.avatar_url}}" mode="aspectFill"></image>
|
||||||
|
<block wx:else>{{item.nickname[0]}}</block>
|
||||||
|
</view>
|
||||||
|
<view class="rel-name">{{item.nickname}}<text wx:if="{{item.is_bot}}" class="ai-tag">AI</text></view>
|
||||||
|
<view wx:if="{{!item.is_self}}" class="follow-btn {{item.followed ? 'on' : ''}}"
|
||||||
|
catchtap="toggleFollow" data-index="{{index}}">{{item.followed ? '已关注' : '+ 关注'}}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view wx:if="{{loaded && !list.length}}" class="empty lg">
|
||||||
|
{{kind === 'followers' ? '还没有人关注 TA' : '还没有关注任何人,去宠友圈逛逛'}}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.rel{
|
||||||
|
display:flex;align-items:center;gap:var(--sp-3);
|
||||||
|
background:#fff;border-radius:var(--r-md);box-shadow:var(--sd-1);
|
||||||
|
padding:var(--sp-3) var(--sp-4);margin-bottom:var(--sp-2);
|
||||||
|
}
|
||||||
|
.rel-av{
|
||||||
|
width:80rpx;height:80rpx;border-radius:50%;flex:none;overflow:hidden;
|
||||||
|
display:flex;align-items:center;justify-content:center;
|
||||||
|
background:var(--primary-soft);color:var(--primary-ink);font-size:var(--fs-lg);font-weight:var(--fw-b);
|
||||||
|
}
|
||||||
|
.rel-av-img{width:100%;height:100%}
|
||||||
|
.rel-name{flex:1;min-width:0;font-size:var(--fs-md);font-weight:var(--fw-b)}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
const api = require('../../utils/api.js');
|
||||||
|
const { toastErr } = require('../../utils/ui.js');
|
||||||
|
|
||||||
|
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);
|
||||||
|
return p(d.getMonth() + 1) + '-' + p(d.getDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
Page({
|
||||||
|
data: { uid: '', card: null, posts: [], page: 1, hasMore: false, loading: true },
|
||||||
|
onLoad(q) {
|
||||||
|
const uid = q && q.id;
|
||||||
|
if (!uid) return wx.showToast({ title: '缺少用户', icon: 'none' });
|
||||||
|
this.setData({ uid });
|
||||||
|
this.loadCard();
|
||||||
|
this.loadPosts(1);
|
||||||
|
},
|
||||||
|
loadCard() {
|
||||||
|
api
|
||||||
|
.userCard(this.data.uid)
|
||||||
|
.then((card) => this.setData({ card, loading: false }))
|
||||||
|
.catch((e) => {
|
||||||
|
this.setData({ loading: false });
|
||||||
|
toastErr(e);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
loadPosts(page) {
|
||||||
|
api
|
||||||
|
.userPosts(this.data.uid, page)
|
||||||
|
.then((res) => {
|
||||||
|
const list = (res.list || []).map((p) => ({ ...p, timeText: fmtAgo(p.created_at) }));
|
||||||
|
const merged = page === 1 ? list : this.data.posts.concat(list);
|
||||||
|
this.setData({ posts: merged, page, hasMore: merged.length < (res.total || 0) });
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
},
|
||||||
|
loadMore() {
|
||||||
|
if (this.data.hasMore) this.loadPosts(this.data.page + 1);
|
||||||
|
},
|
||||||
|
// 关注状态先本地翻转,请求失败再翻回去——等接口返回按钮才变会显得很迟钝
|
||||||
|
toggleFollow() {
|
||||||
|
const c = this.data.card;
|
||||||
|
if (!c || c.is_self) return;
|
||||||
|
const next = !c.followed;
|
||||||
|
this.setData({
|
||||||
|
'card.followed': next,
|
||||||
|
'card.follower_count': Math.max(0, c.follower_count + (next ? 1 : -1)),
|
||||||
|
});
|
||||||
|
(next ? api.followUser(c.id) : api.unfollowUser(c.id)).catch((e) => {
|
||||||
|
this.setData({ 'card.followed': !next, 'card.follower_count': c.follower_count });
|
||||||
|
toastErr(e, '操作失败');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
goRelations(e) {
|
||||||
|
const kind = e.currentTarget.dataset.kind;
|
||||||
|
wx.navigateTo({ url: `/pages/relations/relations?id=${this.data.uid}&kind=${kind}` });
|
||||||
|
},
|
||||||
|
previewImage(e) {
|
||||||
|
const { urls, cur } = e.currentTarget.dataset;
|
||||||
|
if (urls && urls.length) wx.previewImage({ urls, current: cur });
|
||||||
|
},
|
||||||
|
onShareAppMessage() {
|
||||||
|
const n = (this.data.card && this.data.card.nickname) || '宠友';
|
||||||
|
return { title: `${n} 的主页`, path: `/pages/user/user?id=${this.data.uid}` };
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"usingComponents": {
|
||||||
|
"nav-bar": "/components/nav-bar/nav-bar",
|
||||||
|
"pt-icon": "/components/pt-icon/index"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<wxs module="im">
|
||||||
|
module.exports.isUrl = function (s) { return s && s.indexOf('http') === 0; }
|
||||||
|
</wxs>
|
||||||
|
<nav-bar title="{{card ? card.nickname : '宠友主页'}}" show-back="{{true}}"></nav-bar>
|
||||||
|
|
||||||
|
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}"
|
||||||
|
bindscrolltolower="loadMore">
|
||||||
|
<view class="page-body no-fab">
|
||||||
|
<view wx:if="{{loading}}" class="empty lg">加载中…</view>
|
||||||
|
|
||||||
|
<block wx:elif="{{card}}">
|
||||||
|
<view class="card u-head">
|
||||||
|
<view class="u-av">
|
||||||
|
<image wx:if="{{card.avatar_url}}" class="u-av-img" src="{{card.avatar_url}}" mode="aspectFill"></image>
|
||||||
|
<block wx:else>{{card.nickname[0]}}</block>
|
||||||
|
</view>
|
||||||
|
<view class="u-name">{{card.nickname}}<text wx:if="{{card.is_bot}}" class="ai-tag">AI</text></view>
|
||||||
|
|
||||||
|
<view class="u-stats">
|
||||||
|
<view class="u-stat"><text class="u-n">{{card.post_count}}</text><text class="u-l">帖子</text></view>
|
||||||
|
<view class="u-stat" data-kind="following" bindtap="goRelations">
|
||||||
|
<text class="u-n">{{card.following_count}}</text><text class="u-l">关注</text>
|
||||||
|
</view>
|
||||||
|
<view class="u-stat" data-kind="followers" bindtap="goRelations">
|
||||||
|
<text class="u-n">{{card.follower_count}}</text><text class="u-l">粉丝</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view wx:if="{{!card.is_self}}" class="btn {{card.followed ? 'btn-ghost' : 'btn-dark'}} btn-block" bindtap="toggleFollow">
|
||||||
|
<pt-icon wx:if="{{!card.followed}}" name="plus" size="{{28}}"></pt-icon>{{card.followed ? '已关注' : '关注 TA'}}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view wx:for="{{posts}}" wx:key="id" class="post-card">
|
||||||
|
<view class="post-head">
|
||||||
|
<view class="pu-s">{{item.timeText}}</view>
|
||||||
|
</view>
|
||||||
|
<view class="post-content">{{item.content}}</view>
|
||||||
|
<view wx:if="{{item.images.length}}" class="photo-grid">
|
||||||
|
<block wx:for="{{item.images}}" wx:for-item="img" wx:key="*this">
|
||||||
|
<image wx:if="{{im.isUrl(img)}}" class="photo-tile" src="{{img}}" mode="aspectFill"
|
||||||
|
bindtap="previewImage" data-urls="{{item.images}}" data-cur="{{img}}"></image>
|
||||||
|
<view wx:else class="photo-tile">{{img}}</view>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
<view class="post-actions">
|
||||||
|
<view class="pa-btn"><pt-icon name="like" size="{{32}}"></pt-icon><text class="pa-n">{{item.like_count || 0}}</text></view>
|
||||||
|
<view class="pa-btn"><pt-icon name="comment" size="{{32}}"></pt-icon><text class="pa-n">{{item.comment_count || 0}}</text></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view wx:if="{{!posts.length}}" class="empty lg">TA 还没有发过帖子</view>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.u-head{text-align:center;padding:var(--sp-6) var(--sp-5)}
|
||||||
|
.u-av{
|
||||||
|
width:140rpx;height:140rpx;border-radius:50%;margin:0 auto var(--sp-3);overflow:hidden;
|
||||||
|
display:flex;align-items:center;justify-content:center;
|
||||||
|
background:var(--primary-soft);color:var(--primary-ink);font-size:56rpx;font-weight:var(--fw-b);
|
||||||
|
}
|
||||||
|
.u-av-img{width:100%;height:100%}
|
||||||
|
.u-name{font-size:var(--fs-lg);font-weight:var(--fw-b)}
|
||||||
|
.u-stats{display:flex;justify-content:center;gap:var(--sp-6);margin:var(--sp-4) 0}
|
||||||
|
.u-stat{display:flex;flex-direction:column;align-items:center;gap:4rpx}
|
||||||
|
.u-n{font-size:var(--fs-lg);font-weight:var(--fw-b)}
|
||||||
|
.u-l{font-size:var(--fs-cap);color:var(--muted)}
|
||||||
@@ -103,6 +103,10 @@ const api = {
|
|||||||
request({ url: `/api/posts?tab=${encodeURIComponent(tab || '')}&page=${page || 1}&page_size=10` }),
|
request({ url: `/api/posts?tab=${encodeURIComponent(tab || '')}&page=${page || 1}&page_size=10` }),
|
||||||
createPost: (body) => request({ url: '/api/posts', method: 'POST', data: body }),
|
createPost: (body) => request({ url: '/api/posts', method: 'POST', data: body }),
|
||||||
likePost: (id) => request({ url: `/api/posts/${id}/like`, method: 'POST' }),
|
likePost: (id) => request({ url: `/api/posts/${id}/like`, method: 'POST' }),
|
||||||
|
userCard: (userId) => request({ url: `/api/users/${userId}/profile` }),
|
||||||
|
userPosts: (userId, page) => request({ url: `/api/users/${userId}/posts?page=${page || 1}&page_size=10` }),
|
||||||
|
relations: (userId, kind, page) =>
|
||||||
|
request({ url: `/api/users/${userId}/relations?kind=${kind}&page=${page || 1}&page_size=20` }),
|
||||||
followUser: (userId) => request({ url: `/api/users/${userId}/follow`, method: 'POST' }),
|
followUser: (userId) => request({ url: `/api/users/${userId}/follow`, method: 'POST' }),
|
||||||
unfollowUser: (userId) => request({ url: `/api/users/${userId}/follow`, method: 'DELETE' }),
|
unfollowUser: (userId) => request({ url: `/api/users/${userId}/follow`, method: 'DELETE' }),
|
||||||
deletePost: (id) => request({ url: `/api/posts/${id}`, method: 'DELETE' }),
|
deletePost: (id) => request({ url: `/api/posts/${id}`, method: 'DELETE' }),
|
||||||
|
|||||||
Reference in New Issue
Block a user