mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2026-03-22 01:53:11 +00:00
1031 lines
26 KiB
Vue
1031 lines
26 KiB
Vue
<template>
|
|
<!-- Floating toggle button -->
|
|
<div
|
|
v-if="showToggleButton"
|
|
:style="toggleStyle"
|
|
class="chat-toggle"
|
|
>
|
|
<v-btn
|
|
color="primary"
|
|
icon
|
|
variant="flat"
|
|
@click="open()"
|
|
>
|
|
<v-badge
|
|
:content="unreadCount || undefined"
|
|
:model-value="unreadCount > 0"
|
|
color="error"
|
|
overlap
|
|
>
|
|
<v-icon>
|
|
mdi-chat
|
|
</v-icon>
|
|
</v-badge>
|
|
</v-btn>
|
|
</div>
|
|
|
|
<!-- Chat panel -->
|
|
<div
|
|
v-show="visible"
|
|
:style="panelStyle"
|
|
class="chat-panel"
|
|
>
|
|
<v-card
|
|
border
|
|
class="chat-card"
|
|
elevation="8"
|
|
>
|
|
<v-card-title class="d-flex align-center">
|
|
<v-icon class="mr-2">
|
|
mdi-chat-processing
|
|
</v-icon>
|
|
<span class="text-subtitle-1">{{ modeTitle }}</span>
|
|
<v-spacer />
|
|
<!-- 模式切换按钮 -->
|
|
<v-btn-toggle
|
|
v-model="currentMode"
|
|
class="mr-2"
|
|
mandatory
|
|
size="small"
|
|
variant="outlined"
|
|
>
|
|
<v-btn
|
|
value="chat"
|
|
size="small"
|
|
>
|
|
<v-icon>mdi-chat</v-icon>
|
|
</v-btn>
|
|
<v-btn
|
|
value="events"
|
|
size="small"
|
|
>
|
|
<v-icon>mdi-format-list-bulleted</v-icon>
|
|
</v-btn>
|
|
</v-btn-toggle>
|
|
<v-tooltip location="top">
|
|
<template #activator="{ props }">
|
|
<v-chip
|
|
:color="connected ? 'success' : 'grey'"
|
|
size="x-small"
|
|
v-bind="props"
|
|
variant="tonal"
|
|
>
|
|
{{ connected ? '已连接' : '未连接' }}
|
|
</v-chip>
|
|
</template>
|
|
<span>Socket {{ socketId || '-' }}</span>
|
|
</v-tooltip>
|
|
<v-btn
|
|
icon
|
|
variant="text"
|
|
@click="close()"
|
|
>
|
|
<v-icon>mdi-close</v-icon>
|
|
</v-btn>
|
|
</v-card-title>
|
|
|
|
<v-divider />
|
|
|
|
<v-card-text class="chat-body">
|
|
<!-- 聊天模式 -->
|
|
<div
|
|
v-if="currentMode === 'chat'"
|
|
ref="listRef"
|
|
class="messages"
|
|
>
|
|
<template
|
|
v-for="msg in decoratedMessages"
|
|
:key="msg._id"
|
|
>
|
|
<div
|
|
v-if="msg._type === 'divider'"
|
|
class="divider-row"
|
|
>
|
|
<v-divider class="my-2" />
|
|
<div class="divider-text">
|
|
今天 - 上次访问
|
|
</div>
|
|
<v-divider class="my-2" />
|
|
</div>
|
|
<div
|
|
v-else
|
|
:class="{ self: msg.self }"
|
|
class="message-row"
|
|
>
|
|
<div class="avatar">
|
|
<v-avatar
|
|
:color="msg.self ? 'primary' : 'grey'"
|
|
size="24"
|
|
>
|
|
<v-icon size="small">
|
|
{{ msg.self ? 'mdi-account' : 'mdi-account-outline' }}
|
|
</v-icon>
|
|
</v-avatar>
|
|
</div>
|
|
<div class="bubble">
|
|
<div
|
|
v-if="!msg.self && msg.deviceName"
|
|
class="sender-name"
|
|
>
|
|
{{ msg.deviceName }}
|
|
</div>
|
|
<div class="text">
|
|
{{ msg.text }}
|
|
</div>
|
|
<div class="meta">
|
|
<span
|
|
v-if="msg.self && msg.deviceName"
|
|
class="device-name"
|
|
>
|
|
{{ msg.deviceName }} •
|
|
</span>
|
|
{{ formatTime(msg.at) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- 事件模式 -->
|
|
<div
|
|
v-else
|
|
class="events-container"
|
|
>
|
|
<!-- 事件统计 -->
|
|
<div class="event-stats mb-3">
|
|
<v-row dense>
|
|
<v-col cols="4">
|
|
<v-card
|
|
color="success"
|
|
dark
|
|
size="small"
|
|
>
|
|
<v-card-text class="text-center pa-2">
|
|
<div class="text-h6">
|
|
{{ eventStats.chat }}
|
|
</div>
|
|
<div class="text-caption">
|
|
聊天
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
<v-col cols="4">
|
|
<v-card
|
|
color="info"
|
|
dark
|
|
size="small"
|
|
>
|
|
<v-card-text class="text-center pa-2">
|
|
<div class="text-h6">
|
|
{{ eventStats.kvChanged }}
|
|
</div>
|
|
<div class="text-caption">
|
|
KV变化
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
<v-col cols="4">
|
|
<v-card
|
|
color="warning"
|
|
dark
|
|
size="small"
|
|
>
|
|
<v-card-text class="text-center pa-2">
|
|
<div class="text-h6">
|
|
{{ eventStats.other }}
|
|
</div>
|
|
<div class="text-caption">
|
|
其他
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
</div>
|
|
|
|
<!-- 事件列表 -->
|
|
<div class="events-list">
|
|
<div
|
|
v-for="event in paginatedEvents"
|
|
:key="event._id"
|
|
class="event-item mb-2"
|
|
>
|
|
<v-card
|
|
:color="getEventColor(event.type)"
|
|
size="small"
|
|
variant="outlined"
|
|
>
|
|
<v-card-text class="pa-2">
|
|
<div class="d-flex align-center mb-1">
|
|
<v-chip
|
|
:color="getEventColor(event.type)"
|
|
size="x-small"
|
|
>
|
|
{{ getEventTypeLabel(event.type) }}
|
|
</v-chip>
|
|
<v-spacer />
|
|
<span class="text-caption">{{ formatTime(event.timestamp || event.at) }}</span>
|
|
</div>
|
|
|
|
<div
|
|
v-if="event.senderInfo"
|
|
class="mb-1 text-caption"
|
|
>
|
|
<strong>发送者:</strong> {{ formatDeviceInfo(event.senderInfo) }}
|
|
</div>
|
|
|
|
<div class="event-content">
|
|
<template v-if="event.type === 'chat' || event.type === 'chat:message'">
|
|
<div class="chat-content">
|
|
{{ event.content?.text || event.text }}
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<pre class="text-caption event-data">{{ JSON.stringify(event.content || event, null, 1) }}</pre>
|
|
</template>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</div>
|
|
|
|
<div
|
|
v-if="allEvents.length === 0"
|
|
class="text-center text-grey pa-4"
|
|
>
|
|
暂无事件
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 分页控件 -->
|
|
<div
|
|
v-if="totalPages > 1"
|
|
class="pagination mt-2"
|
|
>
|
|
<v-pagination
|
|
v-model="currentPage"
|
|
:length="totalPages"
|
|
:total-visible="3"
|
|
size="small"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</v-card-text>
|
|
|
|
<v-divider v-if="currentMode === 'chat'" />
|
|
|
|
<v-card-actions
|
|
v-if="currentMode === 'chat'"
|
|
class="chat-input"
|
|
>
|
|
<v-btn
|
|
class="mr-1"
|
|
icon
|
|
variant="text"
|
|
@click="insertEmoji('😄')"
|
|
>
|
|
<v-icon>mdi-emoticon-outline</v-icon>
|
|
</v-btn>
|
|
<v-textarea
|
|
ref="inputRef"
|
|
v-model="text"
|
|
auto-grow
|
|
class="flex-grow-1"
|
|
hide-details
|
|
placeholder="输入消息"
|
|
rows="1"
|
|
variant="solo"
|
|
@keydown.enter.prevent="handleEnter"
|
|
@keydown.shift.enter.stop
|
|
/>
|
|
<v-btn
|
|
:disabled="!canSend"
|
|
class="ml-2"
|
|
color="primary"
|
|
@click="send"
|
|
>
|
|
<v-icon start>
|
|
mdi-send
|
|
</v-icon>
|
|
发送
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</div>
|
|
|
|
<!-- 紧急通知组件 -->
|
|
<UrgentNotification ref="urgentNotification" />
|
|
</template>
|
|
|
|
<script>
|
|
import {getSetting} from '@/utils/settings'
|
|
import {getSocket, joinToken, on as socketOn} from '@/utils/socketClient'
|
|
import {sendChatMessage, createDeviceEventHandler, formatDeviceInfo} from '@/utils/deviceEvents'
|
|
import UrgentNotification from '@/components/UrgentNotification.vue'
|
|
|
|
export default {
|
|
name: 'ChatWidget',
|
|
components: {
|
|
UrgentNotification
|
|
},
|
|
props: {
|
|
modelValue: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
showButton: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
offset: {
|
|
type: Number,
|
|
default: 16,
|
|
},
|
|
width: {
|
|
type: Number,
|
|
default: 380,
|
|
},
|
|
height: {
|
|
type: Number,
|
|
default: 520,
|
|
},
|
|
},
|
|
emits: ['update:modelValue'],
|
|
data() {
|
|
return {
|
|
visible: this.modelValue,
|
|
text: '',
|
|
messages: [], // 聊天消息
|
|
allEvents: [], // 所有事件
|
|
lastVisit: null,
|
|
unreadCount: 0,
|
|
connected: false,
|
|
socketId: '',
|
|
// 分页和显示模式
|
|
currentMode: 'chat', // 'chat' 或 'events'
|
|
currentPage: 1,
|
|
itemsPerPage: 20,
|
|
loading: false,
|
|
// 组件状态
|
|
isDestroying: false,
|
|
// 事件统计
|
|
eventStats: {
|
|
chat: 0,
|
|
kvChanged: 0,
|
|
other: 0
|
|
},
|
|
// 事件监听器清理函数
|
|
cleanupFunctions: []
|
|
}
|
|
},
|
|
computed: {
|
|
panelStyle() {
|
|
return {
|
|
right: this.offset + 'px',
|
|
bottom: this.offset + 'px',
|
|
width: this.width + 'px',
|
|
height: this.height + 'px',
|
|
}
|
|
},
|
|
toggleStyle() {
|
|
return {
|
|
right: this.offset + 'px',
|
|
bottom: this.offset + 'px',
|
|
}
|
|
},
|
|
canSend() {
|
|
const token = getSetting('server.kvToken')
|
|
return !!(token && this.text.trim())
|
|
},
|
|
showToggleButton() {
|
|
return this.$props.showButton && !this.visible
|
|
},
|
|
decoratedMessages() {
|
|
// Insert divider between lastVisit and now
|
|
if (!this.lastVisit) return this.messages
|
|
const idx = this.messages.findIndex(m => m.at && new Date(m.at).getTime() >= new Date(this.lastVisit).getTime())
|
|
if (idx <= 0) return this.messages
|
|
const before = this.messages.slice(0, idx)
|
|
const after = this.messages.slice(idx)
|
|
return [
|
|
...before,
|
|
{_id: 'divider', _type: 'divider'},
|
|
...after,
|
|
]
|
|
},
|
|
// 当前显示的内容(根据模式)
|
|
currentDisplayItems() {
|
|
if (this.currentMode === 'chat') {
|
|
return this.decoratedMessages
|
|
} else {
|
|
return this.paginatedEvents
|
|
}
|
|
},
|
|
// 分页后的事件
|
|
paginatedEvents() {
|
|
if (this.isDestroying || !this.allEvents) return []
|
|
const start = (this.currentPage - 1) * this.itemsPerPage
|
|
const end = start + this.itemsPerPage
|
|
return this.allEvents.slice(start, end)
|
|
},
|
|
// 总页数
|
|
totalPages() {
|
|
if (this.isDestroying || !this.allEvents) return 1
|
|
return Math.ceil(this.allEvents.length / this.itemsPerPage)
|
|
},
|
|
// 模式标签
|
|
modeTitle() {
|
|
return this.currentMode === 'chat' ? '设备聊天室' : '所有事件'
|
|
},
|
|
},
|
|
watch: {
|
|
modelValue(val) {
|
|
this.visible = val
|
|
if (val) {
|
|
this.onOpen()
|
|
}
|
|
},
|
|
},
|
|
mounted() {
|
|
try {
|
|
const stored = localStorage.getItem('chat.lastVisit')
|
|
if (stored) this.lastVisit = stored
|
|
} catch (e) {
|
|
void e
|
|
}
|
|
|
|
// Prepare socket
|
|
const s = getSocket()
|
|
this.connected = !!s.connected
|
|
this.socketId = s.id || ''
|
|
|
|
s.on('connect', () => {
|
|
this.connected = true
|
|
this.socketId = s.id || ''
|
|
})
|
|
s.on('disconnect', () => {
|
|
this.connected = false
|
|
})
|
|
|
|
// Auto join by token if exists
|
|
const token = getSetting('server.kvToken')
|
|
if (token) joinToken(token)
|
|
|
|
// 创建安全的事件处理器
|
|
const createSafeHandler = (handler) => {
|
|
return (...args) => {
|
|
if (this.isDestroying) return
|
|
try {
|
|
handler(...args)
|
|
} catch (error) {
|
|
console.error('ChatWidget 事件处理错误:', error)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Listen chat messages (旧接口兼容)
|
|
const offMessage = socketOn('chat:message', createSafeHandler((msg) => {
|
|
this.pushMessage(msg)
|
|
this.addEvent({
|
|
_id: `legacy-chat-${Date.now()}-${Math.random()}`,
|
|
type: 'chat:message',
|
|
content: msg,
|
|
timestamp: msg.at || new Date().toISOString(),
|
|
senderId: msg.senderId,
|
|
uuid: msg.uuid,
|
|
senderInfo: msg.senderInfo
|
|
})
|
|
}))
|
|
|
|
// Listen direct chat events (新的直接聊天事件)
|
|
const offDirectChat = socketOn('chat', createSafeHandler((eventData) => {
|
|
if (eventData && eventData.content && eventData.content.text) {
|
|
// 处理新格式的直接聊天事件
|
|
const chatMsg = {
|
|
text: eventData.content.text,
|
|
senderId: eventData.senderId,
|
|
at: eventData.timestamp,
|
|
uuid: eventData.senderId, // 使用 senderId 作为 uuid
|
|
senderInfo: eventData.senderInfo
|
|
}
|
|
|
|
this.pushMessage(chatMsg)
|
|
this.addEvent({
|
|
_id: eventData.eventId || `chat-${Date.now()}-${Math.random()}`,
|
|
type: 'chat',
|
|
content: eventData.content,
|
|
timestamp: eventData.timestamp,
|
|
eventId: eventData.eventId,
|
|
senderId: eventData.senderId,
|
|
senderInfo: eventData.senderInfo
|
|
})
|
|
}
|
|
}))
|
|
|
|
// Listen device events (通用事件接口 - 保留兼容)
|
|
this.deviceEventHandler = createDeviceEventHandler({
|
|
onChat: createSafeHandler((chatMsg, originalEvent) => {
|
|
this.pushMessage(chatMsg)
|
|
this.addEvent(originalEvent)
|
|
}),
|
|
onKvChanged: createSafeHandler((kvMsg, originalEvent) => {
|
|
this.addEvent(originalEvent)
|
|
}),
|
|
onUrgentNotice: createSafeHandler((urgentData, originalEvent) => {
|
|
// 添加到事件列表
|
|
this.addEvent(originalEvent)
|
|
// 立即显示紧急通知弹窗
|
|
this.showUrgentNotification(originalEvent)
|
|
}),
|
|
onNotification: createSafeHandler((notificationData, originalEvent) => {
|
|
console.log('收到通知事件:', notificationData, originalEvent)
|
|
// 添加到事件列表
|
|
this.addEvent(originalEvent)
|
|
// 立即显示通知弹窗
|
|
this.showUrgentNotification(originalEvent)
|
|
}),
|
|
onOtherEvent: createSafeHandler((eventData) => {
|
|
// 检查是否是通知相关事件
|
|
if (eventData.type === 'urgent-notice' || eventData.type === 'notification') {
|
|
this.showUrgentNotification(eventData)
|
|
}
|
|
this.addEvent(eventData)
|
|
}),
|
|
enableLegacySupport: true
|
|
})
|
|
const offDeviceEvent = socketOn('device-event', this.deviceEventHandler)
|
|
|
|
// 监听 KV 变化事件(支持新旧格式)
|
|
const offKvChanged = socketOn('kv-key-changed', createSafeHandler((eventData) => {
|
|
// 新格式:直接事件数据
|
|
if (eventData.content && eventData.timestamp) {
|
|
this.addEvent({
|
|
_id: `kv-${Date.now()}-${Math.random()}`,
|
|
type: 'kv-key-changed',
|
|
content: eventData.content,
|
|
timestamp: eventData.timestamp,
|
|
eventId: eventData.eventId,
|
|
senderId: eventData.senderId,
|
|
senderInfo: eventData.senderInfo
|
|
})
|
|
} else {
|
|
// 旧格式:兼容处理
|
|
this.addEvent({
|
|
_id: `legacy-kv-${Date.now()}-${Math.random()}`,
|
|
type: 'kv-key-changed',
|
|
content: eventData,
|
|
timestamp: eventData.updatedAt || new Date().toISOString(),
|
|
uuid: eventData.uuid
|
|
})
|
|
}
|
|
}))
|
|
|
|
// 监听紧急通知事件
|
|
const offUrgentNotice = socketOn('urgent-notice', createSafeHandler((notificationData) => {
|
|
console.log('收到紧急通知:', notificationData)
|
|
|
|
// 添加到事件列表
|
|
this.addEvent({
|
|
_id: `urgent-${Date.now()}-${Math.random()}`,
|
|
type: 'urgent-notice',
|
|
content: notificationData.content || notificationData,
|
|
timestamp: notificationData.timestamp || new Date().toISOString(),
|
|
eventId: notificationData.eventId,
|
|
senderId: notificationData.senderId,
|
|
senderInfo: notificationData.senderInfo
|
|
})
|
|
|
|
// 立即显示紧急通知弹窗
|
|
this.showUrgentNotification(notificationData)
|
|
}))
|
|
|
|
// 监听通知事件
|
|
const offNotification = socketOn('notification', createSafeHandler((notificationData) => {
|
|
console.log('收到通知事件:', notificationData)
|
|
|
|
// 添加到事件列表
|
|
this.addEvent({
|
|
_id: `notification-${Date.now()}-${Math.random()}`,
|
|
type: 'notification',
|
|
content: notificationData.content || notificationData,
|
|
timestamp: notificationData.timestamp || new Date().toISOString(),
|
|
eventId: notificationData.eventId,
|
|
senderId: notificationData.senderId,
|
|
senderInfo: notificationData.senderInfo || notificationData.content?.senderInfo
|
|
})
|
|
|
|
// 立即显示通知弹窗
|
|
this.showUrgentNotification(notificationData)
|
|
})) // 保存清理函数
|
|
this.cleanupFunctions = [
|
|
offMessage,
|
|
offDirectChat,
|
|
offUrgentNotice,
|
|
offNotification,
|
|
offDeviceEvent,
|
|
offKvChanged
|
|
] // If initially visible, run open logic
|
|
if (this.visible) this.onOpen()
|
|
},
|
|
beforeUnmount() {
|
|
// 设置销毁状态
|
|
this.isDestroying = true
|
|
|
|
// 清理所有事件监听器
|
|
if (this.cleanupFunctions && Array.isArray(this.cleanupFunctions)) {
|
|
this.cleanupFunctions.forEach(cleanup => {
|
|
try {
|
|
if (typeof cleanup === 'function') {
|
|
cleanup()
|
|
}
|
|
} catch (error) {
|
|
console.warn('ChatWidget 清理函数执行失败:', error)
|
|
}
|
|
})
|
|
}
|
|
|
|
// 兼容旧的清理方式
|
|
try {
|
|
if (this.offMessage) this.offMessage()
|
|
if (this.offDeviceEvent) this.offDeviceEvent()
|
|
if (this.offKvChanged) this.offKvChanged()
|
|
} catch (error) {
|
|
console.warn('ChatWidget 旧清理函数执行失败:', error)
|
|
}
|
|
|
|
// 清空数据
|
|
this.cleanupFunctions = []
|
|
this.messages = []
|
|
this.allEvents = []
|
|
},
|
|
methods: {
|
|
open() {
|
|
this.visible = true
|
|
this.$emit('update:modelValue', true)
|
|
this.onOpen()
|
|
},
|
|
close() {
|
|
this.visible = false
|
|
this.$emit('update:modelValue', false)
|
|
try {
|
|
localStorage.setItem('chat.lastVisit', new Date().toISOString())
|
|
} catch (e) {
|
|
void e
|
|
}
|
|
this.unreadCount = 0
|
|
},
|
|
onOpen() {
|
|
// Scroll to bottom on open
|
|
this.$nextTick(() => this.scrollToBottom())
|
|
},
|
|
insertEmoji(ch) {
|
|
this.text += ch
|
|
this.$nextTick(() => {
|
|
if (this.$refs.inputRef?.$el?.querySelector) {
|
|
const ta = this.$refs.inputRef.$el.querySelector('textarea')
|
|
ta?.focus()
|
|
}
|
|
})
|
|
},
|
|
handleEnter(e) {
|
|
if (e.shiftKey) return
|
|
this.send()
|
|
},
|
|
send() {
|
|
const val = this.text.trim()
|
|
if (!val) return
|
|
|
|
// 立即添加自己的消息到本地显示
|
|
const selfMsg = {
|
|
_id: `self-${Date.now()}-${Math.random()}`,
|
|
text: val,
|
|
at: new Date().toISOString(),
|
|
senderId: this.socketId,
|
|
self: true,
|
|
senderInfo: {
|
|
deviceName: '我',
|
|
deviceType: 'client',
|
|
isReadOnly: false
|
|
}
|
|
}
|
|
this.pushMessage(selfMsg)
|
|
|
|
// 添加到事件列表
|
|
this.addEvent({
|
|
_id: `self-event-${Date.now()}-${Math.random()}`,
|
|
type: 'chat',
|
|
content: { text: val },
|
|
timestamp: new Date().toISOString(),
|
|
senderId: this.socketId,
|
|
senderInfo: {
|
|
deviceName: '本设备',
|
|
deviceType: 'client',
|
|
isReadOnly: false
|
|
}
|
|
})
|
|
|
|
// 发送到服务器
|
|
sendChatMessage(val)
|
|
this.text = ''
|
|
},
|
|
pushMessage(msg) {
|
|
if (this.isDestroying || !msg) return
|
|
|
|
try {
|
|
const entry = {
|
|
_id: `${msg.at || Date.now()}-${Math.random()}`,
|
|
text: typeof msg?.text === 'string' ? msg.text : (msg?.text || ''),
|
|
at: msg.at || new Date().toISOString(),
|
|
senderId: msg.senderId,
|
|
self: !!(msg.senderId && msg.senderId === this.socketId),
|
|
senderInfo: msg.senderInfo || null, // 保存发送者信息
|
|
deviceName: this.getDeviceName(msg.senderInfo, msg.senderId === this.socketId)
|
|
}
|
|
// ignore empty
|
|
if (!entry.text) return
|
|
|
|
this.messages.push(entry)
|
|
// unread when hidden
|
|
if (!this.visible) this.unreadCount++
|
|
|
|
// 安全的 nextTick 调用
|
|
this.$nextTick(() => {
|
|
if (!this.isDestroying) {
|
|
this.scrollToBottom()
|
|
}
|
|
})
|
|
|
|
// trim
|
|
if (this.messages.length > 500) this.messages.shift()
|
|
} catch (error) {
|
|
console.error('ChatWidget pushMessage 错误:', error)
|
|
}
|
|
},
|
|
formatTime(iso) {
|
|
try {
|
|
const d = new Date(iso)
|
|
const hh = String(d.getHours()).padStart(2, '0')
|
|
const mm = String(d.getMinutes()).padStart(2, '0')
|
|
return `${hh}:${mm}`
|
|
} catch (e) {
|
|
void e
|
|
return ''
|
|
}
|
|
},
|
|
scrollToBottom() {
|
|
if (this.isDestroying) return
|
|
|
|
try {
|
|
const el = this.$refs.listRef
|
|
if (!el) return
|
|
|
|
// 使用 requestAnimationFrame 确保 DOM 更新完成
|
|
requestAnimationFrame(() => {
|
|
if (!this.isDestroying && el) {
|
|
el.scrollTop = el.scrollHeight
|
|
}
|
|
})
|
|
} catch (error) {
|
|
console.warn('ChatWidget scrollToBottom 错误:', error)
|
|
}
|
|
},
|
|
// 添加事件到列表
|
|
addEvent(eventData) {
|
|
if (this.isDestroying || !eventData) return
|
|
|
|
try {
|
|
this.allEvents.unshift(eventData)
|
|
|
|
// 更新统计
|
|
if (eventData.type === 'chat' || eventData.type === 'chat:message') {
|
|
this.eventStats.chat++
|
|
} else if (eventData.type === 'kv-key-changed') {
|
|
this.eventStats.kvChanged++
|
|
} else {
|
|
this.eventStats.other++
|
|
}
|
|
|
|
// 限制事件数量
|
|
if (this.allEvents.length > 200) {
|
|
this.allEvents = this.allEvents.slice(0, 200)
|
|
}
|
|
} catch (error) {
|
|
console.error('ChatWidget addEvent 错误:', error)
|
|
}
|
|
},
|
|
// 获取事件颜色
|
|
getEventColor(eventType) {
|
|
switch (eventType) {
|
|
case 'chat':
|
|
case 'chat:message':
|
|
return 'success'
|
|
case 'kv-key-changed':
|
|
return 'info'
|
|
default:
|
|
return 'warning'
|
|
}
|
|
},
|
|
// 获取事件类型标签
|
|
getEventTypeLabel(eventType) {
|
|
switch (eventType) {
|
|
case 'chat':
|
|
case 'chat:message':
|
|
return '聊天'
|
|
case 'kv-key-changed':
|
|
return 'KV变化'
|
|
default:
|
|
return eventType
|
|
}
|
|
},
|
|
// 格式化设备信息 - 暴露导入的函数给模板使用
|
|
formatDeviceInfo(senderInfo) {
|
|
return formatDeviceInfo(senderInfo)
|
|
},
|
|
// 获取设备名称用于显示
|
|
getDeviceName(senderInfo, isSelf = false) {
|
|
if (isSelf) {
|
|
return '我'
|
|
}
|
|
|
|
if (!senderInfo) {
|
|
return '未知设备'
|
|
}
|
|
|
|
// 实时同步事件
|
|
if (senderInfo.deviceName === 'realtime') {
|
|
return '系统'
|
|
}
|
|
|
|
// 使用设备名称或设备类型
|
|
return senderInfo.deviceName ||
|
|
senderInfo.deviceType ||
|
|
'未知设备'
|
|
},
|
|
// 显示紧急通知
|
|
showUrgentNotification(notificationData) {
|
|
try {
|
|
if (this.$refs.urgentNotification) {
|
|
this.$refs.urgentNotification.show(notificationData)
|
|
} else {
|
|
console.warn('紧急通知组件未找到')
|
|
}
|
|
} catch (error) {
|
|
console.error('显示紧急通知失败:', error)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.chat-toggle {
|
|
position: fixed;
|
|
z-index: 1100;
|
|
}
|
|
|
|
.chat-panel {
|
|
position: fixed;
|
|
z-index: 1101;
|
|
}
|
|
|
|
.chat-card {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.chat-body {
|
|
padding: 8px 12px;
|
|
height: calc(100% - 120px);
|
|
}
|
|
|
|
.messages {
|
|
height: 100%;
|
|
overflow: auto;
|
|
}
|
|
|
|
.message-row {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
margin: 8px 0;
|
|
}
|
|
|
|
.message-row.self {
|
|
flex-direction: row-reverse;
|
|
}
|
|
|
|
.message-row .avatar {
|
|
width: 28px;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.message-row .bubble {
|
|
max-width: 70%;
|
|
background: rgba(255, 255, 255, 0.06);
|
|
border-radius: 10px;
|
|
padding: 6px 10px;
|
|
margin: 0 8px;
|
|
}
|
|
|
|
.message-row.self .bubble {
|
|
background: rgba(33, 150, 243, 0.15);
|
|
}
|
|
|
|
.bubble .text {
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.bubble .meta {
|
|
font-size: 12px;
|
|
opacity: 0.6;
|
|
margin-top: 2px;
|
|
text-align: right;
|
|
}
|
|
|
|
.bubble .sender-name {
|
|
font-size: 11px;
|
|
color: rgba(255, 255, 255, 0.8);
|
|
margin-bottom: 2px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.message-row.self .bubble .sender-name {
|
|
color: rgba(33, 150, 243, 0.8);
|
|
}
|
|
|
|
.device-name {
|
|
font-weight: 500;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.divider-row {
|
|
text-align: center;
|
|
color: rgba(255, 255, 255, 0.6);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.divider-text {
|
|
margin: 4px 0;
|
|
}
|
|
|
|
.chat-input {
|
|
padding: 8px;
|
|
}
|
|
|
|
/* 事件相关样式 */
|
|
.events-container {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.events-list {
|
|
flex-grow: 1;
|
|
overflow-y: auto;
|
|
max-height: calc(100% - 120px);
|
|
}
|
|
|
|
.event-item {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.event-item:hover {
|
|
transform: translateX(2px);
|
|
}
|
|
|
|
.event-content {
|
|
max-width: 100%;
|
|
}
|
|
|
|
.chat-content {
|
|
background: rgba(0,0,0,0.05);
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.event-data {
|
|
background: rgba(0,0,0,0.05);
|
|
padding: 4px;
|
|
border-radius: 4px;
|
|
font-size: 10px;
|
|
max-height: 100px;
|
|
overflow-y: auto;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.pagination {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.event-stats {
|
|
flex-shrink: 0;
|
|
}
|
|
</style>
|