Files
2026-06-02 10:42:33 +08:00

778 lines
22 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="status-bar"></view>
<!-- 添加好友弹窗 -->
<view class="popup-overlay" v-if="currentAddFriend">
<view class="popup-card">
<view class="close-popup-card" @click="closeAddFriendCard">
<view class="iconfont icon-quxiao"></view>
</view>
<view class="popup-title">{{isAgreeBeFriend ? '通过好友申请' : '申请添加朋友'}}</view>
<view class="friend-card">
<view class="friend-card-left">
<view class="friend-avatar">{{currentAddFriend.avatar}}</view>
</view>
<view class="friend-card-middle">
<view class="friend-name">{{currentAddFriend.username}}</view>
<view class="friend-label">{{currentAddFriend.sessionId}}</view>
</view>
</view>
<view class="nickname-wrapper">
<view>好友备注</view>
<input class="input-nickname" placeholder="请输入好友名称" />
</view>
<view class="text-btn confirm-btn">{{isAgreeBeFriend ? '确认通过' : '发送申请'}}</view>
</view>
</view>
<view class="tabbar-page page-container">
<swiper class="content-swiper" :current="currentTab" :duration="300" @change="onSwiperChange">
<swiper-item class="content-swiper-item">
<view class="tabpage-containner">
<view class="tabpage-header">
<view class="iconfont icon-fanhui" @click="handleBack"></view>
<view class="tabpage-title">通讯录</view>
</view>
<view class="tabpage-body">
<scroll-view class="group-member" direction="vertical" scroll-y="true">
<view class="friend-list">
<view class="friend-card" @click="changeChatType(0)">
<view class="friend-card-left">
<view class="friend-avatar" style=" background-color: #38bdf8;">
<view class="iconfont icon-Robot" style="font-size: 80rpx; color: #fff;">
</view>
</view>
</view>
<view class="friend-card-middle">
<view class="friend-name">{{FriendAI.name}}</view>
<view class="friend-label"></view>
</view>
<view class="friend-card-right">
<uni-icons type="right"></uni-icons>
</view>
</view>
<view class="friend-card" v-for="friend in FriendInfoList" :key="friend.sessionId" @click="changeChatType(friend.type,friend.sessionId)">
<view class="friend-card-left">
<!-- <view class="friend-avatar">{{friend.avatar}}</view> -->
<!-- 显示头像图片 -->
<image v-if="friend.avatar" :src="friend.avatar" class="friend-avatar"
mode="aspectFill"></image>
<!-- 如果没有头像显示默认头像 -->
<view v-else class="friend-avatar">
<uni-icons type="person-filled" size="100rpx" color="#ffffff"></uni-icons>
</view>
</view>
<view class="friend-card-middle">
<view class="friend-name">{{friend.friendNickName}}</view>
<view class="friend-label">{{friend.sessionId}}</view>
</view>
<view class="friend-card-right">
<uni-icons type="right"></uni-icons>
</view>
</view>
<view>群聊</view>
<view class="solid-line"></view>
<view class="friend-card" v-for="group in GroupList" :key="group.id" @click="changeChatType(group.type,group.id)">
<view class="friend-card-left">
<!-- 群聊只有默认头像 -->
<view class="friend-avatar" style="background-color: #ffaaff;">
<view class="iconfont icon-qunliao" style="font-size: 80rpx; color: #fff;">
</view>
</view>
</view>
<view class="friend-card-middle">
<view class="friend-name">{{group.name}}</view>
<view class="friend-label">{{group.createTime}}</view>
</view>
<view class="friend-card-right">
<uni-icons type="right"></uni-icons>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</swiper-item>
<swiper-item class="content-swiper-item">
<view class="tabpage-containner">
<view class="tabpage-header">
<view class="iconfont icon-fanhui" @click="handleBack"></view>
<view class="tabpage-title">新建群聊</view>
</view>
<view class="tabpage-body">
<view class="group-info">
<view class="group-name">
<view>群名称</view>
<input class="input" placeholder="请输入群名称" v-model="createGroupName" />
</view>
<view class="group-member-wrapper">
<view class="member-title">群成员</view>
<view class="group-ismember-list">
<view class="group-ismember-info" v-for="(item,index) in selectGroupMember"
:key="index">
<view class="group-ismember-name">{{item.friendNickName}}</view>
</view>
</view>
</view>
<view class="create-group-btn" @click="createGroup">创建群聊</view>
</view>
<view class="solid-line"></view>
<scroll-view class="group-member" direction="vertical" scroll-y="true">
<view class="friend-list">
<view class="friend-card" v-for="friend in FriendInfoList" :key="friend.receiver">
<view class="friend-card-left">
<image v-if="friend.avatar" :src="friend.avatar" class="friend-avatar"
mode="aspectFill"></image>
<!-- 如果没有头像显示默认头像 -->
<view v-else class="friend-avatar">
<uni-icons type="person-filled" size="100rpx" color="#ffffff"></uni-icons>
</view>
</view>
<view class="friend-card-middle">
<view class="friend-name">{{friend.friendNickName}}</view>
<view class="friend-label">{{friend.email}}</view>
</view>
<view class="friend-card-right" @click="handlCreateEmember(friend.receiver)">
<view class="friend-checkbox">
<uni-icons type="checkmarkempty"
v-if="selectGroupMember?.find(item => item.id === friend.receiver)"></uni-icons>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</swiper-item>
<swiper-item class="content-swiper-item">
<view class="tabpage-containner">
<view class="tabpage-header">
<view class="iconfont icon-fanhui" @click="handleBack"></view>
<view class="tabpage-title">添加好友</view>
</view>
<view class="tabpage-body">
<view class="search-warpper">
<input class="search-file-input" :class="{active:isSearchFriend}"
@focus="handleFriendSearchFocus" @blur="handleFriendSearchFocus"
v-model="searchFriendName" />
<view class="iconfont icon-sousuo" @click="searchFriend"></view>
</view>
<view v-if="!searchFriendName" class="search-tip">请输入用户名或邮箱进行搜索</view>
<view v-else-if="searchResultList.length" class="search-result">
<scroll-view class="group-member" direction="vertical" scroll-y="true">
<view class="friend-list">
<view class="friend-card" v-for="friend in searchResultList" :key="friend.id">
<view class="friend-card-left">
<view class="friend-avatar">{{friend.avatar}}</view>
</view>
<view class="friend-card-middle">
<view class="friend-name">{{friend.nickname}}</view>
<view class="friend-label">{{friend.email}}</view>
</view>
<view class="friend-card-right" @click="addNewFriend(friend.id)">
<view class="add-btn text-btn">添加</view>
</view>
</view>
</view>
</scroll-view>
</view>
<view v-else class="no-result">未找到相关用户请尝试其他关键词</view>
</view>
</view>
</swiper-item>
<swiper-item class="content-swiper-item">
<view class="tabpage-containner">
<view class="tabpage-header">
<view class="iconfont icon-fanhui" @click="handleBack"></view>
<view class="tabpage-title">视频会议</view>
</view>
<view class="tabpage-body">
<view class="join-meeting-wrapper">
<view>加入会议</view>
<input class="input-meeting" placeholder="请输入会议号" />
<view class="meeting-btn text-btn">加入会议</view>
</view>
<view class="solid-line"></view>
<view class="create-meeting-wrapper">
<view>创建会议</view>
<view class="meeting-field">
<view class="meeting-label">会议主题</view>
<input class="input-meeting" placeholder="例如XXX" />
</view>
<view class="meeting-field">
<view class="meeting-label">会议密码</view>
<input class="input-meeting" placeholder="留空则无密码" />
</view>
<view class="meeting-field">
<view class="meeting-label">邀请好友</view>
<view class="friend-card">
<view class="friend-card-left">
<view class="friend-avatar">avatar</view>
</view>
<view class="friend-card-middle">
<view class="friend-name">friend.nickname</view>
<view class="friend-label">friend.email</view>
</view>
<view class="friend-card-right" @click="addNewFriend(friend.id)">
<view class="add-btn text-btn">添加</view>
</view>
</view>
</view>
<view class="meeting-toggles">
<view class="meeting-toggles-item">
<view class="meeting-toggles-icon icon-btn">
<uni-icons type="mic-filled"></uni-icons>开启麦克风入会
</view>
<view class="meeting-toggles-switch" @click="togglesVoice">
<view class="switch-box" :class="{active : isVoice}">
<view class="switch-track"></view>
</view>
</view>
</view>
<view class="meeting-toggles-item">
<view class="meeting-toggles-icon icon-btn">
<uni-icons type="eye-filled"></uni-icons>开启摄像头入会
</view>
<view class="meeting-toggles-switch" @click="togglesCamera">
<view class="switch-box" :class="{active : isCamera}">
<view class="switch-track"></view>
</view>
</view>
</view>
</view>
<view class="meeting-btn text-btn">加入会议</view>
</view>
</view>
</view>
</swiper-item>
<swiper-item class="content-swiper-item">
<view class="tabpage-containner">
<view class="tabpage-header">
<view class="iconfont icon-fanhui" @click="handleBack"></view>
<view class="tabpage-title">好友申请</view>
</view>
<view class="tabpage-body">
<scroll-view class="group-member" direction="vertical" scroll-y="true">
<view class="friend-list">
<view class="friend-card" v-for="friend in mockFriendDB" :key="friend.id">
<view class="friend-card-left">
<view class="friend-avatar">{{friend.avatar}}</view>
</view>
<view class="friend-card-middle">
<view class="friend-name">{{friend.nickname}}</view>
<view class="friend-label">{{friend.email}}</view>
</view>
<view class="friend-card-right">
<view class="friend-option">
<view class="refuse-btn text-btn" @click="refuseBeFriend(friend)">拒绝</view>
<view class="agree-btn text-btn" @click="agreeBeFriend(friend)">同意</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</swiper-item>
</swiper>
<view class="bottom-tabbar">
<view v-for="(tab, index) in tabList" :key="tab.name" @click="switchTab(index)" class="tab-item">
<view class="tab-icon">{{tab.icon}}</view>
<view class="tab-label">{{tab.label}}</view>
</view>
</view>
</view>
</template>
<script setup>
import {
onMounted,
reactive,
ref,
watch
} from 'vue';
import {
getToken
} from '@/utils/user-info.js'
import {
getChatFriend,
getGroup
} from '@/utils/friend-api.js'
import {
getUserInfo,
getUserAvatar,
searchUsers
} from '@/utils/cloud-api.js'
import {
useFriendSocketStore
} from '@/stores/friend-socket.js'
const friendSocketStore = useFriendSocketStore()
// 连接实时通讯socket
const handleFriendConnect = () => {
if (friendSocketStore.isConnected) return
if (!userToken.value || !UserId.value) {
console.warn('Token或UserId未准备好');
return
}
friendSocketStore.connect({
token: userToken.value,
UserId: UserId.value
})
}
// 返回聊天界面
const handleBack = () => {
uni.navigateBack({
delta: 1,
fail() {
uni.reLaunch({
url: '/pages/Chat/Chat'
})
}
})
}
// 选择群聊或对话时,修改聊天类型并返回聊天页面
const changeChatType = (type,sessionId='') =>{
uni.setStorageSync('chatType', type)
if(sessionId){
uni.setStorageSync('currentSessionId',sessionId)
}
uni.reLaunch({
url: '/pages/Chat/Chat'
})
}
// 底部Tab相关
// 当前选中的tab索引 (0:新建群聊, 1:添加好友, 2:视频会议, 3:好友申请, 4:通讯录)
const currentTab = ref(0)
// Tab列表
const tabList = reactive([{
name: 'contacts',
label: '通讯录',
icon: '📞'
},
{
name: 'createGroup',
label: '新建群聊',
icon: '👥'
},
{
name: 'addFriend',
label: '添加好友',
icon: ''
},
{
name: 'videoCall',
label: '视频会议',
icon: '📹'
},
{
name: 'friendRequest',
label: '好友申请',
icon: '📨'
}
])
const onSwiperChange = (e) => {
currentTab.value = e.detail.current
}
const switchTab = (index) => {
currentTab.value = index
}
// 用户token
const userToken = ref('')
// 用户id
const UserId = ref('')
// 通讯录相关
// AI好友
const FriendAI = {
id: 'ai_robot',
name: '宇恒一号',
avatar: '',
type: 'ai',
description: 'AI智能助手',
lastMessage: null
}
// 好友列表
// const FriendList = ref([])
// 好友消息列表(不包括头像)
const FriendInfoList = ref([])
// 获取用户好友列表
const takeFriendList = async () => {
try {
console.log("开始获取好友列表");
userToken.value = getToken();
const userInfo = await getUserInfo(userToken.value)
UserId.value = userInfo._id
console.log("UserId.value", UserId.value);
const friendList = await getChatFriend(UserId.value)
console.log("friendList:", friendList);
if (friendList && friendList.length) {
FriendInfoList.value = await takeUserAvatar(friendList)
} else {
FriendInfoList.value = []
console.log("好友列表为空");
}
} catch (error) {
console.error("获取好友列表失败:", error);
FriendInfoList.value = []
}
}
// 获取好友头像
const takeUserAvatar = async (friendList) => {
if (!friendList?.length) return []
try {
const friendIds = friendList.map(item => item.receiver)
const friendAvatarList = await getUserAvatar(userToken.value, friendIds)
// 创建map,用于快速查找
const userMap = new Map(friendAvatarList.map(user => [user.user_id, user]) || [])
// 合并数据
return friendList.map(friend => {
const userInfo = userMap.get(friend.receiver)
return {
...friend,
avatar: userInfo?.avatar || null
}
})
} catch (err) {
console.error('获取好友头像失败', err);
return friendList
}
}
onMounted(async () => {
await takeFriendList()
await takeGroupList()
handleFriendConnect()
})
// 群聊相关
// 群聊列表
const GroupList = ref([])
// 获取用户群聊列表
const takeGroupList = async () => {
try {
console.log("开始获取群聊列表");
userToken.value = getToken();
const userInfo = await getUserInfo(userToken.value)
UserId.value = userInfo._id
console.log("UserId.value", UserId.value);
GroupList.value = await getGroup(UserId.value)
} catch (error) {
console.error("获取群聊列表失败:", error);
GroupList.value = []
}
}
// 新建群聊相关
const createGroupName = ref('')
const createGroupData = ref()
// 确保 friendSocket 已连接
const ensureFriendSocketConnected = () => {
return new Promise((resolve, reject) => {
// 已连接则直接返回
if (friendSocketStore.isConnected) {
resolve()
return
}
// 未连接,先建立连接
if (!userToken.value || !UserId.value) {
reject(new Error('Token或UserId未准备好'))
return
}
console.log('friendSocket 未连接,正在建立连接...')
friendSocketStore.connect({
token: userToken.value,
UserId: UserId.value
})
// 等待连接建立
const checkInterval = setInterval(() => {
if (friendSocketStore.isConnected) {
clearInterval(checkInterval)
console.log('friendSocket 连接已建立')
resolve()
}
}, 100)
// 最多等待10秒
setTimeout(() => {
clearInterval(checkInterval)
if (!friendSocketStore.isConnected) {
reject(new Error('连接超时'))
}
}, 10000)
})
}
const createGroup = async () => {
try {
//检查是否有群成员和群名称
if(!createGroupName.value) {
uni.showToast({
title:'群名称不能为空',
icon:'none'
})
return
}
//检查是否有群成员和群名称
if(selectGroupMember.value.length === 0) {
uni.showToast({
title:'请选择群成员',
icon:'none'
})
return
}
// 确保 socket 已连接
await ensureFriendSocketConnected()
const memberIds = selectGroupMember.value.map(member => member.id)
createGroupData.value = {
"version": "1.1",
"body": {
"command": 3,
"userIds": memberIds,
"message": "我来邀请你们了",
"sender": UserId.value,
"callBackMessage": true,
"groupName": createGroupName.value,
"teamCreateReq": {
"access_token": userToken.value
}
}
}
const isSend = await friendSocketStore.send(createGroupData.value)
if (isSend) {
await sendCreateGroup();
} else {
console.log("发送新建群聊消息失败");
}
} catch (err) {
console.log(err);
uni.showToast({
title: `新建群聊失败${err}`,
icon: 'none'
})
}
}
// 发送创建群聊的socket消息
const sendCreateGroup = () => {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('超时未返回群聊创建结果'))
}, 180000)
// 创建一个一次性监听器, // 监听command的值如果是3就检查有没有创建群聊成功
const unwatch =
watch(() => friendSocketStore.command, async (newVal) => {
console.log("创建一个一次性监听器");
if (newVal && friendSocketStore.command === 3) {
console.log("监听到command的值是3,检查群聊是否创建成功", friendSocketStore.command);
try {
await takeGroupList()
const newGroup = GroupList.value.find(g => g.id === friendSocketStore
.groupId)
// 2. 判断是否新建成功
if (newGroup) {
console.log("✅ 新建群聊成功", newGroup);
selectGroupMember.value = []
currentTab.value = 0
} else {
console.log("❌ 新建群聊失败 / 群组不存在");
}
} catch (error) {
console.error('保存AI回复失败', error);
reject(error);
} finally {
friendSocketStore.command = null
friendSocketStore.groupId = null
unwatch();
clearTimeout(timeout);
}
}
})
})
}
// 已选群成员列表
const selectGroupMember = ref([]);
const handlCreateEmember = (id) => {
console.log("选择的好友id",id);
console.log("selectGroupMember.value=",JSON.stringify(selectGroupMember.value));
const index = selectGroupMember.value.findIndex(item => item.id === id);
if (index === -1) {
const friendItem = FriendInfoList.value.find(item => item.receiver === id);
if (friendItem) {
selectGroupMember.value.push({
id: friendItem.receiver,
friendNickName: friendItem.friendNickName
})
}
} else {
selectGroupMember.value.splice(index, 1);
}
}
// 添加好友相关
const isSearchFriend = ref(false)
const searchFriendName = ref('')
const searchResultList = ref([]);
const searchFriend = async() => {
if (!searchFriendName.value.trim()) {
searchResultList.value = []
return
}
// 模糊查询:遍历 mockFriendDB匹配 nickname、username 或 email
const keyword = searchFriendName.value.toLowerCase().trim();
searchResultList.value = await searchUsers(userToken.value,keyword)
console.log("searchResultList.value",searchResultList.value);
// const results = mockFriendDB.value.filter(friend => {
// const isMatch = friend.nickname.toLowerCase().includes(keyword) || friend.username.toLowerCase()
// .includes(keyword) || friend.email.toLowerCase().includes(keyword);
// return isMatch
// })
// searchResultList.value = results
}
const handleFriendSearchFocus = () => {
isSearchFriend.value = !isSearchFriend.value
}
const currentAddFriend = ref(null)
const addNewFriend = (id) => {
const friend = mockFriendDB.value.find(friend => friend.id === id)
if (friend) {
// 创建副本避免引用问题
currentAddFriend.value = {
...friend
}
} else {
console.warn('未找到该好友')
currentAddFriend.value = null
}
}
const closeAddFriendCard = () => {
currentAddFriend.value = null
isAgreeBeFriend.value = false
}
// 视频会议相关
const isVoice = ref(false);
const isCamera = ref(false)
const togglesVoice = () => {
isVoice.value = !isVoice.value
}
const togglesCamera = () => {
isCamera.value = !isCamera.value
}
// 好友申请相关
const isAgreeBeFriend = ref(false)
const refuseBeFriend = (friend) => {
}
const agreeBeFriend = (friend) => {
isAgreeBeFriend.value = true
currentAddFriend.value = {
...friend
}
}
// 模拟好友数据库
const mockFriendDB = ref([{
id: 1,
nickname: '张三',
username: 'zhangsan',
email: 'zhangsan@example.com',
avatar: '👤'
},
{
id: 2,
nickname: '李四',
username: 'lisi',
email: 'lisi@example.com',
avatar: '👥'
},
{
id: 3,
nickname: '王小明',
username: 'wangxm',
email: 'wang@example.com',
avatar: '👪'
},
{
id: 4,
nickname: '赵磊',
username: 'zhaolei',
email: 'zhao@example.com',
avatar: '🗣️'
},
{
id: 5,
nickname: '张三2',
username: 'zhangsan',
email: 'zhangsan@example.com',
avatar: '👤'
},
{
id: 6,
nickname: '李四2',
username: 'lisi',
email: 'lisi@example.com',
avatar: '👥'
},
{
id: 7,
nickname: '王小明2',
username: 'wangxm',
email: 'wang@example.com',
avatar: '👪'
},
{
id: 8,
nickname: '赵磊2',
username: 'zhaolei',
email: 'zhao@example.com',
avatar: '🗣️'
}
])
// 通讯录相关
</script>
<style scoped>
@import url("ContactPages.css");
</style>