Files
phone-app------test1-/components/ChatSidebar.vue
2026-06-02 10:42:33 +08:00

633 lines
15 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="chat-sidebar-container">
<view class="status-bar"></view>
<view class="sidebar-header">
<view class="user-profile-card" @click="openUserCardVisible">
<view class="left">
<view v-if="UserAvatar" class="avatar-preview">
<image :src="UserAvatar" mode="aspectFill"></image>
</view>
<view v-else class="avatar-placeholder">
<uni-icons type="contact-filled" size="50" color="#007aff"></uni-icons>
</view>
</view>
<view class="center">
<text>欢迎回来</text>
<text>{{UserName}}</text>
</view>
<view class="right">
<view class="status-dot"></view>
<view class="status-text"><text>在线</text></view>
</view>
</view>
<view class="new-chat-btn" @click="goContactPages">
<text>+ 聊天列表</text>
</view>
</view>
<view class="chat-history">
<view class="history-header">
<view class="history-title-section">
<text>历史会话</text>
<text>{{UserConversations.length}}个对话</text>
</view>
<view class="history-management">
<view v-if="!isSelectMode" class="history-management-btn iconfont icon-jia icon-jia-style"
@click="openNewChatModal"></view>
<view v-if="!isSelectMode" class="history-management-btn iconfont icon-duoxuan icon-duoxuan-style"
@click="toggleSelectMode"></view>
<view v-if="!isSelectMode" class="history-management-btn iconfont icon-saochu icon-saochu-style" @click="clearAllConversations">
</view>
<view v-if="isSelectMode && selectedHistoryIds.length > 0"
class="history-management-btn iconfont icon-shanchu icon-shanchu-style"
@click="deleteConversations"></view>
<view v-if="isSelectMode"
class="history-management-btn iconfont icon-total_selection icon-total_selection-style"
:class="{active:isAllHistorySelected}" @click="toggleSelectAllHistory"></view>
<view v-if="isSelectMode" class="history-management-btn iconfont icon-quxiao icon-quxiao-style"
@click="toggleSelectMode"></view>
</view>
</view>
<view v-if="!isSelectMode" class="history-search-wrap">
<view class="history-search-inner" :class="{'has-value' : chatHistoryQuery || isHistorySearchFocused}">
<uni-icons type="search"
:class="[chatHistoryQuery || isHistorySearchFocused ? 'chat-search-icon' : 'search-icon']"></uni-icons>
<input v-model="chatHistoryQuery" @focus="isHistorySearchFocused=true"
@blur="isHistorySearchFocused=false" type="text" placeholder="搜索会话..." />
</view>
</view>
<view class="chat-history-list">
<!-- ai对话列表 -->
<view v-if="chatType === 0" v-for="chat in UserConversations" :key="chat._id" :data-chat-id="chat._id"
class="chat-history-card"
:class="{'is-select-chat': currentChatId === chat._id || selectedHistoryIds.includes(chat._id)}"
@click="isSelectMode ? toggleHistorySelect(chat._id) : selectChat(chat._id)">
<view v-if="isSelectMode" class="history-chat-checkbox">
<uni-icons v-if="selectedHistoryIds.includes(chat._id)" type="checkmarkempty" size="20"
color="#007aff"></uni-icons>
</view>
<view class="history-chat-avatar">
<uni-icons type="chat-filled" size="26" color="#007aff"></uni-icons>
</view>
<view v-if="chat.agent_id">
{{chat.agent_data.title}}
</view>
<view v-else>{{chat.title}}</view>
</view>
<!-- 好友对话列表 -->
<view v-if="chatType === 1" v-for="chat in UserConversations" :key="chat.sessionId"
:data-chat-id="chat.sessionId" class="chat-history-card"
:class="{'is-select-chat': currentChatId === chat.sessionId || selectedHistoryIds.includes(chat.sessionId)}"
@click="isSelectMode ? toggleHistorySelect(chat.sessionId) : selectChat(chat.sessionId,chat.receiver)">
<view v-if="isSelectMode" class="history-chat-checkbox">
<uni-icons v-if="selectedHistoryIds.includes(chat.sessionId)" type="checkmarkempty" size="20"
color="#007aff"></uni-icons>
</view>
<view class="history-chat-avatar">
<image v-if="chat.avatar" :src="chat.avatar" class="friend-avatar" mode="aspectFill"></image>
</view>
<view>{{chat.friendNickName}}</view>
</view>
<!-- 群聊对话列表 -->
<view v-if="chatType === 2" v-for="chat in UserConversations" :key="chat.id" :data-chat-id="chat.id"
class="chat-history-card"
:class="{'is-select-chat': currentChatId === chat.id || selectedHistoryIds.includes(chat.id)}"
@click="isSelectMode ? toggleHistorySelect(chat.id) : selectChat(chat.id)">
<view v-if="isSelectMode" class="history-chat-checkbox">
<uni-icons v-if="selectedHistoryIds.includes(chat.id)" type="checkmarkempty" size="20"
color="#007aff"></uni-icons>
</view>
<view class="history-chat-avatar">
<view class="iconfont icon-qunliao" style="font-size: 80rpx; color: #fff;"></view>
</view>
<view>{{chat.name}}</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
computed,
onMounted,
onUnmounted,
ref,
toRef
} from 'vue'
import {
deleteConversation
} from '@/utils/cloud-api.js'
import {
getToken
} from '@/utils/user-info.js'
import {
getUserInfo
} from '@/utils/cloud-api.js'
defineOptions({
name: "ChatSidebar"
})
// 接收Chat.vue传过来的数据
// 定义props,接收父组件传过来的数据
const props = defineProps({
chatList: {
type: Array,
required: true
},
showNewChatModal: {
type: Boolean,
default: false
},
currentSessionId: {
type: [String, Number],
required: true
},
chatType: {
type: Number,
required: true
}
})
const emit = defineEmits([
'update:showNewChatModal',
'update:currentSessionId',
'refresh-conversations',
'update:chatType'
])
const UserConversations = toRef(props, 'chatList')
const showNewChatModal = toRef(props, 'showNewChatModal')
const chatType = toRef(props, 'chatType')
// 打开用户资料卡
const openUserCardVisible = () => {
uni.navigateTo({
url: '/pages/UserProfileModal/UserProfileModal'
})
}
// 选择模式
const isSelectMode = ref(false)
// 已选历史会话id列表
const selectedHistoryIds = ref([])
// 删除会话
const deleteConversations = () => {
if(selectedHistoryIds.value.length === 0) return
uni.showModal({
title:'确认删除',
content:`确定要删除全部 ${selectedHistoryIds.value.length} 个对话吗?此操作不可恢复。`,
confirmText: '确认删除',
confirmColor: '#ff0000',
success: async(res) => {
if(res.confirm) {
try {
const userToken = getToken();
await deleteConversation(userToken, selectedHistoryIds.value)
// 4. 删除成功提示
uni.showToast({
title: '删除成功',
icon: 'success'
})
selectedHistoryIds.value = []
isSelectMode.value = false
// 通知父组件刷新会话列表
emit('refresh-conversations')
} catch (error) {
// 6. 统一捕获错误,防止崩溃
console.error('删除会话失败:', error)
uni.showToast({
title: '删除失败,请重试',
icon: 'none'
})
}
}
}
})
}
// 删除所有会话
const clearAllConversations = () => {
if (!UserConversations.value.length) {
uni.showToast({ title: '没有可删除的会话', icon: 'none' })
return
}
uni.showModal({
title: '确认删除',
content: `确定要删除全部 ${UserConversations.value.length} 个对话吗?此操作不可恢复。`,
confirmText: '删除全部',
confirmColor: '#ff0000',
success: async (res) => {
if (res.confirm) {
try {
const userToken = getToken()
const allIds = UserConversations.value.map(item => item._id)
await deleteConversation(userToken, allIds)
uni.showToast({ title: '已删除全部对话', icon: 'success' })
emit('refresh-conversations')
} catch (error) {
console.error('清空全部对话失败:', error)
uni.showToast({ title: '删除失败,请重试', icon: 'none' })
}
}
}
})
}
// 是否全选历史会话
const isAllHistorySelected = computed(() => {
return UserConversations.value.length > 0 && selectedHistoryIds.value.length === UserConversations.value
.length
})
// 当前选择的聊天会话
const currentChatId = toRef(props, 'currentSessionId')
// 选择聊天会话
const selectChat = (chatId, receiverId = '') => {
console.log("选择的id是", chatId);
if (receiverId) {
uni.setStorageSync('receiverId', receiverId)
}
uni.setStorageSync('currentSessionId', chatId)
emit('update:currentSessionId', chatId)
}
// 新建会话
const openNewChatModal = () => {
console.log("点击了新建会话");
emit('update:showNewChatModal', true)
}
const goContactPages = () => {
uni.navigateTo({
url: '/pages/ContactPages/ContactPages'
})
}
// 会话历史搜索
const chatHistoryQuery = ref('')
const isHistorySearchFocused = ref(false)
// ======批量处理历史会话操作方法=========
// 切换多选模式
const toggleSelectMode = () => {
isSelectMode.value = !isSelectMode.value;
if (!isSelectMode.value) {
selectedHistoryIds.value = []
}
}
// 多选模式下选择历史会话
const toggleHistorySelect = (chatId) => {
const index = selectedHistoryIds.value.indexOf(chatId)
if (index === -1) {
selectedHistoryIds.value.push(chatId)
} else {
selectedHistoryIds.value.splice(index, 1)
}
}
// 切换全选会话
const toggleSelectAllHistory = () => {
if (isAllHistorySelected.value) {
selectedHistoryIds.value = []
} else {
selectedHistoryIds.value = UserConversations.value.map(chat => chat._id)
}
}
const UserToken = ref('')
const UserAvatar = ref('')
const UserName = ref('')
onMounted(async () => {
UserToken.value = getToken()
const UserInfo = await getUserInfo(UserToken.value)
UserAvatar.value = UserInfo.avatar || ''
UserName.value = UserInfo.username || ''
console.log("菜单收到的会话列表:",UserConversations.value);
})
</script>
<style lang="scss" scoped>
.chat-sidebar-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
background-color: #f8f8f8;
color: black;
box-sizing: border-box;
}
.sidebar-header {
padding: 40rpx;
width: 100%;
box-sizing: border-box;
display: flex;
flex-shrink: 0;
justify-content: center;
flex-direction: column;
border-bottom: 1rpx solid #bbbbbb;
}
.user-profile-card {
padding: 10rpx 20rpx;
width: 100%;
display: flex;
align-items: center;
background-color: #ffffff;
box-sizing: border-box;
border-radius: 30rpx;
box-shadow: 0 0 8rpx #cccccc;
}
.user-profile-card .left {
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
margin-right: 10rpx;
.avatar-preview {
display: flex;
justify-content: center;
align-items: center;
width: 120rpx;
height: 120rpx;
border-radius: 50%;
overflow: hidden;
image {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.avatar-placeholder {
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
border-radius: 50%;
width: 120rpx;
height: 120rpx;
}
}
.user-profile-card .center {
flex: 1;
height: 0;
display: flex;
flex-direction: column;
align-items: start;
justify-content: center;
}
.user-profile-card .right {
flex-shrink: 0;
margin-left: 6rpx;
display: flex;
align-items: center;
.status-dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background-color: #10B981;
margin-right: 16rpx;
animation: statusDotPulse 2s ease-in-out infinite;
}
@keyframes statusDotPulse {
0%,
100% {
box-shadow: 0 0 15rpx rgba(59, 130, 246, 0.8);
transform: scale(1);
}
50% {
box-shadow: 0 0 30rpx rgba(59, 130, 246, 0.7);
transform: scale(1.1);
}
}
}
.new-chat-btn {
margin-top: 20rpx;
width: 100%;
padding: 30rpx;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(to right, #3b86ff, #2463df);
border-radius: 30rpx;
box-shadow: 0 0 8rpx rgba(59, 134, 255, 0.6);
text {
font-size: 28rpx;
font-weight: bold;
color: #ffffff;
}
}
.chat-history {
width: 100%;
padding: 20rpx 0;
box-sizing: border-box;
flex: 1;
height: 0;
overflow-y: auto;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 8rpx;
}
&::-webkit-scrollbar-thumb {
background: rgba(59, 134, 255, 0.4);
border-radius: 3rpx;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
.history-header {
width: 100%;
padding: 30rpx;
box-sizing: border-box;
border-bottom: 1rpx solid #bbbbbb;
display: flex;
justify-content: space-between;
align-items: center;
.history-title-section {
display: flex;
flex-direction: column;
}
.history-management {
display: flex;
.history-management-btn {
margin-left: 20rpx;
width: 60rpx;
height: 60rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 26rpx;
background-color: rgba(221, 221, 221, 0.5);
border-radius: 10rpx;
}
.icon-jia-style {
background-color: rgba(16, 185, 129, 0.2);
color: #059669;
}
.icon-duoxuan-style {
color: #666666;
}
.icon-saochu-style {
color: red;
}
.icon-quxiao-style {
color: #3b86ff;
// border: 1rpx solid #3b86ff;
box-sizing: border-box;
}
.icon-total_selection-style {
color: #666666;
}
.icon-total_selection-style.active {
color: #2463df;
background: rgba(59, 134, 255, 0.2);
}
.icon-shanchu-style {
color: red;
}
}
}
.history-search-wrap {
display: flex;
flex-direction: column;
width: 100%;
padding: 20rpx 30rpx 6rpx 30rpx;
box-sizing: border-box;
.history-search-inner {
display: flex;
width: 100%;
padding: 10rpx 20rpx;
box-sizing: border-box;
align-items: center;
background-color: rgba(194, 191, 211, 0.1);
border-radius: 30rpx;
.search-icon {
color: #666666 !important;
font-weight: bold;
font-size: 36rpx !important;
}
.chat-search-icon {
color: #aaaaff !important;
font-weight: bold;
font-size: 36rpx !important;
}
input {
margin-left: 10rpx;
box-sizing: border-box;
}
}
.history-search-inner.has-value {
background-color: #ffffff;
border: 2rpx solid #aaaaff;
box-shadow: 0 0 15rpx #aaaaff;
}
}
.chat-history-list {
padding: 10rpx 30rpx;
.chat-history-card {
display: flex;
align-items: center;
border: 1rpx solid #3b86ff;
background-color: #ffffff;
border-radius: 30rpx;
padding: 30rpx;
margin-top: 10rpx;
box-sizing: border-box;
}
.is-select-chat {
box-shadow: 0 0 5rpx #2463df;
background-color: rgba(59, 134, 255, 0.1);
}
.history-chat-checkbox {
width: 30rpx;
height: 30rpx;
border-radius: 10rpx;
display: flex;
justify-content: center;
align-items: center;
border: 1rpx solid #3b86ff;
}
.history-chat-avatar {
width: 66rpx;
height: 66rpx;
border-radius: 20rpx;
display: flex;
justify-content: center;
align-items: center;
background: rgba(59, 134, 255, 0.4);
margin-left: 20rpx;
box-sizing: border-box;
}
.friend-avatar {
display: flex;
justify-content: center;
align-items: center;
border-radius: 20rpx;
width: 100%;
height: 100%;
overflow: hidden;
}
}
</style>