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
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>
|