Files
hejiu/pages/friends/friends.vue
T
cg fabbb167cf feat(circle): 添加酒友圈社交模块
- 新增酒友圈、圈子详情、发布动态、好友列表、活动创建等页面配置
- 添加酒友圈相关API接口,包括动态、评论、好友、活动等功能
- 实现模拟数据用于前端开发和测试
- 创建评论项、空状态、活动卡片、动态卡片、好友项等组件
- 编写酒友圈模块详细的接口文档,定义数据结构和业务规则
2026-07-20 22:53:04 +08:00

379 lines
8.7 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 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>
</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 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
}
},
onLoad() {
this.loadData()
},
methods: {
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() {
return {
title: '来「干杯日记」一起记录饮酒生活吧!',
path: '/pages/index/index'
}
}
}
</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;
}
</style>