first commit

This commit is contained in:
2026-06-02 10:42:33 +08:00
commit dd4975fd2c
1084 changed files with 442416 additions and 0 deletions

View File

@@ -0,0 +1,778 @@
<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>