Files
cg 57eef74eb1 feat(sdk): 升级HaveADrink SDK并集成邀请系统
- 将HaveADrink依赖从1.0.8升级至1.0.12版本
- 集成邀请码功能,新增common/invite.js处理邀请链路
- 实现发送好友请求返回邀请码的新流程
- 添加删除动态功能,新增DeleteFeed接口
- 集成UniAppWebSocket适配器,重构WebSocket连接管理
- 优化好友请求支持关键词搜索功能
- 在FeedCard组件中添加删除按钮和分享按钮
- 更新SDK类型定义文件以匹配新接口规范
2026-08-04 23:32:45 +08:00

448 lines
11 KiB
Vue
Raw Permalink 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 friends-page">
<!-- 顶部导航 -->
<view class="friends-nav" :style="{ paddingTop: navPaddingTop }">
<view class="friends-nav-inner">
<view class="friends-back" @click="goBack">
<text class="friends-back-icon"></text>
</view>
<text class="friends-nav-title">我的酒友</text>
<view class="friends-nav-ph"></view>
</view>
</view>
<!-- 搜索栏 -->
<view class="friends-search">
<view class="search-box">
<text class="search-icon">🔍</text>
<input
class="search-input"
v-model="keyword"
placeholder="搜索酒友"
placeholder-class="search-placeholder"
confirm-type="search"
@confirm="doSearch"
@input="onSearchInput"
/>
<view v-if="keyword" class="search-clear" @click="clearSearch">
<text class="search-clear-icon">×</text>
</view>
</view>
</view>
<scroll-view class="friends-scroll" scroll-y>
<view class="friends-body">
<!-- 好友请求区 -->
<view v-if="requests.length && !keyword" class="requests-section">
<text class="section-label">新的好友请求</text>
<view class="request-item" v-for="req in requests" :key="req.id">
<view class="request-avatar">
<text class="request-avatar-text">{{ req.nickname[0] }}</text>
</view>
<view class="request-info">
<text class="request-name">{{ req.nickname }}</text>
<text class="request-msg">{{ req.message }}</text>
</view>
<view class="request-accept" @click="acceptRequest(req)">
<text class="request-accept-text">接受</text>
</view>
</view>
</view>
<!-- 酒友列表 -->
<view v-if="friends.length" class="friends-list">
<text class="section-label" v-if="!keyword">全部酒友 ({{ friends.length }})</text>
<text class="section-label" v-else>搜索结果 ({{ friends.length }})</text>
<FriendItem
v-for="f in friends"
:key="f.id"
:friend="f"
>
<template #action>
<view class="friend-del" @click.stop="confirmRemove(f)">
<text class="friend-del-text">删除</text>
</view>
</template>
</FriendItem>
<!-- 有好友时也保留邀请入口直接触发带邀请码的转发 -->
<button v-if="!keyword" class="invite-btn" open-type="share">
<text class="invite-btn-icon">👥</text>
<text class="invite-btn-text">邀请更多酒友</text>
</button>
</view>
<!-- 空状态 -->
<EmptyState
v-if="!friends.length && !requests.length"
icon="👥"
title="还没有酒友"
desc="分享小程序给好友,邀请他们加入"
actionText="邀请好友"
@action="inviteFriends"
/>
<EmptyState
v-if="keyword && !friends.length"
icon="🔍"
title="未找到相关酒友"
desc="换个关键词试试"
/>
</view>
</scroll-view>
</view>
</template>
<script>
import FriendItem from '../../components/FriendItem.vue'
import EmptyState from '../../components/EmptyState.vue'
import { GetFriends, GetFriendRequests, AcceptFriendRequest, RemoveFriend } from '../../common/api'
import { savePendingInvite, handlePendingInvite, getMyInviteCode } from '../../common/invite'
import themeMixin from '../../common/theme-mixin'
export default {
mixins: [themeMixin],
components: { FriendItem, EmptyState },
data() {
const menuBtn = uni.getMenuButtonBoundingClientRect()
return {
navPaddingTop: menuBtn.top + 'px',
keyword: '',
friends: [],
requests: [],
searchTimer: null,
// 我的邀请码(分享时携带,页面加载时预生成)
inviteCode: ''
}
},
onLoad(options) {
this.loadData()
// 被分享进入本页时,若携带邀请码参数,继续处理邀请链路
if (options && options.inviteCode) {
savePendingInvite(options.inviteCode, options.inviterNick)
if (uni.getStorageSync('is_logged_in') === 'true') {
handlePendingInvite().then(() => this.loadData())
}
}
// 预生成新的邀请码,供右上角分享使用(后端每次邀请都生成新码)
this.refreshInviteCode()
},
methods: {
/** 生成新的邀请码(每次分享后调用,保证下次分享用新码) */
refreshInviteCode() {
getMyInviteCode().then(code => {
this.inviteCode = code
})
},
goBack() {
uni.navigateBack()
},
async loadData() {
try {
const [friendsRes, reqRes] = await Promise.all([
GetFriends({}),
GetFriendRequests()
])
this.friends = friendsRes.data.list
this.requests = reqRes.data.list
} catch (e) {
console.warn('加载酒友失败', e)
}
},
onSearchInput() {
clearTimeout(this.searchTimer)
this.searchTimer = setTimeout(() => this.doSearch(), 300)
},
async doSearch() {
try {
const res = await GetFriends({ keyword: this.keyword.trim() })
this.friends = res.data.list
} catch (e) { /* ignore */ }
},
clearSearch() {
this.keyword = ''
this.doSearch()
},
async acceptRequest(req) {
try {
await AcceptFriendRequest({ requestId: req.id })
this.requests = this.requests.filter(r => r.id !== req.id)
uni.showToast({ title: '已添加酒友 🍻', icon: 'none' })
this.loadData()
} catch (e) {
uni.showToast({ title: '操作失败', icon: 'none' })
}
},
confirmRemove(friend) {
uni.showModal({
title: '删除酒友',
content: `确定要删除「${friend.nickname}」吗?`,
confirmText: '删除',
confirmColor: '#FF6B6B',
success: async (res) => {
if (res.confirm) {
try {
await RemoveFriend({ userId: friend.id })
this.friends = this.friends.filter(f => f.id !== friend.id)
uni.showToast({ title: '已删除', icon: 'none' })
} catch (e) {
uni.showToast({ title: '操作失败', icon: 'none' })
}
}
}
})
},
inviteFriends() {
// 触发分享
uni.showToast({ title: '请点击右上角分享', icon: 'none' })
}
},
onShareAppMessage() {
// 分享链接携带当前邀请码(一次性),好友点开后接受邀请即可成为酒友
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.refreshInviteCode()
return {
title: `${nick}邀请你成为酒友,一起记录饮酒生活 🍻`,
path: `/pages/index/index${query}`
}
}
}
</script>
<style lang="scss" scoped>
.friends-page {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.friends-nav {
background: $bg-base;
padding-left: $sp-lg;
padding-right: $sp-lg;
}
.friends-nav-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
}
.friends-back {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: $bg-card;
&:active { background: $bg-card-alt; }
}
.friends-back-icon {
font-size: $fs-xl;
color: $text-primary;
font-weight: $fw-bold;
}
.friends-nav-title {
font-size: $fs-base;
font-weight: $fw-bold;
color: $text-primary;
}
.friends-nav-ph {
width: 64rpx;
}
/* 搜索 */
.friends-search {
padding: $sp-md $sp-lg;
}
.search-box {
display: flex;
align-items: center;
gap: $sp-sm;
padding: 0 $sp-lg;
height: 76rpx;
background: $bg-card;
border-radius: $radius-full;
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
}
.search-icon {
font-size: $fs-sm;
}
.search-input {
flex: 1;
font-size: $fs-base;
color: $text-primary;
}
.search-placeholder {
color: $text-tertiary;
}
.search-clear {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
}
.search-clear-icon {
font-size: $fs-lg;
color: $text-tertiary;
}
.friends-scroll {
flex: 1;
height: 0;
}
.friends-body {
padding: 0 $sp-lg;
padding-bottom: calc(env(safe-area-inset-bottom) + #{$sp-2xl});
}
.section-label {
display: block;
font-size: $fs-sm;
color: $text-tertiary;
font-weight: $fw-medium;
margin-bottom: $sp-md;
margin-top: $sp-lg;
}
/* 好友请求 */
.requests-section {
margin-bottom: $sp-lg;
}
.request-item {
display: flex;
align-items: center;
gap: $sp-md;
padding: $sp-lg;
background: rgba(232,168,56,0.05);
border: 1rpx solid rgba(232,168,56,0.12);
border-radius: $radius-lg;
margin-bottom: $sp-sm;
}
.request-avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: $bg-elevated;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.request-avatar-text {
font-size: $fs-base;
font-weight: $fw-bold;
color: $amber;
}
.request-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4rpx;
}
.request-name {
font-size: $fs-base;
font-weight: $fw-bold;
color: $text-primary;
}
.request-msg {
font-size: $fs-xs;
color: $text-tertiary;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.request-accept {
padding: 8rpx 24rpx;
background: linear-gradient(135deg, $amber, $amber-deep);
border-radius: $radius-full;
flex-shrink: 0;
&:active { transform: scale(0.95); }
}
.request-accept-text {
font-size: $fs-sm;
font-weight: $fw-bold;
color: $text-on-amber;
}
/* 删除按钮 */
.friend-del {
padding: 8rpx 20rpx;
border-radius: $radius-full;
background: rgba(255,107,107,0.1);
&:active { background: rgba(255,107,107,0.2); }
}
.friend-del-text {
font-size: $fs-xs;
color: $coral;
}
/* 邀请更多酒友按钮(重置 button 默认样式) */
.invite-btn {
display: flex;
align-items: center;
justify-content: center;
gap: $sp-sm;
width: 100%;
margin: $sp-lg 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;
}
</style>