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

633
components/ChatSidebar.vue Normal file
View File

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