|
|
|
|
@ -1,31 +1,272 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="phone-page">
|
|
|
|
|
<h2>手机</h2>
|
|
|
|
|
<el-alert title="这是独立的手机页面(也可在社区中以弹窗形式使用)" type="info" show-icon />
|
|
|
|
|
<div class="widget">
|
|
|
|
|
<PhoneWidget v-model="open" />
|
|
|
|
|
<el-button type="primary" @click="open = true">打开手机</el-button>
|
|
|
|
|
<el-dialog
|
|
|
|
|
v-model="modelValueLocal"
|
|
|
|
|
title="手机通信"
|
|
|
|
|
width="900px"
|
|
|
|
|
destroy-on-close
|
|
|
|
|
append-to-body
|
|
|
|
|
>
|
|
|
|
|
<div class="chat-layout">
|
|
|
|
|
<div class="left">
|
|
|
|
|
<div class="left-header">
|
|
|
|
|
<img class="me-avatar" :src="selfAvatar" alt="me" />
|
|
|
|
|
<div class="me-meta">
|
|
|
|
|
<div class="me-name">我</div>
|
|
|
|
|
<div class="me-sub">在线</div>
|
|
|
|
|
</div>
|
|
|
|
|
<el-button size="small" type="info" @click="showMyProfile" style="margin-left:auto;">我的资料</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<el-tabs v-model="leftTab" class="left-tabs">
|
|
|
|
|
<el-tab-pane label="聊天" name="chat" />
|
|
|
|
|
<el-tab-pane label="联系人" name="contacts" />
|
|
|
|
|
</el-tabs>
|
|
|
|
|
<ContactsList v-model="activeContactId" :contacts="contacts" :simple-mode="true" v-show="leftTab === 'contacts'" />
|
|
|
|
|
<div v-show="leftTab === 'chat'" class="chat-list">
|
|
|
|
|
<div
|
|
|
|
|
v-for="c in contacts"
|
|
|
|
|
:key="c.id"
|
|
|
|
|
class="contact-item"
|
|
|
|
|
:class="{ active: c.id === activeContactId }"
|
|
|
|
|
@click="selectContact(c.id)"
|
|
|
|
|
>
|
|
|
|
|
<img class="avatar" :src="c.avatar" alt="avatar" />
|
|
|
|
|
<div class="meta">
|
|
|
|
|
<div class="name">{{ c.name }}</div>
|
|
|
|
|
<div class="last">{{ c.lastMessage || '...' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="time" v-if="c.lastTime">{{ formatTime(c.lastTime) }}</div>
|
|
|
|
|
<el-badge v-if="c.unread" :value="c.unread" class="unread" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="right">
|
|
|
|
|
<ChatWindow
|
|
|
|
|
:active-contact="activeContact"
|
|
|
|
|
v-model="activeMessages"
|
|
|
|
|
:self-id="selfId"
|
|
|
|
|
:self-avatar="selfAvatar"
|
|
|
|
|
@send="handleSend"
|
|
|
|
|
@open-info="showInfoDialog"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<el-button @click="close">关闭</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
<!-- OC信息弹窗 -->
|
|
|
|
|
<OCInfoDialog
|
|
|
|
|
v-model="infoDialogVisible"
|
|
|
|
|
:oc="infoDialogOC"
|
|
|
|
|
/>
|
|
|
|
|
<!-- 我的资料弹窗 -->
|
|
|
|
|
<UserCenterDialog v-model="myProfileVisible" />
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref } from 'vue'
|
|
|
|
|
import PhoneWidget from './PhoneWidget.vue'
|
|
|
|
|
import { computed, ref, watch } from 'vue'
|
|
|
|
|
import ContactsList from './ContactsList.vue'
|
|
|
|
|
import ChatWindow from './ChatWindow.vue'
|
|
|
|
|
import { ocList } from '../stores/ocStore'
|
|
|
|
|
import { currentUser } from '../stores/userStore'
|
|
|
|
|
|
|
|
|
|
const open = ref(true)
|
|
|
|
|
import OCInfoDialog from './OCInfoDialog.vue'
|
|
|
|
|
import UserCenterDialog from './UserCenterDialog.vue'
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
modelValue: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: false
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
|
|
|
|
|
|
const modelValueLocal = computed({
|
|
|
|
|
get: () => props.modelValue,
|
|
|
|
|
set: value => emit('update:modelValue', value)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 使用当前用户OC作为联系人
|
|
|
|
|
const selfId = currentUser.value.username || 'me'
|
|
|
|
|
const selfAvatar = currentUser.value.avatar || 'https://avatars.githubusercontent.com/u/9919?v=4'
|
|
|
|
|
const contacts = ocList
|
|
|
|
|
|
|
|
|
|
// 每个OC独立消息
|
|
|
|
|
const messagesByContact = ref({})
|
|
|
|
|
|
|
|
|
|
const activeContactId = ref(contacts.value.length > 0 ? contacts.value[0].id : '')
|
|
|
|
|
const activeContact = computed(() => contacts.value.find(c => c.id === activeContactId.value) || null)
|
|
|
|
|
|
|
|
|
|
// OC信息弹窗相关
|
|
|
|
|
const infoDialogVisible = ref(false)
|
|
|
|
|
const infoDialogOC = computed(() => activeContact.value)
|
|
|
|
|
const showInfoDialog = () => {
|
|
|
|
|
// 若当前未选中联系人,自动选中第一个
|
|
|
|
|
if (!activeContact.value && contacts.value.length > 0) {
|
|
|
|
|
activeContactId.value = contacts.value[0].id
|
|
|
|
|
}
|
|
|
|
|
if (activeContact.value) {
|
|
|
|
|
infoDialogVisible.value = true
|
|
|
|
|
} else {
|
|
|
|
|
ElMessage.warning('暂无可查看的OC信息')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 我的资料弹窗
|
|
|
|
|
const myProfileVisible = ref(false)
|
|
|
|
|
const showMyProfile = () => {
|
|
|
|
|
myProfileVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const leftTab = ref('chat')
|
|
|
|
|
|
|
|
|
|
const activeMessages = computed({
|
|
|
|
|
get: () => messagesByContact.value[activeContactId.value] || [],
|
|
|
|
|
set: val => {
|
|
|
|
|
messagesByContact.value[activeContactId.value] = val
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const close = () => {
|
|
|
|
|
modelValueLocal.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const selectContact = (contactId) => {
|
|
|
|
|
activeContactId.value = contactId
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formatTime = (ts) => {
|
|
|
|
|
try {
|
|
|
|
|
const d = new Date(ts)
|
|
|
|
|
const h = String(d.getHours()).padStart(2, '0')
|
|
|
|
|
const m = String(d.getMinutes()).padStart(2, '0')
|
|
|
|
|
return `${h}:${m}`
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handleSend = async ({ contact, message }) => {
|
|
|
|
|
const idx = contacts.value.findIndex(c => c.id === contact.id)
|
|
|
|
|
if (idx !== -1) {
|
|
|
|
|
contacts.value[idx] = {
|
|
|
|
|
...contacts.value[idx],
|
|
|
|
|
lastMessage: message.text,
|
|
|
|
|
lastTime: message.timestamp
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 统一AI回复逻辑交由ChatWindow处理,去除本地AI自动回复和默认“收到!”等内容。
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// clear unread when switching to a contact
|
|
|
|
|
watch(activeContactId, id => {
|
|
|
|
|
const i = contacts.value.findIndex(c => c.id === id)
|
|
|
|
|
if (i !== -1) {
|
|
|
|
|
contacts.value[i] = { ...contacts.value[i], unread: 0 }
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<!-- OC信息弹窗 -->
|
|
|
|
|
<OCInfoDialog
|
|
|
|
|
v-model="infoDialogVisible"
|
|
|
|
|
:oc="infoDialogOC"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.phone-page {
|
|
|
|
|
.chat-layout {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
height: 600px;
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
}
|
|
|
|
|
.left {
|
|
|
|
|
width: 280px;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
.widget {
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
.left-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-bottom: 1px solid #ebeef5;
|
|
|
|
|
}
|
|
|
|
|
.me-avatar {
|
|
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
}
|
|
|
|
|
.me-name {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
.me-sub {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #909399;
|
|
|
|
|
}
|
|
|
|
|
.left-tabs {
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-bottom: 1px solid #ebeef5;
|
|
|
|
|
}
|
|
|
|
|
.right {
|
|
|
|
|
flex: 1;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-list {
|
|
|
|
|
height: 100%;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
background: #fff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.contact-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.contact-item:hover {
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.contact-item.active {
|
|
|
|
|
background: #ecf5ff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.avatar {
|
|
|
|
|
width: 36px;
|
|
|
|
|
height: 36px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.meta {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.name {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.last {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #909399;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #909399;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|