Files
hejiu/pages/circle/circle.vue
T
cg 81e7a3afa5 feat(event): 添加酒局报名审核功能并优化前端组件
- 新增 ApproveEvent API 接口用于审核报名用户
- 在 EventCard 组件中显示待审核状态(⏳ 待审核)
- 实现酒局发起人审核报名用户的完整流程
- 添加用户协议和隐私政策页面
- 优化 FeedCard 组件中的图片展示逻辑
- 修复连续打卡天数计算问题
- 更新 HaveADrink SDK 至 1.0.15 版本
2026-08-16 14:50:50 +08:00

648 lines
17 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>
<!-- 发布入口:标题栏右侧胶囊按钮,替代悬浮 FAB 彻底避免遮挡 -->
<view class="publish-btn" @click="goPublish">
<text class="publish-btn-icon">✏️</text>
<text class="publish-btn-text">发布</text>
</view>
<!-- <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 -->
<!-- v-if="currentTab === 0" -->
<scroll-view
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"
:can-delete="isMyFeed(feed)"
@like="handleLike"
@comment="goDetail"
@share="handleShare"
@delete="confirmDeleteFeed"
@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>
</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, DeleteFeed, GetFriends, GetFriendRequests, GetEvents, DeleteEvent, GetUnreadCount } from '../../common/api'
import wsManager from '../../common/websocket'
import { getMyInviteCode } from '../../common/invite'
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,
// 当前分享的动态
shareFeed: null,
// 分享模式:feed(分享动态) / invite(邀请好友) / default
shareMode: 'default',
// 我的邀请码(分享时携带,一次性)
inviteCode: '',
// 当前登录用户ID(用于判断动态是否为自己发布)
myUserId: ''
}
},
onShow() {
this.loadFeeds(true)
// 消息入口已隐藏,未读角标暂不展示,空转请求先停掉(恢复入口时打开)
// this.loadUnread()
// this.bindWsEvents()
// 读取当前用户ID(删除自己的动态时用)
try {
const user = JSON.parse(uni.getStorageSync('user_info') || '{}')
this.myUserId = user.id !== undefined && user.id !== null ? String(user.id) : ''
} catch (e) { this.myUserId = '' }
// 页面显示时确保 WS 已连接(登录后/断线后兼容)
wsManager.connect()
},
onHide() {
this.unbindWsEvents()
},
onUnload() {
this.unbindWsEvents()
},
methods: {
// === WebSocket 事件 ===
bindWsEvents() {
if (this._wsBound) return
this._wsBound = true
this._wsMsgHandler = this.onWsMessage
this._wsConnectHandler = this.onWsConnect
wsManager.on('message', this._wsMsgHandler)
wsManager.on('connect', this._wsConnectHandler)
},
unbindWsEvents() {
if (!this._wsBound) return
this._wsBound = false
if (this._wsMsgHandler) {
wsManager.off('message', this._wsMsgHandler)
this._wsMsgHandler = null
}
if (this._wsConnectHandler) {
wsManager.off('connect', this._wsConnectHandler)
this._wsConnectHandler = null
}
},
/** 收到新私信:未读角标实时 +1(自己发的/自己读的不计数) */
onWsMessage(data) {
if (!data) return
const senderId = data.senderId
if (senderId && wsManager.userId && String(senderId) === String(wsManager.userId)) return
this.unreadTotal++
},
/** (重)连成功后拉取真实未读数,修正断线期间的漏计/多计 */
onWsConnect() {
this.loadUnread()
},
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 */ }
},
/** 判断是否为自己发布的动态(仅自己可删) */
isMyFeed(feed) {
if (!this.myUserId) return false
return feed && feed.userId !== undefined && feed.userId !== null && String(feed.userId) === this.myUserId
},
/** 删除自己的动态(二次确认) */
confirmDeleteFeed(feed) {
uni.showModal({
title: '删除动态',
content: '确定要删除这条动态吗?删除后不可恢复',
confirmText: '删除',
confirmColor: '#FF6B6B',
success: async (res) => {
if (!res.confirm) return
try {
await DeleteFeed({ feedId: feed.id })
this.feeds = this.feeds.filter(f => String(f.id) !== String(feed.id))
uni.showToast({ title: '已删除', icon: 'none' })
} catch (e) {
// httpRequest 已全局弹错误提示(如非本人动态后端会拒绝)
}
}
})
},
goDetail(feed) {
uni.navigateTo({ url: `/pages/circle-detail/circle-detail?id=${feed.id}` })
},
/** 记录当前要分享的动态,供 onShareAppMessage 读取 */
handleShare(feed) {
this.shareMode = 'feed'
this.shareFeed = feed
},
/** 点击邀请按钮:标记本次分享为邀请模式 */
prepareInviteShare() {
this.shareMode = 'invite'
},
/** 确保邀请码已生成(仅缺失时拉取,避免频繁创建一次性邀请) */
ensureInviteCode() {
if (this.inviteCode) return
getMyInviteCode().then(code => {
if (code) this.inviteCode = code
})
},
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()
if (res.data && typeof res.data.total === 'number') {
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}` })
},
/** 编辑自己发起的酒局(复用发起页的编辑模式) */
goEditEvent(evt) {
uni.navigateTo({ url: `/pages/event-create/event-create?id=${evt.id}` })
},
/** 删除自己发起的酒局(二次确认) */
confirmDeleteEvent(evt) {
uni.showModal({
title: '删除酒局',
content: '确定要删除这场酒局吗?删除后不可恢复',
confirmText: '删除',
confirmColor: '#FF6B6B',
success: async (res) => {
if (!res.confirm) return
try {
await DeleteEvent({ eventId: evt.id })
this.events = this.events.filter(e => String(e.id) !== String(evt.id))
uni.showToast({ title: '已删除', icon: 'none' })
} catch (e) {
// httpRequest 已全局弹错误提示(如非本人酒局后端会拒绝)
}
}
})
},
goCreateEvent() {
uni.navigateTo({ url: '/pages/event-create/event-create' })
},
// === FAB ===
handleFab() {
this.goPublish()
}
},
onShareAppMessage() {
// 邀请好友分享:链接携带一次性邀请码,好友点开后自动接受邀请
if (this.shareMode === 'invite') {
let user = {}
try {
user = JSON.parse(uni.getStorageSync('user_info') || '{}')
} catch (e) { /* ignore */ }
const parts = []
if (this.inviteCode) parts.push(`inviteCode=${encodeURIComponent(this.inviteCode)}`)
if (user.nickname) parts.push(`inviterNick=${encodeURIComponent(user.nickname)}`)
const query = parts.length ? `?${parts.join('&')}` : ''
const nick = user.nickname ? `「${user.nickname}」` : ''
// 本次分享已用掉当前邀请码,异步生成新码供下次使用
this.inviteCode = ''
this.ensureInviteCode()
this.shareMode = 'default'
return {
title: `${nick}邀请你成为酒友,一起记录饮酒生活 🍻`,
path: `/pages/index/index${query}`
}
}
const feed = this.shareFeed
if (this.shareMode === 'feed' && feed && feed.id) {
this.shareMode = 'default'
const text = (feed.text || '').replace(/\s+/g, ' ').trim()
const title = text
? `${feed.nickname || '酒友'}:${text.length > 24 ? text.slice(0, 24) + '…' : text}`
: `${feed.nickname || '酒友'}的动态 - 碰盏日记`
return {
title,
path: `/pages/circle-detail/circle-detail?id=${feed.id}&nick=${encodeURIComponent(feed.nickname || '')}&text=${encodeURIComponent(text.slice(0, 60))}&time=${encodeURIComponent(feed.time || '')}`
}
}
return {
title: '酒友圈 - 碰盏日记',
path: '/pages/index/index'
}
},
onShareTimeline() {
return {
title: '酒友圈 - 碰盏日记'
}
}
}
</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;
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) + 48rpx);
}
.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;
}
/* 酒友管理入口 */
.friends-entry {
display: flex;
align-items: center;
gap: $sp-sm;
padding: $sp-md $sp-lg;
background: $bg-card;
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
border-radius: $radius-lg;
margin-bottom: $sp-md;
&:active {
background: $bg-card-alt;
}
}
.friends-entry-icon {
font-size: $fs-base;
}
.friends-entry-text {
flex: 1;
font-size: $fs-sm;
color: $text-secondary;
font-weight: $fw-medium;
}
.friends-entry-arrow {
font-size: $fs-xl;
color: $text-tertiary;
opacity: 0.6;
}
/* 邀请更多酒友按钮(重置 button 默认样式) */
.invite-btn {
display: flex;
align-items: center;
justify-content: center;
gap: $sp-sm;
width: 100%;
margin: $sp-md 0 0;
padding: $sp-lg 0;
background: rgba(232,168,56,0.08);
border: 1rpx dashed rgba(232,168,56,0.35);
border-radius: $radius-lg;
line-height: 1;
font-size: inherit;
&:active {
background: rgba(232,168,56,0.14);
}
&::after {
border: none;
}
}
.invite-btn-icon {
font-size: $fs-base;
}
.invite-btn-text {
font-size: $fs-sm;
color: $amber;
font-weight: $fw-bold;
}
/* 发布入口:标题栏右侧胶囊按钮(替代悬浮 FAB,不遮挡任何内容) */
.publish-btn {
display: flex;
align-items: center;
gap: 6rpx;
height: 60rpx;
padding: 0 24rpx;
border-radius: $radius-full;
background: rgba(232,168,56,0.12);
border: 1rpx solid rgba(232,168,56,0.30);
transition: transform $duration-fast $ease-out;
&:active {
background: rgba(232,168,56,0.20);
transform: scale(0.94);
}
}
.publish-btn-icon {
font-size: 26rpx;
}
.publish-btn-text {
font-size: $fs-sm;
color: $amber;
font-weight: $fw-bold;
}
</style>