Files
hejiu/pages/circle/circle.vue
T
cg c7c023975d feat(circle): 集成HaveADrink SDK实现酒友圈功能
- 集成HaveADrink SDK版本从1.0.4升级至1.0.8
- 实现酒友圈API接口,替换原有的mock数据
- 添加地理位置权限配置(requiredPrivateInfos)
- 重构API调用方式,统一使用client实例调用真实接口
- 更新WebSocket协议与SDK保持一致,支持聊天、输入状态、已读回执等功能
- 实现游标分页获取动态信息流
- 添加文件上传API支持图片上传
- 优化API错误处理,支持业务级错误提示
- 调整酒局状态显示,新增upcoming待开始状态
- 优化自我感觉标签映射逻辑,支持更多状态类型
- 更新websocket连接认证方式,使用Authorization header传递token
- 添加服务端连接确认和被踢下线事件处理
2026-07-22 23:03:40 +08:00

470 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view :class="themeClass" class="page-container circle-page">
<!-- 顶部导航 -->
<view class="circle-header" :style="{ paddingTop: headerPaddingTop }">
<view class="circle-title-row" :style="{ paddingRight: headerPaddingRight }">
<text class="circle-title">酒友圈</text>
<view class="msg-entry" @click="goChatList">
<text class="msg-entry-icon">💬</text>
<view v-if="unreadTotal > 0" class="msg-badge">
<text class="msg-badge-text">{{ unreadTotal > 99 ? '99+' : unreadTotal }}</text>
</view>
</view>
</view>
<!-- Tab 切换 -->
<view class="circle-tabs">
<view
v-for="(tab, i) in tabs"
:key="i"
class="circle-tab"
:class="{ active: currentTab === i }"
@click="switchTab(i)"
>
<text class="circle-tab-text">{{ tab }}</text>
<view v-if="currentTab === i" class="circle-tab-line"></view>
</view>
</view>
</view>
<!-- 动态 Tab -->
<scroll-view
v-if="currentTab === 0"
class="circle-scroll"
scroll-y
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
>
<view class="circle-content">
<FeedCard
v-for="feed in feeds"
:key="feed.id"
:feed="feed"
@like="handleLike"
@comment="goDetail"
@item-click="goDetail"
/>
<view v-if="loading" class="circle-loading">
<text class="circle-loading-text">加载中...</text>
</view>
<view v-if="!hasMore && feeds.length" class="circle-nomore">
<text class="circle-nomore-text"> 没有更多了 </text>
</view>
<EmptyState
v-if="!feeds.length && !loading"
icon="🍻"
title="还没有动态"
desc="发布你的第一条饮酒动态吧"
actionText="发动态"
@action="goPublish"
/>
</view>
</scroll-view>
<!-- 酒友 Tab -->
<scroll-view v-if="currentTab === 1" class="circle-scroll" scroll-y>
<view class="circle-content">
<!-- 好友请求提醒 -->
<view v-if="friendRequests.length" class="request-banner" @click="goFriends">
<text class="request-icon">🔔</text>
<text class="request-text">{{ friendRequests.length }} 条新的好友请求</text>
<text class="request-arrow"></text>
</view>
<FriendItem
v-for="f in friends"
:key="f.id"
:friend="f"
@item-click="goChat(f)"
/>
<EmptyState
v-if="!friends.length"
icon="👥"
title="还没有酒友"
desc="邀请好友一起记录饮酒生活"
actionText="邀请好友"
@action="goFriends"
/>
</view>
</scroll-view>
<!-- 酒局 Tab -->
<scroll-view v-if="currentTab === 2" class="circle-scroll" scroll-y>
<view class="circle-content">
<EventCard
v-for="evt in events"
:key="evt.id"
:event="evt"
@item-click="goEventDetail"
/>
<EmptyState
v-if="!events.length"
icon="🎉"
title="暂无酒局"
desc="发起一场酒局,约上酒友一起喝"
actionText="发起酒局"
@action="goCreateEvent"
/>
</view>
</scroll-view>
<!-- FAB 发布按钮 -->
<view class="fab" @click="handleFab">
<text class="fab-icon">{{ currentTab === 2 ? '🎉' : '✏️' }}</text>
</view>
</view>
</template>
<script>
import FeedCard from '../../components/FeedCard.vue'
import FriendItem from '../../components/FriendItem.vue'
import EventCard from '../../components/EventCard.vue'
import EmptyState from '../../components/EmptyState.vue'
import { GetCircleFeeds, LikeFeed, UnlikeFeed, GetFriends, GetFriendRequests, GetEvents, GetUnreadCount } from '../../common/api'
import wsManager from '../../common/websocket'
import themeMixin from '../../common/theme-mixin'
export default {
mixins: [themeMixin],
components: { FeedCard, FriendItem, EventCard, EmptyState },
data() {
const menuBtn = uni.getMenuButtonBoundingClientRect()
const sysInfo = uni.getSystemInfoSync()
return {
tabs: ['动态', '酒友', '酒局'],
currentTab: 0,
headerPaddingTop: (menuBtn.top + 8) + 'px',
headerPaddingRight: (sysInfo.windowWidth - menuBtn.left + 8) + 'px',
// 动态
feeds: [],
lastId: 0,
hasMore: true,
loading: false,
refreshing: false,
// 酒友
friends: [],
friendRequests: [],
// 酒局
events: [],
// 私信未读
unreadTotal: 0
}
},
onShow() {
this.loadFeeds(true)
this.loadFriends()
this.loadEvents()
this.loadUnread()
this._wsHandler = () => { this.unreadTotal++ }
wsManager.on('message', this._wsHandler)
},
onHide() {
if (this._wsHandler) {
wsManager.off('message', this._wsHandler)
this._wsHandler = null
}
},
methods: {
switchTab(i) {
this.currentTab = i
},
// === 动态 ===
async loadFeeds(reset = false) {
if (this.loading) return
if (reset) {
this.lastId = 0
this.hasMore = true
}
this.loading = true
try {
const res = await GetCircleFeeds({ lastId: this.lastId, pageSize: 10 })
const data = res.data
if (reset) {
this.feeds = data.list || []
} else {
this.feeds = [...this.feeds, ...(data.list || [])]
}
this.hasMore = data.hasMore
// 记录最后一条ID作为下次请求的游标
if (data.list && data.list.length) {
this.lastId = data.list[data.list.length - 1].id
}
// 存入 globalData 供详情页查找
getApp().globalData = getApp().globalData || {}
getApp().globalData.circleFeeds = this.feeds
} catch (e) {
console.warn('加载动态失败', e)
}
this.loading = false
this.refreshing = false
},
onRefresh() {
this.refreshing = true
this.loadFeeds(true)
},
loadMore() {
if (this.hasMore && !this.loading) {
this.loadFeeds(false)
}
},
async handleLike(feed) {
feed.liked = !feed.liked
feed.likes += feed.liked ? 1 : -1
try {
if (feed.liked) {
await LikeFeed({ feedId: feed.id })
} else {
await UnlikeFeed({ feedId: feed.id })
}
} catch (e) { /* ignore */ }
},
goDetail(feed) {
uni.navigateTo({ url: `/pages/circle-detail/circle-detail?id=${feed.id}` })
},
goPublish() {
uni.navigateTo({ url: '/pages/circle-publish/circle-publish' })
},
// === 酒友 ===
async loadFriends() {
try {
const [friendsRes, reqRes] = await Promise.all([
GetFriends({}),
GetFriendRequests()
])
this.friends = friendsRes.data.list
this.friendRequests = reqRes.data.list
} catch (e) {
console.warn('加载酒友失败', e)
}
},
goFriends() {
uni.navigateTo({ url: '/pages/friends/friends' })
},
goChat(friend) {
// 导航防抖锁:防止事件重复触发导致页面打开两次
if (this._navLock) return
this._navLock = true
setTimeout(() => { this._navLock = false }, 600)
uni.navigateTo({
url: `/pages/chat/chat?friendId=${friend.id}&nickname=${encodeURIComponent(friend.nickname)}`
})
},
goChatList() {
uni.navigateTo({ url: '/pages/chat-list/chat-list' })
},
async loadUnread() {
try {
const res = await GetUnreadCount()
this.unreadTotal = res.data.total
} catch (e) { /* ignore */ }
},
// === 酒局 ===
async loadEvents() {
try {
const res = await GetEvents({})
this.events = res.data.list
} catch (e) {
console.warn('加载酒局失败', e)
}
},
goEventDetail(evt) {
uni.navigateTo({ url: `/pages/event-detail/event-detail?id=${evt.id}` })
},
goCreateEvent() {
uni.navigateTo({ url: '/pages/event-create/event-create' })
},
// === FAB ===
handleFab() {
if (this.currentTab === 2) {
this.goCreateEvent()
} else {
this.goPublish()
}
}
}
}
</script>
<style lang="scss" scoped>
.circle-page {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.circle-header {
padding-left: $sp-lg;
padding-right: $sp-lg;
padding-bottom: $sp-md;
background: $bg-base;
position: relative;
z-index: 10;
}
.circle-title-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $sp-lg;
}
.circle-title {
font-size: $fs-2xl;
font-weight: $fw-black;
color: $text-primary;
}
.msg-entry {
position: relative;
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: $bg-card;
&:active {
background: $bg-card-alt;
}
}
.msg-entry-icon {
font-size: 36rpx;
}
.msg-badge {
position: absolute;
top: -4rpx;
right: -4rpx;
min-width: 32rpx;
height: 32rpx;
padding: 0 8rpx;
border-radius: 16rpx;
background: $coral;
display: flex;
align-items: center;
justify-content: center;
}
.msg-badge-text {
font-size: 18rpx;
color: #fff;
font-weight: $fw-bold;
line-height: 1;
}
.circle-tabs {
display: flex;
gap: $sp-xl;
}
.circle-tab {
position: relative;
padding-bottom: $sp-sm;
}
.circle-tab-text {
font-size: $fs-base;
color: $text-tertiary;
font-weight: $fw-medium;
transition: color $duration-fast $ease-out;
.circle-tab.active & {
color: $text-primary;
font-weight: $fw-bold;
}
}
.circle-tab-line {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 6rpx;
border-radius: 3rpx;
background: $amber;
}
.circle-scroll {
flex: 1;
height: 0;
}
.circle-content {
padding: $sp-md $sp-lg;
padding-bottom: calc(env(safe-area-inset-bottom) + 140rpx);
}
.circle-loading,
.circle-nomore {
text-align: center;
padding: $sp-xl 0;
}
.circle-loading-text,
.circle-nomore-text {
font-size: $fs-sm;
color: $text-tertiary;
}
/* 好友请求横幅 */
.request-banner {
display: flex;
align-items: center;
gap: $sp-sm;
padding: $sp-lg;
background: rgba(232,168,56,0.08);
border: 1rpx solid rgba(232,168,56,0.2);
border-radius: $radius-lg;
margin-bottom: $sp-md;
&:active {
background: rgba(232,168,56,0.12);
}
}
.request-icon {
font-size: $fs-base;
}
.request-text {
flex: 1;
font-size: $fs-sm;
color: $amber;
font-weight: $fw-medium;
}
.request-arrow {
font-size: $fs-xl;
color: $amber;
opacity: 0.6;
}
/* FAB */
.fab {
position: fixed;
right: $sp-xl;
bottom: calc(env(safe-area-inset-bottom) + 180rpx);
width: 108rpx;
height: 108rpx;
border-radius: 50%;
background: linear-gradient(135deg, $amber, $amber-deep);
display: flex;
align-items: center;
justify-content: center;
box-shadow: $shadow-amber;
z-index: 50;
&:active {
transform: scale(0.9);
}
}
.fab-icon {
font-size: 44rpx;
}
</style>