u
This commit is contained in:
1 parent
a6c5e70f85
commit
2598098876
8 files changed
+646
-45
No files matched your search
@@ -8,6 +8,8 @@ import io.netty.channel.socket.SocketChannel;
|
|||||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||||
import io.netty.handler.codec.http.HttpObjectAggregator;
|
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||||
import io.netty.handler.codec.http.HttpServerCodec;
|
import io.netty.handler.codec.http.HttpServerCodec;
|
||||||
|
import io.netty.handler.codec.http.cors.CorsConfig;
|
||||||
|
import io.netty.handler.codec.http.cors.CorsHandler;
|
||||||
import io.netty.handler.stream.ChunkedWriteHandler;
|
import io.netty.handler.stream.ChunkedWriteHandler;
|
||||||
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
|
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
|
||||||
import io.netty.handler.timeout.IdleStateHandler;
|
import io.netty.handler.timeout.IdleStateHandler;
|
||||||
@@ -53,18 +55,11 @@ public class ImServer {
|
|||||||
protected void initChannel(SocketChannel ch) {
|
protected void initChannel(SocketChannel ch) {
|
||||||
ChannelPipeline pipeline = ch.pipeline();
|
ChannelPipeline pipeline = ch.pipeline();
|
||||||
|
|
||||||
// ===== HTTP 协议支持 =====
|
|
||||||
pipeline.addLast(new HttpServerCodec());
|
pipeline.addLast(new HttpServerCodec());
|
||||||
pipeline.addLast(new HttpObjectAggregator(65536));
|
pipeline.addLast(new HttpObjectAggregator(65536));
|
||||||
pipeline.addLast(new ChunkedWriteHandler());
|
pipeline.addLast(new ChunkedWriteHandler());
|
||||||
|
pipeline.addLast(new CorsHandler(CorsConfig.withAnyOrigin().build()));
|
||||||
// ===== 心跳检测 =====
|
|
||||||
pipeline.addLast(new IdleStateHandler(60, 0, 0, TimeUnit.SECONDS));
|
|
||||||
|
|
||||||
// ===== WebSocket 协议支持 =====
|
|
||||||
pipeline.addLast(new WebSocketServerProtocolHandler(websocketPath, null, true, 65536));
|
pipeline.addLast(new WebSocketServerProtocolHandler(websocketPath, null, true, 65536));
|
||||||
|
|
||||||
// ===== 自定义消息处理 =====
|
|
||||||
pipeline.addLast(new ImWebSocketHandler());
|
pipeline.addLast(new ImWebSocketHandler());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { onBeforeUnmount, onMounted } from 'vue'
|
||||||
|
import { useWebSocket } from './stores/websocketStore'
|
||||||
|
|
||||||
|
const websocket = useWebSocket()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
websocket.connect('ws://192.168.1.136:8090/ws')
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
websocket.disconnect()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -41,7 +41,12 @@ const iconSize = computed(() => (props.size ? `${props.size}px` : '24px'))
|
|||||||
</i>
|
</i>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
.icon-spin {
|
.icon-spin {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
animation: icon-spin 1s linear infinite;
|
animation: icon-spin 1s linear infinite;
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { ElNotification } from 'element-plus'
|
||||||
|
import { onScopeDispose, ref } from 'vue'
|
||||||
|
|
||||||
|
export function useWebSocket() {
|
||||||
|
const socket = ref(null)
|
||||||
|
const isConnected = ref(false)
|
||||||
|
const listeners = ref(new Set())
|
||||||
|
|
||||||
|
let reconnecting = false
|
||||||
|
let reconnectNotification = null
|
||||||
|
|
||||||
|
function connect(url) {
|
||||||
|
if (isConnected.value)
|
||||||
|
return
|
||||||
|
if (socket.value)
|
||||||
|
disconnect()
|
||||||
|
|
||||||
|
const ws = new WebSocket(url)
|
||||||
|
socket.value = ws
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
isConnected.value = true
|
||||||
|
reconnecting = false
|
||||||
|
// 关闭重连提示
|
||||||
|
if (reconnectNotification) {
|
||||||
|
reconnectNotification.close()
|
||||||
|
reconnectNotification = null
|
||||||
|
}
|
||||||
|
console.log('WebSocket 已连接')
|
||||||
|
ElNotification({
|
||||||
|
title: '成功',
|
||||||
|
message: 'WebSocket 已连接',
|
||||||
|
type: 'success',
|
||||||
|
showClose: false,
|
||||||
|
duration: 2000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(event.data)
|
||||||
|
notifyListeners(message)
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error('消息解析失败:', err)
|
||||||
|
notifyListeners({ type: 'ERROR', data: '消息解析失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = () => scheduleReconnect(url)
|
||||||
|
ws.onerror = () => scheduleReconnect(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
socket.value?.close()
|
||||||
|
isConnected.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage(type, data) {
|
||||||
|
if (isConnected.value && socket.value) {
|
||||||
|
socket.value.send(JSON.stringify({ type, data }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMessage(callback) {
|
||||||
|
listeners.value.add(callback)
|
||||||
|
return () => listeners.value.delete(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyListeners(message) {
|
||||||
|
listeners.value.forEach(cb => cb(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleReconnect(url) {
|
||||||
|
if (!reconnecting) {
|
||||||
|
reconnecting = true
|
||||||
|
reconnectNotification = ElNotification({
|
||||||
|
title: '提示',
|
||||||
|
message: 'WebSocket 连接断开,正在重连...',
|
||||||
|
type: 'warning',
|
||||||
|
showClose: false,
|
||||||
|
duration: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
console.log('WebSocket 即将重连...')
|
||||||
|
setTimeout(() => {
|
||||||
|
connect(url)
|
||||||
|
}, 3000) // 固定延迟重连,可按需改
|
||||||
|
}
|
||||||
|
|
||||||
|
onScopeDispose(() => disconnect())
|
||||||
|
|
||||||
|
return {
|
||||||
|
isConnected,
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
sendMessage,
|
||||||
|
onMessage,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,26 +36,48 @@ import Icon from '@/components/icon.vue'
|
|||||||
</div>
|
</div>
|
||||||
<div class="im__body">
|
<div class="im__body">
|
||||||
<div class="im__list">
|
<div class="im__list">
|
||||||
<div class="im__list-input">
|
<div class="im__list-search">
|
||||||
<el-input placeholder="请根据名称搜索" :prefix-icon="Search" />
|
<el-input placeholder="请根据名称搜索" :prefix-icon="Search" />
|
||||||
</div>
|
</div>
|
||||||
<div class="im__list-item">
|
<div class="im__list-item">
|
||||||
<div class="im__list-header">
|
<div class="im__list-header">
|
||||||
单聊
|
<Icon icon="tabler:message-circle" :size="16" />
|
||||||
|
<span class="im__list-header-title">
|
||||||
|
单聊
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="im__list-body">
|
<div class="im__list-body">
|
||||||
<div class="im__chat-user">
|
<div
|
||||||
<Avatar name="A" />
|
v-for="i in 10" :key="i" class="im__chat-item im__chat-item"
|
||||||
|
:class="[{ 'im__chat-item--selected': i === 1 }]"
|
||||||
<div class="im__chat-user-info">
|
>
|
||||||
<div class="im__chat-user-main">
|
<el-badge
|
||||||
<div class="im__chat-user-name">张三</div>
|
is-dot
|
||||||
<div class="im__chat-user-last-time">聊天时间</div>
|
color="#24e68a"
|
||||||
|
:badge-style="{
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
}"
|
||||||
|
:offset="[-4, 34]"
|
||||||
|
>
|
||||||
|
<!-- 未登录:b3b7c2 -->
|
||||||
|
<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">
|
||||||
|
张三张三张三张三张三张三张三张三张三张三
|
||||||
|
</div>
|
||||||
|
<div class="im__chat-item-last-time">
|
||||||
|
10:20
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="im__chat-user-message">
|
<div class="im__chat-item-message">
|
||||||
<div class="im__chat-user-last-message">下周一起吃个饭</div>
|
<div class="im__chat-item-last-message">
|
||||||
<div class="im__chat-user-unread-count">2</div>
|
下周下周下周下周下周下周下周下周下周下周
|
||||||
|
</div>
|
||||||
|
<el-badge :value="100" :max="10" class="im__chat-item-unread-count" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,23 +85,160 @@ import Icon from '@/components/icon.vue'
|
|||||||
</div>
|
</div>
|
||||||
<div class="im__list-item">
|
<div class="im__list-item">
|
||||||
<div class="im__list-header">
|
<div class="im__list-header">
|
||||||
群聊
|
<Icon icon="bi:people" :size="16" />
|
||||||
|
<span class="im__list-header-title">
|
||||||
|
群聊
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="im__list-body">
|
<div class="im__list-body">
|
||||||
<div>
|
<div v-for="i in 10" :key="i" class="im__chat-item">
|
||||||
Operation feedback: enable the users to clearly perceive their
|
<Avatar name="#" class="im__chat-item-avatar" bg-color="#574b90" />
|
||||||
operations by style updates and interactive effects;
|
<div class="im__chat-item-info">
|
||||||
</div>
|
<div class="im__chat-item-main">
|
||||||
<div>
|
<div class="im__chat-item-name">
|
||||||
Visual feedback: reflect current state by updating or rearranging
|
张三张三张三张三张三张三张三张三张三张三
|
||||||
elements of the page.
|
</div>
|
||||||
|
<div class="im__chat-item-last-time">
|
||||||
|
10:20
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="im__chat-item-message">
|
||||||
|
<div class="im__chat-item-last-message">
|
||||||
|
下周下周下周下周下周下周下周下周下周下周
|
||||||
|
</div>
|
||||||
|
<el-badge :value="100" :max="10" class="im__chat-item-unread-count" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="im__window">
|
<div class="im__window">
|
||||||
chat
|
<div class="im__window-header">
|
||||||
|
<div class="im__window-peer">
|
||||||
|
<div class="im__window-peer-avatar">
|
||||||
|
<Avatar name="A" />
|
||||||
|
</div>
|
||||||
|
<div class="im__window-peer-info">
|
||||||
|
<div class="im__window-peer-name">
|
||||||
|
张三
|
||||||
|
</div>
|
||||||
|
<div class="im__window-peer-desc">
|
||||||
|
<div class="im__window-peer-status im__window-peer-status--online">
|
||||||
|
<span>[ </span>
|
||||||
|
<div class="im__window-peer-status-dot" />
|
||||||
|
<span>在线</span>
|
||||||
|
<span> ]</span>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
公司描述等等等...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="im__window-actions" />
|
||||||
|
</div>
|
||||||
|
<div class="im__window-body">
|
||||||
|
<el-splitter layout="vertical">
|
||||||
|
<el-splitter-panel>
|
||||||
|
<div class="im__chat-main">
|
||||||
|
<div class="im__chat-message im__chat-message--from">
|
||||||
|
<div class="im__chat-message-avatar">
|
||||||
|
<Avatar name="A" />
|
||||||
|
</div>
|
||||||
|
<div class="im__chat-message-body">
|
||||||
|
<div class="im__chat-message-name">
|
||||||
|
张三
|
||||||
|
</div>
|
||||||
|
<div class="im__chat-message-content">
|
||||||
|
<div class="im__chat-message-content--text">
|
||||||
|
这是一条文本消息这是一条文本消息这是一条文本消息这是一条文本消息这是一条文本消息这是一条文本消息这是一条文本消息这是一条文本消息
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="im__chat-message im__chat-message--to">
|
||||||
|
<Avatar name="B" />
|
||||||
|
<div class="im__chat-message-body">
|
||||||
|
<div class="im__chat-message-name">
|
||||||
|
张三
|
||||||
|
</div>
|
||||||
|
<div class="im__chat-message-content">
|
||||||
|
<div class="im__chat-message-content--text">
|
||||||
|
这是一条文本消息
|
||||||
|
</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
|
||||||
|
class="im__chat-editor-input"
|
||||||
|
contenteditable="true"
|
||||||
|
/>
|
||||||
|
</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">群成员(222)</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="i in 20" :key="i" class="im__group-item">
|
||||||
|
<el-badge
|
||||||
|
is-dot
|
||||||
|
color="#24e68a"
|
||||||
|
: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">
|
||||||
|
name
|
||||||
|
</div>
|
||||||
|
<div class="im__group-item-desc">
|
||||||
|
desc
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,23 +255,23 @@ import Icon from '@/components/icon.vue'
|
|||||||
background-color: #f5f6fa;
|
background-color: #f5f6fa;
|
||||||
|
|
||||||
& *::-webkit-scrollbar {
|
& *::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 6px;
|
||||||
height: 8px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
& *::-webkit-scrollbar-track {
|
& *::-webkit-scrollbar-track {
|
||||||
background-color: #f1f1f1;
|
background-color: #fff;
|
||||||
border-radius: 10px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
& *::-webkit-scrollbar-thumb {
|
& *::-webkit-scrollbar-thumb {
|
||||||
background-color: #888;
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
border-radius: 10px;
|
border-radius: 3px;
|
||||||
border: 2px solid #f1f1f1;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
& *::-webkit-scrollbar-thumb:hover {
|
& *::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: #555;
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.im__container {
|
.im__container {
|
||||||
@@ -208,9 +367,9 @@ import Icon from '@/components/icon.vue'
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|
||||||
.im__list-input {
|
.im__list-search {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #1553f1;
|
border-bottom: 1px solid #e6e9ef;
|
||||||
}
|
}
|
||||||
|
|
||||||
.im__list-item {
|
.im__list-item {
|
||||||
@@ -218,26 +377,104 @@ import Icon from '@/components/icon.vue'
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
transition: background-color 0.25s ease;
|
||||||
|
|
||||||
.im__list-header {
|
.im__list-header {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
background-color: #f8f9fc;
|
background-color: #f8f9fc;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.im__list-header-title {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.im__list-body {
|
.im__list-body {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
.im__chat-user {
|
.im__chat-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
|
overflow-x: hidden;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
cursor: pointer;
|
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 {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,8 +483,262 @@ import Icon from '@/components/icon.vue'
|
|||||||
.im__window {
|
.im__window {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
padding: 10px;
|
display: flex;
|
||||||
overflow-y: auto;
|
flex-direction: column;
|
||||||
|
|
||||||
|
.im__window-header {
|
||||||
|
height: 64px;
|
||||||
|
border-bottom: 1px solid #e6e9ef;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
.im__window-peer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px 12px;
|
||||||
|
|
||||||
|
.im__window-peer-info {
|
||||||
|
margin-left: 8px;
|
||||||
|
.im__window-peer-name {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.im__window-peer-desc {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #666;
|
||||||
|
height: 12px;
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
display: flex;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 70%;
|
||||||
|
.im__chat-message-avatar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.im__chat-message-body {
|
||||||
|
.im__chat-message-name {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.im__chat-message-content {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.im__chat-message-content--text {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
word-break: break-all;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.im__chat-message--from {
|
||||||
|
align-items: flex-start;
|
||||||
|
.im__chat-message-body {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.im__chat-message--to {
|
||||||
|
align-self: flex-end;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
.im__chat-message-body {
|
||||||
|
margin-right: 8px;
|
||||||
|
.im__chat-message-content--text {
|
||||||
|
background-color: #3c73ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.im__chat-message-name {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 56px;
|
||||||
|
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: 6px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -490,7 +490,6 @@ function getRollbackContent(data: GetHistoryResult) {
|
|||||||
}
|
}
|
||||||
.chat-center-item-left {
|
.chat-center-item-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -498,7 +497,6 @@ function getRollbackContent(data: GetHistoryResult) {
|
|||||||
}
|
}
|
||||||
.chat-center-item-right {
|
.chat-center-item-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
|
|||||||
Reference in new issue
Block a user