Merge branch 'main' of https://git.1024tool.vip/xz/wx-chant-admin into main
This commit is contained in:
commit
82366257e0
@ -1,6 +1,6 @@
|
||||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||
NODE_ENV = 'development'
|
||||
VITE_APP_BASE_API = 'https://mini-chat.1024tool.vip/api'
|
||||
VITE_SERVE = "https://mini-chat.1024tool.vip/api"
|
||||
VITE_SERVE = "https://mini-chat.1024tool.vip/api/"
|
||||
|
||||
VITE_APP_BASE_API_img = "https://mini-chat.1024tool.vip/"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||
NODE_ENV = 'production'
|
||||
VITE_APP_BASE_API = 'https://dsjhd9s.tbmw.cn/api/'
|
||||
VITE_SERVE = "https://dsjhd9s.tbmw.cn/api/"
|
||||
VITE_APP_BASE_API_img = "https://dsjhd9s.tbmw.cn/"
|
||||
VITE_APP_BASE_API = 'https://mini-chat.1024tool.vip/api'
|
||||
VITE_SERVE = "https://mini-chat.1024tool.vip/api/"
|
||||
VITE_APP_BASE_API_img = "https://mini-chat.1024tool.vip/"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
NODE_ENV = 'development'
|
||||
|
||||
VITE_APP_BASE_API = 'http://dev-scrm.1024tool.vip/'
|
||||
VITE_SERVE = "http://dev-scrm.1024tool.vip/"
|
||||
VITE_APP_BASE_API = 'https://mini-chat.1024tool.vip/api'
|
||||
VITE_SERVE = "https://mini-chat.1024tool.vip/api/"
|
||||
|
||||
11369
pnpm-lock.yaml
generated
11369
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -57,13 +57,12 @@ const props = defineProps({
|
||||
const keyWordsText = ref(props.keyWords)
|
||||
const list = ref(props.msgList)
|
||||
watch(() => props.msgList, (val) => {
|
||||
console.log('ChatRecord 接收到消息列表:', val.length, '条消息')
|
||||
list.value = val
|
||||
})
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
console.log('ChatRecord 组件挂载,初始消息数:', props.msgList.length)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -9,8 +9,11 @@
|
||||
<span v-if="item.unread_count && item.unread_count > 0" class="unread-badge">{{ item.unread_count > 99 ? '99+' : item.unread_count }}</span>
|
||||
</div>
|
||||
<div class="name-block">
|
||||
<span class="nickname">{{ item.sender_name }}</span>
|
||||
<span v-if="item.send_time" class="send-time">{{ item.send_time }}</span>
|
||||
<div class="user-info">
|
||||
<span class="nickname">{{ item.sender_name }}</span>
|
||||
<span v-if="item.send_time" class="send-time">{{ item.send_time }}</span>
|
||||
</div>
|
||||
<div v-if="item.content" class="last-message">{{ formatLastMessage(item) }}</div>
|
||||
</div>
|
||||
<span class="sex">
|
||||
<!-- <el-icon v-if="item.sex == '男'" style="color: rgb(121.3, 187.1, 255);">
|
||||
@ -95,6 +98,56 @@ const handleItem = (row) => {
|
||||
|
||||
}
|
||||
|
||||
// 格式化最新消息内容
|
||||
const formatLastMessage = (user) => {
|
||||
if (!user.content) return ''
|
||||
|
||||
try {
|
||||
// 尝试解析JSON格式的content
|
||||
const contentObj = JSON.parse(user.content)
|
||||
|
||||
// 优先返回messages字段
|
||||
if (contentObj.messages) {
|
||||
const content = String(contentObj.messages).slice(0, 30)
|
||||
return content.length > 30 ? content + '...' : content
|
||||
}
|
||||
|
||||
// 其次返回text字段
|
||||
if (contentObj.text) {
|
||||
const content = String(contentObj.text).slice(0, 30)
|
||||
return content.length > 30 ? content + '...' : content
|
||||
}
|
||||
|
||||
// 最后返回content字段
|
||||
if (contentObj.content) {
|
||||
const content = String(contentObj.content).slice(0, 30)
|
||||
return content.length > 30 ? content + '...' : content
|
||||
}
|
||||
|
||||
// 如果都没有,返回JSON字符串的前30个字符
|
||||
const content = JSON.stringify(contentObj).slice(0, 30)
|
||||
return content.length > 30 ? content + '...' : content
|
||||
|
||||
} catch (error) {
|
||||
// JSON解析失败,说明是纯字符串
|
||||
let content = String(user.content)
|
||||
|
||||
// 根据消息类型添加前缀
|
||||
if (user.msg_type === 2) {
|
||||
content = '[图片] ' + content
|
||||
} else if (user.msg_type === 3) {
|
||||
content = '[文件] ' + content
|
||||
} else if (user.msg_type === 4) {
|
||||
content = '[语音] ' + content
|
||||
} else if (user.msg_type === 5) {
|
||||
content = '[视频] ' + content
|
||||
}
|
||||
|
||||
// 限制长度
|
||||
content = content.slice(0, 30)
|
||||
return content.length > 30 ? content + '...' : content
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
})
|
||||
@ -107,9 +160,9 @@ onMounted(() => {
|
||||
|
||||
.infinite-list .infinite-list-item {
|
||||
display: flex;
|
||||
height: 46px;
|
||||
height: 60px;
|
||||
// margin: 10px 0;
|
||||
padding: 0 10px;
|
||||
padding: 8px 10px;
|
||||
line-height: 22px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
@ -124,11 +177,11 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: 4px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 6px;
|
||||
vertical-align: middle;
|
||||
margin-top: 6px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.unread-badge {
|
||||
@ -159,14 +212,47 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-left: 8px;
|
||||
flex: 1;
|
||||
min-width: 0; // 允许内容收缩
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.send-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
white-space: nowrap;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.last-message {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.sex {
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
|
||||
781
src/composables/useChat.ts
Normal file
781
src/composables/useChat.ts
Normal file
@ -0,0 +1,781 @@
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { get_messages, send_message } from '@/api/chat'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { MessageManager, type MessageItem } from '@/utils/messageManager'
|
||||
|
||||
export interface ChatMessage extends MessageItem {
|
||||
// 继承MessageItem的所有属性
|
||||
}
|
||||
|
||||
export interface ChatUser {
|
||||
sender_id: string
|
||||
sender_name: string
|
||||
sender_avatar?: string
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
export interface ChatState {
|
||||
messages: ChatMessage[]
|
||||
activeUser: ChatUser | null
|
||||
isLoading: boolean
|
||||
isConnected: boolean
|
||||
lastMessageTime: string | null
|
||||
unreadCount: number
|
||||
}
|
||||
|
||||
export interface PollingConfig {
|
||||
baseInterval: number // 基础轮询间隔(毫秒)
|
||||
maxInterval: number // 最大轮询间隔
|
||||
minInterval: number // 最小轮询间隔
|
||||
backoffMultiplier: number // 退避倍数
|
||||
maxRetries: number // 最大重试次数
|
||||
}
|
||||
|
||||
export function useChat(appId: string, config?: Partial<PollingConfig>) {
|
||||
// 默认配置
|
||||
const defaultConfig: PollingConfig = {
|
||||
baseInterval: 1000, // 1秒
|
||||
maxInterval: 30000, // 30秒
|
||||
minInterval: 500, // 0.5秒
|
||||
backoffMultiplier: 1.5,
|
||||
maxRetries: 3
|
||||
}
|
||||
|
||||
const pollingConfig = { ...defaultConfig, ...config }
|
||||
|
||||
// 自适应轮询配置
|
||||
const adaptiveConfig = {
|
||||
activeInterval: 1000, // 用户活跃时的轮询间隔
|
||||
inactiveInterval: 5000, // 用户不活跃时的轮询间隔
|
||||
idleInterval: 15000, // 用户空闲时的轮询间隔
|
||||
inactiveThreshold: 30000, // 不活跃阈值(30秒)
|
||||
idleThreshold: 300000, // 空闲阈值(5分钟)
|
||||
messageFrequencyThreshold: 5, // 消息频率阈值(每分钟)
|
||||
adaptiveEnabled: true // 是否启用自适应轮询
|
||||
}
|
||||
|
||||
// 性能优化配置
|
||||
const performanceConfig = {
|
||||
maxMessages: 1000, // 最大消息数量
|
||||
cleanupThreshold: 1200, // 清理阈值
|
||||
cleanupBatchSize: 200, // 每次清理的消息数量
|
||||
debounceDelay: 100, // 防抖延迟(毫秒)
|
||||
throttleDelay: 200, // 节流延迟(毫秒)
|
||||
enableVirtualScroll: true, // 启用虚拟滚动
|
||||
enableLazyLoading: true // 启用图片懒加载
|
||||
}
|
||||
|
||||
// 消息管理器
|
||||
const messageManager = new MessageManager(1000)
|
||||
|
||||
// 状态管理
|
||||
const state = reactive<ChatState>({
|
||||
messages: [],
|
||||
activeUser: null,
|
||||
isLoading: false,
|
||||
isConnected: true,
|
||||
lastMessageTime: null,
|
||||
unreadCount: 0
|
||||
})
|
||||
|
||||
// 轮询相关状态
|
||||
const pollingState = reactive({
|
||||
timer: null as number | null,
|
||||
currentInterval: pollingConfig.baseInterval,
|
||||
retryCount: 0,
|
||||
isPolling: false,
|
||||
lastPollTime: 0,
|
||||
consecutiveEmptyPolls: 0
|
||||
})
|
||||
|
||||
// 用户活动状态跟踪
|
||||
const activityState = reactive({
|
||||
lastActivity: Date.now(),
|
||||
lastMessageTime: Date.now(),
|
||||
messageCount: 0,
|
||||
messageFrequency: 0, // 每分钟消息数
|
||||
userStatus: 'active' as 'active' | 'inactive' | 'idle'
|
||||
})
|
||||
|
||||
// 滚动和UI状态
|
||||
const uiState = reactive({
|
||||
isAutoScroll: true,
|
||||
isNearBottom: true,
|
||||
loadingMore: false,
|
||||
noMoreMessages: false
|
||||
})
|
||||
|
||||
// 虚拟滚动状态
|
||||
const virtualScrollState = reactive({
|
||||
containerHeight: 400,
|
||||
itemHeight: 80,
|
||||
scrollTop: 0,
|
||||
visibleStart: 0,
|
||||
visibleEnd: 0
|
||||
})
|
||||
|
||||
// 分页查询参数
|
||||
const queryParams = reactive({
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
app_id: appId,
|
||||
user_id: ''
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const hasActiveUser = computed(() => !!state.activeUser?.sender_id)
|
||||
const shouldPoll = computed(() => hasActiveUser.value) // 只要有活跃用户就应该轮询
|
||||
const hasMessages = computed(() => state.messages.length > 0)
|
||||
const isLoading = computed(() => uiState.loadingMore || pollingState.isPolling)
|
||||
const connectionStatus = computed(() => {
|
||||
if (!state.isConnected) return 'disconnected'
|
||||
if (pollingState.isPolling) return 'polling'
|
||||
return 'connected'
|
||||
})
|
||||
|
||||
// 虚拟滚动计算属性
|
||||
const virtualScrollHeight = computed(() => {
|
||||
return state.messages.length * virtualScrollState.itemHeight
|
||||
})
|
||||
|
||||
const virtualScrollOffset = computed(() => {
|
||||
return virtualScrollState.visibleStart * virtualScrollState.itemHeight
|
||||
})
|
||||
|
||||
const visibleMessages = computed(() => {
|
||||
if (!performanceConfig.enableVirtualScroll || state.messages.length <= 50) {
|
||||
return state.messages
|
||||
}
|
||||
|
||||
const start = Math.max(0, virtualScrollState.visibleStart)
|
||||
const end = Math.min(state.messages.length, virtualScrollState.visibleEnd)
|
||||
return state.messages.slice(start, end)
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取消息的唯一标识
|
||||
*/
|
||||
function getMessageId(message: ChatMessage): string {
|
||||
return messageManager.getMessageId(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算自适应轮询间隔
|
||||
*/
|
||||
function calculateAdaptiveInterval(): number {
|
||||
if (!adaptiveConfig.adaptiveEnabled) {
|
||||
return pollingConfig.baseInterval
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const timeSinceLastActivity = now - activityState.lastActivity
|
||||
const timeSinceLastMessage = now - activityState.lastMessageTime
|
||||
|
||||
// 更新用户状态
|
||||
if (timeSinceLastActivity < adaptiveConfig.inactiveThreshold) {
|
||||
activityState.userStatus = 'active'
|
||||
} else if (timeSinceLastActivity < adaptiveConfig.idleThreshold) {
|
||||
activityState.userStatus = 'inactive'
|
||||
} else {
|
||||
activityState.userStatus = 'idle'
|
||||
}
|
||||
|
||||
// 计算消息频率(每分钟消息数)
|
||||
const messageTimeWindow = 60000 // 1分钟
|
||||
if (timeSinceLastMessage < messageTimeWindow) {
|
||||
activityState.messageFrequency = activityState.messageCount / (timeSinceLastMessage / messageTimeWindow)
|
||||
} else {
|
||||
activityState.messageFrequency = 0
|
||||
}
|
||||
|
||||
// 根据用户状态和消息频率确定轮询间隔
|
||||
let interval = pollingConfig.baseInterval
|
||||
|
||||
switch (activityState.userStatus) {
|
||||
case 'active':
|
||||
interval = adaptiveConfig.activeInterval
|
||||
// 如果消息频率高,减少轮询间隔
|
||||
if (activityState.messageFrequency > adaptiveConfig.messageFrequencyThreshold) {
|
||||
interval = Math.max(500, interval * 0.5)
|
||||
}
|
||||
break
|
||||
case 'inactive':
|
||||
interval = adaptiveConfig.inactiveInterval
|
||||
break
|
||||
case 'idle':
|
||||
interval = adaptiveConfig.idleInterval
|
||||
break
|
||||
}
|
||||
|
||||
return Math.min(interval, pollingConfig.maxInterval)
|
||||
}
|
||||
|
||||
// 防抖函数
|
||||
const debounce = (func: Function, delay: number) => {
|
||||
let timeoutId: number | null = null
|
||||
return (...args: any[]) => {
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
timeoutId = window.setTimeout(() => func.apply(null, args), delay)
|
||||
}
|
||||
}
|
||||
|
||||
// 节流函数
|
||||
const throttle = (func: Function, delay: number) => {
|
||||
let lastCall = 0
|
||||
return (...args: any[]) => {
|
||||
const now = Date.now()
|
||||
if (now - lastCall >= delay) {
|
||||
lastCall = now
|
||||
func.apply(null, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户活动时间(防抖处理)
|
||||
*/
|
||||
const updateUserActivity = debounce(() => {
|
||||
activityState.lastActivity = Date.now()
|
||||
activityState.userStatus = 'active'
|
||||
// 固定轮询模式下不需要重启轮询
|
||||
}, performanceConfig.debounceDelay)
|
||||
|
||||
// 检查并清理旧消息
|
||||
const checkAndCleanupMessages = () => {
|
||||
const currentCount = messageManager.getStats().totalMessages
|
||||
if (currentCount > performanceConfig.cleanupThreshold) {
|
||||
const messagesToRemove = currentCount - performanceConfig.maxMessages
|
||||
messageManager.cleanup(messagesToRemove)
|
||||
state.messages = messageManager.getAllMessages()
|
||||
console.log(`清理了 ${messagesToRemove} 条旧消息,当前消息数: ${messageManager.getStats().totalMessages}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 内存监控
|
||||
const memoryMonitor = {
|
||||
lastCheck: Date.now(),
|
||||
checkInterval: 30000, // 30秒检查一次
|
||||
|
||||
check() {
|
||||
const now = Date.now()
|
||||
if (now - this.lastCheck < this.checkInterval) return
|
||||
|
||||
this.lastCheck = now
|
||||
const stats = messageManager.getStats()
|
||||
|
||||
console.log('内存监控:', {
|
||||
消息数量: stats.totalMessages,
|
||||
缓存大小: stats.cacheSize,
|
||||
估算内存: `${(stats.memoryUsage / 1024 / 1024).toFixed(2)}MB`,
|
||||
虚拟滚动: performanceConfig.enableVirtualScroll ? '启用' : '禁用',
|
||||
懒加载: performanceConfig.enableLazyLoading ? '启用' : '禁用'
|
||||
})
|
||||
|
||||
// 如果内存使用过高,强制清理
|
||||
if (stats.memoryUsage > 50 * 1024 * 1024) { // 50MB
|
||||
console.warn('内存使用过高,执行强制清理')
|
||||
checkAndCleanupMessages()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能轮询策略
|
||||
*/
|
||||
function calculateNextInterval(hasNewMessages: boolean, errorOccurred: boolean): number {
|
||||
if (errorOccurred) {
|
||||
// 发生错误时使用指数退避
|
||||
pollingState.currentInterval = Math.min(
|
||||
pollingState.currentInterval * pollingConfig.backoffMultiplier,
|
||||
pollingConfig.maxInterval
|
||||
)
|
||||
} else if (hasNewMessages) {
|
||||
// 有新消息时加快轮询
|
||||
pollingState.currentInterval = pollingConfig.minInterval
|
||||
pollingState.consecutiveEmptyPolls = 0
|
||||
} else {
|
||||
// 没有新消息时逐渐减慢轮询
|
||||
pollingState.consecutiveEmptyPolls++
|
||||
if (pollingState.consecutiveEmptyPolls > 3) {
|
||||
pollingState.currentInterval = Math.min(
|
||||
pollingState.currentInterval * 1.2,
|
||||
pollingConfig.maxInterval
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return pollingState.currentInterval
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最新消息
|
||||
*/
|
||||
async function fetchLatestMessages(force: boolean = false): Promise<boolean> {
|
||||
if (!force && (pollingState.isPolling || !hasActiveUser.value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!hasActiveUser.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
pollingState.isPolling = true
|
||||
pollingState.lastPollTime = Date.now()
|
||||
|
||||
try {
|
||||
const query = {
|
||||
...queryParams,
|
||||
page: 1, // 始终获取第一页的最新消息
|
||||
timestamp: state.lastMessageTime // 可选:传递时间戳进行增量更新
|
||||
}
|
||||
|
||||
console.log('轮询获取消息:', query)
|
||||
|
||||
const response = await get_messages(query)
|
||||
const newMessages = (response as any).list || []
|
||||
|
||||
if (newMessages.length > 0) {
|
||||
// 处理消息内容
|
||||
const processedMessages = newMessages.map((msg: any) => {
|
||||
try {
|
||||
if (typeof msg.content === 'string') {
|
||||
try {
|
||||
msg.content = JSON.parse(msg.content)
|
||||
} catch (parseErr) {
|
||||
msg.content = { messages: msg.content }
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 保持原始内容(已在上方处理字符串情况)
|
||||
}
|
||||
return msg
|
||||
})
|
||||
|
||||
// 使用MessageManager进行去重和管理
|
||||
const result = messageManager.addMessages(processedMessages)
|
||||
|
||||
if (result.newMessages.length > 0) {
|
||||
// 更新状态中的消息列表
|
||||
const oldMessagesCount = state.messages.length
|
||||
state.messages = messageManager.getAllMessages()
|
||||
const newMessagesCount = state.messages.length
|
||||
|
||||
console.log(`消息状态更新: 旧消息数量=${oldMessagesCount}, 新消息数量=${newMessagesCount}`)
|
||||
console.log('新增消息:', result.newMessages.map(m => ({ id: messageManager.getMessageId(m), content: m.content })))
|
||||
|
||||
// 检查是否需要清理旧消息
|
||||
checkAndCleanupMessages()
|
||||
|
||||
// 内存监控检查
|
||||
memoryMonitor.check()
|
||||
|
||||
// 更新最后消息时间
|
||||
const latestMessage = result.newMessages[result.newMessages.length - 1]
|
||||
state.lastMessageTime = latestMessage.send_time
|
||||
|
||||
// 如果用户不在底部,增加未读计数
|
||||
if (!uiState.isNearBottom) {
|
||||
state.unreadCount += result.newMessages.length
|
||||
}
|
||||
|
||||
console.log(`添加了 ${result.newMessages.length} 条新消息,更新了 ${result.updatedMessages.length} 条消息`)
|
||||
console.log('消息管理器统计:', messageManager.getStats())
|
||||
console.log('当前消息列表长度:', state.messages.length)
|
||||
|
||||
pollingState.retryCount = 0
|
||||
state.isConnected = true
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
pollingState.retryCount = 0
|
||||
state.isConnected = true
|
||||
return false
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取消息失败:', error)
|
||||
pollingState.retryCount++
|
||||
|
||||
if (pollingState.retryCount >= pollingConfig.maxRetries) {
|
||||
state.isConnected = false
|
||||
ElMessage.error('连接失败,请检查网络')
|
||||
}
|
||||
|
||||
return false
|
||||
} finally {
|
||||
pollingState.isPolling = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动轮询
|
||||
*/
|
||||
function startPolling() {
|
||||
stopPolling()
|
||||
|
||||
if (!shouldPoll.value) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('启动固定1秒轮询消息')
|
||||
|
||||
const poll = async () => {
|
||||
console.log(`开始轮询 - 用户: ${state.activeUser?.sender_name}, shouldPoll: ${shouldPoll.value}`)
|
||||
const hasNewMessages = await fetchLatestMessages()
|
||||
|
||||
if (hasNewMessages) {
|
||||
activityState.messageCount++
|
||||
activityState.lastMessageTime = Date.now()
|
||||
console.log('轮询发现新消息,已更新状态')
|
||||
} else {
|
||||
console.log('轮询未发现新消息')
|
||||
}
|
||||
|
||||
// 使用固定1秒轮询间隔
|
||||
const nextInterval = pollingConfig.baseInterval // 固定1秒
|
||||
|
||||
console.log(`轮询状态: 固定间隔, 间隔: ${nextInterval}ms, 下次轮询: ${shouldPoll.value}`)
|
||||
|
||||
if (shouldPoll.value) {
|
||||
pollingState.timer = setTimeout(poll, nextInterval)
|
||||
} else {
|
||||
console.log('停止轮询 - shouldPoll为false')
|
||||
}
|
||||
}
|
||||
|
||||
// 立即执行一次,然后开始定时轮询
|
||||
poll()
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止轮询
|
||||
*/
|
||||
function stopPolling() {
|
||||
if (pollingState.timer) {
|
||||
clearTimeout(pollingState.timer)
|
||||
pollingState.timer = null
|
||||
console.log('停止消息轮询')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置轮询状态
|
||||
*/
|
||||
function resetPolling() {
|
||||
pollingState.currentInterval = pollingConfig.baseInterval
|
||||
pollingState.retryCount = 0
|
||||
pollingState.consecutiveEmptyPolls = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载历史消息
|
||||
*/
|
||||
async function loadHistoryMessages() {
|
||||
if (uiState.loadingMore || uiState.noMoreMessages || !hasActiveUser.value) {
|
||||
return
|
||||
}
|
||||
|
||||
uiState.loadingMore = true
|
||||
|
||||
try {
|
||||
queryParams.page++
|
||||
const response = await get_messages({ ...queryParams })
|
||||
const historyMessages = (response as any).list || []
|
||||
|
||||
if (historyMessages.length > 0) {
|
||||
// 处理消息内容
|
||||
const processedMessages = historyMessages.map((msg: any) => {
|
||||
try {
|
||||
if (typeof msg.content === 'string') {
|
||||
try {
|
||||
msg.content = JSON.parse(msg.content)
|
||||
} catch (parseErr) {
|
||||
msg.content = { messages: msg.content }
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 保持原始内容(已在上方处理字符串情况)
|
||||
}
|
||||
return msg
|
||||
})
|
||||
|
||||
// 使用MessageManager添加历史消息
|
||||
messageManager.addMessages(processedMessages)
|
||||
state.messages = messageManager.getAllMessages()
|
||||
|
||||
// 检查是否还有更多消息
|
||||
if (historyMessages.length < queryParams.page_size) {
|
||||
uiState.noMoreMessages = true
|
||||
}
|
||||
} else {
|
||||
uiState.noMoreMessages = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载历史消息失败:', error)
|
||||
queryParams.page-- // 回滚页码
|
||||
} finally {
|
||||
uiState.loadingMore = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
async function sendMessage(content: string, msgType: number = 1) {
|
||||
if (!hasActiveUser.value || !content.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新用户活动
|
||||
updateUserActivity()
|
||||
|
||||
const tempId = `temp_${Date.now()}_${Math.random()}`
|
||||
const tempMessage: ChatMessage = {
|
||||
_id: tempId,
|
||||
sender_id: 'current_user',
|
||||
sender_name: '我',
|
||||
content: { messages: content },
|
||||
msg_type: msgType,
|
||||
send_time: new Date().toISOString(),
|
||||
_sending: true,
|
||||
_failed: false,
|
||||
_local: true
|
||||
}
|
||||
|
||||
// 使用MessageManager添加临时消息
|
||||
messageManager.addMessage(tempMessage)
|
||||
state.messages = messageManager.getAllMessages()
|
||||
|
||||
try {
|
||||
const response = await send_message({
|
||||
app_id: appId,
|
||||
content,
|
||||
msg_type: msgType,
|
||||
to_user_id: state.activeUser!.sender_id
|
||||
})
|
||||
|
||||
// 更新消息状态
|
||||
messageManager.markMessageSent(tempId, response as any)
|
||||
state.messages = messageManager.getAllMessages()
|
||||
|
||||
// 立即强制轮询一次获取最新消息
|
||||
await fetchLatestMessages(true)
|
||||
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error)
|
||||
|
||||
// 标记消息发送失败
|
||||
messageManager.markMessageFailed(tempId)
|
||||
state.messages = messageManager.getAllMessages()
|
||||
|
||||
ElMessage.error('消息发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试发送失败的消息
|
||||
*/
|
||||
async function retryMessage(messageId: string) {
|
||||
const message = state.messages.find(msg => getMessageId(msg) === messageId)
|
||||
if (!message || !message._failed) {
|
||||
return
|
||||
}
|
||||
|
||||
message._sending = true
|
||||
message._failed = false
|
||||
|
||||
try {
|
||||
const content = typeof message.content === 'object'
|
||||
? message.content.messages || message.content.content || ''
|
||||
: message.content
|
||||
|
||||
await sendMessage(content, message.msg_type)
|
||||
} catch (error) {
|
||||
message._sending = false
|
||||
message._failed = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换用户
|
||||
*/
|
||||
function switchUser(user: ChatUser) {
|
||||
// 停止当前轮询
|
||||
stopPolling()
|
||||
|
||||
// 重置状态
|
||||
state.activeUser = user
|
||||
state.messages = []
|
||||
state.lastMessageTime = null
|
||||
state.unreadCount = 0
|
||||
|
||||
queryParams.user_id = user.sender_id
|
||||
queryParams.page = 1
|
||||
|
||||
uiState.noMoreMessages = false
|
||||
resetPolling()
|
||||
|
||||
// 加载初始消息
|
||||
loadInitialMessages()
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载初始消息
|
||||
*/
|
||||
async function loadInitialMessages() {
|
||||
if (!hasActiveUser.value) return
|
||||
|
||||
state.isLoading = true
|
||||
|
||||
try {
|
||||
const response = await get_messages({ ...queryParams })
|
||||
const messages = (response as any).list || []
|
||||
|
||||
if (messages.length > 0) {
|
||||
const processedMessages = messages.map((msg: any) => {
|
||||
try {
|
||||
if (typeof msg.content === 'string') {
|
||||
try {
|
||||
msg.content = JSON.parse(msg.content)
|
||||
} catch (parseErr) {
|
||||
msg.content = { messages: msg.content }
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 保持原始内容(已在上方处理字符串情况)
|
||||
}
|
||||
return msg
|
||||
})
|
||||
|
||||
// 使用MessageManager初始化消息
|
||||
messageManager.addMessages(processedMessages)
|
||||
state.messages = messageManager.getAllMessages()
|
||||
|
||||
// 设置最后消息时间
|
||||
if (processedMessages.length > 0) {
|
||||
state.lastMessageTime = processedMessages[processedMessages.length - 1].send_time
|
||||
}
|
||||
|
||||
// 检查是否还有更多消息
|
||||
if (messages.length < queryParams.page_size) {
|
||||
uiState.noMoreMessages = true
|
||||
}
|
||||
}
|
||||
|
||||
// 开始轮询
|
||||
startPolling()
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载初始消息失败:', error)
|
||||
ElMessage.error('加载消息失败')
|
||||
} finally {
|
||||
state.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理滚动事件(节流处理)
|
||||
*/
|
||||
const handleScroll = throttle((scrollTop: number, scrollHeight: number, clientHeight: number) => {
|
||||
// 更新用户活动
|
||||
updateUserActivity()
|
||||
|
||||
// 更新虚拟滚动状态
|
||||
if (performanceConfig.enableVirtualScroll && state.messages.length > 50) {
|
||||
virtualScrollState.scrollTop = scrollTop
|
||||
virtualScrollState.containerHeight = clientHeight
|
||||
|
||||
const visibleCount = Math.ceil(clientHeight / virtualScrollState.itemHeight) + 2
|
||||
const startIndex = Math.floor(scrollTop / virtualScrollState.itemHeight)
|
||||
|
||||
virtualScrollState.visibleStart = Math.max(0, startIndex - 1)
|
||||
virtualScrollState.visibleEnd = Math.min(
|
||||
state.messages.length,
|
||||
virtualScrollState.visibleStart + visibleCount
|
||||
)
|
||||
}
|
||||
|
||||
const isNearBottom = scrollHeight - scrollTop - clientHeight < 50
|
||||
const wasNearBottom = uiState.isNearBottom
|
||||
|
||||
uiState.isNearBottom = isNearBottom
|
||||
uiState.isAutoScroll = isNearBottom
|
||||
|
||||
// 清除未读计数
|
||||
if (isNearBottom && state.unreadCount > 0) {
|
||||
state.unreadCount = 0
|
||||
}
|
||||
|
||||
// 控制轮询
|
||||
// 保持轮询持续进行,与滚动位置无关
|
||||
if (shouldPoll.value && !pollingState.timer) {
|
||||
startPolling()
|
||||
}
|
||||
|
||||
// 加载更多历史消息
|
||||
if (scrollTop <= 0) {
|
||||
loadHistoryMessages()
|
||||
}
|
||||
}, performanceConfig.throttleDelay)
|
||||
|
||||
// 生命周期管理
|
||||
onMounted(() => {
|
||||
// 监听页面可见性变化
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
stopPolling()
|
||||
} else if (shouldPoll.value) {
|
||||
startPolling()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
})
|
||||
|
||||
// 重启轮询(用于自适应调整)
|
||||
const restartPolling = () => {
|
||||
if (pollingState.isPolling) {
|
||||
stopPolling()
|
||||
startPolling()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
state,
|
||||
pollingState,
|
||||
uiState,
|
||||
activityState,
|
||||
virtualScrollState,
|
||||
|
||||
// 计算属性
|
||||
hasActiveUser,
|
||||
shouldPoll,
|
||||
hasMessages,
|
||||
isLoading,
|
||||
connectionStatus,
|
||||
virtualScrollHeight,
|
||||
virtualScrollOffset,
|
||||
visibleMessages,
|
||||
|
||||
// 方法
|
||||
switchUser,
|
||||
sendMessage,
|
||||
retryMessage,
|
||||
loadHistoryMessages,
|
||||
handleScroll,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
restartPolling,
|
||||
updateUserActivity,
|
||||
|
||||
// 工具方法
|
||||
getMessageId,
|
||||
|
||||
// 性能配置
|
||||
performanceConfig
|
||||
}
|
||||
}
|
||||
405
src/utils/messageManager.ts
Normal file
405
src/utils/messageManager.ts
Normal file
@ -0,0 +1,405 @@
|
||||
/**
|
||||
* 消息管理工具
|
||||
* 提供消息去重、状态管理、缓存等功能
|
||||
*/
|
||||
|
||||
export interface MessageItem {
|
||||
id?: string | number
|
||||
_id?: string | number
|
||||
message_id?: string | number
|
||||
sender_id: string
|
||||
sender_name: string
|
||||
sender_avatar?: string
|
||||
content: any
|
||||
msg_type: number
|
||||
send_time: string
|
||||
_sending?: boolean
|
||||
_failed?: boolean
|
||||
_local?: boolean // 本地临时消息标记
|
||||
}
|
||||
|
||||
export interface MessageCache {
|
||||
messages: Map<string, MessageItem>
|
||||
messageOrder: string[] // 保持消息顺序
|
||||
lastUpdateTime: number
|
||||
maxCacheSize: number
|
||||
}
|
||||
|
||||
export class MessageManager {
|
||||
private cache: MessageCache
|
||||
private readonly MAX_CACHE_SIZE = 1000 // 最大缓存消息数量
|
||||
private readonly CACHE_CLEANUP_THRESHOLD = 1200 // 触发清理的阈值
|
||||
|
||||
constructor(maxCacheSize: number = 1000) {
|
||||
this.cache = {
|
||||
messages: new Map(),
|
||||
messageOrder: [],
|
||||
lastUpdateTime: Date.now(),
|
||||
maxCacheSize
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息的唯一标识
|
||||
*/
|
||||
getMessageId(message: MessageItem): string {
|
||||
// 优先级:message_id > id > _id > 组合键
|
||||
if (message.message_id) {
|
||||
return String(message.message_id)
|
||||
}
|
||||
if (message.id) {
|
||||
return String(message.id)
|
||||
}
|
||||
if (message._id) {
|
||||
return String(message._id)
|
||||
}
|
||||
|
||||
// 使用组合键作为后备方案
|
||||
const timeKey = new Date(message.send_time).getTime()
|
||||
const contentKey = this.getContentHash(message.content)
|
||||
return `${message.sender_id}-${timeKey}-${contentKey}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内容哈希值(简单实现)
|
||||
*/
|
||||
private getContentHash(content: any): string {
|
||||
const str = typeof content === 'string' ? content : JSON.stringify(content)
|
||||
let hash = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i)
|
||||
hash = ((hash << 5) - hash) + char
|
||||
hash = hash & hash // 转换为32位整数
|
||||
}
|
||||
return Math.abs(hash).toString(36)
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息到缓存
|
||||
*/
|
||||
addMessage(message: MessageItem): boolean {
|
||||
const messageId = this.getMessageId(message)
|
||||
|
||||
// 检查是否已存在
|
||||
if (this.cache.messages.has(messageId)) {
|
||||
// 更新现有消息(可能是状态变化)
|
||||
const existingMessage = this.cache.messages.get(messageId)!
|
||||
const updatedMessage = { ...existingMessage, ...message }
|
||||
this.cache.messages.set(messageId, updatedMessage)
|
||||
return false // 表示不是新消息
|
||||
}
|
||||
|
||||
// 添加新消息
|
||||
this.cache.messages.set(messageId, { ...message })
|
||||
this.cache.messageOrder.push(messageId)
|
||||
this.cache.lastUpdateTime = Date.now()
|
||||
|
||||
// 检查是否需要清理缓存
|
||||
if (this.cache.messageOrder.length > this.CACHE_CLEANUP_THRESHOLD) {
|
||||
this.cleanupCache()
|
||||
}
|
||||
|
||||
return true // 表示是新消息
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加消息
|
||||
*/
|
||||
addMessages(messages: MessageItem[]): { newMessages: MessageItem[], updatedMessages: MessageItem[] } {
|
||||
const newMessages: MessageItem[] = []
|
||||
const updatedMessages: MessageItem[] = []
|
||||
|
||||
for (const message of messages) {
|
||||
const isNew = this.addMessage(message)
|
||||
if (isNew) {
|
||||
newMessages.push(message)
|
||||
} else {
|
||||
updatedMessages.push(message)
|
||||
}
|
||||
}
|
||||
|
||||
return { newMessages, updatedMessages }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有消息(按时间排序)
|
||||
*/
|
||||
getAllMessages(): MessageItem[] {
|
||||
return this.cache.messageOrder
|
||||
.map(id => this.cache.messages.get(id))
|
||||
.filter(Boolean) as MessageItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定范围的消息
|
||||
*/
|
||||
getMessages(start: number = 0, count: number = 50): MessageItem[] {
|
||||
const allMessages = this.getAllMessages()
|
||||
return allMessages.slice(start, start + count)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最新的N条消息
|
||||
*/
|
||||
getLatestMessages(count: number = 50): MessageItem[] {
|
||||
const allMessages = this.getAllMessages()
|
||||
return allMessages.slice(-count)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息状态
|
||||
*/
|
||||
updateMessageStatus(messageId: string, updates: Partial<MessageItem>): boolean {
|
||||
const message = this.cache.messages.get(messageId)
|
||||
if (!message) {
|
||||
return false
|
||||
}
|
||||
|
||||
const updatedMessage = { ...message, ...updates }
|
||||
this.cache.messages.set(messageId, updatedMessage)
|
||||
this.cache.lastUpdateTime = Date.now()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记消息为发送中
|
||||
*/
|
||||
markMessageSending(messageId: string): boolean {
|
||||
return this.updateMessageStatus(messageId, { _sending: true, _failed: false })
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记消息发送成功
|
||||
*/
|
||||
markMessageSent(messageId: string, serverMessage?: Partial<MessageItem>): boolean {
|
||||
const updates: Partial<MessageItem> = {
|
||||
_sending: false,
|
||||
_failed: false,
|
||||
_local: false,
|
||||
...serverMessage
|
||||
}
|
||||
return this.updateMessageStatus(messageId, updates)
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记消息发送失败
|
||||
*/
|
||||
markMessageFailed(messageId: string): boolean {
|
||||
return this.updateMessageStatus(messageId, { _sending: false, _failed: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除消息
|
||||
*/
|
||||
removeMessage(messageId: string): boolean {
|
||||
if (!this.cache.messages.has(messageId)) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.cache.messages.delete(messageId)
|
||||
const index = this.cache.messageOrder.indexOf(messageId)
|
||||
if (index > -1) {
|
||||
this.cache.messageOrder.splice(index, 1)
|
||||
}
|
||||
|
||||
this.cache.lastUpdateTime = Date.now()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存(移除最旧的消息)
|
||||
*/
|
||||
private cleanupCache(): void {
|
||||
const excessCount = this.cache.messageOrder.length - this.cache.maxCacheSize
|
||||
if (excessCount <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 移除最旧的消息
|
||||
const messagesToRemove = this.cache.messageOrder.splice(0, excessCount)
|
||||
for (const messageId of messagesToRemove) {
|
||||
this.cache.messages.delete(messageId)
|
||||
}
|
||||
|
||||
console.log(`清理了 ${excessCount} 条旧消息,当前缓存大小: ${this.cache.messageOrder.length}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有消息
|
||||
*/
|
||||
clear(): void {
|
||||
this.cache.messages.clear()
|
||||
this.cache.messageOrder = []
|
||||
this.cache.lastUpdateTime = Date.now()
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理旧消息
|
||||
*/
|
||||
cleanup(removeCount: number) {
|
||||
if (removeCount <= 0 || removeCount >= this.cache.messageOrder.length) return
|
||||
|
||||
// 移除最旧的消息
|
||||
const messagesToRemove = this.cache.messageOrder.splice(0, removeCount)
|
||||
|
||||
// 从缓存中移除对应的消息
|
||||
messagesToRemove.forEach(messageId => {
|
||||
this.cache.messages.delete(messageId)
|
||||
})
|
||||
|
||||
this.cache.lastUpdateTime = Date.now()
|
||||
console.log(`MessageManager: 清理了 ${removeCount} 条旧消息`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存统计信息
|
||||
*/
|
||||
getStats() {
|
||||
return {
|
||||
totalMessages: this.cache.messageOrder.length,
|
||||
cacheSize: this.cache.messages.size,
|
||||
lastUpdateTime: this.cache.lastUpdateTime,
|
||||
maxCacheSize: this.cache.maxCacheSize,
|
||||
memoryUsage: this.estimateMemoryUsage()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算内存使用量(字节)
|
||||
*/
|
||||
private estimateMemoryUsage(): number {
|
||||
let totalSize = 0
|
||||
|
||||
for (const message of this.cache.messages.values()) {
|
||||
// 粗略估算每个消息对象的大小
|
||||
totalSize += JSON.stringify(message).length * 2 // UTF-16编码,每字符2字节
|
||||
}
|
||||
|
||||
// 加上Map和Array的开销
|
||||
totalSize += this.cache.messageOrder.length * 50 // 每个字符串引用约50字节
|
||||
|
||||
return totalSize
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出消息数据(用于持久化)
|
||||
*/
|
||||
export(): { messages: MessageItem[], order: string[], timestamp: number } {
|
||||
return {
|
||||
messages: Array.from(this.cache.messages.values()),
|
||||
order: [...this.cache.messageOrder],
|
||||
timestamp: this.cache.lastUpdateTime
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入消息数据(从持久化恢复)
|
||||
*/
|
||||
import(data: { messages: MessageItem[], order: string[], timestamp: number }): void {
|
||||
this.clear()
|
||||
|
||||
// 重建缓存
|
||||
for (const message of data.messages) {
|
||||
const messageId = this.getMessageId(message)
|
||||
this.cache.messages.set(messageId, message)
|
||||
}
|
||||
|
||||
this.cache.messageOrder = [...data.order]
|
||||
this.cache.lastUpdateTime = data.timestamp
|
||||
|
||||
// 验证数据完整性
|
||||
this.validateCache()
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证缓存数据完整性
|
||||
*/
|
||||
private validateCache(): void {
|
||||
// 移除order中不存在的消息ID
|
||||
this.cache.messageOrder = this.cache.messageOrder.filter(id =>
|
||||
this.cache.messages.has(id)
|
||||
)
|
||||
|
||||
// 移除messages中不在order中的消息
|
||||
for (const messageId of this.cache.messages.keys()) {
|
||||
if (!this.cache.messageOrder.includes(messageId)) {
|
||||
this.cache.messages.delete(messageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找消息
|
||||
*/
|
||||
findMessage(predicate: (message: MessageItem) => boolean): MessageItem | undefined {
|
||||
for (const message of this.cache.messages.values()) {
|
||||
if (predicate(message)) {
|
||||
return message
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤消息
|
||||
*/
|
||||
filterMessages(predicate: (message: MessageItem) => boolean): MessageItem[] {
|
||||
return this.getAllMessages().filter(predicate)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取发送失败的消息
|
||||
*/
|
||||
getFailedMessages(): MessageItem[] {
|
||||
return this.filterMessages(msg => msg._failed === true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取正在发送的消息
|
||||
*/
|
||||
getSendingMessages(): MessageItem[] {
|
||||
return this.filterMessages(msg => msg._sending === true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本地临时消息
|
||||
*/
|
||||
getLocalMessages(): MessageItem[] {
|
||||
return this.filterMessages(msg => msg._local === true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建消息管理器实例
|
||||
*/
|
||||
export function createMessageManager(maxCacheSize?: number): MessageManager {
|
||||
return new MessageManager(maxCacheSize)
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息去重工具函数
|
||||
*/
|
||||
export function deduplicateMessages(messages: MessageItem[]): MessageItem[] {
|
||||
const manager = createMessageManager()
|
||||
manager.addMessages(messages)
|
||||
return manager.getAllMessages()
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并消息列表(去重并保持顺序)
|
||||
*/
|
||||
export function mergeMessages(existingMessages: MessageItem[], newMessages: MessageItem[]): MessageItem[] {
|
||||
const manager = createMessageManager()
|
||||
|
||||
// 先添加现有消息
|
||||
manager.addMessages(existingMessages)
|
||||
|
||||
// 再添加新消息
|
||||
const result = manager.addMessages(newMessages)
|
||||
|
||||
return {
|
||||
allMessages: manager.getAllMessages(),
|
||||
newCount: result.newMessages.length,
|
||||
updatedCount: result.updatedMessages.length
|
||||
} as any
|
||||
}
|
||||
@ -1,398 +0,0 @@
|
||||
<template>
|
||||
<div class="chat-page">
|
||||
<div class="left-panel">
|
||||
<UserCard @load="onLoadUserList" :cardlist="userList" height="calc(100vh - 120px)" @change="onSelectUser" />
|
||||
</div>
|
||||
<div class="right-panel">
|
||||
<div class="chat-body" ref="chatBody" @scroll="handleScroll">
|
||||
<ChatRecord :msgList="messages" :sendeInfo="sendeInfo" msgType="user" @retry="handleRetry" />
|
||||
</div>
|
||||
|
||||
<div class="chat-footer">
|
||||
<div class="footer-left" style="">
|
||||
<div class="upload-row" style="display:flex; align-items:center; gap:12px;margin-left: 6px;">
|
||||
<el-icon style="font-size:24px;cursor:pointer" @click="triggerImageInput">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-tupian1"></use>
|
||||
</svg>
|
||||
</el-icon>
|
||||
<input ref="imageInput" type="file" accept="image/*" style="display:none"
|
||||
@change="handleImageChange" />
|
||||
<V3Emoji @click-emoji="onEmojiSelect" :recent="true"></V3Emoji>
|
||||
</div>
|
||||
<el-input type="textarea" v-model="draft" placeholder="输入消息..." rows="2" style="flex:1"
|
||||
@keydown="handleInputKeydown" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, nextTick, watch } from 'vue'
|
||||
import ChatRecord from '@/components/ChatRecord/index.vue'
|
||||
import UserCard from '@/components/UserCard/index.vue'
|
||||
import UserMessage from '@/components/UserMessage/index.vue'
|
||||
import mihoutai from '@/assets/images/mihoutai.png'
|
||||
import V3Emoji from "vue3-emoji";
|
||||
import { getUserList, send_message, get_messages } from '@/api/chat'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { uploadFile } from '@/api/upload'
|
||||
const route = useRoute()
|
||||
|
||||
// mock data for now — integrate with store/api as needed
|
||||
const userList = ref([
|
||||
{ user_id: 'u1', user_name: '张三', user_avatar: '' },
|
||||
{ user_id: 'u2', user_name: '李四', user_avatar: '' },
|
||||
{ user_id: 'u3', user_name: '王五', user_avatar: '' },
|
||||
])
|
||||
|
||||
const activeUser = ref({})
|
||||
const messages = ref([])
|
||||
const draft = ref('')
|
||||
const imageInput = ref(null)
|
||||
const chatBody = ref(null)
|
||||
|
||||
const sendeInfo = reactive({
|
||||
userInfo: { user_id: '', user_name: '', user_avatar: '' },
|
||||
robotInfo: { wx_id: '', head_url: '' }
|
||||
})
|
||||
|
||||
// 添加消息分页查询参数
|
||||
const msgQuery = reactive({
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
app_id: route.query.app_id,
|
||||
user_id: ''
|
||||
})
|
||||
|
||||
// 添加加载状态
|
||||
const loadingMore = ref(false)
|
||||
const noMoreMessages = ref(false)
|
||||
|
||||
const onSelectUser = (id) => {
|
||||
// id may be user_id or array when multiple; handle only single selection here
|
||||
const u = userList.value.find((it) => it.user_id == id)
|
||||
if (u) {
|
||||
// mark selected and clear others
|
||||
userList.value.forEach((it) => (it.active = it.user_id === id))
|
||||
activeUser.value = u
|
||||
sendeInfo.userInfo = { user_id: u.user_id, user_name: u.user_name, user_avatar: u.user_avatar }
|
||||
// set robot identity used by ChatRecord to determine right-side messages
|
||||
sendeInfo.robotInfo = { wx_id: 'robot', head_url: mihoutai }
|
||||
// load messages for user (placeholder)
|
||||
messages.value = []
|
||||
// 重置分页参数
|
||||
msgQuery.page = 1
|
||||
msgQuery.user_id = u.user_id
|
||||
noMoreMessages.value = false
|
||||
// scroll after messages rendered
|
||||
scrollToBottom()
|
||||
getMessages()
|
||||
}
|
||||
}
|
||||
|
||||
// 修改获取消息的方法
|
||||
const getMessages = () => {
|
||||
if (loadingMore.value || noMoreMessages.value) return
|
||||
|
||||
loadingMore.value = true
|
||||
get_messages({ ...msgQuery }).then(res => {
|
||||
if (res.list && res.list.length > 0) {
|
||||
// 解析消息内容
|
||||
const newMessages = res.list.map(item => {
|
||||
item.content = JSON.parse(item.content)
|
||||
return item
|
||||
})
|
||||
|
||||
if (msgQuery.page === 1) {
|
||||
// 首次加载
|
||||
messages.value = newMessages
|
||||
} else {
|
||||
// 加载更多,将新消息添加到列表开头
|
||||
messages.value = [...newMessages, ...messages.value]
|
||||
}
|
||||
|
||||
// 如果返回的消息少于请求的数量,说明没有更多消息了
|
||||
if (res.list.length < msgQuery.page_size) {
|
||||
noMoreMessages.value = true
|
||||
}
|
||||
} else {
|
||||
// 没有更多消息
|
||||
noMoreMessages.value = true
|
||||
}
|
||||
|
||||
loadingMore.value = false
|
||||
}).catch(() => {
|
||||
loadingMore.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 添加滚动事件处理函数
|
||||
const handleScroll = (e) => {
|
||||
const { scrollTop } = e.target
|
||||
|
||||
// 当滚动到顶部时加载更多消息
|
||||
if (scrollTop <= 0 && !loadingMore.value && !noMoreMessages.value) {
|
||||
loadMoreMessages()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多消息
|
||||
const loadMoreMessages = () => {
|
||||
if (msgQuery.user_id) {
|
||||
msgQuery.page++
|
||||
getMessages()
|
||||
}
|
||||
}
|
||||
|
||||
const send = () => {
|
||||
if (!activeUser.value.user_id) {
|
||||
ElMessage({ type: 'warning', message: '请先选择一个联系人' })
|
||||
return
|
||||
}
|
||||
if (!draft.value) return
|
||||
const content = draft.value
|
||||
const id = Date.now() + Math.random()
|
||||
const msg = {
|
||||
_id: id,
|
||||
_sending: true,
|
||||
_failed: false,
|
||||
sender_name: '机器人',
|
||||
msg_type: 0,
|
||||
content: { messages: content },
|
||||
send_time: '刚刚',
|
||||
"sender_id": "888888",
|
||||
"sender_name": "平台",
|
||||
"msg_type": 1
|
||||
}
|
||||
messages.value.push(msg)
|
||||
draft.value = ''
|
||||
// ensure scroll
|
||||
scrollToBottom()
|
||||
// send to server
|
||||
send_message({
|
||||
app_id: route.query.app_id,
|
||||
content: JSON.stringify({
|
||||
messages: content
|
||||
}),
|
||||
msg_type: 1,
|
||||
to_user_id: activeUser.value.user_id
|
||||
}).then(() => {
|
||||
msg._sending = false
|
||||
msg._failed = false
|
||||
}).catch(() => {
|
||||
msg._sending = false
|
||||
msg._failed = true
|
||||
ElMessage({ type: 'error', message: '消息发送失败' })
|
||||
})
|
||||
}
|
||||
|
||||
const handleInputKeydown = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
send()
|
||||
}
|
||||
}
|
||||
|
||||
const triggerImageInput = () => {
|
||||
imageInput.value && imageInput.value.click()
|
||||
}
|
||||
|
||||
const handleImageChange = (e) => {
|
||||
const file = e.target.files[0]
|
||||
if (!file) return
|
||||
// 直接用本地URL预览和发送
|
||||
const url = URL.createObjectURL(file)
|
||||
const id = Date.now() + Math.random()
|
||||
const msg = {
|
||||
_id: id,
|
||||
_sending: true,
|
||||
_failed: false,
|
||||
sender_id: '888888',
|
||||
sender_name: '平台',
|
||||
msg_type: 2,
|
||||
content: { messages: url },
|
||||
send_time: '刚刚'
|
||||
}
|
||||
messages.value.push(msg)
|
||||
// 清空input,允许重复选择同一图片
|
||||
e.target.value = ''
|
||||
// 图片可能增加高度,延迟滚动以保证图片渲染
|
||||
setTimeout(() => scrollToBottom(), 100)
|
||||
// 上传 file to server using FormData
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
form.append('app_id', route.query.app_id)
|
||||
form.append('msg_type', 2)
|
||||
form.append('to_user_id', activeUser.value.user_id)
|
||||
uploadFile(form, import.meta.env.VITE_APP_BASE_API + 'admin/upload/image').then((resp) => {
|
||||
send_message({
|
||||
app_id: route.query.app_id,
|
||||
content: JSON.stringify({ messages: import.meta.env.VITE_APP_BASE_API + resp.preview_image_url }),
|
||||
msg_type: 2,
|
||||
to_user_id: activeUser.value.user_id
|
||||
}).then(() => {
|
||||
msg._sending = false
|
||||
msg._failed = false
|
||||
}).catch(() => {
|
||||
msg._sending = false
|
||||
msg._failed = true
|
||||
ElMessage({ type: 'error', message: '图片发送失败' })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
const handleRetry = (id) => {
|
||||
const msg = messages.value.find((m) => m._id === id)
|
||||
if (!msg) return
|
||||
msg._failed = false
|
||||
msg._sending = true
|
||||
if (msg.msg_type === 0) {
|
||||
send_message({ app_id: route.query.app_id, content: msg.content.content, msg_type: 1, to_user_id: activeUser.value.user_id })
|
||||
.then(() => { msg._sending = false })
|
||||
.catch(() => { msg._sending = false; msg._failed = true; ElMessage({ type: 'error', message: '重发失败' }) })
|
||||
} else if (msg.msg_type === 2) {
|
||||
const form = new FormData()
|
||||
form.append('file', msg.content.file)
|
||||
form.append('app_id', route.query.app_id)
|
||||
form.append('msg_type', 2)
|
||||
form.append('to_user_id', activeUser.value.user_id)
|
||||
send_message(form).then(() => { msg._sending = false }).catch(() => { msg._sending = false; msg._failed = true; ElMessage({ type: 'error', message: '重发失败' }) })
|
||||
}
|
||||
}
|
||||
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick()
|
||||
const el = chatBody.value
|
||||
if (!el) return
|
||||
try {
|
||||
el.scrollTop = el.scrollHeight
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// 当 messages 变化时自动滚动(仅在首次加载或发送新消息时)
|
||||
// 移除原来的watcher,因为现在需要更精确地控制滚动行为
|
||||
|
||||
const onEmojiSelect = (emoji) => {
|
||||
if (draft.value) {
|
||||
draft.value += emoji
|
||||
} else {
|
||||
draft.value = emoji
|
||||
}
|
||||
}
|
||||
|
||||
const params = reactive({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
app_id: route.query.app_id
|
||||
})
|
||||
|
||||
const getUsers = (append = false) => {
|
||||
getUserList(params).then((res) => {
|
||||
if (append) {
|
||||
// append new items
|
||||
userList.value = userList.value.concat(res.list || [])
|
||||
} else {
|
||||
userList.value = res.list || []
|
||||
}
|
||||
// if first load and no active user, select first
|
||||
if (!append && userList.value.length && !activeUser.value.user_id) {
|
||||
onSelectUser(userList.value[0].user_id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 当 UserCard 触发 load(分页)时调用,加载下一页并滚动列表到底部以显示新数据
|
||||
const onLoadUserList = async () => {
|
||||
params.page = params.page + 1
|
||||
await getUsers(true)
|
||||
// 等待 DOM 更新后滚动
|
||||
await nextTick()
|
||||
const el = document.querySelector('.left-panel .infinite-list')
|
||||
if (el) {
|
||||
// 滚动到底部以展示新加载的用户
|
||||
el.scrollTop = el.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
const sendMessage = async (message) => {
|
||||
send_message(message).then((res) => {
|
||||
}).catch((err) => {
|
||||
ElMessage({ type: 'error', message: '消息发送失败' })
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getUsers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chat-page {
|
||||
display: flex;
|
||||
height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
background-color: transparent;
|
||||
padding-left: 8px !important;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
width: 280px;
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
background: var(--el-bg-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.left-header {
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
padding: 0 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
.chat-header .avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.chat-body {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
background: #f5f7fa
|
||||
}
|
||||
|
||||
.chat-footer {
|
||||
height: 96px;
|
||||
padding: 5px 5px;
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
.chat-footer .actions {
|
||||
display: flex;
|
||||
align-items: center
|
||||
}
|
||||
</style>
|
||||
@ -23,11 +23,21 @@
|
||||
@change="handleImageChange" />
|
||||
<V3Emoji @click-emoji="onEmojiSelect" :recent="true"></V3Emoji>
|
||||
</div>
|
||||
<el-input type="textarea" v-model="draft" placeholder="输入消息..." rows="2" style="flex:1"
|
||||
<el-input type="textarea" v-model="draft" placeholder="输入消息..." :rows="2" style="flex:1"
|
||||
@keydown="handleInputKeydown" />
|
||||
</div>
|
||||
<!-- 添加发送按钮 -->
|
||||
<!-- <el-button type="primary" @click="send">发送</el-button> -->
|
||||
<!-- 发送按钮 -->
|
||||
<div class="send-button-container" style="margin-left: 12px;">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="send"
|
||||
:disabled="!draft.trim() || isSending || isUploading"
|
||||
:loading="isSending"
|
||||
style="height: 60px;"
|
||||
>
|
||||
{{ isSending ? '发送中...' : '发送' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -80,6 +90,9 @@ const loadingMore = ref(false)
|
||||
const noMoreMessages = ref(false)
|
||||
const isAutoScroll = ref(true) // 是否自动滚动到底部
|
||||
const messageTimer = ref(null) // 定时器实例
|
||||
const isSending = ref(false) // 防止重复发送
|
||||
const lastSendTime = ref(0) // 最后发送时间,用于防抖
|
||||
const isUploading = ref(false) // 防止重复上传
|
||||
|
||||
// 选择用户
|
||||
const onSelectUser = async (id) => {
|
||||
@ -118,6 +131,83 @@ const onSelectUser = async (id) => {
|
||||
|
||||
|
||||
let msgTotal = 0
|
||||
// 消息排序函数
|
||||
const sortMessagesByTime = (messages) => {
|
||||
const sorted = messages.sort((a, b) => {
|
||||
// 尝试解析时间戳
|
||||
const getTimestamp = (msg) => {
|
||||
// 优先使用真实的时间戳字段
|
||||
if (msg.timestamp) {
|
||||
const ts = new Date(msg.timestamp).getTime()
|
||||
if (!isNaN(ts)) return ts
|
||||
}
|
||||
|
||||
if (msg.created_at) {
|
||||
const ts = new Date(msg.created_at).getTime()
|
||||
if (!isNaN(ts)) return ts
|
||||
}
|
||||
|
||||
// 处理send_time字段
|
||||
if (msg.send_time) {
|
||||
// 标准时间格式:2025-10-22 01:52:19
|
||||
if (typeof msg.send_time === 'string') {
|
||||
// 如果是相对时间(如"刚刚"、"3分钟前"),使用当前时间减去相应的时间
|
||||
if (msg.send_time.includes('刚刚')) {
|
||||
return Date.now() - 1000 // 1秒前
|
||||
} else if (msg.send_time.includes('分钟前')) {
|
||||
const minutes = parseInt(msg.send_time.match(/(\d+)分钟前/)?.[1] || '0')
|
||||
return Date.now() - (minutes * 60 * 1000)
|
||||
} else if (msg.send_time.includes('小时前')) {
|
||||
const hours = parseInt(msg.send_time.match(/(\d+)小时前/)?.[1] || '0')
|
||||
return Date.now() - (hours * 60 * 60 * 1000)
|
||||
} else if (msg.send_time.includes('天前')) {
|
||||
const days = parseInt(msg.send_time.match(/(\d+)天前/)?.[1] || '0')
|
||||
return Date.now() - (days * 24 * 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
// 尝试解析标准时间格式
|
||||
const timestamp = new Date(msg.send_time).getTime()
|
||||
if (!isNaN(timestamp)) return timestamp
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有ID,尝试从ID中提取时间信息
|
||||
if (msg.id || msg._id || msg.message_id) {
|
||||
const id = String(msg.id || msg._id || msg.message_id)
|
||||
// 如果ID是纯数字且可能是时间戳
|
||||
if (/^\d+$/.test(id)) {
|
||||
const numId = parseInt(id)
|
||||
// 检查是否是合理的时间戳范围(2020年到2030年)
|
||||
if (numId > 1577836800000 && numId < 1893456000000) { // 13位时间戳
|
||||
return numId
|
||||
} else if (numId > 1577836800 && numId < 1893456000) { // 10位时间戳
|
||||
return numId * 1000
|
||||
}
|
||||
}
|
||||
// 如果ID包含时间戳
|
||||
const timestampMatch = id.match(/(\d{13})/) // 13位时间戳
|
||||
if (timestampMatch) return parseInt(timestampMatch[1])
|
||||
}
|
||||
|
||||
// 默认返回当前时间
|
||||
return Date.now()
|
||||
}
|
||||
|
||||
return getTimestamp(a) - getTimestamp(b)
|
||||
})
|
||||
|
||||
// 调试信息:显示排序后的消息时间
|
||||
console.log('消息排序结果:', sorted.map(m => ({
|
||||
id: m.id || m._id || m.message_id,
|
||||
send_time: m.send_time,
|
||||
content: typeof m.content === 'object' ?
|
||||
(m.content.messages || JSON.stringify(m.content)) :
|
||||
String(m.content).slice(0, 20) + '...'
|
||||
})))
|
||||
|
||||
return sorted
|
||||
}
|
||||
|
||||
// 获取消息(区分加载历史和最新消息)
|
||||
// isHistory: 是否为向上翻页加载历史
|
||||
// pageOverride: 可选的临时 page 值(不会修改 msgQuery.page),用于定时拉取最新时使用 page=1
|
||||
@ -126,7 +216,12 @@ const getMessages = async (isHistory = true, pageOverride = null) => {
|
||||
if (isHistory && noMoreMessages.value) return
|
||||
if (!msgQuery.user_id) return
|
||||
|
||||
loadingMore.value = true
|
||||
// 只有在历史加载时才设置loadingMore状态,避免轮询时的界面抖动
|
||||
const isPolling = !isHistory && pageOverride === 1
|
||||
if (!isPolling) {
|
||||
loadingMore.value = true
|
||||
}
|
||||
|
||||
// 在第一次加载(page=1 且 isHistory=true)期间也将 isLoadingMessages 保持为 true
|
||||
if ((pageOverride ?? msgQuery.page) === 1 && isHistory) {
|
||||
isLoadingMessages.value = true
|
||||
@ -139,9 +234,17 @@ const getMessages = async (isHistory = true, pageOverride = null) => {
|
||||
msgTotal = res.total || 0
|
||||
const list = res.list || []
|
||||
|
||||
console.log(`获取消息 - isHistory: ${isHistory}, page: ${queryPage}, 返回消息数: ${list.length}`)
|
||||
|
||||
if (list.length > 0) {
|
||||
const newMessages = list.map(item => {
|
||||
try { item.content = JSON.parse(item.content) } catch (e) { /* keep original */ }
|
||||
// 处理消息内容,确保与ChatRecord组件兼容
|
||||
try {
|
||||
item.content = JSON.parse(item.content)
|
||||
} catch (e) {
|
||||
// 如果解析失败,说明是纯字符串,包装为对象格式
|
||||
item.content = { messages: item.content }
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
@ -151,11 +254,12 @@ const getMessages = async (isHistory = true, pageOverride = null) => {
|
||||
const prevScrollHeight = el ? el.scrollHeight : 0
|
||||
|
||||
if (queryPage === 1) {
|
||||
// 第一次加载(第一页)直接替换
|
||||
messages.value = newMessages
|
||||
// 第一次加载(第一页)直接替换,并排序
|
||||
messages.value = sortMessagesByTime([...newMessages])
|
||||
} else {
|
||||
// 向上翻页,prepend
|
||||
messages.value = [...newMessages, ...messages.value]
|
||||
// 向上翻页,prepend,然后重新排序整个列表
|
||||
const allMessages = [...newMessages, ...messages.value]
|
||||
messages.value = sortMessagesByTime(allMessages)
|
||||
// 等待 DOM 更新后恢复滚动位置,避免跳动
|
||||
await nextTick()
|
||||
if (el) {
|
||||
@ -164,10 +268,113 @@ const getMessages = async (isHistory = true, pageOverride = null) => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 拉取最新消息(追加到末尾),避免重复
|
||||
const existIds = new Set(messages.value.map(m => m.id || m._id))
|
||||
const unique = newMessages.filter(m => !(existIds.has(m.id || m._id)))
|
||||
if (unique.length) messages.value = [...messages.value, ...unique]
|
||||
// 拉取最新消息(追加到末尾),改进去重逻辑
|
||||
const currentMessages = [...messages.value]
|
||||
let hasUpdates = false
|
||||
|
||||
// 处理每条新消息
|
||||
for (const newMsg of newMessages) {
|
||||
let isProcessed = false
|
||||
|
||||
// 1. 首先尝试匹配临时消息(基于内容和时间)
|
||||
const tempMsgIndex = currentMessages.findIndex(existingMsg => {
|
||||
if (!existingMsg._isTemp || existingMsg._failed) return false
|
||||
|
||||
// 检查发送者和消息类型
|
||||
const senderMatch = existingMsg.sender_id === newMsg.sender_id
|
||||
const typeMatch = existingMsg.msg_type === newMsg.msg_type
|
||||
|
||||
// 检查时间差(10秒内)
|
||||
const timeDiff = Math.abs(Date.now() - (existingMsg._timestamp || 0))
|
||||
const timeMatch = timeDiff < 10000
|
||||
|
||||
// 根据消息类型进行不同的内容匹配
|
||||
let contentMatch = false
|
||||
if (existingMsg.msg_type === 1) {
|
||||
// 文本消息:比较文本内容
|
||||
const existingContent = existingMsg._tempContent ||
|
||||
(typeof existingMsg.content === 'object' ? existingMsg.content.messages : existingMsg.content)
|
||||
const newContent = typeof newMsg.content === 'object' ? newMsg.content.messages : newMsg.content
|
||||
contentMatch = existingContent === newContent
|
||||
} else if (existingMsg.msg_type === 2) {
|
||||
// 图片消息:比较文件信息(文件名和大小)
|
||||
if (existingMsg._tempImageFile) {
|
||||
const newContent = typeof newMsg.content === 'object' ? newMsg.content.messages : newMsg.content
|
||||
// 如果新消息包含文件名,尝试匹配
|
||||
if (typeof newContent === 'string' && newContent.includes(existingMsg._tempImageFile.name.split('.')[0])) {
|
||||
contentMatch = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return senderMatch && typeMatch && contentMatch && timeMatch
|
||||
})
|
||||
|
||||
if (tempMsgIndex !== -1) {
|
||||
// 找到匹配的临时消息,替换它
|
||||
console.log('找到匹配的临时消息,进行替换:', {
|
||||
tempId: currentMessages[tempMsgIndex]._id,
|
||||
realId: newMsg.id || newMsg._id || newMsg.message_id
|
||||
})
|
||||
currentMessages[tempMsgIndex] = {
|
||||
...newMsg,
|
||||
_isTemp: false // 标记为非临时消息
|
||||
}
|
||||
hasUpdates = true
|
||||
isProcessed = true
|
||||
} else {
|
||||
// 2. 检查是否已存在相同的真实消息(基于ID)
|
||||
const existingMsgIndex = currentMessages.findIndex(existingMsg => {
|
||||
if (existingMsg._isTemp) return false // 跳过临时消息
|
||||
|
||||
// 优先使用真实ID字段进行匹配
|
||||
const existingId = existingMsg.id || existingMsg._id || existingMsg.message_id
|
||||
const newId = newMsg.id || newMsg._id || newMsg.message_id
|
||||
|
||||
if (existingId && newId && existingId === newId) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 如果没有ID,使用多字段组合进行匹配
|
||||
const existingContentStr = typeof existingMsg.content === 'object' ?
|
||||
(existingMsg.content.messages || JSON.stringify(existingMsg.content)) :
|
||||
String(existingMsg.content)
|
||||
const newContentStr = typeof newMsg.content === 'object' ?
|
||||
(newMsg.content.messages || JSON.stringify(newMsg.content)) :
|
||||
String(newMsg.content)
|
||||
|
||||
const compositeMatch = existingMsg.sender_id === newMsg.sender_id &&
|
||||
existingMsg.send_time === newMsg.send_time &&
|
||||
existingMsg.msg_type === newMsg.msg_type &&
|
||||
existingContentStr === newContentStr
|
||||
|
||||
return compositeMatch
|
||||
})
|
||||
|
||||
if (existingMsgIndex === -1) {
|
||||
// 3. 真正的新消息,添加到列表
|
||||
console.log('发现新消息:', {
|
||||
id: newMsg.id || newMsg._id || newMsg.message_id,
|
||||
content: typeof newMsg.content === 'object' ? newMsg.content.messages : newMsg.content
|
||||
})
|
||||
currentMessages.push(newMsg)
|
||||
hasUpdates = true
|
||||
isProcessed = true
|
||||
} else {
|
||||
console.log('消息已存在,跳过:', {
|
||||
id: newMsg.id || newMsg._id || newMsg.message_id
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUpdates) {
|
||||
// 重新排序并更新消息列表
|
||||
messages.value = sortMessagesByTime(currentMessages)
|
||||
console.log('消息列表已更新,当前消息数:', messages.value.length)
|
||||
} else {
|
||||
console.log('没有新消息需要处理')
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 total 判断是否还有更多历史消息(仅在已知 total 时判断)
|
||||
@ -188,7 +395,12 @@ const getMessages = async (isHistory = true, pageOverride = null) => {
|
||||
} catch (err) {
|
||||
// 可扩展错误处理
|
||||
} finally {
|
||||
loadingMore.value = false
|
||||
// 只有在非轮询时才清理loadingMore状态
|
||||
const isPolling = !isHistory && pageOverride === 1
|
||||
if (!isPolling) {
|
||||
loadingMore.value = false
|
||||
}
|
||||
|
||||
// 如果这是第一页历史加载,确保 loading 标志被清理(页面外也会在调用者 finally 中清理)
|
||||
if ((pageOverride ?? msgQuery.page) === 1 && isHistory) {
|
||||
isLoadingMessages.value = false
|
||||
@ -244,32 +456,56 @@ const loadMoreMessages = () => {
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const send = () => {
|
||||
if (!activeUser.value.sender_id) {
|
||||
ElMessage({ type: 'warning', message: '请先选择一个联系人' })
|
||||
const send = async () => {
|
||||
const content = draft.value.trim()
|
||||
if (!content || isSending.value || isUploading.value) return
|
||||
|
||||
// 防抖检查
|
||||
const now = Date.now()
|
||||
if (now - lastSendTime.value < 500) {
|
||||
console.log('发送过于频繁,已忽略')
|
||||
return
|
||||
}
|
||||
if (!draft.value) return
|
||||
|
||||
const content = draft.value
|
||||
const id = Date.now() + Math.random()
|
||||
// 检查是否有相同内容的发送中消息
|
||||
const hasSendingMessage = messages.value.some(msg =>
|
||||
msg._sending &&
|
||||
msg.sender_id === '888888' &&
|
||||
msg.msg_type === 1 &&
|
||||
typeof msg.content === 'object' &&
|
||||
msg.content.messages === content &&
|
||||
(now - (msg._timestamp || 0)) < 5000
|
||||
)
|
||||
|
||||
if (hasSendingMessage) {
|
||||
console.log('存在相同内容的发送中消息,已忽略')
|
||||
return
|
||||
}
|
||||
|
||||
isSending.value = true
|
||||
lastSendTime.value = now
|
||||
|
||||
// 生成临时消息ID,使用特殊前缀标识
|
||||
const tempId = `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
const msg = {
|
||||
_id: id,
|
||||
_id: tempId,
|
||||
_sending: true,
|
||||
_failed: false,
|
||||
sender_name: '机器人',
|
||||
msg_type: 0,
|
||||
_isTemp: true, // 标记为临时消息
|
||||
sender_id: '888888',
|
||||
sender_name: '平台',
|
||||
msg_type: 1,
|
||||
content: { messages: content },
|
||||
send_time: '刚刚',
|
||||
"sender_id": "888888",
|
||||
"sender_name": "平台",
|
||||
"msg_type": 1
|
||||
_timestamp: now,
|
||||
_tempContent: content // 保存原始内容用于匹配
|
||||
}
|
||||
|
||||
messages.value.push(msg)
|
||||
draft.value = ''
|
||||
|
||||
// 发送后强制滚动到底部并设置自动滚动状态
|
||||
scrollToBottom()
|
||||
// 滚动到底部
|
||||
setTimeout(() => scrollToBottom(), 100)
|
||||
isAutoScroll.value = true
|
||||
|
||||
// 发送到服务器
|
||||
@ -280,13 +516,23 @@ const send = () => {
|
||||
}),
|
||||
msg_type: 1,
|
||||
to_user_id: activeUser.value.sender_id
|
||||
}).then(() => {
|
||||
}).then((response) => {
|
||||
msg._sending = false
|
||||
msg._failed = false
|
||||
}).catch(() => {
|
||||
console.log('消息发送成功', response)
|
||||
|
||||
// 发送成功后,立即轮询获取最新消息以获取真实ID
|
||||
setTimeout(() => {
|
||||
getMessages(false, 1) // 轮询最新消息
|
||||
}, 500)
|
||||
}).catch((error) => {
|
||||
msg._sending = false
|
||||
msg._failed = true
|
||||
console.error('消息发送失败:', error)
|
||||
ElMessage({ type: 'error', message: '消息发送失败' })
|
||||
}).finally(() => {
|
||||
// 重置发送状态
|
||||
isSending.value = false
|
||||
})
|
||||
}
|
||||
|
||||
@ -294,7 +540,10 @@ const send = () => {
|
||||
const handleInputKeydown = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
send()
|
||||
// 添加防抖延迟
|
||||
setTimeout(() => {
|
||||
send()
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
|
||||
@ -307,18 +556,36 @@ const handleImageChange = (e) => {
|
||||
const file = e.target.files[0]
|
||||
if (!file) return
|
||||
|
||||
// 防止重复上传
|
||||
if (isUploading.value) {
|
||||
console.log('图片正在上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件大小(限制为10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
ElMessage({ type: 'error', message: '图片大小不能超过10MB' })
|
||||
return
|
||||
}
|
||||
|
||||
isUploading.value = true
|
||||
|
||||
// 本地预览
|
||||
const url = URL.createObjectURL(file)
|
||||
const id = Date.now() + Math.random()
|
||||
const tempId = `temp_img_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
const now = Date.now()
|
||||
const msg = {
|
||||
_id: id,
|
||||
_id: tempId,
|
||||
_sending: true,
|
||||
_failed: false,
|
||||
_isTemp: true, // 标记为临时消息
|
||||
sender_id: '888888',
|
||||
sender_name: '平台',
|
||||
msg_type: 2,
|
||||
content: { messages: url },
|
||||
send_time: '刚刚'
|
||||
send_time: '刚刚',
|
||||
_timestamp: now,
|
||||
_tempImageFile: file // 保存文件信息用于匹配
|
||||
}
|
||||
messages.value.push(msg)
|
||||
e.target.value = ''
|
||||
@ -334,19 +601,48 @@ const handleImageChange = (e) => {
|
||||
form.append('msg_type', 2)
|
||||
form.append('to_user_id', activeUser.value.sender_id)
|
||||
uploadFile(form, import.meta.env.VITE_APP_BASE_API + 'admin/upload/image').then((resp) => {
|
||||
// 上传成功,调用send_message接口发送图片消息
|
||||
const imageUrl = import.meta.env.VITE_APP_BASE_API_img + resp.preview_image_url.replace('/api/', '')
|
||||
|
||||
send_message({
|
||||
app_id: route.query.app_id,
|
||||
content: JSON.stringify({ messages: import.meta.env.VITE_APP_BASE_API + resp.preview_image_url }),
|
||||
msg_type: 2,
|
||||
content: JSON.stringify({ messages: imageUrl }),
|
||||
to_user_id: activeUser.value.sender_id
|
||||
}).then(() => {
|
||||
msg._sending = false
|
||||
msg._failed = false
|
||||
}).catch(() => {
|
||||
// 发送成功,移除临时消息
|
||||
const index = messages.value.findIndex(m => m._id === tempId)
|
||||
if (index !== -1) {
|
||||
messages.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 清理本地预览URL
|
||||
if (url.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
// 轮询获取最新消息
|
||||
setTimeout(() => {
|
||||
getMessages(false, 1)
|
||||
}, 500)
|
||||
}).catch((sendError) => {
|
||||
// 发送失败,更新临时消息状态
|
||||
msg._sending = false
|
||||
msg._failed = true
|
||||
ElMessage({ type: 'error', message: '图片发送失败' })
|
||||
console.error('发送图片消息失败:', sendError)
|
||||
ElMessage({ type: 'error', message: '发送图片失败' })
|
||||
})
|
||||
}).catch((error) => {
|
||||
msg._sending = false
|
||||
msg._failed = true
|
||||
console.error('图片上传失败:', error)
|
||||
ElMessage({ type: 'error', message: '图片上传失败' })
|
||||
}).finally(() => {
|
||||
isUploading.value = false
|
||||
// 清理本地预览URL
|
||||
if (url.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -446,10 +742,85 @@ const startMessageTimer = () => {
|
||||
// 每秒获取一次最新消息,不修改 msgQuery.page,使用 pageOverride=1
|
||||
messageTimer.value = setInterval(() => {
|
||||
if (!msgQuery.user_id) return
|
||||
// 获取最新消息(第一页),用于追加新消息
|
||||
console.log('定时器轮询消息,用户ID:', msgQuery.user_id)
|
||||
getMessages(false, 1)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 用户列表轮询定时器
|
||||
const userListTimer = ref(null)
|
||||
|
||||
const startUserListTimer = () => {
|
||||
// 清除现有定时器
|
||||
if (userListTimer.value) {
|
||||
clearInterval(userListTimer.value)
|
||||
}
|
||||
|
||||
// 每秒获取一次用户列表
|
||||
userListTimer.value = setInterval(() => {
|
||||
getUsers(false) // 不追加,直接替换
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const stopUserListTimer = () => {
|
||||
if (userListTimer.value) {
|
||||
clearInterval(userListTimer.value)
|
||||
userListTimer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化最新消息内容
|
||||
const formatLastMessage = (user) => {
|
||||
if (!user.content) return ''
|
||||
|
||||
try {
|
||||
// 尝试解析JSON格式的content
|
||||
const contentObj = JSON.parse(user.content)
|
||||
|
||||
// 优先返回messages字段
|
||||
if (contentObj.messages) {
|
||||
const content = String(contentObj.messages).slice(0, 30)
|
||||
return content.length > 30 ? content + '...' : content
|
||||
}
|
||||
|
||||
// 其次返回text字段
|
||||
if (contentObj.text) {
|
||||
const content = String(contentObj.text).slice(0, 30)
|
||||
return content.length > 30 ? content + '...' : content
|
||||
}
|
||||
|
||||
// 最后返回content字段
|
||||
if (contentObj.content) {
|
||||
const content = String(contentObj.content).slice(0, 30)
|
||||
return content.length > 30 ? content + '...' : content
|
||||
}
|
||||
|
||||
// 如果都没有,返回JSON字符串的前30个字符
|
||||
const content = JSON.stringify(contentObj).slice(0, 30)
|
||||
return content.length > 30 ? content + '...' : content
|
||||
|
||||
} catch (error) {
|
||||
// JSON解析失败,说明是纯字符串
|
||||
let content = String(user.content)
|
||||
|
||||
// 根据消息类型添加前缀
|
||||
if (user.msg_type === 2) {
|
||||
content = '[图片] ' + content
|
||||
} else if (user.msg_type === 3) {
|
||||
content = '[文件] ' + content
|
||||
} else if (user.msg_type === 4) {
|
||||
content = '[语音] ' + content
|
||||
} else if (user.msg_type === 5) {
|
||||
content = '[视频] ' + content
|
||||
}
|
||||
|
||||
// 限制长度
|
||||
content = content.slice(0, 30)
|
||||
return content.length > 30 ? content + '...' : content
|
||||
}
|
||||
}
|
||||
|
||||
const stopMessageTimer = () => {
|
||||
if (messageTimer.value) {
|
||||
clearInterval(messageTimer.value)
|
||||
@ -467,14 +838,20 @@ const restartMessageTimer = () => {
|
||||
// 组件挂载时
|
||||
onMounted(() => {
|
||||
getUsers()
|
||||
// 启动用户列表轮询
|
||||
startUserListTimer()
|
||||
|
||||
// 监听窗口关闭,清理定时器
|
||||
window.addEventListener('beforeunload', stopMessageTimer)
|
||||
window.addEventListener('beforeunload', () => {
|
||||
stopMessageTimer()
|
||||
stopUserListTimer()
|
||||
})
|
||||
})
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
stopMessageTimer()
|
||||
stopUserListTimer()
|
||||
})
|
||||
|
||||
// 监听活跃用户变化,重启定时器
|
||||
|
||||
@ -88,7 +88,15 @@
|
||||
<el-input v-model="ruleForm.nickname" placeholder="请输入客服昵称" size="large" />
|
||||
</el-form-item>
|
||||
<el-form-item label="客服密码" prop="password">
|
||||
<el-input v-model="ruleForm.password" placeholder="请输入客服密码" size="large" />
|
||||
<el-input
|
||||
v-model="ruleForm.password"
|
||||
type="password"
|
||||
:placeholder="isEdit ? '留空则不修改密码' : '请输入客服密码'"
|
||||
size="large"
|
||||
/>
|
||||
<div v-if="isEdit" style="font-size: 12px; color: #999; margin-top: 4px;">
|
||||
留空则不修改密码
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="客服手机号">
|
||||
<el-input v-model="ruleForm.mobile" placeholder="请输入客服手机号" size="large" />
|
||||
@ -162,6 +170,7 @@ import { appList } from '@/api/miniProgram'
|
||||
import { ElNotification } from 'element-plus';
|
||||
import robotStore from '@/store/modules/robotStore'
|
||||
import { useRouter } from 'vue-router';
|
||||
import md5 from 'js-md5'
|
||||
const userRobotStore = robotStore()
|
||||
const router = useRouter()
|
||||
const robotCards = ref([])
|
||||
@ -172,35 +181,52 @@ const ruleForm = ref({})
|
||||
const isEdit = ref(false)
|
||||
const newRobotId = ref('')
|
||||
let robotCopy = {}
|
||||
const rules = ref({
|
||||
username: [
|
||||
{
|
||||
// 动态验证规则,根据是否为编辑模式调整密码验证
|
||||
const getRules = () => {
|
||||
const baseRules = {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入客服名称',
|
||||
trigger: 'blur',
|
||||
}
|
||||
],
|
||||
nickname: [{
|
||||
required: true,
|
||||
message: '请输入客服名称',
|
||||
message: '请输入客服昵称',
|
||||
trigger: 'blur',
|
||||
}
|
||||
],
|
||||
nickname: [{
|
||||
required: true,
|
||||
message: '请输入客服昵称',
|
||||
trigger: 'blur',
|
||||
}],
|
||||
mobile: [{
|
||||
required: true,
|
||||
message: '请输入客服手机号',
|
||||
trigger: 'blur',
|
||||
}],
|
||||
avatar: [{
|
||||
required: true,
|
||||
message: '请上传客服头像',
|
||||
trigger: 'change',
|
||||
}],
|
||||
password: [{
|
||||
required: true,
|
||||
message: '请输入客服密码',
|
||||
trigger: 'blur',
|
||||
}]
|
||||
})
|
||||
}],
|
||||
mobile: [{
|
||||
required: true,
|
||||
message: '请输入客服手机号',
|
||||
trigger: 'blur',
|
||||
}],
|
||||
avatar: [{
|
||||
required: true,
|
||||
message: '请上传客服头像',
|
||||
trigger: 'change',
|
||||
}],
|
||||
}
|
||||
|
||||
// 在创建模式下,密码是必填的;在编辑模式下,密码是可选的
|
||||
if (!isEdit.value) {
|
||||
baseRules.password = [{
|
||||
required: true,
|
||||
message: '请输入客服密码',
|
||||
trigger: 'blur',
|
||||
}]
|
||||
} else {
|
||||
baseRules.password = [{
|
||||
required: false,
|
||||
message: '请输入客服密码',
|
||||
trigger: 'blur',
|
||||
}]
|
||||
}
|
||||
|
||||
return baseRules
|
||||
}
|
||||
|
||||
const rules = ref(getRules())
|
||||
const query = reactive({
|
||||
username: '',
|
||||
nikename: '',
|
||||
@ -234,20 +260,21 @@ const handleAdd = () => {
|
||||
return
|
||||
}
|
||||
isEdit.value = false
|
||||
rules.value = getRules() // 更新验证规则
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
ruleForm.value = {}
|
||||
ruleFormRef.value.resetFields()
|
||||
|
||||
})
|
||||
}
|
||||
let editItem = {}
|
||||
let editIndex = 0
|
||||
const handleEdit = (item) => {
|
||||
isEdit.value = true
|
||||
// editItem = item
|
||||
// editIndex = index
|
||||
rules.value = getRules() // 更新验证规则
|
||||
ruleForm.value = JSON.parse(JSON.stringify(item))
|
||||
// 清空密码字段,避免显示原密码
|
||||
ruleForm.value.password = ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
const mhtLoading = ref(false)
|
||||
@ -257,16 +284,20 @@ const submitRobot = async () => {
|
||||
if (valid) {
|
||||
if (isEdit.value) {
|
||||
try {
|
||||
|
||||
const res = await editRobot(ruleForm.value.id, {
|
||||
// 编辑时的数据对象
|
||||
const editData = {
|
||||
username: ruleForm.value.username,
|
||||
nickname: ruleForm.value.nickname,
|
||||
avatar: ruleForm.value.avatar,
|
||||
mobile: ruleForm.value.mobile,
|
||||
})
|
||||
// editItem = ruleForm.value
|
||||
// robotCards.value[editIndex] = editItem
|
||||
// getTable()
|
||||
}
|
||||
|
||||
// 只有当密码字段有值且不为空时才传入密码字段
|
||||
if (ruleForm.value.password && ruleForm.value.password.trim() !== '') {
|
||||
editData.password = md5(ruleForm.value.password)
|
||||
}
|
||||
|
||||
const res = await editRobot(ruleForm.value.id, editData)
|
||||
dialogVisible.value = false
|
||||
mhtLoading.value = false
|
||||
getTable()
|
||||
@ -279,15 +310,18 @@ const submitRobot = async () => {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const res = await addRobot(ruleForm.value)
|
||||
// 创建时对密码进行MD5加密
|
||||
const createData = {
|
||||
...ruleForm.value,
|
||||
password: md5(ruleForm.value.password)
|
||||
}
|
||||
|
||||
const res = await addRobot(createData)
|
||||
robotCards.value.push({
|
||||
...ruleForm.value,
|
||||
|
||||
})
|
||||
// getTable()
|
||||
dialogVisible.value = false
|
||||
mhtLoading.value = false
|
||||
|
||||
getTable()
|
||||
ElNotification({
|
||||
type: 'success',
|
||||
@ -296,7 +330,6 @@ const submitRobot = async () => {
|
||||
} catch {
|
||||
mhtLoading.value = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
@ -23,6 +23,8 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="小程序名称" />
|
||||
<el-table-column prop="app_id" label="小程序ID" />
|
||||
<el-table-column prop="app_secret" label="AppSecret" />
|
||||
<el-table-column prop="template_id" label="模版ID" />
|
||||
<el-table-column prop="description" label="描述" />
|
||||
<el-table-column label="意图关键字" align="center">
|
||||
<template #default="scoped">
|
||||
@ -100,11 +102,13 @@
|
||||
<el-form-item label="小程序ID" prop="app_id">
|
||||
<el-input v-model="ruleForm.app_id" placeholder="请输入小程序ID" size="large" />
|
||||
</el-form-item>
|
||||
<el-form-item label="AppSecret" prop="app_secret">
|
||||
<el-input v-model="ruleForm.app_secret" placeholder="请输入AppSecret" size="large" type="password" show-password />
|
||||
<el-form-item label="AppSecret" prop="app_secret">
|
||||
<el-input v-model="ruleForm.app_secret" placeholder="请输入AppSecret" size="large" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="模板ID" prop="template_id">
|
||||
<el-input v-model="ruleForm.template_id" placeholder="请输入模板ID" size="large" />
|
||||
</el-form-item>
|
||||
|
||||
|
||||
<el-form-item label="头像" prop="avatar">
|
||||
<Upload v-model="ruleForm.avatar" type="image" accept="image/*" :action="robotHost"
|
||||
@change="changeFile" @success="uploadSuccess" @error="uploadError"></Upload>
|
||||
@ -294,6 +298,11 @@ const rules = ref({
|
||||
required: true,
|
||||
message: '请上传小程序头像',
|
||||
trigger: 'change',
|
||||
}],
|
||||
template_id: [{
|
||||
required: true,
|
||||
message: '请输入模板ID',
|
||||
trigger: 'change',
|
||||
}]
|
||||
})
|
||||
const query = reactive({
|
||||
@ -349,7 +358,8 @@ const submitRobot = async () => {
|
||||
name: ruleForm.value.name,
|
||||
app_id: ruleForm.value.app_id,
|
||||
app_secret: ruleForm.value.app_secret,
|
||||
avatar: ruleForm.value.avatar
|
||||
avatar: ruleForm.value.avatar,
|
||||
template_id: ruleForm.value.template_id
|
||||
})
|
||||
// editItem = ruleForm.value
|
||||
// robotCards.value[editIndex] = editItem
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user