feat: 补齐关注的入口——用户主页 + 关注/粉丝列表
关注之前只做了一半:能从帖子上点「+关注」,也有「关注」信息流,但 关注完就石沉大海——没有用户主页、看不到自己关注了谁、也不知道谁关注 了自己。功能是通的,路是断的。 后端新增: - GET /api/users/:id/profile 公开资料 + 帖子/关注/粉丝三个计数 + 我是否已关注 - GET /api/users/:id/posts TA 发布的帖子 - GET /api/users/:id/relations?kind=following|followers - /api/user/summary 补 following / followers,否则「我的」页那两行永远是 0 关系列表里「我是否关注了这批人」用一次 IN 查询查完,不是每行一次。 小程序端新增两个页面: - pages/user/user 用户主页:头像/昵称/三项计数/关注按钮 + TA 的帖子 - pages/relations 关注列表与粉丝列表(同一页,kind 区分) 入口: - 社区点头像或昵称 → TA 的主页 - 主页点「关注/粉丝」数字 → 对应列表 - 「我的」页新增「我的关注」「我的粉丝」两行 关注按钮先本地翻转再发请求,失败回滚——等接口返回按钮才变会显得很迟钝。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)}
|
||||
Reference in New Issue
Block a user