|
|
|
|
@ -3,7 +3,10 @@
|
|
|
|
|
<div class="chat-header">
|
|
|
|
|
<div class="chat-title-only">
|
|
|
|
|
<div class="chat-title">{{ activeContact?.name || '选择联系人' }}</div>
|
|
|
|
|
<div class="chat-subtitle" v-if="activeContact">{{ activeContact.status || '在线' }}</div>
|
|
|
|
|
<div class="chat-subtitle" v-if="activeContact">
|
|
|
|
|
<span class="status-dot" :class="statusClass"></span>
|
|
|
|
|
<span>{{ statusText }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="header-actions">
|
|
|
|
|
<el-button text :icon="Message" @click="$emit('open-info')">资料</el-button>
|
|
|
|
|
@ -36,7 +39,7 @@
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { onMounted, onUpdated, ref, watch, nextTick, computed } from 'vue'
|
|
|
|
|
import { onMounted, onUpdated, ref, watch, nextTick, computed, onBeforeUnmount } from 'vue'
|
|
|
|
|
import { Message, Phone, VideoCamera } from '@element-plus/icons-vue'
|
|
|
|
|
import axios from 'axios'
|
|
|
|
|
|
|
|
|
|
@ -82,6 +85,85 @@ onUpdated(scrollToBottom)
|
|
|
|
|
watch(messages, scrollToBottom, { deep: true })
|
|
|
|
|
|
|
|
|
|
import { currentUser } from '../stores/userStore.js'
|
|
|
|
|
// 状态栏相关:基于 activeContact.schedule 计算当前状态并定时刷新
|
|
|
|
|
const statusText = ref('在线')
|
|
|
|
|
const statusClass = ref('online')
|
|
|
|
|
const currentActivity = ref('')
|
|
|
|
|
let statusTimer = null
|
|
|
|
|
|
|
|
|
|
function computeStatusFromSchedule(oc) {
|
|
|
|
|
try {
|
|
|
|
|
if (!oc || !Array.isArray(oc.schedule) || oc.schedule.length === 0) {
|
|
|
|
|
statusText.value = '在线'
|
|
|
|
|
statusClass.value = 'online'
|
|
|
|
|
currentActivity.value = ''
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const now = new Date()
|
|
|
|
|
const nowT = now.getTime()
|
|
|
|
|
const todayStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0')
|
|
|
|
|
const todays = oc.schedule.filter(s => String(s.date || '').startsWith(todayStr))
|
|
|
|
|
if (!todays.length) {
|
|
|
|
|
statusText.value = '空闲'
|
|
|
|
|
statusClass.value = 'idle'
|
|
|
|
|
currentActivity.value = ''
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for (const s of todays) {
|
|
|
|
|
const timeStr = (s.time || '00:00').slice(0,5)
|
|
|
|
|
const start = new Date(`${s.date || todayStr}T${timeStr}`)
|
|
|
|
|
const end = new Date(start.getTime() + (s.durationMinutes ? s.durationMinutes*60000 : 60*60000))
|
|
|
|
|
if (nowT >= start.getTime() - 30*60000 && nowT <= end.getTime() + 30*60000) {
|
|
|
|
|
const eventText = (s.event || '').toLowerCase()
|
|
|
|
|
if (/游泳|跑步|健身|运动|爬山|滑雪|球/ig.test(eventText)) {
|
|
|
|
|
statusText.value = `正在${s.event || '运动'}`
|
|
|
|
|
statusClass.value = 'busy'
|
|
|
|
|
currentActivity.value = s.event || '运动'
|
|
|
|
|
} else if (/会议|上班|工作|上课|学习/ig.test(eventText)) {
|
|
|
|
|
statusText.value = `忙碌:${s.event || ''}`
|
|
|
|
|
statusClass.value = 'busy'
|
|
|
|
|
currentActivity.value = s.event || '忙碌'
|
|
|
|
|
} else if (/休息|放松|休闲|看电影|娱乐/ig.test(eventText)) {
|
|
|
|
|
statusText.value = `休闲:${s.event || ''}`
|
|
|
|
|
statusClass.value = 'relax'
|
|
|
|
|
currentActivity.value = s.event || '休闲'
|
|
|
|
|
} else {
|
|
|
|
|
statusText.value = `忙碌:${s.event || ''}`
|
|
|
|
|
statusClass.value = 'busy'
|
|
|
|
|
currentActivity.value = s.event || '忙碌'
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const upcoming = todays.find(s => {
|
|
|
|
|
const start = new Date(`${s.date || todayStr}T${(s.time||'00:00').slice(0,5)}`)
|
|
|
|
|
return start.getTime() > nowT
|
|
|
|
|
})
|
|
|
|
|
if (upcoming) {
|
|
|
|
|
statusText.value = `稍后:${upcoming.time || ''} ${upcoming.event || ''}`
|
|
|
|
|
statusClass.value = 'soon'
|
|
|
|
|
currentActivity.value = ''
|
|
|
|
|
} else {
|
|
|
|
|
statusText.value = '空闲'
|
|
|
|
|
statusClass.value = 'idle'
|
|
|
|
|
currentActivity.value = ''
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
statusText.value = '在线'
|
|
|
|
|
statusClass.value = 'online'
|
|
|
|
|
currentActivity.value = ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(() => props.activeContact, (c) => {
|
|
|
|
|
computeStatusFromSchedule(c || {})
|
|
|
|
|
if (statusTimer) clearInterval(statusTimer)
|
|
|
|
|
statusTimer = setInterval(() => computeStatusFromSchedule(props.activeContact || {}), 60 * 1000)
|
|
|
|
|
}, { immediate: true })
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
if (statusTimer) clearInterval(statusTimer)
|
|
|
|
|
})
|
|
|
|
|
const handleSend = async () => {
|
|
|
|
|
if (!canSend.value) return
|
|
|
|
|
const userMsg = {
|
|
|
|
|
@ -106,6 +188,20 @@ const handleSend = async () => {
|
|
|
|
|
nextMessages = [...messages.value, aiMsg]
|
|
|
|
|
messages.value = nextMessages
|
|
|
|
|
emit('update:modelValue', nextMessages)
|
|
|
|
|
const isBusy = statusClass.value === 'busy' || statusText.value.startsWith('正在')
|
|
|
|
|
const delayMs = isBusy ? 5 * 60 * 1000 : 0 // 忙碌时延时 5 分钟
|
|
|
|
|
// 如果忙碌,先把占位消息替换为“我正在...,请稍等”格式,真实回复延时替换
|
|
|
|
|
if (isBusy) {
|
|
|
|
|
const placeholder = {
|
|
|
|
|
...aiMsg,
|
|
|
|
|
text: `我正在${currentActivity.value || '忙碌'},请稍等`,
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
}
|
|
|
|
|
// 替换占位消息
|
|
|
|
|
messages.value = [...messages.value.slice(0, -1), placeholder]
|
|
|
|
|
emit('update:modelValue', messages.value)
|
|
|
|
|
// 在 respond() 中会替换为真实回复
|
|
|
|
|
}
|
|
|
|
|
if (!API_KEY) {
|
|
|
|
|
aiMsg.text = 'API_KEY未配置,请检查.env文件和重启项目'
|
|
|
|
|
messages.value = [...messages.value.slice(0, -1), aiMsg]
|
|
|
|
|
@ -301,4 +397,20 @@ const handleSend = async () => {
|
|
|
|
|
text-align: center; /* 标题居中显示 */
|
|
|
|
|
padding: 4px 0; /* 适当增加垂直内边距 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-dot {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
width: 8px;
|
|
|
|
|
height: 8px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
margin-right: 6px;
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-dot.online { background: #4caf50; }
|
|
|
|
|
.status-dot.idle { background: #9e9e9e; }
|
|
|
|
|
.status-dot.busy { background: #f56c6c; }
|
|
|
|
|
.status-dot.relax { background: #ffb020; }
|
|
|
|
|
.status-dot.soon { background: #409eff; }
|
|
|
|
|
|
|
|
|
|
</style>
|
|
|
|
|
|