From e8b9fc6cc8e4f15baa5752d572250ff901f12c5e Mon Sep 17 00:00:00 2001 From: cuitw <1051735452@qq.com> Date: Wed, 25 Mar 2026 13:18:32 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-ui/src/assets/styles/element-variables.scss | 2 +- ruoyi-ui/src/assets/styles/variables.scss | 2 +- ruoyi-ui/src/components/ThemePicker/index.vue | 4 +- ruoyi-ui/src/lang/en.js | 12 +- ruoyi-ui/src/lang/zh.js | 12 +- ruoyi-ui/src/layout/components/Settings/index.vue | 2 +- ruoyi-ui/src/store/modules/settings.js | 2 +- ruoyi-ui/src/views/cesiumMap/ContextMenu.vue | 44 +- ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue | 26 +- ruoyi-ui/src/views/cesiumMap/HoverTooltip.vue | 3 +- .../src/views/cesiumMap/MapScreenDomLabels.vue | 20 +- ruoyi-ui/src/views/cesiumMap/MeasurementPanel.vue | 10 +- ruoyi-ui/src/views/cesiumMap/index.vue | 218 ++++- ruoyi-ui/src/views/childRoom/BottomLeftPanel.vue | 16 +- ruoyi-ui/src/views/childRoom/BottomTimeline.vue | 20 +- ruoyi-ui/src/views/childRoom/ConflictDrawer.vue | 8 +- ruoyi-ui/src/views/childRoom/FourTPanel.vue | 24 +- ruoyi-ui/src/views/childRoom/GanttDrawer.vue | 10 +- ruoyi-ui/src/views/childRoom/LeftMenu.vue | 42 +- ruoyi-ui/src/views/childRoom/RightPanel.vue | 74 +- ruoyi-ui/src/views/childRoom/SixStepsOverlay.vue | 16 +- ruoyi-ui/src/views/childRoom/StepCanvasContent.vue | 28 +- ruoyi-ui/src/views/childRoom/TaskPageContent.vue | 12 +- ruoyi-ui/src/views/childRoom/TopHeader.vue | 176 +++- .../views/childRoom/UnderstandingStepContent.vue | 28 +- ruoyi-ui/src/views/childRoom/WhiteboardPanel.vue | 8 +- ruoyi-ui/src/views/childRoom/index.vue | 223 +++-- ruoyi-ui/src/views/dialogs/IconSelectDialog.vue | 16 +- ruoyi-ui/src/views/dialogs/KTimeSetDialog.vue | 407 ++++++++- ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue | 993 ++++++++++++++++----- ruoyi-ui/src/views/dialogs/PageLayoutDialog.vue | 12 +- ruoyi-ui/src/views/dialogs/PlatformEditDialog.vue | 8 +- .../src/views/dialogs/PlatformImportDialog.vue | 2 +- ruoyi-ui/src/views/dialogs/RouteEditDialog.vue | 4 +- ruoyi-ui/src/views/ganttChart/index.vue | 6 +- ruoyi-ui/src/views/index.vue | 4 +- ruoyi-ui/src/views/login.vue | 24 +- ruoyi-ui/src/views/selectRoom/index.vue | 34 +- ruoyi-ui/src/views/system/lib/index.vue | 2 +- ruoyi-ui/src/views/system/rooms/index.vue | 2 +- ruoyi-ui/src/views/system/routes/index.vue | 2 +- ruoyi-ui/src/views/system/scenario/index.vue | 2 +- ruoyi-ui/src/views/system/users/index.vue | 2 +- ruoyi-ui/src/views/tool/build/RightPanel.vue | 4 +- ruoyi-ui/src/views/tool/build/index.vue | 6 +- 45 files changed, 1900 insertions(+), 672 deletions(-) diff --git a/ruoyi-ui/src/assets/styles/element-variables.scss b/ruoyi-ui/src/assets/styles/element-variables.scss index 1615ff2..636b9e1 100644 --- a/ruoyi-ui/src/assets/styles/element-variables.scss +++ b/ruoyi-ui/src/assets/styles/element-variables.scss @@ -4,7 +4,7 @@ **/ /* theme color */ -$--color-primary: #1890ff; +$--color-primary: #165dff; $--color-success: #13ce66; $--color-warning: #ffba00; $--color-danger: #ff4949; diff --git a/ruoyi-ui/src/assets/styles/variables.scss b/ruoyi-ui/src/assets/styles/variables.scss index 34484d4..26d1390 100644 --- a/ruoyi-ui/src/assets/styles/variables.scss +++ b/ruoyi-ui/src/assets/styles/variables.scss @@ -1,6 +1,6 @@ // base color $blue:#324157; -$light-blue:#3A71A8; +$light-blue:#165dff; $red:#C03639; $pink: #E65D6E; $green: #30B08F; diff --git a/ruoyi-ui/src/components/ThemePicker/index.vue b/ruoyi-ui/src/components/ThemePicker/index.vue index a69ee45..5ace9c2 100644 --- a/ruoyi-ui/src/components/ThemePicker/index.vue +++ b/ruoyi-ui/src/components/ThemePicker/index.vue @@ -1,14 +1,14 @@ diff --git a/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue b/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue index 89fb0f9..d63270b 100644 --- a/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue +++ b/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue @@ -8,8 +8,9 @@
- + +
{{ $t('onlineMembersDialog.editing') }}
+
+

{{ $t('onlineMembersDialog.editStatus') }}

@@ -63,9 +66,11 @@
+
+

{{ $t('onlineMembersDialog.objectOperationLogs') }}

@@ -164,35 +169,36 @@
+
+
-
- +
+
+ {{ $t('onlineMembersDialog.groupChat') }} {{ $t('onlineMembersDialog.privateChat') }} - {{ displayOnlineMembers.length }} {{ $t('onlineMembersDialog.onlineCount') }} -
- - -
- {{ $t('onlineMembersDialog.selectMember') }}: - - - {{ m.name }} - ({{ m.role }}) - - +
+ + {{ displayOnlineMembers.length }} {{ $t('onlineMembersDialog.onlineCount') }} + + +
- - +
+ @@ -216,38 +255,42 @@ :key="'p-' + index" :class="['chat-message', isPrivateMessageSelf(message) ? 'self-message' : 'other-message']" > -
- {{ getPrivateMessageSenderName(message).charAt(0) }} -
-
-
{{ getPrivateMessageSenderName(message) }}
-
{{ message.content }}
-
{{ formatMessageTime(message.timestamp) }}
+
+
+ {{ getPrivateMessageSenderName(message).charAt(0) }} +
+
+ {{ getPrivateMessageSenderName(message) }} + {{ formatMessageTime(message.timestamp) }} +
+
{{ message.content }}
-
- {{ $t('onlineMembersDialog.selectMemberToChat') }} -
- - -
+ +
- {{ $t('onlineMembersDialog.send') }} + /> + {{ $t('onlineMembersDialog.send') }} +
+
- -
@@ -257,6 +300,9 @@ import { listObjectLog, rollbackObjectLog } from '@/api/system/objectLog' const STORAGE_KEY_PREFIX = 'onlineMembersPanel_' const OP_TYPE_MAP = { 1: '新增', 2: '修改', 3: '删除', 4: '选择', 5: '回滚' } +/** 固定外框尺寸;小屏时由 panelStyle 收缩以不超出视口 */ +const PANEL_FIXED_WIDTH = 960 +const PANEL_FIXED_HEIGHT = 720 export default { name: 'OnlineMembersDialog', @@ -303,25 +349,20 @@ export default { activeTab: 'members', panelLeft: null, panelTop: null, - panelWidth: 700, - panelHeight: 520, + panelWidth: PANEL_FIXED_WIDTH, + panelHeight: PANEL_FIXED_HEIGHT, 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' } + { id: 1, userId: 1, userName: 'zhangsan', name: '张三', role: '指挥官', status: '在线', isEditing: true, avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png' }, + { id: 2, userId: 2, userName: 'lisi', name: '李四', role: '参谋', status: '在线', isEditing: false, avatar: 'https://cube.elemecdn.com/1/88/03b0d39583f48206768a7534e55bcpng.png' }, + { id: 3, userId: 3, userName: 'wangwu', name: '王五', role: '操作员', status: '在线', isEditing: false, avatar: 'https://cube.elemecdn.com/2/88/03b0d39583f48206768a7534e55bcpng.png' }, + { id: 4, userId: 4, userName: 'zhaoliu', name: '赵六', role: '观察员', status: '离线', isEditing: false, avatar: 'https://cube.elemecdn.com/3/88/03b0d39583f48206768a7534e55bcpng.png' }, + { id: 5, userId: 5, userName: 'sunqi', name: '孙七', role: '分析师', status: '在线', isEditing: false, avatar: 'https://cube.elemecdn.com/4/88/03b0d39583f48206768a7534e55bcpng.png' } ], // 当前操作状态 @@ -343,7 +384,8 @@ export default { newMessage: '', chatMode: 'group', - privateChatTarget: null + privateChatTarget: null, + privateContactPending: null }; }, computed: { @@ -368,15 +410,25 @@ export default { if (!this.privateChatTarget) return null; return this.chatableMembers.find(m => Number(m.userId) === Number(this.privateChatTarget)); }, + privateChatTargetName() { + const m = this.selectedPrivateMember; + if (m) return m.name || m.userName || ''; + return this.$t('onlineMembersDialog.selectMemberToChat'); + }, 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; + const pad = 16 + const effW = Math.min(this.panelWidth, Math.max(320, window.innerWidth - pad)) + const effH = Math.min(this.panelHeight, Math.max(400, window.innerHeight - pad)) + let left = this.panelLeft != null ? this.panelLeft : (window.innerWidth - effW) / 2 + let top = this.panelTop != null ? this.panelTop : (window.innerHeight - effH) / 2 + left = Math.max(0, Math.min(left, window.innerWidth - effW)) + top = Math.max(0, Math.min(top, window.innerHeight - effH)) return { left: `${left}px`, top: `${top}px`, - width: `${this.panelWidth}px`, - height: `${this.panelHeight}px` - }; + width: `${effW}px`, + height: `${effH}px` + } } }, methods: { @@ -386,90 +438,68 @@ export default { getStorageKey() { return STORAGE_KEY_PREFIX + (this.roomId || 'default'); }, + effectivePanelSize() { + const pad = 16 + const effW = Math.min(this.panelWidth, Math.max(320, window.innerWidth - pad)) + const effH = Math.min(this.panelHeight, Math.max(400, window.innerHeight - pad)) + return { effW, effH } + }, loadPosition() { try { - const key = this.getStorageKey(); - const raw = localStorage.getItem(key); + const key = this.getStorageKey() + const raw = localStorage.getItem(key) if (raw) { - const d = JSON.parse(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; + const left = Number(d.panelPosition.left) + const top = Number(d.panelPosition.top) + const { effW, effH } = this.effectivePanelSize() + if (!isNaN(left)) this.panelLeft = Math.max(0, Math.min(left, window.innerWidth - effW)) + if (!isNaN(top)) this.panelTop = Math.max(0, Math.min(top, window.innerHeight - effH)) } } } catch (e) { - console.warn('加载在线成员弹窗位置失败:', e); + console.warn('加载在线成员弹窗位置失败:', e) } }, savePosition() { try { - const payload = { panelSize: { width: this.panelWidth, height: this.panelHeight } }; + const payload = {} if (this.panelLeft != null && this.panelTop != null) { - payload.panelPosition = { left: this.panelLeft, top: this.panelTop }; + payload.panelPosition = { left: this.panelLeft, top: this.panelTop } } - localStorage.setItem(this.getStorageKey(), JSON.stringify(payload)); + localStorage.setItem(this.getStorageKey(), JSON.stringify(payload)) } catch (e) { - console.warn('保存在线成员弹窗位置失败:', 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); + e.preventDefault() + this.isDragging = true + const { effW, effH } = this.effectivePanelSize() + const currentLeft = this.panelLeft != null ? this.panelLeft : (window.innerWidth - effW) / 2 + const currentTop = this.panelTop != null ? this.panelTop : (window.innerHeight - effH) / 2 + 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; + if (!this.isDragging) return + e.preventDefault() + const { effW, effH } = this.effectivePanelSize() + let left = e.clientX - this.dragStartX + let top = e.clientY - this.dragStartY + left = Math.max(0, Math.min(window.innerWidth - effW, left)) + top = Math.max(0, Math.min(window.innerHeight - effH, 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(); + this.isDragging = false + document.removeEventListener('mousemove', this.onDragMove) + document.removeEventListener('mouseup', this.onDragEnd) + this.savePosition() }, fetchOperationLogs() { @@ -711,6 +741,44 @@ export default { 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; + }, + contactAvatarColor(index) { + const colors = ['#1877f2', '#10b981', '#f59e0b', '#7239ea', '#86909c', '#ea4c89']; + return colors[index % colors.length]; + }, + memberInitial(m) { + const n = (m && (m.name || m.userName)) || ''; + return n ? String(n).charAt(0) : '?'; + }, + isMemberOnline(m) { + if (!m || !m.status) return true; + return m.status !== '离线' && String(m.status).toLowerCase() !== 'offline'; + }, + memberOnlineLabel(m) { + return this.isMemberOnline(m) + ? this.$t('onlineMembersDialog.memberStatusOnline') + : this.$t('onlineMembersDialog.memberStatusOffline'); + }, + isPrivateContactRowActive(m) { + if (this.privateContactPending == null || !m || m.userId == null) return false; + return Number(this.privateContactPending) === Number(m.userId); + }, + cancelPrivateContactPick() { + this.privateContactPending = null; + this.chatMode = 'group'; + }, + confirmPrivateContactPick() { + if (this.privateContactPending == null || this.privateContactPending === '') { + this.$message.warning(this.$t('onlineMembersDialog.selectMemberToChat')); + return; + } + this.privateChatTarget = this.privateContactPending; + this.privateContactPending = null; + this.$nextTick(() => this.scrollChatToBottom()); + }, + clearPrivateChatTarget() { + this.privateChatTarget = null; + this.privateContactPending = null; } }, watch: { @@ -742,6 +810,7 @@ export default { handler(members) { if (this.privateChatTarget && !members.some(m => Number(m.userId) === Number(this.privateChatTarget))) { this.privateChatTarget = null; + this.privateContactPending = null; } } }, @@ -749,6 +818,9 @@ export default { if (val && this.sendPrivateChatHistoryRequest) { this.sendPrivateChatHistoryRequest(val); } + }, + chatMode() { + this.privateContactPending = null; } } }; @@ -766,9 +838,9 @@ export default { .panel-container { position: fixed; - background: white; - border-radius: 8px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + background: #ffffff; + border-radius: 10px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); overflow: hidden; z-index: 1001; pointer-events: auto; @@ -792,16 +864,18 @@ export default { display: flex; align-items: center; justify-content: space-between; - padding: 16px 20px; - border-bottom: 1px solid #e8e8e8; + padding: 14px 18px; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); cursor: move; + flex-shrink: 0; + background: #ffffff; } .dialog-header h3 { margin: 0; font-size: 16px; font-weight: 600; - color: #333; + color: #1d2129; } .close-btn { @@ -823,56 +897,131 @@ export default { } .dialog-body { - padding: 20px; + padding: 0; + flex: 1; + min-height: 0; + overflow: hidden; + display: flex; + flex-direction: column; + background: #f9fafb; +} + +/* Tab 区域撑满弹窗余项,各 Tab 内容在 tab-pane-fill 内再滚动 */ +.online-members-el-tabs { + --tab-accent: #1877f2; + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; +} + +.online-members-el-tabs ::v-deep .el-tabs__header { + flex-shrink: 0; + margin: 0; + padding: 0 14px 8px; + background: #ffffff; +} + +.online-members-el-tabs ::v-deep .el-tabs__nav-wrap { + background: #ffffff; +} + +.online-members-el-tabs ::v-deep .el-tabs__nav-wrap::after { + height: 1px; + background-color: rgba(0, 0, 0, 0.06); +} + +.online-members-el-tabs ::v-deep .el-tabs__item { + height: 44px; + line-height: 44px; + padding: 0 20px; + font-size: 15px; + font-weight: 500; + color: #86909c; +} + +.online-members-el-tabs ::v-deep .el-tabs__item:hover { + color: #4e5969; +} + +.online-members-el-tabs ::v-deep .el-tabs__item.is-active { + color: var(--tab-accent); + font-weight: 600; +} + +.online-members-el-tabs ::v-deep .el-tabs__active-bar { + height: 3px; + border-radius: 3px 3px 0 0; + background-color: var(--tab-accent); +} + +.online-members-el-tabs ::v-deep .el-tabs__content { flex: 1; - overflow-y: auto; min-height: 0; + height: 0; + overflow: hidden; + background: transparent; } -.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%); +.online-members-el-tabs ::v-deep .el-tab-pane { + height: 100%; + overflow: hidden; + background: transparent; +} + +.tab-pane-fill { + height: 100%; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + background: transparent; + padding: 8px 14px 14px; + box-sizing: border-box; } -.resize-handle:hover { - background: linear-gradient(to top left, transparent 50%, rgba(0, 138, 255, 0.4) 50%); +/* 聊天 Tab:与 body 同色铺满,无额外白边 */ +.tab-pane-fill--chat { + margin: 0; + width: 100%; + padding: 0; + background: transparent; + border-radius: 0; + overflow: hidden; } /* 在线成员样式 */ .members-list { display: flex; flex-direction: column; - gap: 12px; - height: 400px; + gap: 0; + flex: 1; + min-height: 0; + overflow-x: hidden; 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; + padding: 14px 6px; + border-radius: 0; + background: transparent; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + transition: background 0.2s ease; +} + +.member-item:last-child { + border-bottom: none; } .member-item:hover { - background: rgba(220, 233, 255, 0.8); - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(0, 138, 255, 0.15); + background: rgba(255, 255, 255, 0.35); } .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); + background: rgba(24, 119, 242, 0.1); + box-shadow: inset 3px 0 0 #1877f2; } .member-avatar { @@ -929,15 +1078,22 @@ export default { .current-operation { display: flex; flex-direction: column; - gap: 20px; - height: 400px; + gap: 0; + flex: 1; + min-height: 0; + overflow-x: hidden; overflow-y: auto; } .operation-section { - background: rgba(240, 242, 245, 0.8); - padding: 16px; - border-radius: 8px; + background: transparent; + padding: 14px 4px; + border-radius: 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); +} + +.operation-section:last-child { + border-bottom: none; } .operation-section h4 { @@ -967,9 +1123,11 @@ export default { /* 操作日志样式 */ .operation-logs { position: relative; - max-height: 480px; + flex: 1; + min-height: 0; display: flex; flex-direction: column; + overflow: hidden; } .logs-header { @@ -977,6 +1135,7 @@ export default { align-items: center; justify-content: space-between; margin-bottom: 16px; + flex-shrink: 0; } .logs-header h4 { @@ -988,8 +1147,9 @@ export default { .logs-body { flex: 1; + min-height: 0; + overflow-x: hidden; overflow-y: auto; - min-height: 200px; } .logs-empty { @@ -1003,24 +1163,26 @@ export default { margin-top: 12px; padding: 8px 0; text-align: right; + flex-shrink: 0; } .log-content { - background: rgba(240, 242, 245, 0.8); - padding: 12px; - border-radius: 6px; - margin-left: 16px; + background: transparent; + padding: 10px 8px 10px 12px; + border-radius: 0; + margin-left: 8px; cursor: pointer; - border: 2px solid transparent; + border: none; + border-left: 3px solid rgba(24, 119, 242, 0.35); } .log-content:hover { - background: rgba(230, 235, 242, 0.9); + background: rgba(255, 255, 255, 0.35); } .log-content.selected { - border-color: #e6a23c; - background: rgba(253, 246, 236, 0.95); + border-left-color: #e6a23c; + background: rgba(255, 255, 255, 0.45); } .log-user { @@ -1037,7 +1199,7 @@ export default { .log-object { font-size: 12px; - color: #008aff; + color: #165dff; margin-bottom: 2px; } @@ -1055,132 +1217,493 @@ export default { color: #fa8c16; } -/* 新增:聊天室样式 */ +/* 聊天室:整块 #f9fafb(含群聊/私聊工具条)| 底部白底悬浮输入条 */ .chat-room { + --chat-primary: #1877f2; + --chat-primary-dark: #166fe5; + --chat-surface-bg: #f9fafb; display: flex; flex-direction: column; - height: 400px; + flex: 1; + min-height: 0; + font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; } -.chat-header { +.chat-dark-wrap { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + background: var(--chat-surface-bg); + border-radius: 0 0 10px 10px; + overflow: hidden; + position: relative; +} + +.chat-top-bar { display: flex; - align-items: center; justify-content: space-between; - margin-bottom: 12px; + align-items: center; + padding: 12px 14px; + flex-shrink: 0; + background: var(--chat-surface-bg); + border-bottom: none; } -.chat-header h4 { - margin: 0; +.chat-status-tips { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 8px; + min-height: 32px; +} + +.change-private-contact-btn { + border: none; + background: #f0f7ff; + color: var(--chat-primary); + font-size: 13px; + font-weight: 500; + padding: 6px 12px; + border-radius: 8px; + cursor: pointer; + transition: background 0.2s ease, color 0.2s ease; +} + +.change-private-contact-btn:hover { + background: #e8f4ff; + color: var(--chat-primary-dark); +} + +.online-tip { + font-size: 14px; + color: var(--chat-primary); + font-weight: 500; + background: #f0f7ff; + padding: 6px 12px; + border-radius: 12px; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.online-tip::before { + content: ''; + width: 8px; + height: 8px; + background: var(--chat-primary); + border-radius: 50%; + flex-shrink: 0; +} + +.private-chat-tip { font-size: 14px; + color: var(--chat-primary); + font-weight: 500; + background: #f0f7ff; + padding: 6px 12px; + border-radius: 12px; + display: inline-flex; + align-items: center; + gap: 6px; + max-width: min(280px, 42vw); + overflow: hidden; +} + +.private-chat-tip::before { + content: ''; + width: 8px; + height: 8px; + background: var(--chat-primary); + border-radius: 50%; + flex-shrink: 0; +} + +.private-contact-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; font-weight: 600; - color: #333; } -.private-target { +/* 群聊/私聊切换 → 胶囊按钮外观 */ +.chat-room .chat-mode-switch { + display: inline-flex; + gap: 8px; +} + +.chat-room .chat-mode-switch ::v-deep .el-radio-button__inner { + padding: 8px 20px; + border: 1px solid #dcdfe6; + border-radius: 6px !important; + font-size: 14px; + font-weight: 500; + color: #4e5969; + background: #ffffff; + box-shadow: none; + transition: all 0.2s ease; +} + +.chat-room .chat-mode-switch ::v-deep .el-radio-button:first-child .el-radio-button__inner { + border-radius: 6px !important; +} + +.chat-room .chat-mode-switch ::v-deep .el-radio-button:last-child .el-radio-button__inner { + border-radius: 6px !important; +} + +.chat-room .chat-mode-switch ::v-deep .el-radio-button__orig-radio:checked + .el-radio-button__inner { + background: var(--chat-primary); + color: #ffffff; + border-color: var(--chat-primary); + box-shadow: 0 2px 8px rgba(24, 119, 242, 0.2); +} + +.contact-selector-panel { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + background: transparent; + border-radius: 0; + box-shadow: none; + border: none; + overflow: hidden; +} + +.contact-selector-title { + font-size: 16px; + font-weight: 600; + color: #1d2129; + padding: 4px 2px 12px; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + flex-shrink: 0; +} + +.contact-list-empty { + flex: 1; display: flex; align-items: center; - margin-bottom: 10px; + justify-content: center; + padding: 32px 20px; + color: #86909c; + font-size: 14px; } -.target-label { - font-size: 13px; - color: #666; - margin-right: 8px; - white-space: nowrap; +.contact-list-scroll { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 8px 0; + display: flex; + flex-direction: column; + gap: 0; +} + +.contact-row { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 10px; + border-radius: 0; + cursor: pointer; + transition: background 0.2s ease, box-shadow 0.2s ease; + border: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.contact-row:last-of-type { + border-bottom: none; } -.target-select { +.contact-row:hover { + background: rgba(255, 255, 255, 0.85); +} + +.contact-row.active { + background: rgba(24, 119, 242, 0.12); + border-color: transparent; + box-shadow: inset 3px 0 0 #1877f2; +} + +.contact-row-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 14px; + font-weight: 600; + flex-shrink: 0; +} + +.contact-row-info { flex: 1; - min-width: 120px; + min-width: 0; +} + +.contact-row-name { + font-size: 14px; + font-weight: 500; + color: #1d2129; } -.target-role { +.contact-row-status { font-size: 12px; - color: #999; - margin-left: 4px; + margin-top: 2px; + color: #86909c; +} + +.contact-row-status.online { + color: #10b981; +} + +.contact-row-status.offline { + color: #86909c; +} + +.contact-selector-footer { + flex-shrink: 0; + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 12px 4px 4px; + border-top: 1px solid rgba(0, 0, 0, 0.06); + background: transparent; } .chat-empty { - color: #999; + color: #86909c; font-size: 14px; text-align: center; - padding: 40px 0; -} - -.online-count { - font-size: 12px; - color: #008aff; + padding: 48px 16px; + line-height: 1.6; } .chat-content { flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + overflow-x: hidden; overflow-y: auto; - padding: 12px; - background: rgba(240, 242, 245, 0.5); - border-radius: 8px; - margin-bottom: 12px; + padding: 14px 12px 20px; + margin-bottom: 0; + background: var(--chat-surface-bg); + border-radius: 0; + scroll-behavior: smooth; +} + +.chat-content > .contact-selector-panel { + flex: 1; + min-height: 0; +} + +.chat-content::-webkit-scrollbar { + width: 8px; +} + +.chat-content::-webkit-scrollbar-track { + background: transparent; + border-radius: 4px; +} + +.chat-content::-webkit-scrollbar-thumb { + background: #d0d3d9; + border-radius: 4px; +} + +.chat-content::-webkit-scrollbar-thumb:hover { + background: #b1b5c0; } .chat-message { display: flex; - margin-bottom: 16px; - max-width: 80%; + flex-direction: column; + margin-bottom: 22px; + width: 100%; + max-width: 100%; + animation: chat-msg-fade-in 0.3s ease-out; +} + +@keyframes chat-msg-fade-in { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.other-message { + align-items: flex-start; + align-self: flex-start; + max-width: 78%; } .self-message { - flex-direction: row-reverse; + align-items: flex-end; + align-self: flex-end; + max-width: 78%; margin-left: auto; } -.other-message { - margin-right: auto; +.message-header-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.self-message .message-header-row { + flex-direction: row-reverse; +} + +.message-meta { + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: 8px; +} + +.self-message .message-time { + margin-left: 0; } .message-avatar { - margin: 0 8px; + flex-shrink: 0; } -.message-content { - background: white; - padding: 8px 12px; - border-radius: 8px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +.message-avatar ::v-deep .el-avatar { + border: 2px solid #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); } -.self-message .message-content { - background: #e6f7ff; +.other-message .message-avatar ::v-deep .el-avatar { + background: linear-gradient(135deg, #e8eaed, #d0d3d9) !important; + color: #4e5969 !important; } -.message-sender { - font-size: 12px; - font-weight: 600; - color: #333; - margin-bottom: 4px; +.self-message .message-avatar ::v-deep .el-avatar { + background: linear-gradient(135deg, var(--chat-primary), var(--chat-primary-dark)) !important; + color: #fff !important; + box-shadow: 0 2px 8px rgba(24, 119, 242, 0.3); } -.message-text { +.message-sender { font-size: 13px; - color: #333; - line-height: 1.4; - margin-bottom: 4px; + font-weight: 500; +} + +.other-message .message-sender { + color: #4e5969; +} + +.self-message .message-sender { + color: var(--chat-primary); } .message-time { font-size: 11px; - color: #999; - text-align: right; + color: #86909c; +} + +.message-bubble { + padding: 14px 18px; + border-radius: 10px; + max-width: 100%; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + font-size: 14px; + line-height: 1.5; + word-wrap: break-word; + word-break: break-word; +} + +.other-message .message-bubble { + background: #ffffff; + color: #1d2129; + border: 1px solid #f0f2f5; + border-radius: 0 10px 10px 10px; +} + +.self-message .message-bubble { + background: linear-gradient(135deg, var(--chat-primary), var(--chat-primary-dark)); + color: #ffffff; + border-radius: 10px 0 10px 10px; + box-shadow: 0 2px 8px rgba(24, 119, 242, 0.15); } -.chat-input { +.chat-input-area { display: flex; - gap: 8px; + gap: 12px; + align-items: flex-end; + flex-shrink: 0; + position: relative; + z-index: 2; + padding: 12px 14px; + margin: -8px 14px 14px; + background: #ffffff; + border: none; + border-radius: 12px; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.04); } -.chat-input .el-input { +.chat-input-field { flex: 1; + min-width: 0; +} + +.chat-input-field ::v-deep .el-input__inner { + min-height: 44px; + line-height: 1.5; + padding: 12px 16px; + border-radius: 8px; + border: 1px solid #e8eaed; + background: #f9fafb; + font-size: 14px; + transition: all 0.2s ease; +} + +.chat-input-field ::v-deep .el-input__inner:focus { + border-color: var(--chat-primary); + box-shadow: 0 0 0 3px rgba(24, 119, 242, 0.1); + background: #fff; +} + +.chat-input-field ::v-deep .el-input.is-disabled .el-input__inner { + background: #f5f7fa; + color: #c0c4cc; +} + +.chat-send-btn { + height: 44px; + padding: 0 24px; + flex-shrink: 0; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + background: var(--chat-primary) !important; + box-shadow: 0 2px 8px rgba(24, 119, 242, 0.15); + transition: all 0.2s ease; +} + +.chat-send-btn:hover:not(.is-disabled), +.chat-send-btn:focus:not(.is-disabled) { + background: var(--chat-primary-dark) !important; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(24, 119, 242, 0.2); } -.send-btn { - width: 80px; +.chat-send-btn.is-disabled, +.chat-send-btn.is-disabled:hover { + transform: none; + box-shadow: none; + opacity: 0.55; } \ No newline at end of file diff --git a/ruoyi-ui/src/views/dialogs/PageLayoutDialog.vue b/ruoyi-ui/src/views/dialogs/PageLayoutDialog.vue index e61ba49..4f8f0fb 100644 --- a/ruoyi-ui/src/views/dialogs/PageLayoutDialog.vue +++ b/ruoyi-ui/src/views/dialogs/PageLayoutDialog.vue @@ -160,15 +160,15 @@ export default { } .position-option:hover { - border-color: #008aff; - background: rgba(0, 138, 255, 0.05); + border-color: #165dff; + background: rgba(22, 93, 255, 0.05); transform: translateY(-2px); - box-shadow: 0 2px 8px rgba(0, 138, 255, 0.15); + box-shadow: 0 2px 8px rgba(22, 93, 255, 0.15); } .position-option.active { - border-color: #008aff; - background: rgba(0, 138, 255, 0.1); + border-color: #165dff; + background: rgba(22, 93, 255, 0.1); } .position-option i { @@ -180,7 +180,7 @@ export default { .position-option:hover i, .position-option.active i { - color: #008aff; + color: #165dff; } .position-option span { diff --git a/ruoyi-ui/src/views/dialogs/PlatformEditDialog.vue b/ruoyi-ui/src/views/dialogs/PlatformEditDialog.vue index 0e136da..f198820 100644 --- a/ruoyi-ui/src/views/dialogs/PlatformEditDialog.vue +++ b/ruoyi-ui/src/views/dialogs/PlatformEditDialog.vue @@ -453,7 +453,7 @@ export default { border-radius: 4px; display: inline-block; margin: 15px 0 10px; - border-left: 3px solid #1890ff; + border-left: 3px solid #165dff; } /* 5. 图标预览:强制左对齐 */ @@ -476,7 +476,7 @@ export default { } .upload-preview-box:hover { - border-color: #409EFF; + border-color: #165dff; } .avatar-img { @@ -571,10 +571,10 @@ export default { cursor: nwse-resize; user-select: none; z-index: 10; - background: linear-gradient(to top left, transparent 50%, rgba(24, 144, 255, 0.2) 50%); + background: linear-gradient(to top left, transparent 50%, rgba(22, 93, 255, 0.2) 50%); } .resize-handle:hover { - background: linear-gradient(to top left, transparent 50%, rgba(24, 144, 255, 0.4) 50%); + background: linear-gradient(to top left, transparent 50%, rgba(22, 93, 255, 0.4) 50%); } diff --git a/ruoyi-ui/src/views/dialogs/PlatformImportDialog.vue b/ruoyi-ui/src/views/dialogs/PlatformImportDialog.vue index 0efc954..a88b407 100644 --- a/ruoyi-ui/src/views/dialogs/PlatformImportDialog.vue +++ b/ruoyi-ui/src/views/dialogs/PlatformImportDialog.vue @@ -101,7 +101,7 @@ export default { diff --git a/ruoyi-ui/src/views/childRoom/WhiteboardPanel.vue b/ruoyi-ui/src/views/childRoom/WhiteboardPanel.vue index e5ac70e..f612d5e 100644 --- a/ruoyi-ui/src/views/childRoom/WhiteboardPanel.vue +++ b/ruoyi-ui/src/views/childRoom/WhiteboardPanel.vue @@ -29,12 +29,15 @@ - +
绘制: 空域 + + 框选 +
@@ -83,15 +86,17 @@ - +
- 平台: - - 全部 - 空中 - 海上 - 地面 - +
+ 平台: + + 全部 + 空中 + 海上 + 地面 + +
@@ -433,6 +437,12 @@ :room-id="currentRoomId" /> + + @@ -527,6 +539,7 @@ import RightPanel from './RightPanel' import BottomLeftPanel from './BottomLeftPanel' import TopHeader from './TopHeader' import FourTPanel from './FourTPanel' +import ScreenshotGalleryPanel from './ScreenshotGalleryPanel' import ConflictDrawer from './ConflictDrawer' import WhiteboardPanel from './WhiteboardPanel' import { createRoomWebSocket } from '@/utils/websocket'; @@ -579,6 +592,7 @@ export default { BottomLeftPanel, TopHeader, FourTPanel, + ScreenshotGalleryPanel, ConflictDrawer, WhiteboardPanel }, @@ -680,6 +694,7 @@ export default { { id: 'deduction', name: '推演', icon: 'el-icon-video-play' }, { id: 'modify', name: '测距', icon: 'cj' }, { id: 'refresh', name: '截图', icon: 'screenshot', action: 'refresh' }, + { id: 'screenshotGallery', name: '截图展示', icon: 'el-icon-picture-outline-round' }, { id: 'basemap', name: '底图', icon: 'dt' }, { id: 'datacard', name: '数据卡', icon: 'shujukapian' }, { id: 'save', name: '保存', icon: 'el-icon-document-checked' }, @@ -741,6 +756,8 @@ export default { showKTimePopup: false, // 4T悬浮窗显示控制(仅点击4T图标时打开/关闭) show4TPanel: false, + /** 截图展示悬浮窗(多图翻页,与 4T 同属穿透交互) */ + showScreenshotGalleryPanel: false, /** 冲突列表弹窗(点击左侧冲突按钮即打开并自动执行检测) */ showConflictDrawer: false, /** 冲突检测进行中(避免主线程长时间阻塞导致页面卡死) */ @@ -2379,33 +2396,42 @@ export default { this.selectedPlatform = JSON.parse(JSON.stringify(platform)); this.showPlatformDialog = true; }, + /** + * 平台库 type 与 UI 分类对齐:后端为 Aircraft/Ship/Vehicle/Radar(见平台库管理), + * 历史上曾用 Air/Sea/Ground,需一并兼容。 + */ + classifyPlatformLibBucket(typeStr) { + const t = String(typeStr || '').trim().toLowerCase() + if (['ship', 'sea'].includes(t)) return 'sea' + if (['ground', 'vehicle'].includes(t)) return 'ground' + if (['air', 'aircraft', 'radar'].includes(t)) return 'air' + return 'air' + }, /** 从数据库查询并分拣平台库数据 */ getPlatformList() { listLib().then(res => { - const allData = res.rows || []; - this.airPlatforms = []; - this.seaPlatforms = []; - this.groundPlatforms = []; + const allData = res.rows || [] + this.airPlatforms = [] + this.seaPlatforms = [] + this.groundPlatforms = [] allData.forEach(item => { + const iconUrl = item.iconUrl || '' const platform = { id: item.id, name: item.name, type: item.type, specsJson: item.specsJson, - imageUrl: item.iconUrl || '', - icon: item.iconUrl ? '' : 'el-icon-picture-outline', + imageUrl: iconUrl, + iconUrl, + icon: iconUrl ? '' : 'el-icon-picture-outline', status: 'ready' - }; - if (item.type === 'Air') { - this.airPlatforms.push(platform); - } else if (item.type === 'Sea') { - this.seaPlatforms.push(platform); - } else if (item.type === 'Ground') { - this.groundPlatforms.push(platform); } - }); - - }); + const bucket = this.classifyPlatformLibBucket(item.type) + if (bucket === 'sea') this.seaPlatforms.push(platform) + else if (bucket === 'ground') this.groundPlatforms.push(platform) + else this.airPlatforms.push(platform) + }) + }) }, /** 导入确认:将弹窗填写的模版数据存入数据库 */ handleImportConfirm(formData) { @@ -3755,6 +3781,7 @@ export default { } else { this.drawDom = false this.airspaceDrawDom = false + this.whiteboardAirspaceDraw = false this.isRightPanelHidden = true } }, @@ -3859,7 +3886,9 @@ export default { this.whiteboardAirspaceDraw = false this.isRightPanelHidden = true this.show4TPanel = false + this.showScreenshotGalleryPanel = false await this.loadWhiteboards() + this.getPlatformList() if (this.whiteboards.length === 0) { await this.handleWhiteboardCreate() } else { @@ -3938,6 +3967,14 @@ export default { handleWhiteboardExit() { this.showWhiteboardPanel = false this.whiteboardAirspaceDraw = false + if (this.platformBoxSelectMode) { + this.platformBoxSelectMode = false + this.activeMenu = '' + const cm = this.$refs.cesiumMap + if (cm && typeof cm.exitPlatformBoxSelectMode === 'function') { + cm.exitPlatformBoxSelectMode() + } + } this.currentWhiteboard = null this.currentWhiteboardTimeBlock = null this.loadRoomDrawings() @@ -4005,6 +4042,14 @@ export default { handleWhiteboardDrawModeChange(mode) { this.whiteboardAirspaceDraw = mode === 'airspace' + if (mode === 'airspace' && this.platformBoxSelectMode) { + this.platformBoxSelectMode = false + this.activeMenu = '' + const cm = this.$refs.cesiumMap + if (cm && typeof cm.exitPlatformBoxSelectMode === 'function') { + cm.exitPlatformBoxSelectMode() + } + } }, handleWhiteboardDrawComplete(entityData) { @@ -4071,7 +4116,12 @@ export default { }) if (idx >= 0) { const updated = { ...ents[idx] } - if (stylePayload.color) updated.color = stylePayload.color + if ('color' in stylePayload) { + updated.color = + stylePayload.color != null && String(stylePayload.color).trim() !== '' + ? stylePayload.color + : null + } if (stylePayload.iconScale != null) updated.iconScale = stylePayload.iconScale ents[idx] = updated contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents } @@ -4081,18 +4131,51 @@ export default { styleScale = updated.iconScale } - // 无有效样式不写默认值,避免误覆盖 - if (!styleColor || styleScale == null) return + const redisStyle = {} + if ('color' in stylePayload) redisStyle.color = styleColor + if (stylePayload.iconScale != null) redisStyle.iconScale = styleScale + if (!Object.keys(redisStyle).length) return saveWhiteboardPlatformStyle({ schemeId: this.currentWhiteboard.id, platformInstanceId, - style: { - color: styleColor, - iconScale: styleScale - } + style: redisStyle }).catch(() => {}) }, + /** 房间地图独立平台:图标颜色写入 Redis platformStyle(与探测区等同键合并,不影响 icon_scale 表字段) */ + async handleRoomPlatformIconStyleUpdated(payload) { + if (!payload || payload.serverId == null || payload.roomId == null) return + const roomId = payload.roomId + const serverId = payload.serverId + const platformId = payload.platformId + const platformColor = payload.platformColor + try { + const res = await getPlatformStyle({ + roomId, + routeId: 0, + platformId: platformId || undefined, + platformIconInstanceId: serverId + }) + const base = res && res.data && typeof res.data === 'object' ? { ...res.data } : {} + const stylePayload = { + ...base, + roomId: String(roomId), + routeId: 0, + platformId: platformId != null ? platformId : base.platformId, + platformIconInstanceId: serverId, + platformColor: + platformColor != null && String(platformColor).trim() !== '' + ? String(platformColor).trim() + : null + } + await savePlatformStyle(stylePayload) + this.wsConnection?.sendSyncPlatformStyles?.() + } catch (e) { + console.warn('handleRoomPlatformIconStyleUpdated failed', e) + this.$message && this.$message.error('保存图标颜色失败') + } + }, + /** 白板平台从右键菜单删除后,从 contentByTime 移除并保存 */ handleWhiteboardEntityDeleted(entityData) { if (!this.currentWhiteboard || !this.currentWhiteboardTimeBlock || !entityData || !entityData.id) return @@ -4136,7 +4219,10 @@ export default { heading: e.heading != null ? e.heading : 0, iconScale: e.iconScale != null ? e.iconScale : 1.5, label: e.label || '', - color: e.color || '#165dff' + color: + e.color != null && String(e.color).trim() !== '' + ? e.color + : null } } const base = { type: e.type, id: e.id, color: e.color || '#165dff' } @@ -4277,7 +4363,10 @@ export default { heading: ent.heading != null ? ent.heading : 0, iconScale: ent.iconScale != null ? ent.iconScale : 1.5, label: ent.label || '平台', - color: ent.color || '#165dff' + color: + ent.color != null && String(ent.color).trim() !== '' + ? ent.color + : null } } if (ent.type === 'text' && ent.data) { @@ -4470,7 +4559,11 @@ export default { lng: pos.lng, heading: 0, label: platform.name || '平台', - color: platform.color || '#165dff' + // null:与底部列表原图一致(不着色);仅在平台元数据显式配置颜色时才预着色 + color: + platform.color != null && String(platform.color).trim() !== '' + ? platform.color + : null } const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) } const currentContent = contentByTime[this.currentWhiteboardTimeBlock] || { entities: [] } @@ -4546,6 +4639,145 @@ export default { }) .catch(() => {}) }, + /** 框选复制摆放确定:批量新增实例并同步探测/威力区样式(与导入 JSON 逻辑一致) */ + async onPlatformIconsCopyPlaced({ roomId, platforms }) { + const rId = roomId != null ? roomId : this.currentRoomId + if (!rId || !platforms || !platforms.length) return + let ok = 0 + for (const item of platforms) { + if (item == null || item.platformId == null || item.lat == null || item.lng == null) continue + const payload = { + roomId: rId, + platformId: item.platformId, + platformName: item.platformName || '', + platformType: item.platformType || '', + iconUrl: item.iconUrl || '', + lng: Number(item.lng), + lat: Number(item.lat), + heading: item.heading != null ? Number(item.heading) : 0, + iconScale: item.iconScale != null ? Number(item.iconScale) : 1 + } + try { + const res = await addRoomPlatformIcon(payload) + if (res.code !== 200 || !res.data || res.data.id == null) continue + const instanceId = res.data.id + const det = Array.isArray(item.detectionZones) ? item.detectionZones : [] + const pow = Array.isArray(item.powerZones) ? item.powerZones : [] + const firstD = det[0] + const firstP = pow[0] + const stylePayload = { + roomId: String(rId), + routeId: 0, + platformId: item.platformId || undefined, + platformIconInstanceId: instanceId, + platformName: item.platformName || undefined, + detectionZones: det, + powerZones: pow, + detectionZoneRadius: firstD ? firstD.radiusKm : undefined, + detectionZoneColor: firstD ? firstD.color : undefined, + detectionZoneOpacity: firstD ? firstD.opacity : undefined, + detectionZoneVisible: firstD ? firstD.visible !== false : undefined, + powerZoneRadius: firstP ? firstP.radiusKm : undefined, + powerZoneAngle: firstP ? firstP.angleDeg : undefined, + powerZoneColor: firstP ? firstP.color : undefined, + powerZoneOpacity: firstP ? firstP.opacity : undefined, + powerZoneVisible: firstP ? firstP.visible !== false : undefined + } + try { + await savePlatformStyle(stylePayload) + } catch (se) { + console.warn('复制平台样式保存失败', se) + } + ok++ + } catch (e) { + console.warn('复制平台新增失败', e) + } + } + const cm = this.$refs.cesiumMap + if (cm && typeof cm.loadRoomPlatformIcons === 'function') { + const listRes = await listRoomPlatformIcons(rId) + if (listRes.code === 200 && listRes.data) { + cm.loadRoomPlatformIcons(rId, listRes.data) + } + } + this.wsConnection?.sendSyncPlatformIcons?.() + this.wsConnection?.sendSyncPlatformStyles?.() + if (ok === 0) { + this.$message.warning('未能复制任何平台,请检查网络或权限') + } else { + this.$message.success(`已复制并放置 ${ok} 个平台图标`) + } + }, + /** 白板框选:整体拖拽结束后批量写回当前时间块 */ + onWhiteboardPlatformsBatchUpdated(updates) { + if (!this.currentWhiteboard || !this.currentWhiteboardTimeBlock || !updates || !updates.length) return + const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) } + const currentContent = contentByTime[this.currentWhiteboardTimeBlock] || { entities: [] } + const ents = [...(currentContent.entities || [])] + const normalizeId = (id) => { + if (id == null) return '' + const str = String(id) + return str.startsWith('wb_') ? str.slice(3) : str + } + let changed = false + for (const u of updates) { + if (!u || u.id == null) continue + const targetId = String(u.id) + const targetIdNormalized = normalizeId(targetId) + const idx = ents.findIndex((e) => { + const eid = String((e && e.id) || '') + return eid === targetId || normalizeId(eid) === targetIdNormalized + }) + if (idx >= 0) { + ents[idx] = { + ...ents[idx], + lat: u.lat, + lng: u.lng, + heading: u.heading != null ? u.heading : 0 + } + changed = true + } + } + if (changed) { + contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents } + this.saveCurrentWhiteboard({ contentByTime }) + } + }, + /** 白板框选复制:左键放置后写入当前时间块 */ + async onWhiteboardPlatformsCopyPlaced({ platforms }) { + if (!this.currentWhiteboard || !this.currentWhiteboardTimeBlock || !platforms || !platforms.length) return + const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) } + const currentContent = contentByTime[this.currentWhiteboardTimeBlock] || { entities: [] } + const ents = [...(currentContent.entities || [])] + const t0 = Date.now() + let added = 0 + for (let i = 0; i < platforms.length; i++) { + const p = platforms[i] + if (!p || p.platformId == null || p.lat == null || p.lng == null) continue + const entityId = 'wb_' + t0 + '_' + i + '_' + Math.random().toString(36).slice(2) + ents.push({ + id: entityId, + type: 'platformIcon', + platformId: p.platformId, + platform: p.platform || { id: p.platformId, name: p.platformName || '' }, + platformName: p.platformName || (p.platform && p.platform.name) || '', + lat: Number(p.lat), + lng: Number(p.lng), + heading: p.heading != null ? Number(p.heading) : 0, + iconScale: p.iconScale != null ? Number(p.iconScale) : 1.5, + label: p.label || p.platformName || '', + color: + p.color != null && String(p.color).trim() !== '' + ? p.color + : null + }) + added++ + } + if (!added) return + contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents } + await this.saveCurrentWhiteboard({ contentByTime }) + this.$message.success(`已复制并放置 ${added} 个白板平台`) + }, /** 平台图标从地图删除时同步删除服务端记录 */ onPlatformIconRemoved({ serverId }) { if (!serverId) return @@ -4934,6 +5166,8 @@ export default { } else if (item.id === '4t') { // 4T:切换4T悬浮窗显示 this.show4TPanel = !this.show4TPanel; + } else if (item.id === 'screenshotGallery') { + this.showScreenshotGalleryPanel = !this.showScreenshotGalleryPanel; } else if (item.id === 'whiteboard') { // 白板:进入/退出白板模式 this.toggleWhiteboardMode(); diff --git a/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue b/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue index db5881f..88312d2 100644 --- a/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue +++ b/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue @@ -548,6 +548,13 @@ export default { this.airPlatforms = [] this.seaPlatforms = [] this.groundPlatforms = [] + const classify = (typeStr) => { + const t = String(typeStr || '').trim().toLowerCase() + if (['ship', 'sea'].includes(t)) return 'sea' + if (['ground', 'vehicle'].includes(t)) return 'ground' + if (['air', 'aircraft', 'radar'].includes(t)) return 'air' + return 'air' + } allData.forEach(item => { const platform = { id: item.id, @@ -556,9 +563,10 @@ export default { imageUrl: item.iconUrl || '', icon: item.iconUrl ? '' : 'el-icon-picture-outline' } - if (item.type === 'Air') this.airPlatforms.push(platform) - else if (item.type === 'Sea') this.seaPlatforms.push(platform) - else if (item.type === 'Ground') this.groundPlatforms.push(platform) + const b = classify(item.type) + if (b === 'sea') this.seaPlatforms.push(platform) + else if (b === 'ground') this.groundPlatforms.push(platform) + else this.airPlatforms.push(platform) }) }).catch(() => { this.$message.error('加载平台列表失败')