Initial commit
This commit is contained in:
6
miniprogram/pages/feed/feed.json
Normal file
6
miniprogram/pages/feed/feed.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"post-card": "/components/post-card/post-card"
|
||||
}
|
||||
}
|
||||
155
miniprogram/pages/feed/feed.ts
Normal file
155
miniprogram/pages/feed/feed.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { Post } from '../../types/index'
|
||||
import { api } from '../../utils/api'
|
||||
import { getAvatarGradient, getPetEmoji } from '../../utils/format'
|
||||
import { PAGE_SIZE } from '../../utils/constants'
|
||||
|
||||
interface FeedTab { key: 'feed' | 'nearby' | 'hot'; label: string }
|
||||
|
||||
const app = getApp<{ userInfo: any; currentLocation: any }>()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 0,
|
||||
tabBarHeight: 56,
|
||||
posts: [] as Post[],
|
||||
stories: [] as any[],
|
||||
activeTab: 'feed' as 'feed' | 'nearby' | 'hot',
|
||||
feedTabs: [
|
||||
{ key: 'feed', label: '关注' },
|
||||
{ key: 'nearby', label: '附近' },
|
||||
{ key: 'hot', label: '热门' },
|
||||
] as FeedTab[],
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
refreshing: false,
|
||||
hasMore: true,
|
||||
page: 0,
|
||||
hasUnread: false,
|
||||
myAvatarGradient: 'linear-gradient(135deg, #ffd6e8, #fff1a6)',
|
||||
myEmoji: '🐶',
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const info = wx.getSystemInfoSync()
|
||||
this.setData({
|
||||
statusBarHeight: info.statusBarHeight,
|
||||
tabBarHeight: info.windowHeight - info.safeArea.bottom + 56,
|
||||
})
|
||||
this.loadFeed(true)
|
||||
this.loadStories()
|
||||
this.syncMyInfo()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
const tabBar = this.getTabBar() as any
|
||||
tabBar?.setSelected?.(0)
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.onRefresh()
|
||||
},
|
||||
|
||||
async onRefresh() {
|
||||
this.setData({ refreshing: true, page: 0, hasMore: true })
|
||||
await this.loadFeed(true)
|
||||
this.setData({ refreshing: false })
|
||||
},
|
||||
|
||||
async loadFeed(reset = false) {
|
||||
if (!reset && !this.data.hasMore) return
|
||||
if (this.data.loading && !reset) return
|
||||
|
||||
const page = reset ? 0 : this.data.page
|
||||
this.setData(reset ? { loading: true } : { loadingMore: true })
|
||||
|
||||
try {
|
||||
const loc = app.globalData?.currentLocation
|
||||
const res = await api.getPosts({
|
||||
page,
|
||||
type: this.data.activeTab,
|
||||
latitude: loc?.latitude,
|
||||
longitude: loc?.longitude,
|
||||
})
|
||||
|
||||
const posts = reset ? res.list : [...this.data.posts, ...res.list]
|
||||
this.setData({
|
||||
posts,
|
||||
page: page + 1,
|
||||
hasMore: res.hasMore,
|
||||
})
|
||||
} catch (e) {
|
||||
wx.showToast({ title: '加载失败,下拉重试', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ loading: false, loadingMore: false })
|
||||
}
|
||||
},
|
||||
|
||||
async loadStories() {
|
||||
try {
|
||||
const res = await wx.cloud.callFunction({
|
||||
name: 'getStories',
|
||||
data: {},
|
||||
}) as any
|
||||
if (res.result?.code === 0) {
|
||||
this.setData({ stories: res.result.data || [] })
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
|
||||
syncMyInfo() {
|
||||
const user = app.globalData?.userInfo
|
||||
if (!user) return
|
||||
const pet = user.pets?.[0]
|
||||
this.setData({
|
||||
myAvatarGradient: getAvatarGradient(user.openid || ''),
|
||||
myEmoji: pet ? getPetEmoji(pet.species, pet.breed) : '🐶',
|
||||
})
|
||||
},
|
||||
|
||||
onSwitchTab(e: WechatMiniprogram.CustomEvent) {
|
||||
const key = e.currentTarget.dataset.key as 'feed' | 'nearby' | 'hot'
|
||||
if (key === this.data.activeTab) return
|
||||
this.setData({ activeTab: key })
|
||||
this.loadFeed(true)
|
||||
},
|
||||
|
||||
onLoadMore() {
|
||||
this.loadFeed(false)
|
||||
},
|
||||
|
||||
onLikeChange(e: WechatMiniprogram.CustomEvent) {
|
||||
const { postId, liked, count } = e.detail
|
||||
const posts = this.data.posts.map(p =>
|
||||
p._id === postId ? { ...p, isLiked: liked, likeCount: count } : p
|
||||
)
|
||||
this.setData({ posts })
|
||||
},
|
||||
|
||||
onSearch() {
|
||||
wx.navigateTo({ url: '/pages/search/search' })
|
||||
},
|
||||
|
||||
onNotify() {
|
||||
wx.navigateTo({ url: '/pages/notifications/notifications' })
|
||||
},
|
||||
|
||||
onMyStory() {
|
||||
wx.navigateTo({ url: '/pages/post/post?type=story' })
|
||||
},
|
||||
|
||||
onStoryTap(e: WechatMiniprogram.CustomEvent) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
wx.navigateTo({ url: `/pages/story/story?id=${id}` })
|
||||
},
|
||||
|
||||
onGoPost() {
|
||||
wx.navigateTo({ url: '/pages/post/post' })
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: '汪圈 — 宠物社区',
|
||||
path: '/pages/feed/feed',
|
||||
}
|
||||
},
|
||||
})
|
||||
111
miniprogram/pages/feed/feed.wxml
Normal file
111
miniprogram/pages/feed/feed.wxml
Normal file
@@ -0,0 +1,111 @@
|
||||
<view class="feed-page">
|
||||
<!-- 状态栏占位 -->
|
||||
<view class="safe-top" style="height: {{statusBarHeight}}px"></view>
|
||||
|
||||
<!-- 顶部栏 -->
|
||||
<view class="topbar">
|
||||
<view class="topbar-logo">汪<text class="logo-accent">圈</text></view>
|
||||
<view class="topbar-tabs">
|
||||
<view
|
||||
wx:for="{{feedTabs}}"
|
||||
wx:key="key"
|
||||
class="feed-tab {{activeTab === item.key ? 'feed-tab-active' : ''}}"
|
||||
bindtap="onSwitchTab"
|
||||
data-key="{{item.key}}"
|
||||
>{{item.label}}</view>
|
||||
</view>
|
||||
<view class="topbar-actions">
|
||||
<view class="topbar-btn" bindtap="onSearch">🔍</view>
|
||||
<view class="topbar-btn {{hasUnread ? 'has-badge' : ''}}" bindtap="onNotify">🔔</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 故事圈 -->
|
||||
<scroll-view class="stories-scroll" scroll-x="true" enhanced="true" show-scrollbar="false">
|
||||
<!-- 我的故事 -->
|
||||
<view class="story" bindtap="onMyStory">
|
||||
<view class="story-ring story-ring-me">
|
||||
<view class="story-inner" style="background: {{myAvatarGradient}}">
|
||||
<text class="story-emoji">{{myEmoji}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="story-name">我的</text>
|
||||
</view>
|
||||
<!-- 关注的宠物故事 -->
|
||||
<view
|
||||
wx:for="{{stories}}"
|
||||
wx:key="_id"
|
||||
class="story"
|
||||
bindtap="onStoryTap"
|
||||
data-id="{{item._id}}"
|
||||
>
|
||||
<view class="story-ring {{item.hasNew ? '' : 'story-ring-seen'}}">
|
||||
<view class="story-inner" style="background: {{item.gradient}}">
|
||||
<text class="story-emoji">{{item.emoji}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="story-name">{{item.name}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 信息流 -->
|
||||
<scroll-view
|
||||
class="feed-scroll"
|
||||
scroll-y="true"
|
||||
enhanced="true"
|
||||
show-scrollbar="false"
|
||||
bindscrolltolower="onLoadMore"
|
||||
refresher-enabled="true"
|
||||
bindrefresherrefresh="onRefresh"
|
||||
refresher-triggered="{{refreshing}}"
|
||||
>
|
||||
<!-- 骨架屏 -->
|
||||
<view wx:if="{{loading && posts.length === 0}}" class="skeleton-wrap">
|
||||
<view wx:for="{{[1,2,3]}}" wx:key="index" class="skeleton-card">
|
||||
<view class="skeleton-header">
|
||||
<view class="skeleton skeleton-avatar"></view>
|
||||
<view class="skeleton-meta">
|
||||
<view class="skeleton skeleton-line w60"></view>
|
||||
<view class="skeleton skeleton-line w40"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="skeleton skeleton-img"></view>
|
||||
<view style="padding: 20rpx 24rpx;">
|
||||
<view class="skeleton skeleton-line w100"></view>
|
||||
<view class="skeleton skeleton-line w80" style="margin-top:12rpx"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 帖子列表 -->
|
||||
<post-card
|
||||
wx:for="{{posts}}"
|
||||
wx:key="_id"
|
||||
post="{{item}}"
|
||||
bind:likeChange="onLikeChange"
|
||||
/>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view class="load-more" wx:if="{{loadingMore}}">
|
||||
<view class="load-dot"></view>
|
||||
<view class="load-dot load-dot-2"></view>
|
||||
<view class="load-dot load-dot-3"></view>
|
||||
</view>
|
||||
|
||||
<!-- 到底了 -->
|
||||
<view class="end-tip" wx:if="{{!hasMore && posts.length > 0}}">
|
||||
<text>— 已经到底啦,去遛遛狗吧 🐾 —</text>
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<view wx:if="{{!loading && posts.length === 0}}" class="empty-feed">
|
||||
<text class="empty-emoji">🐾</text>
|
||||
<text class="empty-title">还没有内容</text>
|
||||
<text class="empty-desc">去附近找找小伙伴,或者发一条动态吧</text>
|
||||
<view class="btn-primary empty-btn" bindtap="onGoPost">发第一条</view>
|
||||
</view>
|
||||
|
||||
<view class="safe-bottom"></view>
|
||||
<view style="height: {{tabBarHeight}}px"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
264
miniprogram/pages/feed/feed.wxss
Normal file
264
miniprogram/pages/feed/feed.wxss
Normal file
@@ -0,0 +1,264 @@
|
||||
.feed-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #fff8f2 0%, #fff4fb 50%, #f5f0ff 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部栏 */
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 28rpx 12rpx;
|
||||
gap: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.topbar-logo {
|
||||
font-size: 44rpx;
|
||||
font-weight: 900;
|
||||
color: #272235;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-accent { color: #ff4f91; }
|
||||
|
||||
.topbar-tabs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.feed-tab {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #9b8fa8;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.feed-tab-active {
|
||||
color: #ff4f91;
|
||||
background: rgba(255, 79, 145, 0.10);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.topbar-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
box-shadow: 0 8rpx 18rpx rgba(78, 56, 96, 0.10);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.has-badge::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 6rpx;
|
||||
right: 6rpx;
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 50%;
|
||||
background: #ff4f91;
|
||||
border: 2rpx solid #fff;
|
||||
}
|
||||
|
||||
/* 故事圈 */
|
||||
.stories-scroll {
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
padding: 16rpx 28rpx 24rpx;
|
||||
}
|
||||
|
||||
.story {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
margin-right: 24rpx;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.story-ring {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 50%;
|
||||
padding: 5rpx;
|
||||
box-sizing: border-box;
|
||||
background: conic-gradient(from 120deg, #ff4f91, #ff9f1c, #ffe15a, #67e8c9, #4d8dff, #8c5cff, #ff4f91);
|
||||
box-shadow: 0 10rpx 18rpx rgba(96, 60, 115, 0.16);
|
||||
}
|
||||
|
||||
.story-ring-seen {
|
||||
background: #e8e4f0;
|
||||
}
|
||||
|
||||
.story-ring-me {
|
||||
background: linear-gradient(135deg, #ff4f91, #ff9f1c);
|
||||
}
|
||||
|
||||
.story-inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.90);
|
||||
}
|
||||
|
||||
.story-emoji {
|
||||
font-size: 44rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.story-name {
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #6a6178;
|
||||
}
|
||||
|
||||
/* 信息流 */
|
||||
.feed-scroll {
|
||||
flex: 1;
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
|
||||
/* 骨架屏 */
|
||||
.skeleton-wrap {
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
margin: 0 28rpx 32rpx;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 48rpx;
|
||||
overflow: hidden;
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.skeleton-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.skeleton-avatar {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
border-radius: 28rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.skeleton-meta {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.skeleton-line {
|
||||
height: 26rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.skeleton-img {
|
||||
width: 100%;
|
||||
height: 360rpx;
|
||||
}
|
||||
|
||||
.w100 { width: 100%; }
|
||||
.w80 { width: 80%; }
|
||||
.w60 { width: 60%; }
|
||||
.w40 { width: 40%; }
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #f0eef5 25%, #e8e4f0 50%, #f0eef5 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shine 1.4s infinite;
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* 加载更多 */
|
||||
.load-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.load-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: #ff4f91;
|
||||
animation: bounce 1.2s infinite;
|
||||
}
|
||||
|
||||
.load-dot-2 { animation-delay: 0.2s; }
|
||||
.load-dot-3 { animation-delay: 0.4s; }
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% { transform: scale(0.8); opacity: 0.4; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* 到底了 */
|
||||
.end-tip {
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #c4b8d0;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
/* 空态 */
|
||||
.empty-feed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 60rpx 40rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.empty-emoji {
|
||||
font-size: 80rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 900;
|
||||
color: #272235;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #9b8fa8;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.empty-btn {
|
||||
margin-top: 24rpx;
|
||||
padding: 22rpx 60rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
Reference in New Issue
Block a user