You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

955 lines
27 KiB

<template>
<!-- 4T 弹窗一致透明遮罩可拖动记录位置不阻挡地图 -->
<div v-if="value" class="online-members-dialog">
<div class="panel-container" :style="panelStyle">
<div class="dialog-header" @mousedown="onDragStart">
<h3>{{ $t('onlineMembersDialog.title') }}</h3>
<div class="close-btn" @mousedown.stop @click="closeDialog">×</div>
</div>
<div class="dialog-body">
<el-tabs v-model="activeTab" type="card" size="small">
<el-tab-pane :label="$t('onlineMembersDialog.onlineMembers')" name="members">
<div class="members-list">
<div
v-for="member in displayOnlineMembers"
:key="member.id"
class="member-item"
:class="{ active: member.isEditing }"
>
<div class="member-avatar">
<el-avatar :size="36" :src="member.avatar">{{ member.name.charAt(0) }}</el-avatar>
</div>
<div class="member-info">
<div class="member-name">{{ member.name }}</div>
<div class="member-role">{{ member.role }}</div>
</div>
<div class="member-status">
<span class="status-dot online"></span>
<span class="status-text">{{ member.status }}</span>
</div>
<div v-if="member.isEditing" class="editing-badge">{{ $t('onlineMembersDialog.editing') }}</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane :label="$t('onlineMembersDialog.currentOperation')" name="current">
<div class="current-operation">
<div class="operation-section">
<h4>{{ $t('onlineMembersDialog.editStatus') }}</h4>
<div class="status-item">
<span class="status-label">{{ $t('onlineMembersDialog.currentEditor') }}:</span>
<span class="status-value">{{ currentEditor || $t('onlineMembersDialog.none') }}</span>
</div>
<div class="status-item">
<span class="status-label">{{ $t('onlineMembersDialog.editingObject') }}:</span>
<span class="status-value">{{ editingObject || $t('onlineMembersDialog.none') }}</span>
</div>
<div class="status-item">
<span class="status-label">{{ $t('onlineMembersDialog.editingTime') }}:</span>
<span class="status-value">{{ editingTime || $t('onlineMembersDialog.none') }}</span>
</div>
</div>
<div class="operation-section">
<h4>{{ $t('onlineMembersDialog.selectStatus') }}</h4>
<div class="status-item">
<span class="status-label">{{ $t('onlineMembersDialog.selectedObject') }}:</span>
<span class="status-value">{{ selectedObject || $t('onlineMembersDialog.none') }}</span>
</div>
<div class="status-item">
<span class="status-label">{{ $t('onlineMembersDialog.selectedCount') }}:</span>
<span class="status-value">{{ selectedCount }} {{ $t('onlineMembersDialog.unit') }}</span>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane :label="$t('onlineMembersDialog.operationLogs')" name="logs">
<div class="operation-logs">
<div class="logs-header">
<h4>{{ $t('onlineMembersDialog.objectOperationLogs') }}</h4>
<el-button
type="warning"
size="mini"
@click="showRollbackConfirm"
:disabled="operationLogs.length === 0"
>
<i class="el-icon-refresh-right"></i> {{ $t('onlineMembersDialog.rollbackOperation') }}
</el-button>
</div>
<el-timeline>
<el-timeline-item
v-for="log in operationLogs"
:key="log.id"
:timestamp="log.time"
:type="log.type"
>
<div class="log-content">
<div class="log-user">{{ log.user }}</div>
<div class="log-action">{{ log.action }}</div>
<div class="log-object">{{ $t('onlineMembersDialog.objectOperationLogs') }}:{{ log.object }}</div>
<div class="log-detail">{{ log.detail }}</div>
</div>
</el-timeline-item>
</el-timeline>
<el-dialog
:title="$t('onlineMembersDialog.rollbackConfirm')"
:visible.sync="showRollbackDialog"
width="400px"
center
append-to-body
>
<div class="rollback-confirm">
<p>{{ $t('onlineMembersDialog.rollbackConfirmText') }}</p>
<p class="text-warning mt-2">{{ $t('onlineMembersDialog.rollbackWarning') }}</p>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="showRollbackDialog = false">{{ $t('leftMenu.cancel') }}</el-button>
<el-button type="primary" @click="rollbackOperation">{{ $t('onlineMembersDialog.confirmRollback') }}</el-button>
</span>
</el-dialog>
</div>
</el-tab-pane>
<el-tab-pane :label="$t('onlineMembersDialog.chatRoom')" name="chat">
<div class="chat-room">
<div class="chat-header">
<el-radio-group v-model="chatMode" size="mini">
<el-radio-button label="group">{{ $t('onlineMembersDialog.groupChat') }}</el-radio-button>
<el-radio-button label="private">{{ $t('onlineMembersDialog.privateChat') }}</el-radio-button>
</el-radio-group>
<span class="online-count">{{ displayOnlineMembers.length }} {{ $t('onlineMembersDialog.onlineCount') }}</span>
</div>
<!-- 私聊:选择对象 -->
<div v-if="chatMode === 'private'" class="private-target">
<span class="target-label">{{ $t('onlineMembersDialog.selectMember') }}:</span>
<el-select v-model="privateChatTarget" :placeholder="$t('onlineMembersDialog.selectMemberToChat')" size="small" filterable class="target-select">
<el-option
v-for="m in chatableMembers"
:key="m.userId"
:label="m.name"
:value="m.userId"
>
<span>{{ m.name }}</span>
<span class="target-role">({{ m.role }})</span>
</el-option>
</el-select>
</div>
<!-- 聊天内容区域 -->
<div class="chat-content" ref="chatContent">
<template v-if="chatMode === 'group'">
<div
v-for="(message, index) in displayChatMessages"
:key="'g-' + index"
:class="['chat-message', isMessageSelf(message) ? 'self-message' : 'other-message']"
>
<div class="message-avatar">
<el-avatar :size="32" :src="getMessageAvatar(message)">{{ getMessageSenderName(message).charAt(0) }}</el-avatar>
</div>
<div class="message-content">
<div class="message-sender">{{ getMessageSenderName(message) }}</div>
<div class="message-text">{{ message.content }}</div>
<div class="message-time">{{ formatMessageTime(message.timestamp) }}</div>
</div>
</div>
</template>
<template v-else-if="chatMode === 'private' && privateChatTarget">
<div
v-for="(message, index) in displayPrivateMessages"
:key="'p-' + index"
:class="['chat-message', isPrivateMessageSelf(message) ? 'self-message' : 'other-message']"
>
<div class="message-avatar">
<el-avatar :size="32" :src="getPrivateMessageAvatar(message)">{{ getPrivateMessageSenderName(message).charAt(0) }}</el-avatar>
</div>
<div class="message-content">
<div class="message-sender">{{ getPrivateMessageSenderName(message) }}</div>
<div class="message-text">{{ message.content }}</div>
<div class="message-time">{{ formatMessageTime(message.timestamp) }}</div>
</div>
</div>
</template>
<div v-else-if="chatMode === 'private' && !privateChatTarget" class="chat-empty">
{{ $t('onlineMembersDialog.selectMemberToChat') }}
</div>
</div>
<!-- 聊天输入区域 -->
<div class="chat-input">
<el-input
v-model="newMessage"
:placeholder="chatMode === 'private' && !privateChatTarget ? $t('onlineMembersDialog.selectMemberFirst') : $t('onlineMembersDialog.inputMessage')"
:disabled="chatMode === 'private' && !privateChatTarget"
@keyup.enter="sendMessage"
clearable
></el-input>
<el-button type="primary" @click="sendMessage" class="send-btn" :disabled="chatMode === 'private' && !privateChatTarget">{{ $t('onlineMembersDialog.send') }}</el-button>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 右下角拖拽调整大小 -->
<div class="resize-handle" @mousedown="onResizeStart" title="拖动调整大小"></div>
</div>
</div>
</template>
<script>
const STORAGE_KEY_PREFIX = 'onlineMembersPanel_'
export default {
name: 'OnlineMembersDialog',
props: {
value: {
type: Boolean,
default: false
},
onlineMembers: {
type: Array,
default: () => []
},
roomId: {
type: [String, Number],
default: null
},
chatMessages: {
type: Array,
default: () => []
},
privateChatMessages: {
type: Object,
default: () => ({})
},
sendChat: {
type: Function,
default: null
},
sendPrivateChat: {
type: Function,
default: null
},
sendPrivateChatHistoryRequest: {
type: Function,
default: null
},
currentUserId: {
type: [Number, String],
default: null
}
},
data() {
return {
activeTab: 'members',
panelLeft: null,
panelTop: null,
panelWidth: 700,
panelHeight: 520,
isDragging: false,
dragStartX: 0,
dragStartY: 0,
isResizing: false,
resizeStartX: 0,
resizeStartY: 0,
resizeStartW: 0,
resizeStartH: 0,
showRollbackDialog: false,
// 在线成员数据(当 props.onlineMembers 为空时使用 mock)
_mockOnlineMembers: [
{ id: 1, name: '张三', role: '指挥官', status: '在线', isEditing: true, avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png' },
{ id: 2, name: '李四', role: '参谋', status: '在线', isEditing: false, avatar: 'https://cube.elemecdn.com/1/88/03b0d39583f48206768a7534e55bcpng.png' },
{ id: 3, name: '王五', role: '操作员', status: '在线', isEditing: false, avatar: 'https://cube.elemecdn.com/2/88/03b0d39583f48206768a7534e55bcpng.png' },
{ id: 4, name: '赵六', role: '观察员', status: '在线', isEditing: false, avatar: 'https://cube.elemecdn.com/3/88/03b0d39583f48206768a7534e55bcpng.png' },
{ id: 5, name: '孙七', role: '分析师', status: '在线', isEditing: false, avatar: 'https://cube.elemecdn.com/4/88/03b0d39583f48206768a7534e55bcpng.png' }
],
// 当前操作状态
currentEditor: '张三',
editingObject: 'J-20 歼击机',
editingTime: 'K+00:45:23',
selectedObject: 'Alpha进场航线',
selectedCount: 1,
// 操作日志
operationLogs: [
{
id: 1,
user: '张三',
action: '修改',
object: 'J-20 歼击机',
detail: '更新了速度参数从800km/h到850km/h',
time: 'K+00:45:23',
type: 'success'
},
{
id: 2,
user: '李四',
action: '选择',
object: 'Alpha进场航线',
detail: '选中了Alpha进场航线进行编辑',
time: 'K+00:42:15',
type: 'primary'
},
{
id: 3,
user: '王五',
action: '添加',
object: 'WP5',
detail: '在Beta巡逻航线上添加了新航点WP5',
time: 'K+00:38:47',
type: 'info'
},
{
id: 4,
user: '赵六',
action: '删除',
object: '旧侦察航线',
detail: '删除了过期的侦察航线',
time: 'K+00:35:12',
type: 'warning'
},
{
id: 5,
user: '孙七',
action: '修改',
object: 'HQ-9防空系统',
detail: '更新了射程参数从120km到150km',
time: 'K+00:32:08',
type: 'success'
}
],
newMessage: '',
chatMode: 'group',
privateChatTarget: null
};
},
computed: {
displayOnlineMembers() {
return (this.onlineMembers && this.onlineMembers.length > 0) ? this.onlineMembers : this._mockOnlineMembers;
},
chatableMembers() {
const cur = this.currentUserId != null ? Number(this.currentUserId) : null;
return this.displayOnlineMembers.filter(m => m.userId != null && Number(m.userId) !== cur);
},
displayChatMessages() {
return this.chatMessages || [];
},
displayPrivateMessages() {
if (!this.privateChatTarget) return [];
return this.privateChatMessages[this.privateChatTarget] || [];
},
selectedPrivateMember() {
if (!this.privateChatTarget) return null;
return this.chatableMembers.find(m => Number(m.userId) === Number(this.privateChatTarget));
},
panelStyle() {
const left = this.panelLeft != null ? this.panelLeft : (window.innerWidth - this.panelWidth) / 2 - 20;
const top = this.panelTop != null ? this.panelTop : (window.innerHeight - this.panelHeight) / 2 - 40;
return {
left: `${left}px`,
top: `${top}px`,
width: `${this.panelWidth}px`,
height: `${this.panelHeight}px`
};
}
},
methods: {
closeDialog() {
this.$emit('input', false);
},
getStorageKey() {
return STORAGE_KEY_PREFIX + (this.roomId || 'default');
},
loadPosition() {
try {
const key = this.getStorageKey();
const raw = localStorage.getItem(key);
if (raw) {
const d = JSON.parse(raw);
if (d.panelPosition) {
const left = Number(d.panelPosition.left);
const top = Number(d.panelPosition.top);
if (!isNaN(left) && left >= 0) this.panelLeft = Math.min(left, window.innerWidth - this.panelWidth);
if (!isNaN(top) && top >= 0) this.panelTop = Math.min(top, window.innerHeight - this.panelHeight);
}
if (d.panelSize) {
const w = Number(d.panelSize.width);
const h = Number(d.panelSize.height);
if (!isNaN(w) && w >= 400 && w <= 1000) this.panelWidth = w;
if (!isNaN(h) && h >= 300 && h <= window.innerHeight - 60) this.panelHeight = h;
}
}
} catch (e) {
console.warn('加载在线成员弹窗位置失败:', e);
}
},
savePosition() {
try {
const payload = { panelSize: { width: this.panelWidth, height: this.panelHeight } };
if (this.panelLeft != null && this.panelTop != null) {
payload.panelPosition = { left: this.panelLeft, top: this.panelTop };
}
localStorage.setItem(this.getStorageKey(), JSON.stringify(payload));
} catch (e) {
console.warn('保存在线成员弹窗位置失败:', e);
}
},
onDragStart(e) {
e.preventDefault();
this.isDragging = true;
const currentLeft = this.panelLeft != null ? this.panelLeft : (window.innerWidth - this.panelWidth) / 2 - 20;
const currentTop = this.panelTop != null ? this.panelTop : (window.innerHeight - this.panelHeight) / 2 - 40;
this.dragStartX = e.clientX - currentLeft;
this.dragStartY = e.clientY - currentTop;
document.addEventListener('mousemove', this.onDragMove);
document.addEventListener('mouseup', this.onDragEnd);
},
onDragMove(e) {
if (!this.isDragging) return;
e.preventDefault();
let left = e.clientX - this.dragStartX;
let top = e.clientY - this.dragStartY;
left = Math.max(0, Math.min(window.innerWidth - this.panelWidth, left));
top = Math.max(0, Math.min(window.innerHeight - this.panelHeight, top));
this.panelLeft = left;
this.panelTop = top;
},
onDragEnd() {
this.isDragging = false;
document.removeEventListener('mousemove', this.onDragMove);
document.removeEventListener('mouseup', this.onDragEnd);
this.savePosition();
},
onResizeStart(e) {
e.preventDefault();
e.stopPropagation();
this.isResizing = true;
this.resizeStartX = e.clientX;
this.resizeStartY = e.clientY;
this.resizeStartW = this.panelWidth;
this.resizeStartH = this.panelHeight;
document.addEventListener('mousemove', this.onResizeMove);
document.addEventListener('mouseup', this.onResizeEnd);
},
onResizeMove(e) {
if (!this.isResizing) return;
e.preventDefault();
const dx = e.clientX - this.resizeStartX;
const dy = e.clientY - this.resizeStartY;
this.panelWidth = Math.max(400, Math.min(1000, this.resizeStartW + dx));
this.panelHeight = Math.max(300, Math.min(window.innerHeight - 60, this.resizeStartH + dy));
},
onResizeEnd() {
this.isResizing = false;
document.removeEventListener('mousemove', this.onResizeMove);
document.removeEventListener('mouseup', this.onResizeEnd);
this.savePosition();
},
showRollbackConfirm() {
this.showRollbackDialog = true;
},
rollbackOperation() {
this.showRollbackDialog = false;
this.$message.success(this.$t('onlineMembersDialog.operationRollbackSuccess'));
// 这里可以添加实际的回滚逻辑
},
// 新增:发送聊天消息
sendMessage() {
const text = this.newMessage.trim();
if (!text) {
this.$message.warning(this.$t('onlineMembersDialog.pleaseInputMessage'));
return;
}
if (this.chatMode === 'group') {
if (this.sendChat) this.sendChat(text);
} else {
const target = this.selectedPrivateMember;
if (!target || !this.sendPrivateChat) return;
this.sendPrivateChat(target.userId, target.userName, text);
}
this.newMessage = '';
this.$nextTick(() => this.scrollChatToBottom());
},
formatMessageTime(ts) {
if (!ts) return '';
const d = new Date(typeof ts === 'number' ? ts : parseInt(ts, 10));
return d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0') + ':' + d.getSeconds().toString().padStart(2, '0');
},
isMessageSelf(msg) {
return msg.sender && Number(msg.sender.userId) === Number(this.currentUserId);
},
getMessageAvatar(msg) {
const av = (msg.sender && msg.sender.avatar) || '';
return this.resolveAvatarUrl(av);
},
getMessageSenderName(msg) {
if (!msg.sender) return '';
return msg.sender.nickName || msg.sender.userName || '';
},
isPrivateMessageSelf(msg) {
return msg.sender && Number(msg.sender.userId) === Number(this.currentUserId);
},
getPrivateMessageAvatar(msg) {
const av = (msg.sender && msg.sender.avatar) || '';
return this.resolveAvatarUrl(av);
},
getPrivateMessageSenderName(msg) {
if (!msg.sender) return '';
return msg.sender.nickName || msg.sender.userName || '';
},
scrollChatToBottom() {
const el = this.$refs.chatContent;
if (el) el.scrollTop = el.scrollHeight;
},
resolveAvatarUrl(av) {
if (!av) return '';
if (av.startsWith('http')) return av;
const base = process.env.VUE_APP_BACKEND_URL || (window.location.origin + (process.env.VUE_APP_BASE_API || '/dev-api'));
return base + av;
}
},
watch: {
value(val) {
if (val) this.loadPosition();
},
activeTab(newVal) {
if (newVal === 'chat') {
this.$nextTick(() => this.scrollChatToBottom());
}
},
chatMessages: {
handler() {
this.$nextTick(() => this.scrollChatToBottom());
},
deep: true
},
displayPrivateMessages: {
handler() {
this.$nextTick(() => this.scrollChatToBottom());
},
deep: true
},
chatableMembers: {
handler(members) {
if (this.privateChatTarget && !members.some(m => Number(m.userId) === Number(this.privateChatTarget))) {
this.privateChatTarget = null;
}
}
},
privateChatTarget(val) {
if (val && this.sendPrivateChatHistoryRequest) {
this.sendPrivateChatHistoryRequest(val);
}
}
}
};
</script>
<style scoped>
/* 与 4T 弹窗一致:透明遮罩、不阻挡地图点击 */
.online-members-dialog {
position: fixed;
inset: 0;
z-index: 1000;
background: transparent;
pointer-events: none;
}
.panel-container {
position: fixed;
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
overflow: hidden;
z-index: 1001;
pointer-events: auto;
display: flex;
flex-direction: column;
animation: dialog-fade-in 0.3s ease;
}
@keyframes dialog-fade-in {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #e8e8e8;
cursor: move;
}
.dialog-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.close-btn {
font-size: 20px;
color: #999;
cursor: pointer;
transition: color 0.3s;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: #666;
background: #f5f5f5;
border-radius: 50%;
}
.dialog-body {
padding: 20px;
flex: 1;
overflow-y: auto;
min-height: 0;
}
.resize-handle {
position: absolute;
right: 0;
bottom: 0;
width: 20px;
height: 20px;
cursor: nwse-resize;
user-select: none;
z-index: 10;
background: linear-gradient(to top left, transparent 50%, rgba(0, 138, 255, 0.2) 50%);
}
.resize-handle:hover {
background: linear-gradient(to top left, transparent 50%, rgba(0, 138, 255, 0.4) 50%);
}
/* 在线成员样式 */
.members-list {
display: flex;
flex-direction: column;
gap: 12px;
height: 400px;
overflow-y: auto;
}
.member-item {
display: flex;
align-items: center;
padding: 12px;
border-radius: 8px;
background: rgba(240, 242, 245, 0.8);
transition: all 0.3s;
}
.member-item:hover {
background: rgba(220, 233, 255, 0.8);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.15);
}
.member-item.active {
background: rgba(190, 220, 255, 0.8);
border: 1px solid rgba(0, 138, 255, 0.3);
box-shadow: 0 2px 10px rgba(0, 138, 255, 0.2);
}
.member-avatar {
margin-right: 12px;
}
.member-info {
flex: 1;
}
.member-name {
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.member-role {
font-size: 12px;
color: #666;
}
.member-status {
display: flex;
align-items: center;
gap: 6px;
margin-right: 12px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-dot.online {
background: #52c41a;
box-shadow: 0 0 4px rgba(82, 196, 26, 0.8);
}
.status-text {
font-size: 12px;
color: #666;
}
.editing-badge {
background: #ff7875;
color: white;
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
}
/* 当前操作样式 */
.current-operation {
display: flex;
flex-direction: column;
gap: 20px;
height: 400px;
overflow-y: auto;
}
.operation-section {
background: rgba(240, 242, 245, 0.8);
padding: 16px;
border-radius: 8px;
}
.operation-section h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
.status-item {
display: flex;
margin-bottom: 8px;
}
.status-label {
width: 100px;
font-size: 13px;
color: #666;
}
.status-value {
font-size: 13px;
color: #333;
font-weight: 500;
}
/* 操作日志样式 */
.operation-logs {
position: relative;
max-height: 400px;
overflow-y: auto;
}
.logs-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.logs-header h4 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
.log-content {
background: rgba(240, 242, 245, 0.8);
padding: 12px;
border-radius: 6px;
margin-left: 16px;
}
.log-user {
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.log-action {
font-size: 13px;
color: #666;
margin-bottom: 4px;
}
.log-object {
font-size: 12px;
color: #008aff;
margin-bottom: 2px;
}
.log-detail {
font-size: 12px;
color: #999;
}
/* 回滚确认样式 */
.rollback-confirm {
text-align: center;
}
.text-warning {
color: #fa8c16;
}
/* 新增:聊天室样式 */
.chat-room {
display: flex;
flex-direction: column;
height: 400px;
}
.chat-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.chat-header h4 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
.private-target {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.target-label {
font-size: 13px;
color: #666;
margin-right: 8px;
white-space: nowrap;
}
.target-select {
flex: 1;
min-width: 120px;
}
.target-role {
font-size: 12px;
color: #999;
margin-left: 4px;
}
.chat-empty {
color: #999;
font-size: 14px;
text-align: center;
padding: 40px 0;
}
.online-count {
font-size: 12px;
color: #008aff;
}
.chat-content {
flex: 1;
overflow-y: auto;
padding: 12px;
background: rgba(240, 242, 245, 0.5);
border-radius: 8px;
margin-bottom: 12px;
}
.chat-message {
display: flex;
margin-bottom: 16px;
max-width: 80%;
}
.self-message {
flex-direction: row-reverse;
margin-left: auto;
}
.other-message {
margin-right: auto;
}
.message-avatar {
margin: 0 8px;
}
.message-content {
background: white;
padding: 8px 12px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.self-message .message-content {
background: #e6f7ff;
}
.message-sender {
font-size: 12px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.message-text {
font-size: 13px;
color: #333;
line-height: 1.4;
margin-bottom: 4px;
}
.message-time {
font-size: 11px;
color: #999;
text-align: right;
}
.chat-input {
display: flex;
gap: 8px;
}
.chat-input .el-input {
flex: 1;
}
.send-btn {
width: 80px;
}
</style>