1553 lines
44 KiB
Vue
1553 lines
44 KiB
Vue
<script setup>
|
||
import { Search } from '@element-plus/icons-vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import Avatar from '@/components/avatar.vue'
|
||
import Icon from '@/components/icon.vue'
|
||
import { axiosGet, axiosPost } from '@/utils/axios'
|
||
import { useScrollWatcher } from '../../hooks/useScrollWatcher'
|
||
import { useUserStore } from '../../stores/userStore'
|
||
import { useWebSocket } from '../../stores/websocketStore'
|
||
import { formatImTime, formatTimestamp, generateUUID } from '../../utils/commonUtils'
|
||
|
||
const route = useRoute()
|
||
const websocket = useWebSocket()
|
||
const userStore = useUserStore()
|
||
|
||
const chatInputRef = ref(null)
|
||
const chatInputValue = ref('')
|
||
const chatSendLoading = ref(false)
|
||
|
||
// 群聊列表
|
||
const groups = ref([])
|
||
const groupsPage = ref({
|
||
page: 1,
|
||
pageSize: 10,
|
||
total: 0,
|
||
loading: false,
|
||
})
|
||
|
||
// 好友列表
|
||
const friends = ref([])
|
||
const friendsPage = ref({
|
||
page: 1,
|
||
pageSize: 10,
|
||
total: 0,
|
||
loading: false,
|
||
})
|
||
|
||
// 当前聊天对象
|
||
const initialFriendId = ref(route.query.friendId || '')
|
||
const currentChat = ref(null)
|
||
const isGroupChat = computed(() => currentChat.value?.type === 'group')
|
||
const isPrivateChat = computed(() => currentChat.value?.type === 'private')
|
||
|
||
// 私聊消息列表
|
||
const privateMessages = ref([])
|
||
const privateMessagesPage = ref({
|
||
pageSize: 10,
|
||
loading: false,
|
||
hasMore: true,
|
||
})
|
||
const privateTimeMessages = computed(() => processMessagesWithTime(privateMessages.value))
|
||
|
||
const groupMessages = ref([])
|
||
const groupMessagesPage = ref({
|
||
pageSize: 10,
|
||
loading: false,
|
||
hasMore: true,
|
||
})
|
||
const groupTimeMessages = computed(() => processMessagesWithTime(groupMessages.value))
|
||
|
||
const messageListRef = ref()
|
||
const { scrollToBottom: scrollToLatestMessage } = useScrollWatcher({
|
||
target: messageListRef,
|
||
onReachTop: () => {
|
||
console.log('=========top===========')
|
||
let loading = false
|
||
let hasMore = false
|
||
|
||
if (isPrivateChat.value) {
|
||
loading = privateMessagesPage.value.loading
|
||
hasMore = privateMessagesPage.value.hasMore
|
||
}
|
||
else {
|
||
loading = groupMessagesPage.value.loading
|
||
hasMore = groupMessagesPage.value.hasMore
|
||
}
|
||
|
||
console.log(loading)
|
||
|
||
if (loading)
|
||
return
|
||
|
||
if (!hasMore) {
|
||
ElMessage.warning('没有更多消息了')
|
||
return
|
||
}
|
||
|
||
if (isPrivateChat.value) {
|
||
loadPrivateMessages('up')
|
||
}
|
||
else {
|
||
loadGroupMessages('up')
|
||
}
|
||
},
|
||
})
|
||
|
||
// 消息监听
|
||
const unlistenWebSocket = ref()
|
||
|
||
onMounted(async () => {
|
||
loadFriends()
|
||
loadGroups()
|
||
|
||
unlistenWebSocket.value = websocket.onMessage((msg) => {
|
||
const { type, from, data } = msg
|
||
|
||
// --- 在线/离线状态更新 ---
|
||
if (type === 'online' || type === 'offline') {
|
||
const status = type === 'online' ? 1 : 0
|
||
const friend = friends.value.find(f => f.b_frienduser_id === from)
|
||
if (friend)
|
||
friend.is_online = status
|
||
return
|
||
}
|
||
// 私聊信息处理
|
||
if (type === 'private') {
|
||
const tempId = data?._tempId
|
||
const isSelf = from === userStore.userInfo.userid
|
||
const isCurrentChat = isPrivateChat.value ? from === currentChat.value?.b_frienduser_id : false
|
||
const to = msg.data.b_receiver_id
|
||
|
||
// 更新好友列表内容
|
||
const targetFriendId = isSelf ? to : from
|
||
const friendIndex = friends.value.findIndex(f => f.b_frienduser_id === targetFriendId)
|
||
|
||
if (friendIndex !== -1) {
|
||
const friend = friends.value[friendIndex]
|
||
|
||
// 更新最新消息和时间
|
||
friend.v_last_message = data.b_content
|
||
friend.v_last_message_time = data.b_send_time
|
||
|
||
// 移动到列表顶部
|
||
friends.value.splice(friendIndex, 1)
|
||
friends.value.unshift(friend)
|
||
}
|
||
|
||
// 1. 处理自己发出的消息(回写状态 or 多页面同步)
|
||
if (isSelf) {
|
||
const index = privateMessages.value.findIndex(item => item._tempId === tempId)
|
||
|
||
if (index !== -1) {
|
||
privateMessages.value[index] = {
|
||
...privateMessages.value[index],
|
||
...data,
|
||
_status: 'finished',
|
||
}
|
||
}
|
||
else {
|
||
if (isCurrentChat) {
|
||
privateMessages.value.push({
|
||
...data,
|
||
_status: 'finished',
|
||
})
|
||
nextTick(scrollToLatestMessage)
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
// 2. 收到当前聊天好友的消息
|
||
if (isCurrentChat) {
|
||
privateMessages.value.push(data)
|
||
nextTick(scrollToLatestMessage)
|
||
// 这个还需要进行更新未读数量
|
||
axiosPost('/updatePrivateUnread', {
|
||
b_id: currentChat.value.b_id,
|
||
b_unread_count: 0,
|
||
})
|
||
}
|
||
// 3. 收到其他好友的消息,增加未读数
|
||
else {
|
||
const otherFriend = friends.value.find(f => f.b_frienduser_id === from)
|
||
if (otherFriend) {
|
||
otherFriend.b_unread_count = (otherFriend.b_unread_count || 0) + 1
|
||
}
|
||
}
|
||
}
|
||
|
||
return
|
||
}
|
||
if (type === 'friend_update') {
|
||
friends.value.unshift(data)
|
||
return
|
||
}
|
||
// 群聊
|
||
if (type === 'group') {
|
||
const tempId = data?._tempId
|
||
const isSelf = from === userStore.userInfo.userid
|
||
const isCurrentChat = isGroupChat.value
|
||
? msg.data.b_group_id === currentChat.value?.b_id
|
||
: false
|
||
|
||
const findGroupIndex = groups.value.findIndex(f => f.b_id === msg.data.b_group_id)
|
||
console.log(findGroupIndex)
|
||
|
||
if (findGroupIndex !== -1) {
|
||
const group = groups.value[findGroupIndex]
|
||
|
||
// 更新最新消息和时间
|
||
group.v_last_message_id = data.b_id
|
||
group.v_last_message = data.b_content
|
||
group.v_last_message_time = data.b_send_time
|
||
group.v_last_message_sender_name = data.v_sender_name
|
||
|
||
// 移动到列表顶部
|
||
groups.value.splice(findGroupIndex, 1)
|
||
groups.value.unshift(group)
|
||
|
||
console.log(group)
|
||
console.log(groups.value)
|
||
}
|
||
|
||
// 1. 处理自己发出的消息(回写状态 or 多页面同步)
|
||
if (isSelf) {
|
||
const index = groupMessages.value.findIndex(item => item._tempId === tempId)
|
||
|
||
if (index !== -1) {
|
||
groupMessages.value[index] = {
|
||
...groupMessages.value[index],
|
||
...data,
|
||
_status: 'finished',
|
||
}
|
||
}
|
||
else {
|
||
if (isCurrentChat) {
|
||
groupMessages.value.push({
|
||
...data,
|
||
_status: 'finished',
|
||
})
|
||
nextTick(scrollToLatestMessage)
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
if (isCurrentChat) {
|
||
groupMessages.value.push(data)
|
||
|
||
nextTick(scrollToLatestMessage)
|
||
|
||
// 这个还需要进行更新未读数量
|
||
axiosPost('/updateGroupUnread', {
|
||
b_id: currentChat.value.b_id,
|
||
b_unread_count: 0,
|
||
})
|
||
}
|
||
// 3. 收到其他群组的消息,增加未读数
|
||
else {
|
||
const otherGroup = groups.value.find(f => f.b_id === msg.data.b_group_id)
|
||
if (otherGroup) {
|
||
otherGroup.b_unread_count = (otherGroup.b_unread_count || 0) + 1
|
||
}
|
||
}
|
||
}
|
||
}
|
||
})
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
unlistenWebSocket.value?.()
|
||
})
|
||
|
||
// 加载好友列表
|
||
async function loadFriends() {
|
||
if (friendsPage.value.loading)
|
||
return
|
||
friendsPage.value.loading = true
|
||
|
||
const userId = userStore.userInfo.userid
|
||
|
||
try {
|
||
const res = await axiosGet('/friends', {
|
||
userId,
|
||
page: friendsPage.value.page,
|
||
pageSize: friendsPage.value.pageSize,
|
||
friendId: initialFriendId.value, // 可选参数
|
||
})
|
||
|
||
if (res.data) {
|
||
const { records, total } = res.data
|
||
|
||
if (friendsPage.value.page === 1) {
|
||
friends.value = records
|
||
}
|
||
else {
|
||
friends.value = friends.value.concat(records)
|
||
}
|
||
|
||
// 设置当前聊天对象
|
||
if (initialFriendId.value) {
|
||
const friend = friends.value.find(f => f.b_frienduser_id === initialFriendId.value)
|
||
if (friend) {
|
||
openPrivateChat(friend)
|
||
}
|
||
initialFriendId.value = ''
|
||
}
|
||
|
||
friendsPage.value.total = total
|
||
}
|
||
}
|
||
catch (err) {
|
||
console.error('加载好友列表失败', err)
|
||
ElMessage.error(err.response?.data?.message || err.message || '加载失败')
|
||
}
|
||
finally {
|
||
friendsPage.value.loading = false
|
||
}
|
||
}
|
||
|
||
// 好友列表滚动事件
|
||
function onFriendListScroll(e) {
|
||
const target = e.target
|
||
const totalPages = Math.ceil(friendsPage.value.total / friendsPage.value.pageSize)
|
||
|
||
// 滚动到底部,加载下一页
|
||
if (target.scrollHeight - target.scrollTop - target.clientHeight <= 5) {
|
||
if (friendsPage.value.page < totalPages && !friendsPage.value.loading) {
|
||
friendsPage.value.page += 1
|
||
loadFriends()
|
||
}
|
||
}
|
||
}
|
||
|
||
// 激活私聊
|
||
function openPrivateChat(friend) {
|
||
// 将 friend 的未读置0
|
||
const hadUnread = friend.b_unread_count > 0
|
||
friend.b_unread_count = 0
|
||
|
||
// 设置当前聊天
|
||
currentChat.value = { ...friend, type: 'private' }
|
||
|
||
// 初始化分页
|
||
privateMessagesPage.value = { pageSize: 10, loading: false, hasMore: true }
|
||
|
||
// 加载最新消息
|
||
loadPrivateMessages('down')
|
||
|
||
// 后台更新已读数量(仅当有未读时)
|
||
if (hadUnread) {
|
||
axiosPost('/updatePrivateUnread', { b_id: friend.b_id, b_unread_count: 0 })
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理聊天消息,生成 _showTime 字段
|
||
* 如果与上一条消息相差超过 5 分钟,则显示时间
|
||
*/
|
||
function processMessagesWithTime(messages) {
|
||
let lastTimestamp = 0
|
||
|
||
return messages.map((msg) => {
|
||
if (!msg.b_send_time)
|
||
return { ...msg, _showTime: null }
|
||
|
||
const ts = Number(msg.b_send_time)
|
||
const showTime = !lastTimestamp || ts - lastTimestamp > 5 * 60 * 1000
|
||
lastTimestamp = ts
|
||
|
||
return { ...msg, _showTime: showTime ? formatImTime(ts, 'chat') : null }
|
||
})
|
||
}
|
||
|
||
// 加载私聊消息
|
||
async function loadPrivateMessages(direction = 'down') {
|
||
if (!currentChat.value || privateMessagesPage.value.loading || (direction === 'up' && !privateMessagesPage.value.hasMore))
|
||
return
|
||
|
||
privateMessagesPage.value.loading = true
|
||
|
||
const userId = userStore.userInfo.userid
|
||
const friendId = currentChat.value.b_frienduser_id
|
||
|
||
let beforeTimestamp = null
|
||
let beforeBId = null
|
||
|
||
if (direction === 'up' && privateMessages.value.length > 0) {
|
||
const firstMsg = privateMessages.value[0]
|
||
beforeTimestamp = Number(firstMsg.b_send_time)
|
||
beforeBId = firstMsg.b_id
|
||
}
|
||
|
||
try {
|
||
const res = await axiosGet('/privateMessages', {
|
||
userId,
|
||
friendId,
|
||
pageSize: privateMessagesPage.value.pageSize,
|
||
beforeTimestamp,
|
||
beforeBId,
|
||
})
|
||
|
||
if (res.data && Array.isArray(res.data)) {
|
||
const records = res.data.reverse()
|
||
|
||
if (direction === 'down') {
|
||
privateMessages.value = records
|
||
await nextTick()
|
||
scrollToLatestMessage()
|
||
}
|
||
else if (direction === 'up') {
|
||
if (records.length === 0) {
|
||
privateMessagesPage.value.hasMore = false
|
||
ElMessage.warning('没有更多消息了')
|
||
}
|
||
else {
|
||
privateMessages.value.unshift(...records)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (err) {
|
||
console.error('加载私聊消息失败', err)
|
||
ElMessage.error(err.response?.data?.message || err.message || '加载失败')
|
||
}
|
||
finally {
|
||
privateMessagesPage.value.loading = false
|
||
}
|
||
}
|
||
|
||
// 加载群组列表
|
||
async function loadGroups() {
|
||
if (groupsPage.value.loading)
|
||
return
|
||
groupsPage.value.loading = true
|
||
|
||
const userId = userStore.userInfo.userid
|
||
|
||
try {
|
||
const res = await axiosGet('/groups', {
|
||
userId,
|
||
page: groupsPage.value.page,
|
||
pageSize: groupsPage.value.pageSize,
|
||
})
|
||
|
||
if (res.data) {
|
||
const { records, total } = res.data
|
||
|
||
if (groupsPage.value.page === 1) {
|
||
groups.value = records
|
||
}
|
||
else {
|
||
groups.value = groups.value.concat(records)
|
||
}
|
||
|
||
groupsPage.value.total = total
|
||
}
|
||
}
|
||
catch (err) {
|
||
console.error('加载群组列表失败', err)
|
||
ElMessage.error(err.response?.data?.message || err.message || '加载失败')
|
||
}
|
||
finally {
|
||
groupsPage.value.loading = false
|
||
}
|
||
}
|
||
|
||
// 好友列表滚动事件
|
||
function onGroupListScroll(e) {
|
||
const target = e.target
|
||
const totalPages = Math.ceil(groupsPage.value.total / groupsPage.value.pageSize)
|
||
|
||
// 滚动到底部,加载下一页
|
||
if (target.scrollHeight - target.scrollTop - target.clientHeight <= 5) {
|
||
if (groupsPage.value.page < totalPages && !groupsPage.value.loading) {
|
||
groupsPage.value.page += 1
|
||
loadGroups()
|
||
}
|
||
}
|
||
}
|
||
|
||
// 激活群聊
|
||
function openGroupChat(group) {
|
||
console.log(group)
|
||
// 将 group 的未读置0
|
||
const hadUnread = group.v_unread_count > 0
|
||
group.v_unread_count = 0
|
||
|
||
// 设置当前聊天
|
||
currentChat.value = { ...group, type: 'group' }
|
||
|
||
// 初始化分页
|
||
groupMessagesPage.value = { pageSize: 10, loading: false, hasMore: true }
|
||
|
||
// 加载最新消息
|
||
loadGroupMessages('down')
|
||
loadGroupMembers()
|
||
|
||
// 后台更新已读数量(仅当有未读时)
|
||
if (hadUnread) {
|
||
axiosPost('/updateGroupUnread', { b_id: group.v_group_member_id, b_unread_count: 0 })
|
||
}
|
||
}
|
||
|
||
const groupMembers = ref([])
|
||
|
||
async function loadGroupMembers() {
|
||
if (!isGroupChat.value) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const res = await axiosGet('/groupMembers', {
|
||
groupId: currentChat.value.b_id,
|
||
})
|
||
|
||
groupMembers.value = res.data || []
|
||
|
||
console.log('群成员列表:', groupMembers.value)
|
||
}
|
||
catch (err) {
|
||
console.error(err)
|
||
ElMessage.error(`请求错误:${err.message || '未知错误'}`)
|
||
}
|
||
}
|
||
|
||
// 加载私聊消息
|
||
async function loadGroupMessages(direction = 'down') {
|
||
if (!currentChat.value || groupMessagesPage.value.loading || (direction === 'up' && !groupMessagesPage.value.hasMore))
|
||
return
|
||
|
||
groupMessagesPage.value.loading = true
|
||
|
||
let beforeTimestamp = null
|
||
let beforeBId = null
|
||
|
||
if (direction === 'up' && groupMessages.value.length > 0) {
|
||
const firstMsg = groupMessages.value[0]
|
||
beforeTimestamp = Number(firstMsg.b_send_time)
|
||
beforeBId = firstMsg.b_id
|
||
}
|
||
|
||
try {
|
||
const res = await axiosGet('/groupMessages', {
|
||
groupId: currentChat.value.b_id,
|
||
pageSize: groupMessagesPage.value.pageSize,
|
||
beforeTimestamp,
|
||
beforeBId,
|
||
})
|
||
|
||
if (res.data && Array.isArray(res.data)) {
|
||
const records = res.data.reverse()
|
||
|
||
if (direction === 'down') {
|
||
groupMessages.value = records
|
||
await nextTick()
|
||
scrollToLatestMessage()
|
||
}
|
||
else if (direction === 'up') {
|
||
if (records.length === 0) {
|
||
groupMessagesPage.value.hasMore = false
|
||
ElMessage.warning('没有更多消息了')
|
||
}
|
||
else {
|
||
groupMessages.value.unshift(...records)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (err) {
|
||
console.error('加载私聊消息失败', err)
|
||
ElMessage.error(err.response?.data?.message || err.message || '加载失败')
|
||
}
|
||
finally {
|
||
groupMessagesPage.value.loading = false
|
||
}
|
||
}
|
||
|
||
// 聊天框输入事件
|
||
function handleChatInputChange(event) {
|
||
// 只取文本内容
|
||
const text = event.target.textContent || ''
|
||
chatInputValue.value = text.trim()
|
||
}
|
||
|
||
// 聊天框回车事件
|
||
function handleChatInputKeyDown(event) {
|
||
// 判断是否是回车键
|
||
if (event.key === 'Enter') {
|
||
event.preventDefault()
|
||
if (!chatSendLoading.value) {
|
||
sendMessage()
|
||
}
|
||
}
|
||
}
|
||
|
||
async function sendMessage() {
|
||
const content = chatInputValue.value?.trim()
|
||
if (!content) {
|
||
ElMessage.warning('发送消息不能为空')
|
||
return
|
||
}
|
||
|
||
const userId = userStore.userInfo.userid
|
||
const _tempId = generateUUID()
|
||
const sendTime = Date.now()
|
||
const to = isPrivateChat.value ? currentChat.value.b_frienduser_id : currentChat.value.b_id
|
||
|
||
// 构建发送对象
|
||
const sendObj = {
|
||
type: isPrivateChat.value ? 'private' : 'group',
|
||
from: userId,
|
||
to,
|
||
timestamp: sendTime,
|
||
data: {
|
||
content,
|
||
messageType: 'text',
|
||
_tempId,
|
||
},
|
||
}
|
||
|
||
// 构建本地显示消息
|
||
const localMessage = {
|
||
b_sender_id: userId,
|
||
b_content: content,
|
||
b_message_type: 'text',
|
||
b_send_time: formatTimestamp(sendTime),
|
||
_tempId,
|
||
_status: 'sending',
|
||
}
|
||
|
||
if (isPrivateChat.value) {
|
||
localMessage.b_receiver_id = currentChat.value.b_frienduser_id
|
||
privateMessages.value.push(localMessage)
|
||
}
|
||
else {
|
||
localMessage.b_group_id = currentChat.value.b_id
|
||
|
||
groupMessages.value.push(localMessage)
|
||
}
|
||
|
||
// 清空输入框
|
||
if (chatInputRef.value) {
|
||
chatInputRef.value.innerHTML = ''
|
||
}
|
||
chatInputValue.value = ''
|
||
|
||
// 发送消息
|
||
websocket.sendMessage(sendObj)
|
||
|
||
// 等待 DOM 更新再滚动到底部
|
||
await nextTick()
|
||
scrollToLatestMessage()
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="im">
|
||
<div class="im__container">
|
||
<div class="im__header">
|
||
<div class="im__desc">
|
||
<div class="im__logo">
|
||
<Icon icon="solar:buildings-2-linear" :size="28" color="#fff" />
|
||
</div>
|
||
<div class="im__desc-extra">
|
||
<div class="im__title">
|
||
企业IM通信平台
|
||
</div>
|
||
<div class="im__subtitle">
|
||
Enterprise IM Communication Platform
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="im__user">
|
||
<Avatar name="User" />
|
||
<div class="im__user-info">
|
||
<div class="im__user-name">
|
||
{{ userStore.userInfo.username }}
|
||
</div>
|
||
<div
|
||
class="im__user-status"
|
||
:class="websocket.isConnected.value ? 'im__user-status--online' : 'im__user-status--offline'"
|
||
>
|
||
<span>{{ websocket.isConnected.value ? '在线' : '离线' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="im__body">
|
||
<div class="im__list">
|
||
<div class="im__list-search">
|
||
<el-input placeholder="请根据名称搜索" :prefix-icon="Search" />
|
||
</div>
|
||
<div class="im__list-item">
|
||
<div class="im__list-header">
|
||
<Icon icon="tabler:message-circle" :size="16" />
|
||
<span class="im__list-header-title">
|
||
单聊
|
||
</span>
|
||
</div>
|
||
<div class="im__list-body" @scroll="onFriendListScroll">
|
||
<div
|
||
v-for="item in friends" :key="item.b_id" class="im__chat-item"
|
||
:class="{
|
||
'im__chat-item--selected':
|
||
isPrivateChat && item.b_id === currentChat.b_id,
|
||
}"
|
||
@click="openPrivateChat(item)"
|
||
>
|
||
<el-badge
|
||
is-dot
|
||
:color="item.is_online === 1 ? '#24e68a' : '#bfbfbf'"
|
||
:badge-style="{
|
||
width: '12px',
|
||
height: '12px',
|
||
}"
|
||
:offset="[-4, 34]"
|
||
>
|
||
<Avatar name="A" class="im__chat-item-avatar" />
|
||
</el-badge>
|
||
<div class="im__chat-item-info">
|
||
<div class="im__chat-item-main">
|
||
<div class="im__chat-item-name">
|
||
{{ item.v_frienduser_name }}
|
||
</div>
|
||
<div class="im__chat-item-last-time">
|
||
{{ formatImTime(item.v_last_message_time, 'list') }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="im__chat-item-message">
|
||
<div class="im__chat-item-last-message">
|
||
{{ item.v_last_message }}
|
||
</div>
|
||
<el-badge :value="item.b_unread_count || 0" :show-zero="false" :max="99" class="im__chat-item-unread-count" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="im__list-item">
|
||
<div class="im__list-header">
|
||
<Icon icon="bi:people" :size="16" />
|
||
<span class="im__list-header-title">
|
||
群聊
|
||
</span>
|
||
</div>
|
||
<div class="im__list-body" @scroll="onGroupListScroll">
|
||
<div
|
||
v-for="item in groups" :key="item.b_id" class="im__chat-item"
|
||
:class="{
|
||
'im__chat-item--selected':
|
||
isGroupChat && item.b_id === currentChat.b_id,
|
||
}"
|
||
@click="openGroupChat(item)"
|
||
>
|
||
<Avatar name="#" class="im__chat-item-avatar" bg-color="#574b90" />
|
||
<div class="im__chat-item-info">
|
||
<div class="im__chat-item-main">
|
||
<div class="im__chat-item-name">
|
||
{{ item.b_groupname }}
|
||
</div>
|
||
<div class="im__chat-item-last-time">
|
||
{{ formatImTime(item.v_last_message_time, 'list') }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="im__chat-item-message">
|
||
<div v-if="item.v_last_message_id" class="im__chat-item-last-message">
|
||
{{ item.v_last_message_sender_name }}: {{ item.v_last_message }}
|
||
</div>
|
||
|
||
<el-badge :value="item.v_unread_count || 0" :show-zero="false" :max="99" class="im__chat-item-unread-count" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="currentChat" class="im__window">
|
||
<div class="im__window-header">
|
||
<div class="im__window-peer">
|
||
<div class="im__window-peer-info">
|
||
<div class="im__window-peer-name">
|
||
{{ isPrivateChat ? currentChat.v_frienduser_name : currentChat.b_groupname }}
|
||
</div>
|
||
<div v-if="isPrivateChat" class="im__window-peer-desc">
|
||
<div
|
||
class="im__window-peer-status"
|
||
:class="currentChat.is_online === 1 ? 'im__window-peer-status--online' : 'im__window-peer-status--offline'"
|
||
>
|
||
<span>[ </span>
|
||
<div class="im__window-peer-status-dot" />
|
||
<span>{{ currentChat.is_online === 1 ? '在线' : '离线' }}</span>
|
||
<span> ]</span>
|
||
</div>
|
||
<span>
|
||
公司描述等等等...
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="im__window-actions" />
|
||
</div>
|
||
<div class="im__window-body">
|
||
<template v-if="isGroupChat">
|
||
<el-splitter layout="vertical">
|
||
<el-splitter-panel>
|
||
<div ref="messageListRef" class="im__chat-main">
|
||
<div v-if="groupMessagesPage.loading" class="im__chat-message-loading">
|
||
<Icon icon="svg-spinners:180-ring" color="#1553f1" />
|
||
</div>
|
||
<div
|
||
v-for="item in groupTimeMessages"
|
||
:key="item.b_id"
|
||
class="im__chat-message-wrapper"
|
||
>
|
||
<div v-if="item._showTime" class="im__chat-message-time">
|
||
{{ item._showTime }}
|
||
</div>
|
||
<div
|
||
class="im__chat-message"
|
||
:class="userStore.userInfo.userid === item.b_sender_id
|
||
? 'im__chat-message--from'
|
||
: 'im__chat-message--to'"
|
||
>
|
||
<div class="im__chat-message-avatar">
|
||
<Avatar name="A" />
|
||
</div>
|
||
<div class="im__chat-message-body">
|
||
<div class="im__chat-message-name">
|
||
{{ item.v_sender_name }}
|
||
</div>
|
||
<div class="im__chat-message-content">
|
||
<div v-if="item._status && item._status !== 'sent'" class="im__chat-message-status">
|
||
<Icon
|
||
v-if="item._status === 'sending'"
|
||
icon="svg-spinners:180-ring"
|
||
color="#1553f1"
|
||
/>
|
||
<Icon
|
||
v-else-if="item._status === 'failed'"
|
||
icon="material-symbols:error"
|
||
color="red"
|
||
/>
|
||
</div>
|
||
<div class="im__chat-message-content--text">
|
||
{{ item.b_content }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-splitter-panel>
|
||
<el-splitter-panel :min="100" :max="300" :size="150">
|
||
<div class="im__chat-footer">
|
||
<div class="im__chat-toolbar">
|
||
<Icon icon="proicons:emoji" />
|
||
|
||
<div class="im__chat-toolbar-right">
|
||
<Icon
|
||
icon="solar:history-broken"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="im__chat-editor">
|
||
<div
|
||
ref="chatInputRef"
|
||
class="im__chat-editor-input"
|
||
contenteditable="true"
|
||
@input="handleChatInputChange"
|
||
@keydown="handleChatInputKeyDown"
|
||
/>
|
||
</div>
|
||
<div class="im__chat-sendarea">
|
||
<el-button type="primary" size="small" class="im__chat-send">
|
||
发送(回车)
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</el-splitter-panel>
|
||
</el-splitter>
|
||
<div class="im__chat-group">
|
||
<div class="im__group-section-header">
|
||
<!-- <el-input placeholder="请根据名称搜索" :prefix-icon="Search" /> -->
|
||
<div class="im__group-section-header-title">
|
||
<Icon icon="bi:people" :size="18" color="#666666" />
|
||
<span class="im__group-section-header-text">群成员({{ groupMembers.length }})</span>
|
||
</div>
|
||
<div class="im__group-section-header-extra">
|
||
<div class="im__group-section-header--search">
|
||
<Icon icon="lucide:search" :size="16" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="im__group-section-body">
|
||
<div v-for="item in groupMembers" :key="item.b_member_id" class="im__group-item">
|
||
<el-badge
|
||
is-dot
|
||
:color="item.is_online ? '#24e68a' : '#bfbfbf'"
|
||
:badge-style="{
|
||
width: '12px',
|
||
height: '12px',
|
||
}"
|
||
:offset="[-4, 34]"
|
||
>
|
||
<!-- 未登录:b3b7c2 -->
|
||
<Avatar name="A" />
|
||
</el-badge>
|
||
<div class="im__group-item-info">
|
||
<div class="im__group-item-name">
|
||
{{ item.v_member_name }}
|
||
</div>
|
||
<div class="im__group-item-desc">
|
||
desc
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else-if="isPrivateChat">
|
||
<el-splitter layout="vertical">
|
||
<el-splitter-panel>
|
||
<div ref="messageListRef" class="im__chat-main">
|
||
<div v-if="privateMessagesPage.loading" class="im__chat-message-loading">
|
||
<Icon icon="svg-spinners:180-ring" color="#1553f1" />
|
||
</div>
|
||
<div
|
||
v-for="item in privateTimeMessages"
|
||
:key="item.b_id"
|
||
class="im__chat-message-wrapper"
|
||
>
|
||
<div v-if="item._showTime" class="im__chat-message-time">
|
||
{{ item._showTime }}
|
||
</div>
|
||
<div
|
||
class="im__chat-message"
|
||
:class="userStore.userInfo.userid === item.b_sender_id
|
||
? 'im__chat-message--from'
|
||
: 'im__chat-message--to'"
|
||
>
|
||
<div class="im__chat-message-avatar">
|
||
<Avatar name="A" />
|
||
</div>
|
||
<div class="im__chat-message-body">
|
||
<div class="im__chat-message-name">
|
||
{{
|
||
userStore.userInfo.userid === item.b_sender_id ? userStore.userInfo.username : currentChat.v_frienduser_name
|
||
}}
|
||
</div>
|
||
<div class="im__chat-message-content">
|
||
<div v-if="item._status && item._status !== 'sent'" class="im__chat-message-status">
|
||
<Icon
|
||
v-if="item._status === 'sending'"
|
||
icon="svg-spinners:180-ring"
|
||
color="#1553f1"
|
||
/>
|
||
<Icon
|
||
v-else-if="item._status === 'failed'"
|
||
icon="material-symbols:error"
|
||
color="red"
|
||
/>
|
||
</div>
|
||
<div class="im__chat-message-content--text">
|
||
{{ item.b_content }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-splitter-panel>
|
||
<el-splitter-panel :min="100" :max="300" :size="150">
|
||
<div class="im__chat-footer">
|
||
<div class="im__chat-toolbar">
|
||
<Icon icon="proicons:emoji" />
|
||
|
||
<div class="im__chat-toolbar-right">
|
||
<Icon
|
||
icon="solar:history-broken"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="im__chat-editor">
|
||
<div
|
||
ref="chatInputRef"
|
||
class="im__chat-editor-input"
|
||
contenteditable="true"
|
||
@input="handleChatInputChange"
|
||
@keydown="handleChatInputKeyDown"
|
||
/>
|
||
</div>
|
||
<div class="im__chat-sendarea">
|
||
<el-button type="primary" size="small" class="im__chat-send">
|
||
发送(回车)
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</el-splitter-panel>
|
||
</el-splitter>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="im__window-center">
|
||
<el-result
|
||
icon="primary"
|
||
title="提示"
|
||
sub-title="请选择需要交流的用户或群聊"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
.im {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
height: 100%;
|
||
width: 100%;
|
||
background-color: #f5f6fa;
|
||
|
||
& *::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
|
||
& *::-webkit-scrollbar-track {
|
||
background-color: #fff;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
& *::-webkit-scrollbar-thumb {
|
||
background-color: rgba(0, 0, 0, 0.2);
|
||
border-radius: 3px;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
& *::-webkit-scrollbar-thumb:hover {
|
||
background-color: rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
.im__container {
|
||
width: 1300px;
|
||
height: 80%;
|
||
background-color: #fff;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.im__header {
|
||
padding: 0 16px;
|
||
height: 64px;
|
||
border-bottom: 1px solid #e6e9ef;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-shrink: 0;
|
||
.im__desc {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.im__logo {
|
||
background-color: #1553f1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
box-shadow:
|
||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||
0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.im__desc-extra {
|
||
margin-left: 12px;
|
||
|
||
.im__title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.im__subtitle {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
}
|
||
}
|
||
|
||
.im__user {
|
||
display: flex;
|
||
align-items: center;
|
||
height: 100%;
|
||
padding: 0px 8px;
|
||
&:hover {
|
||
cursor: pointer;
|
||
background-color: #f4f4f4;
|
||
}
|
||
.im__user-info {
|
||
margin-left: 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
.im__user-name {
|
||
font-size: 14px;
|
||
}
|
||
.im__user-status {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
.im__user-status--online {
|
||
color: #4caf50;
|
||
}
|
||
.im__user-status--offline {
|
||
color: #666;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.im__body {
|
||
display: flex;
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
.im__list {
|
||
width: 300px;
|
||
border-right: 1px solid #ddd;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
|
||
.im__list-search {
|
||
height: 56px;
|
||
padding: 10px;
|
||
border-bottom: 1px solid #e6e9ef;
|
||
}
|
||
|
||
.im__list-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
min-height: 0;
|
||
transition: background-color 0.25s ease;
|
||
|
||
.im__list-header {
|
||
font-size: 14px;
|
||
padding: 8px 10px;
|
||
background-color: #f8f9fc;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
.im__list-header-title {
|
||
margin-left: 4px;
|
||
}
|
||
}
|
||
.im__list-body {
|
||
overflow-y: auto;
|
||
flex: 1;
|
||
min-height: 0;
|
||
.im__chat-item {
|
||
display: flex;
|
||
align-items: center;
|
||
height: 64px;
|
||
width: 100%;
|
||
padding: 0 10px;
|
||
overflow-x: hidden;
|
||
&:hover {
|
||
background-color: #f5f7fa;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.im__chat-item-avatar {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.im__chat-item-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
margin-left: 10px;
|
||
min-width: 0;
|
||
|
||
.im__chat-item-main,
|
||
.im__chat-item-message {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
width: 100%;
|
||
}
|
||
|
||
.im__chat-item-message:has(.im__chat-item-last-message:not(:empty)) {
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.im__chat-item-name {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
.im__chat-item-last-time,
|
||
.im__chat-item-last-message {
|
||
flex-shrink: 0;
|
||
color: #666;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.im__chat-item-last-message,
|
||
.im__chat-item-name {
|
||
flex: 1;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.im__chat-item-last-time {
|
||
margin-left: 8px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.im__chat-item-unread-count {
|
||
display: flex;
|
||
margin-left: 8px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.im__chat-item--selected {
|
||
position: relative;
|
||
background-color: #f0f6ff;
|
||
transition: background-color 0.25s ease;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 4px;
|
||
bottom: 4px;
|
||
width: 3px;
|
||
background-color: #3b82f6;
|
||
border-radius: 2px;
|
||
box-shadow: 0 0 4px rgba(59, 130, 246, 0.4);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.im__window {
|
||
flex: 1;
|
||
background-color: #fff;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.im__window-header {
|
||
height: 56px;
|
||
border-bottom: 1px solid #e6e9ef;
|
||
display: flex;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
.im__window-peer {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0px 8px;
|
||
|
||
.im__window-peer-info {
|
||
margin-left: 8px;
|
||
.im__window-peer-name {
|
||
font-size: 16px;
|
||
}
|
||
.im__window-peer-desc {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 12px;
|
||
color: #666;
|
||
|
||
.im__window-peer-status {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-right: 4px;
|
||
|
||
.im__window-peer-status-dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
margin: 0px 4px;
|
||
}
|
||
}
|
||
.im__window-peer-status--online {
|
||
.im__window-peer-status-dot {
|
||
background-color: #24e68a;
|
||
}
|
||
}
|
||
.im__window-peer-status--offline {
|
||
.im__window-peer-status-dot {
|
||
background-color: #b3b7c2;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.im__window-body {
|
||
flex: 1;
|
||
display: flex;
|
||
min-height: 0;
|
||
|
||
:deep(.el-splitter-panel) {
|
||
scrollbar-width: auto;
|
||
}
|
||
.im__chat-main {
|
||
padding: 16px 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
width: 100%;
|
||
scrollbar-gutter: stable both-edges;
|
||
overflow-y: auto;
|
||
min-width: 0;
|
||
background-color: #f8fafc;
|
||
height: 100%;
|
||
.im__chat-message-loading {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.im__chat-message-wrapper {
|
||
width: 100%;
|
||
position: relative;
|
||
}
|
||
|
||
.im__chat-message-time {
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.im__chat-message {
|
||
|
||
display: flex;
|
||
width: fit-content;
|
||
width: 100%;
|
||
.im__chat-message-avatar {
|
||
flex-shrink: 0;
|
||
}
|
||
.im__chat-message-body {
|
||
width: 100%;
|
||
.im__chat-message-name {
|
||
font-size: 14px;
|
||
line-height: 1;
|
||
}
|
||
.im__chat-message-content {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
margin-top: 8px;
|
||
max-width: 70%;
|
||
}
|
||
|
||
.im__chat-message-content--text {
|
||
background-color: #ffffff;
|
||
color: #000000;
|
||
word-break: break-all;
|
||
padding: 6px 12px;
|
||
border-radius: 4px;
|
||
text-align: left;
|
||
}
|
||
}
|
||
}
|
||
.im__chat-message--from {
|
||
align-self: flex-end;
|
||
flex-direction: row-reverse;
|
||
.im__chat-message-body {
|
||
margin-right: 8px;
|
||
text-align: right;
|
||
.im__chat-message-content--text {
|
||
background-color: #3c73ff;
|
||
color: #fff;
|
||
}
|
||
.im__chat-message-name {
|
||
text-align: right;
|
||
}
|
||
|
||
.im__chat-message-status {
|
||
margin-right: 8px;
|
||
}
|
||
}
|
||
}
|
||
.im__chat-message--to {
|
||
text-align: left;
|
||
align-items: flex-start;
|
||
.im__chat-message-body {
|
||
margin-left: 8px;
|
||
.im__chat-message-status {
|
||
margin-left: 8px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.im__chat-group {
|
||
width: 300px;
|
||
border-left: 1px solid #e6e9ef;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex-shrink: 0;
|
||
|
||
.im__group-section-header {
|
||
height: 46px;
|
||
border-bottom: 1px solid #e6e9ef;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0px 8px;
|
||
justify-content: space-between;
|
||
.im__group-section-header-title {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.im__group-section-header-text {
|
||
margin-left: 4px;
|
||
line-height: 1;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.im__group-section-header--search {
|
||
cursor: pointer;
|
||
color: #666666;
|
||
&:hover {
|
||
color: #1553f1 !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
.im__group-section-body {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow-y: auto;
|
||
min-height: 0;
|
||
|
||
.im__group-item {
|
||
height: 64px;
|
||
padding: 0 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
&:hover {
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
.im__group-item-info {
|
||
margin-left: 8px;
|
||
.im__group-item-name {
|
||
line-height: 1;
|
||
font-size: 14px;
|
||
}
|
||
.im__group-item-desc {
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.im__chat-footer {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
|
||
.im__chat-toolbar {
|
||
height: 36px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0px 8px;
|
||
gap: 8px;
|
||
.im__chat-toolbar-right {
|
||
margin-left: auto;
|
||
}
|
||
}
|
||
|
||
.im__chat-editor {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
overflow-y: auto;
|
||
flex: 1;
|
||
min-height: 0;
|
||
width: 100%;
|
||
scrollbar-gutter: stable both-edges;
|
||
|
||
.im__chat-editor-input {
|
||
padding: 4px;
|
||
flex: 1;
|
||
// overflow-y: auto;
|
||
box-sizing: border-box;
|
||
border: none;
|
||
resize: none;
|
||
background-color: transparent;
|
||
outline: none;
|
||
|
||
word-wrap: break-word;
|
||
word-break: break-all;
|
||
white-space: pre-wrap;
|
||
|
||
-webkit-user-select: text;
|
||
-moz-user-select: text;
|
||
user-select: text;
|
||
cursor: text;
|
||
}
|
||
}
|
||
|
||
.im__chat-sendarea {
|
||
height: 36px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
margin-left: auto;
|
||
padding: 0px 16px;
|
||
.im__chat-send {
|
||
background-color: #1553f1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.im__window-center {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
</style>
|