Browse Source

Merge branch 'ctw' of http://124.70.32.114:3100/woka/cesium-map-object into mh

# Conflicts:
#	ruoyi-ui/src/lang/en.js
#	ruoyi-ui/src/lang/zh.js
mh
menghao 6 days ago
parent
commit
8eaad72df3
  1. 43
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java
  2. 18
      ruoyi-ui/src/api/system/routes.js
  3. 2
      ruoyi-ui/src/assets/styles/element-variables.scss
  4. 2
      ruoyi-ui/src/assets/styles/variables.scss
  5. 4
      ruoyi-ui/src/components/ThemePicker/index.vue
  6. 12
      ruoyi-ui/src/lang/en.js
  7. 12
      ruoyi-ui/src/lang/zh.js
  8. 2
      ruoyi-ui/src/layout/components/Settings/index.vue
  9. 2
      ruoyi-ui/src/store/modules/settings.js
  10. 118
      ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
  11. 26
      ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue
  12. 3
      ruoyi-ui/src/views/cesiumMap/HoverTooltip.vue
  13. 20
      ruoyi-ui/src/views/cesiumMap/MapScreenDomLabels.vue
  14. 10
      ruoyi-ui/src/views/cesiumMap/MeasurementPanel.vue
  15. 916
      ruoyi-ui/src/views/cesiumMap/index.vue
  16. 16
      ruoyi-ui/src/views/childRoom/BottomLeftPanel.vue
  17. 20
      ruoyi-ui/src/views/childRoom/BottomTimeline.vue
  18. 8
      ruoyi-ui/src/views/childRoom/ConflictDrawer.vue
  19. 24
      ruoyi-ui/src/views/childRoom/FourTPanel.vue
  20. 10
      ruoyi-ui/src/views/childRoom/GanttDrawer.vue
  21. 42
      ruoyi-ui/src/views/childRoom/LeftMenu.vue
  22. 74
      ruoyi-ui/src/views/childRoom/RightPanel.vue
  23. 481
      ruoyi-ui/src/views/childRoom/ScreenshotGalleryPanel.vue
  24. 16
      ruoyi-ui/src/views/childRoom/SixStepsOverlay.vue
  25. 28
      ruoyi-ui/src/views/childRoom/StepCanvasContent.vue
  26. 12
      ruoyi-ui/src/views/childRoom/TaskPageContent.vue
  27. 176
      ruoyi-ui/src/views/childRoom/TopHeader.vue
  28. 28
      ruoyi-ui/src/views/childRoom/UnderstandingStepContent.vue
  29. 70
      ruoyi-ui/src/views/childRoom/WhiteboardPanel.vue
  30. 503
      ruoyi-ui/src/views/childRoom/index.vue
  31. 16
      ruoyi-ui/src/views/dialogs/IconSelectDialog.vue
  32. 407
      ruoyi-ui/src/views/dialogs/KTimeSetDialog.vue
  33. 993
      ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue
  34. 12
      ruoyi-ui/src/views/dialogs/PageLayoutDialog.vue
  35. 8
      ruoyi-ui/src/views/dialogs/PlatformEditDialog.vue
  36. 2
      ruoyi-ui/src/views/dialogs/PlatformImportDialog.vue
  37. 18
      ruoyi-ui/src/views/dialogs/RouteEditDialog.vue
  38. 6
      ruoyi-ui/src/views/ganttChart/index.vue
  39. 4
      ruoyi-ui/src/views/index.vue
  40. 24
      ruoyi-ui/src/views/login.vue
  41. 34
      ruoyi-ui/src/views/selectRoom/index.vue
  42. 2
      ruoyi-ui/src/views/system/lib/index.vue
  43. 2
      ruoyi-ui/src/views/system/rooms/index.vue
  44. 2
      ruoyi-ui/src/views/system/routes/index.vue
  45. 2
      ruoyi-ui/src/views/system/scenario/index.vue
  46. 2
      ruoyi-ui/src/views/system/users/index.vue
  47. 4
      ruoyi-ui/src/views/tool/build/RightPanel.vue
  48. 6
      ruoyi-ui/src/views/tool/build/index.vue

43
ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java

@ -192,6 +192,46 @@ public class RoutesController extends BaseController
}
/**
* Redis 获取截图展示数据GET + roomId 查询参数
* 与保存共用路径前缀方法不同避免与其它 GET 字面路径网关规则混淆
*/
@PreAuthorize("@ss.hasPermi('system:routes:query')")
@GetMapping("/roomScreenshotGallery")
public AjaxResult getScreenshotGalleryData(@RequestParam Long roomId)
{
if (roomId == null) {
return AjaxResult.error("房间ID不能为空");
}
String key = "room:" + String.valueOf(roomId) + ":screenshot_gallery";
String val = fourTRedisTemplate.opsForValue().get(key);
if (val != null && !val.isEmpty()) {
try {
return success(JSON.parseObject(val));
} catch (Exception e) {
return success(val);
}
}
return success();
}
/**
* 保存截图展示悬浮窗数据到 RedisPOST 与上面 GET 同一路径
*/
@PreAuthorize("@ss.hasPermi('system:routes:edit')")
@PostMapping("/roomScreenshotGallery")
public AjaxResult saveScreenshotGalleryData(@RequestBody java.util.Map<String, Object> params)
{
Object roomId = params.get("roomId");
Object data = params.get("data");
if (roomId == null || data == null) {
return AjaxResult.error("参数不完整");
}
String key = "room:" + String.valueOf(roomId) + ":screenshot_gallery";
fourTRedisTemplate.opsForValue().set(key, data.toString());
return success();
}
/**
* 保存六步法任务页数据到 Redis背景图标文本框
*/
@PreAuthorize("@ss.hasPermi('system:routes:edit')")
@ -395,9 +435,10 @@ public class RoutesController extends BaseController
/**
* 获取实体部署与航线详细信息
* 路径仅匹配数字 id避免与 /get4TData/getScreenshotGalleryData 等字面路径冲突
*/
@PreAuthorize("@ss.hasPermi('system:routes:query')")
@GetMapping(value = "/{id}")
@GetMapping(value = "/{id:\\d+}")
public AjaxResult getInfo(@PathVariable("id") Long id)
{
return success(routesService.selectRoutesById(id));

18
ruoyi-ui/src/api/system/routes.js

@ -93,6 +93,24 @@ export function get4TData(params) {
})
}
// 截图展示:同一路径 GET 读 / POST 写(Redis),与 4T 同权限
export function saveScreenshotGalleryData(data) {
return request({
url: '/system/routes/roomScreenshotGallery',
method: 'post',
data,
headers: { repeatSubmit: false }
})
}
export function getScreenshotGalleryData(params) {
return request({
url: '/system/routes/roomScreenshotGallery',
method: 'get',
params
})
}
// 保存六步法任务页数据到 Redis(背景、图标、文本框)
export function saveTaskPageData(data) {
return request({

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

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

4
ruoyi-ui/src/components/ThemePicker/index.vue

@ -1,14 +1,14 @@
<template>
<el-color-picker
v-model="theme"
:predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
:predefine="['#165dff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
class="theme-picker"
popper-class="theme-picker-dropdown"
/>
</template>
<script>
const ORIGINAL_THEME = '#409EFF' // default color
const ORIGINAL_THEME = '#165dff' // default color
export default {
data() {

12
ruoyi-ui/src/lang/en.js

@ -21,9 +21,11 @@ export default {
importATO: 'Import ATO',
importLayer: 'Import Layer',
importRoute: 'Import Route',
importPlatformGraphics: 'Import Platform Graphics',
export: 'Export',
exportRoute: 'Export Routes',
exportPlan: 'Export Plan'
exportPlan: 'Export Plan',
exportPlatformGraphics: 'Export Platform Graphics'
},
edit: {
routeEdit: 'Route Edit',
@ -164,7 +166,13 @@ export default {
send: 'Send',
pleaseInputMessage: 'Please enter message content',
operationRollbackSuccess: 'Operation rollback successful',
noLogs: 'No operation logs'
noLogs: 'No operation logs',
selectPrivateContact: 'Select private chat contact',
changePrivateContact: 'Change contact',
noChatableMembers: 'No members available for private chat',
confirmPickContact: 'OK',
memberStatusOnline: 'Online',
memberStatusOffline: 'Offline'
},
generateAirspace: {
title: 'Generate Airspace',

12
ruoyi-ui/src/lang/zh.js

@ -21,9 +21,11 @@ export default {
importATO: '导入ATO',
importLayer: '导入图层',
importRoute: '导入航线',
importPlatformGraphics: '导入平台图形',
export: '导出',
exportRoute: '导出航线',
exportPlan: '导出计划'
exportPlan: '导出计划',
exportPlatformGraphics: '导出平台图形'
},
edit: {
routeEdit: '航线编辑',
@ -164,7 +166,13 @@ export default {
send: '发送',
pleaseInputMessage: '请输入消息内容',
operationRollbackSuccess: '操作回滚成功',
noLogs: '暂无操作日志'
noLogs: '暂无操作日志',
selectPrivateContact: '选择私聊联系人',
changePrivateContact: '更换联系人',
noChatableMembers: '暂无可私聊的成员',
confirmPickContact: '确定',
memberStatusOnline: '在线',
memberStatusOffline: '离线'
},
generateAirspace: {
title: '生成空域',

2
ruoyi-ui/src/layout/components/Settings/index.vue

@ -292,7 +292,7 @@ export default {
height: 100%;
padding-top: 15px;
padding-left: 24px;
color: #1890ff;
color: #165dff;
font-weight: 700;
font-size: 14px;
}

2
ruoyi-ui/src/store/modules/settings.js

@ -6,7 +6,7 @@ const { sideTheme, showSettings, navType, tagsView, tagsIcon, fixedHeader, sideb
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
const state = {
title: '',
theme: storageSetting.theme || '#409EFF',
theme: storageSetting.theme || '#165dff',
sideTheme: storageSetting.sideTheme || sideTheme,
showSettings: showSettings,
navType: storageSetting.navType === undefined ? navType : storageSetting.navType,

118
ruoyi-ui/src/views/cesiumMap/ContextMenu.vue

@ -9,9 +9,13 @@
</div>
</div>
<!-- 框选多平台批量删除 -->
<!-- 框选多平台复制摆放 / 批量删除 -->
<div class="menu-section" v-if="entityData && entityData.type === 'platformBoxSelection'">
<div class="menu-title">框选平台{{ entityData.count }} </div>
<div class="menu-item" @click="handleCopyBoxSelection">
<span class="menu-icon">📋</span>
<span>复制</span>
</div>
<div class="menu-item" @click="handleDeleteBoxSelection">
<span class="menu-icon">🗑</span>
<span>删除全部框选平台</span>
@ -546,6 +550,10 @@
<!-- 平台图标拖拽到地图的图标特有选项白板平台不显示探测区/威力区/航线 -->
<div class="menu-section" v-if="entityData.type === 'platformIcon' && !entityData.isWhiteboard">
<div class="menu-title">平台图标</div>
<div class="menu-item" @click="openRoomPlatformIconColorDialog">
<span class="menu-icon">🎨</span>
<span>图标颜色</span>
</div>
<div class="menu-item" @click="handleShowTransformBox">
<span class="menu-icon">🔄</span>
<span>显示伸缩框</span>
@ -610,21 +618,26 @@
</div>
<el-dialog
title="颜色与大小"
:title="platformIconColorDialogScope === 'room' ? '图标颜色' : '颜色与大小'"
:visible.sync="showWhiteboardPlatformStyleDialog"
width="360px"
append-to-body
:close-on-click-modal="false"
>
<el-form :model="whiteboardPlatformStyleForm" label-width="90px" size="small">
<el-form-item label="颜色">
<el-form-item label="着色">
<el-checkbox v-model="whiteboardPlatformStyleForm.useNativeColor" @change="onWhiteboardNativeColorChange">
与列表原图一致不着色
</el-checkbox>
</el-form-item>
<el-form-item label="颜色" v-show="!whiteboardPlatformStyleForm.useNativeColor">
<el-color-picker
v-model="whiteboardPlatformStyleForm.color"
:predefine="presetColors"
@active-change="handleWhiteboardColorActiveChange"
/>
</el-form-item>
<el-form-item label="大小">
<el-form-item v-if="platformIconColorDialogScope === 'whiteboard'" label="大小">
<el-select v-model="whiteboardPlatformStyleForm.iconScale" style="width:100%">
<el-option
v-for="scale in presetPlatformScales"
@ -702,8 +715,11 @@ export default {
showSizePicker: false,
sizePickerType: '',
showWhiteboardPlatformStyleDialog: false,
/** room:房间地图独立平台,仅颜色;whiteboard:白板平台颜色+缩放 */
platformIconColorDialogScope: 'whiteboard',
whiteboardPlatformStyleForm: {
color: '#008aff',
useNativeColor: true,
color: '#165dff',
iconScale: 1.5
},
showOpacityPicker: false,
@ -711,9 +727,9 @@ export default {
showBearingTypeMenu: false,
showRangingUnitMenu: false,
presetColors: [
'#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF',
'#FF0000', '#00FF00', '#165dff', '#FFFF00', '#FF00FF', '#00FFFF',
'#FF6600', '#663399', '#999999', '#000000', '#FFFFFF', '#FF99CC',
'#CC99FF', '#99CCFF', '#99FF99', '#FFFF99', '#FFCC99', '#FF9999'
'#CC99FF', '#165dff', '#99FF99', '#FFFF99', '#FFCC99', '#FF9999'
],
presetWidths: [1, 2, 3, 4, 5, 6, 8, 10, 12],
presetSizes: [6, 8, 10, 12, 14, 16, 18, 20, 24],
@ -782,6 +798,10 @@ export default {
this.$emit('delete-box-selected-platforms')
},
handleCopyBoxSelection() {
this.$emit('copy-box-selected-platforms')
},
handleAdjustPosition() {
this.$emit('adjust-airspace-position')
},
@ -825,19 +845,59 @@ export default {
this.$emit('show-transform-box')
},
openWhiteboardPlatformStyleDialog() {
const color = (this.entityData && this.entityData.color) || '#008aff'
const raw = this.entityData && this.entityData.color
const useNative =
raw == null ||
raw === '' ||
(typeof raw === 'string' && raw.trim() === '')
const color = useNative ? '#165dff' : raw
const iconScale = this.entityData && this.entityData.iconScale != null ? Number(this.entityData.iconScale) : 1.5
this.platformIconColorDialogScope = 'whiteboard'
this.whiteboardPlatformStyleForm = {
useNativeColor: useNative,
color,
iconScale: Number.isFinite(iconScale) ? iconScale : 1.5
}
this.showWhiteboardPlatformStyleDialog = true
},
openRoomPlatformIconColorDialog() {
const raw = this.entityData && this.entityData.color
const useNative =
raw == null ||
raw === '' ||
(typeof raw === 'string' && raw.trim() === '')
const color = useNative ? '#165dff' : raw
const iconScale = this.entityData && this.entityData.iconScale != null ? Number(this.entityData.iconScale) : 1
this.platformIconColorDialogScope = 'room'
this.whiteboardPlatformStyleForm = {
useNativeColor: useNative,
color,
iconScale: Number.isFinite(iconScale) ? iconScale : 1
}
this.showWhiteboardPlatformStyleDialog = true
},
onWhiteboardNativeColorChange(native) {
if (!native && this.whiteboardPlatformStyleForm && !this.whiteboardPlatformStyleForm.color) {
this.whiteboardPlatformStyleForm.color = '#165dff'
}
},
handleWhiteboardColorActiveChange(color) {
if (color) this.whiteboardPlatformStyleForm.color = color
},
confirmWhiteboardPlatformStyle() {
const color = this.whiteboardPlatformStyleForm.color || '#008aff'
const useNative = !!this.whiteboardPlatformStyleForm.useNativeColor
const color = useNative
? null
: this.whiteboardPlatformStyleForm.color || '#165dff'
if (this.platformIconColorDialogScope === 'room') {
this.showWhiteboardPlatformStyleDialog = false
this.$emit('apply-room-platform-icon-color', {
id: this.entityData && this.entityData.id,
color
})
this.$emit('close-menu')
return
}
const iconScale = Number(this.whiteboardPlatformStyleForm.iconScale)
if (!Number.isFinite(iconScale) || iconScale <= 0) {
this.$message && this.$message.warning('请选择有效大小')
@ -1237,7 +1297,7 @@ export default {
}
.menu-item-sub:hover {
background-color: #f0f7ff;
background-color: #ebf3ff;
color: #333;
}
@ -1278,7 +1338,7 @@ export default {
.color-item.active {
transform: scale(1.2);
box-shadow: 0 0 0 2px white, 0 0 0 3px #007bff;
box-shadow: 0 0 0 2px white, 0 0 0 3px #165dff;
}
/* 线宽选择器样式 */
@ -1307,14 +1367,14 @@ export default {
}
.width-item:hover {
background-color: #e3f2fd;
border-color: #2196f3;
background-color: #ebf3ff;
border-color: #165dff;
}
.width-item.active {
background-color: #2196f3;
background-color: #165dff;
color: white;
border-color: #1976d2;
border-color: #165dff;
}
/* 大小选择器样式 */
@ -1343,14 +1403,14 @@ export default {
}
.size-item:hover {
background-color: #e3f2fd;
border-color: #2196f3;
background-color: #ebf3ff;
border-color: #165dff;
}
.size-item.active {
background-color: #2196f3;
background-color: #165dff;
color: white;
border-color: #1976d2;
border-color: #165dff;
}
/* 透明度选择器样式 */
@ -1379,14 +1439,14 @@ export default {
}
.opacity-item:hover {
background-color: #e3f2fd;
border-color: #2196f3;
background-color: #ebf3ff;
border-color: #165dff;
}
.opacity-item.active {
background-color: #2196f3;
background-color: #165dff;
color: white;
border-color: #1976d2;
border-color: #165dff;
}
/* 字号选择器样式 */
@ -1415,14 +1475,14 @@ export default {
}
.font-size-item:hover {
background-color: #e3f2fd;
border-color: #2196f3;
background-color: #ebf3ff;
border-color: #165dff;
}
.font-size-item.active {
background-color: #2196f3;
background-color: #165dff;
color: white;
border-color: #1976d2;
border-color: #165dff;
}
/* 子菜单样式 */
@ -1441,11 +1501,11 @@ export default {
}
.sub-menu-item:hover {
background-color: #e3f2fd;
background-color: #ebf3ff;
}
.sub-menu-item.active {
background-color: #2196f3;
background-color: #165dff;
color: white;
}
</style>

26
ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue

@ -128,10 +128,10 @@ export default {
width: 36px;
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 138, 255, 0.1);
border: 1px solid rgba(22, 93, 255, 0.1);
border-radius: 8px;
z-index: 90;
box-shadow: 0 4px 12px rgba(0, 138, 255, 0.2);
box-shadow: 0 4px 12px rgba(22, 93, 255, 0.2);
padding: 12px 2px;
transition: all 0.3s ease;
overflow: hidden;
@ -165,16 +165,16 @@ export default {
}
.toolbar-item:hover {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
background: rgba(22, 93, 255, 0.1);
color: #165dff;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 138, 255, 0.2);
box-shadow: 0 4px 10px rgba(22, 93, 255, 0.2);
}
.toolbar-item.active {
background: rgba(0, 138, 255, 0.15);
color: #008aff;
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.3);
background: rgba(22, 93, 255, 0.15);
color: #165dff;
box-shadow: 0 2px 8px rgba(22, 93, 255, 0.3);
}
.toolbar-item .toolbar-svg-icon {
@ -189,7 +189,7 @@ export default {
gap: 4px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid rgba(0, 138, 255, 0.2);
border-top: 1px solid rgba(22, 93, 255, 0.2);
}
.constraint-option {
@ -204,13 +204,13 @@ export default {
}
.constraint-option:hover {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
background: rgba(22, 93, 255, 0.1);
color: #165dff;
}
.constraint-option.active {
background: rgba(0, 138, 255, 0.2);
color: #008aff;
background: rgba(22, 93, 255, 0.2);
color: #165dff;
}
.toolbar-item:disabled {

3
ruoyi-ui/src/views/cesiumMap/HoverTooltip.vue

@ -25,7 +25,8 @@ export default {
<style scoped>
.hover-tooltip {
position: absolute;
z-index: 9999;
/* 略高于地图 DOM 标牌层(36),但仍低于房间内弹窗 */
z-index: 37;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 12px;

20
ruoyi-ui/src/views/cesiumMap/MapScreenDomLabels.vue

@ -55,7 +55,8 @@ export default {
right: 0;
bottom: 0;
pointer-events: none;
z-index: 9998;
/* 低于任务房间内悬浮窗(K 时/航线编辑等≥1000),避免盖住弹窗 */
z-index: 36;
overflow: hidden;
}
@ -98,7 +99,7 @@ export default {
font-weight: 600;
line-height: 1.2;
margin-bottom: 2px;
color: #0078ff;
color: #165dff;
}
.map-screen-dom-label__platform-stats {
@ -119,4 +120,19 @@ export default {
.map-screen-dom-label__transparent-text {
display: inline-block;
}
/* 插入定时点预览:白底卡片,与原先 Cesium Label 风格接近 */
.map-screen-dom-label--timed-segment-preview {
background: rgba(255, 255, 255, 0.94);
color: #0f172a;
padding: 6px 10px;
border-radius: 4px;
font-size: 13px;
line-height: 1.4;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);
border: 1px solid rgba(0, 0, 0, 0.06);
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
white-space: pre-line;
max-width: 220px;
}
</style>

10
ruoyi-ui/src/views/cesiumMap/MeasurementPanel.vue

@ -55,7 +55,7 @@ export default {
font-size: 16px;
font-weight: 600;
color: #333;
border-bottom: 2px solid #409EFF;
border-bottom: 2px solid #165dff;
padding-bottom: 8px;
}
@ -69,14 +69,14 @@ export default {
}
.measurement-item strong {
color: #409EFF;
color: #165dff;
font-weight: 600;
}
.close-btn {
width: 100%;
padding: 10px;
background: #409EFF;
background: #165dff;
color: white;
border: none;
border-radius: 6px;
@ -88,9 +88,9 @@ export default {
}
.close-btn:hover {
background: #66b1ff;
background: #165dff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
box-shadow: 0 4px 12px rgba(22, 93, 255, 0.3);
}
@media (max-width: 768px) {

916
ruoyi-ui/src/views/cesiumMap/index.vue

File diff suppressed because it is too large

16
ruoyi-ui/src/views/childRoom/BottomLeftPanel.vue

@ -265,7 +265,7 @@ export default {
.panel-toggle {
width: 50px;
height: 50px;
background: rgba(0, 138, 255, 0.9);
background: rgba(22, 93, 255, 0.9);
border-radius: 50%;
display: flex;
flex-direction: column;
@ -274,13 +274,13 @@ export default {
cursor: pointer;
color: white;
font-size: 20px;
box-shadow: 0 4px 12px rgba(0, 138, 255, 0.4);
box-shadow: 0 4px 12px rgba(22, 93, 255, 0.4);
transition: all 0.3s;
}
.panel-toggle:hover {
transform: scale(1.1);
box-shadow: 0 6px 16px rgba(0, 138, 255, 0.6);
box-shadow: 0 6px 16px rgba(22, 93, 255, 0.6);
}
.toggle-text {
@ -321,20 +321,20 @@ export default {
}
.panel-item:hover {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
background: rgba(22, 93, 255, 0.1);
color: #165dff;
transform: translateX(5px);
}
.panel-item.active {
background: rgba(0, 138, 255, 0.15);
color: #008aff;
background: rgba(22, 93, 255, 0.15);
color: #165dff;
}
.panel-item i {
font-size: 18px;
margin-right: 10px;
color: #008aff;
color: #165dff;
}
.panel-item span {

20
ruoyi-ui/src/views/childRoom/BottomTimeline.vue

@ -677,7 +677,7 @@ export default {
}
.timeline-title i {
color: #008aff;
color: #165dff;
font-size: 16px;
}
@ -688,14 +688,14 @@ export default {
}
.header-actions .el-button {
color: #008aff;
color: #165dff;
transition: all 0.3s ease;
font-size: 15px;
padding: 4px 8px;
}
.header-actions .el-button:hover {
color: #0078e5;
color: #165dff;
transform: translateY(-1px);
}
@ -740,10 +740,10 @@ export default {
top: 0;
left: 0;
height: 100%;
background: linear-gradient(90deg, #008aff 0%, #66b8ff 100%);
background: linear-gradient(90deg, #165dff 0%, #165dff 100%);
transition: width 0.5s ease-in-out;
border-radius: 8px;
box-shadow: 0 0 8px rgba(0, 138, 255, 0.3);
box-shadow: 0 0 8px rgba(22, 93, 255, 0.3);
}
.timeline-markers {
@ -769,7 +769,7 @@ export default {
.timeline-marker:hover .marker-dot {
transform: scale(1.5);
box-shadow: 0 0 0 6px rgba(0, 138, 255, 0.2);
box-shadow: 0 0 0 6px rgba(22, 93, 255, 0.2);
}
.timeline-marker:hover .marker-tooltip {
@ -792,7 +792,7 @@ export default {
.marker-dot {
width: 12px;
height: 12px;
background: #008aff;
background: #165dff;
border-radius: 50%;
border: 2px solid white;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
@ -819,7 +819,7 @@ export default {
.tooltip-time {
font-size: 16px;
color: #008aff;
color: #165dff;
font-weight: 600;
margin-bottom: 4px;
}
@ -982,7 +982,7 @@ export default {
.segment-time {
font-size: 13px;
color: #008aff;
color: #165dff;
font-weight: 700;
min-width: 80px;
}
@ -1056,7 +1056,7 @@ export default {
}
.bottom-timeline .el-dialog__header {
background: linear-gradient(90deg, #008aff 0%, #66b8ff 100%);
background: linear-gradient(90deg, #165dff 0%, #165dff 100%);
color: white;
padding: 12px 20px;
}

8
ruoyi-ui/src/views/childRoom/ConflictDrawer.vue

@ -262,7 +262,7 @@ export default {
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #f0f7ff;
background: #ebf3ff;
border-bottom: 1px solid #cce5ff;
cursor: move;
flex-shrink: 0;
@ -270,7 +270,7 @@ export default {
.conflict-drawer-title {
font-weight: 600;
color: #1994fe;
color: #165dff;
font-size: 15px;
}
@ -279,7 +279,7 @@ export default {
height: 28px;
border: 1px solid #cce5ff;
background: #fff;
color: #1994fe;
color: #165dff;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
@ -408,7 +408,7 @@ export default {
}
.blue-text-btn {
color: #1994fe;
color: #165dff;
font-size: 12px;
}

24
ruoyi-ui/src/views/childRoom/FourTPanel.vue

@ -379,7 +379,7 @@ export default {
top: 20px;
bottom: 20px;
width: 1px;
background-color: #1994fe;
background-color: #165dff;
transform: translateX(-50%);
z-index: 10;
opacity: 0.6;
@ -391,7 +391,7 @@ export default {
left: 20px;
right: 20px;
height: 1px;
background-color: #1994fe;
background-color: #165dff;
transform: translateY(-50%);
z-index: 10;
opacity: 0.6;
@ -404,7 +404,7 @@ export default {
left: 50%;
width: 8px;
height: 8px;
background-color: #1994fe;
background-color: #165dff;
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 15;
@ -448,7 +448,7 @@ export default {
.quadrant-title {
font-size: 18px;
font-weight: 600;
color: #0078e0;
color: #165dff;
margin-bottom: 16px;
letter-spacing: 0.5px;
text-transform: uppercase;
@ -471,7 +471,7 @@ export default {
.quadrant-content:focus {
outline: none;
border-color: #1994fe;
border-color: #165dff;
background-color: #ffffff;
box-shadow: 0 0 0 4px rgba(25, 148, 254, 0.1);
}
@ -479,7 +479,7 @@ export default {
/* 只读状态 */
.quadrant-content[readonly] {
background-color: #e8f4ff;
color: #0078e0;
color: #165dff;
font-weight: 500;
cursor: default;
}
@ -501,8 +501,8 @@ export default {
/* 编辑按钮 */
.btn-edit {
background-color: #f0f7ff;
color: #1994fe;
background-color: #ebf3ff;
color: #165dff;
border: 1px solid #cce5ff;
padding: 6px 12px;
border-radius: 6px;
@ -520,7 +520,7 @@ export default {
.btn-edit:hover {
background-color: #e8f4ff;
border-color: #1994fe;
border-color: #165dff;
}
.btn-edit.complete:hover {
@ -531,10 +531,10 @@ export default {
/* 关闭按钮 */
.btn-close {
background-color: #f0f7ff;
background-color: #ebf3ff;
border: 1px solid #cce5ff;
font-size: 14px;
color: #1994fe;
color: #165dff;
width: 30px;
height: 30px;
display: flex;
@ -547,7 +547,7 @@ export default {
.btn-close:hover {
background-color: #e8f4ff;
color: #0078e0;
color: #165dff;
}
/* 响应式适配 */

10
ruoyi-ui/src/views/childRoom/GanttDrawer.vue

@ -426,7 +426,7 @@ export default {
id: r.id,
name: r.name,
type: 'route',
color: r.color || '#409EFF',
color: r.color || '#165dff',
startMinutes: r.startMinutes,
endMinutes: r.endMinutes,
actualStartKTime: minutesToKTime(r.startMinutes),
@ -1017,7 +1017,7 @@ export default {
border-radius: 2px;
display: inline-block;
}
.legend-color-route { background: #409EFF; }
.legend-color-route { background: #165dff; }
.legend-color-missile { background: #F56C6C; }
.legend-color-hold { background: #E6A23C; }
@ -1049,10 +1049,10 @@ export default {
cursor: nwse-resize;
user-select: none;
z-index: 20;
background: linear-gradient(to top left, transparent 50%, rgba(64, 158, 255, 0.28) 50%);
background: linear-gradient(to top left, transparent 50%, rgba(22, 93, 255, 0.28) 50%);
}
.gantt-panel-resize-handle:hover {
background: linear-gradient(to top left, transparent 50%, rgba(64, 158, 255, 0.42) 50%);
background: linear-gradient(to top left, transparent 50%, rgba(22, 93, 255, 0.42) 50%);
}
.minibar-left {
display: flex;
@ -1067,7 +1067,7 @@ export default {
color: #909399;
}
.minibar-right {
color: #409EFF;
color: #165dff;
font-size: 12px;
}
</style>

42
ruoyi-ui/src/views/childRoom/LeftMenu.vue

@ -248,10 +248,10 @@ export default {
width: 42px;
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 138, 255, 0.1);
border: 1px solid rgba(22, 93, 255, 0.1);
border-radius: 8px;
z-index: 90;
box-shadow: 0 4px 12px rgba(0, 138, 255, 0.2);
box-shadow: 0 4px 12px rgba(22, 93, 255, 0.2);
padding: 12px 5px;
transition: all 0.3s ease;
overflow: hidden;
@ -320,7 +320,7 @@ export default {
justify-content: center;
align-items: center;
cursor: pointer;
color: #008aff;
color: #165dff;
font-size: 16px;
transition: all 0.3s;
background: rgba(255, 255, 255, 0.5);
@ -495,16 +495,16 @@ export default {
}
.menu-item:hover {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
background: rgba(22, 93, 255, 0.1);
color: #165dff;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 138, 255, 0.2);
box-shadow: 0 4px 10px rgba(22, 93, 255, 0.2);
}
.menu-item.active {
background: rgba(0, 138, 255, 0.15);
color: #008aff;
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.3);
background: rgba(22, 93, 255, 0.15);
color: #165dff;
box-shadow: 0 2px 8px rgba(22, 93, 255, 0.3);
}
.menu-item.edit-mode {
@ -512,29 +512,29 @@ export default {
}
.menu-item.edit-mode:hover {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
background: rgba(22, 93, 255, 0.1);
color: #165dff;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 138, 255, 0.2);
box-shadow: 0 4px 10px rgba(22, 93, 255, 0.2);
}
.ghost-item {
opacity: 0.4;
background: rgba(0, 138, 255, 0.05);
border: 2px dashed rgba(0, 138, 255, 0.3);
background: rgba(22, 93, 255, 0.05);
border: 2px dashed rgba(22, 93, 255, 0.3);
}
.chosen-item {
background: rgba(0, 138, 255, 0.2);
color: #008aff;
background: rgba(22, 93, 255, 0.2);
color: #165dff;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 138, 255, 0.4);
box-shadow: 0 4px 12px rgba(22, 93, 255, 0.4);
}
.dragging-item {
opacity: 1;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 8px 20px rgba(0, 138, 255, 0.3);
box-shadow: 0 8px 20px rgba(22, 93, 255, 0.3);
transform: scale(1.1);
}
@ -568,10 +568,10 @@ export default {
}
.add-btn:hover {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
background: rgba(22, 93, 255, 0.1);
color: #165dff;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 138, 255, 0.2);
box-shadow: 0 4px 10px rgba(22, 93, 255, 0.2);
}
.save-btn {

74
ruoyi-ui/src/views/childRoom/RightPanel.vue

@ -491,19 +491,19 @@ export default {
align-items: center;
justify-content: center;
cursor: pointer;
color: #008aff;
color: #165dff;
font-size: 18px;
background: rgba(255, 255, 255, 0.5);
border-radius: 50%;
z-index: 85;
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.3);
box-shadow: 0 2px 8px rgba(22, 93, 255, 0.3);
transition: all 0.3s;
backdrop-filter: blur(5px);
}
.right-external-hide-btn:hover {
color: #0066cc;
background: rgba(0, 138, 255, 0.2);
color: #165dff;
background: rgba(22, 93, 255, 0.2);
transform: scale(1.1);
}
@ -523,7 +523,7 @@ export default {
z-index: 90;
color: #333;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 138, 255, 0.2);
box-shadow: 0 4px 20px rgba(22, 93, 255, 0.2);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(15px);
transition: all 0.3s ease;
@ -554,7 +554,7 @@ export default {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid rgba(0, 138, 255, 0.2);
border-bottom: 2px solid rgba(22, 93, 255, 0.2);
margin-bottom: 15px;
padding-bottom: 10px;
padding-top: 10px;
@ -563,12 +563,12 @@ export default {
.section-title {
font-size: 14px;
font-weight: 600;
color: #008aff;
color: #165dff;
}
.create-route-btn-new {
background-color: #3370ff !important;
border-color: #3370ff !important;
background-color: #165dff !important;
border-color: #165dff !important;
color: #ffffff !important;
padding: 4px 10px;
font-size: 12px;
@ -590,7 +590,7 @@ export default {
.tree-item {
border-radius: 6px;
transition: all 0.3s;
border: 1px solid rgba(0, 138, 255, 0.1);
border: 1px solid rgba(22, 93, 255, 0.1);
}
.tree-item-header {
@ -605,9 +605,9 @@ export default {
}
.tree-item-header:hover {
background: rgba(0, 138, 255, 0.1);
background: rgba(22, 93, 255, 0.1);
transform: translateX(-2px);
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.15);
box-shadow: 0 2px 8px rgba(22, 93, 255, 0.15);
}
.tree-item.plan-item .tree-item-header {
@ -623,19 +623,19 @@ export default {
}
.tree-item.active .tree-item-header {
background: rgba(0, 138, 255, 0.15) !important;
border-color: rgba(0, 138, 255, 0.3);
box-shadow: 0 2px 10px rgba(0, 138, 255, 0.25);
background: rgba(22, 93, 255, 0.15) !important;
border-color: rgba(22, 93, 255, 0.3);
box-shadow: 0 2px 10px rgba(22, 93, 255, 0.25);
}
.tree-item.selected .tree-item-header {
background: rgba(0, 138, 255, 0.1) !important;
border-color: rgba(0, 138, 255, 0.2);
background: rgba(22, 93, 255, 0.1) !important;
border-color: rgba(22, 93, 255, 0.2);
}
.tree-icon {
font-size: 16px;
color: #008aff;
color: #165dff;
flex-shrink: 0;
}
@ -694,7 +694,7 @@ export default {
.tree-item-actions i {
cursor: pointer;
color: #008aff;
color: #165dff;
font-size: 14px;
padding: 4px;
border-radius: 4px;
@ -702,7 +702,7 @@ export default {
}
.tree-item-actions i:hover {
background: rgba(0, 138, 255, 0.1);
background: rgba(22, 93, 255, 0.1);
transform: scale(1.2);
}
@ -745,13 +745,13 @@ export default {
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
border: 1px solid rgba(0, 138, 255, 0.1);
border: 1px solid rgba(22, 93, 255, 0.1);
}
.platform-item:hover {
background: rgba(0, 138, 255, 0.1);
background: rgba(22, 93, 255, 0.1);
transform: translateX(-2px);
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.15);
box-shadow: 0 2px 8px rgba(22, 93, 255, 0.15);
}
.platform-item-draggable {
@ -811,9 +811,9 @@ export default {
}
.status-dot.operating {
background: #008aff;
background: #165dff;
animation: pulse 2s infinite;
box-shadow: 0 0 10px rgba(0, 138, 255, 0.8);
box-shadow: 0 0 10px rgba(22, 93, 255, 0.8);
}
@keyframes pulse {
@ -826,22 +826,22 @@ export default {
}
.blue-btn {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
border: 1px solid rgba(0, 138, 255, 0.3);
background: rgba(22, 93, 255, 0.1);
color: #165dff;
border: 1px solid rgba(22, 93, 255, 0.3);
}
.blue-btn:hover {
background: rgba(0, 138, 255, 0.2);
border-color: rgba(0, 138, 255, 0.5);
background: rgba(22, 93, 255, 0.2);
border-color: rgba(22, 93, 255, 0.5);
}
.blue-text-btn {
color: #008aff;
color: #165dff;
}
.blue-text-btn:hover {
color: #0066cc;
color: #165dff;
}
.blue-badge {
@ -856,21 +856,21 @@ export default {
}
.blue-tabs >>> .el-tabs__item:hover {
color: #008aff;
color: #165dff;
}
.blue-tabs >>> .el-tabs__item.is-active {
color: #008aff;
color: #165dff;
font-weight: 600;
}
.blue-tabs >>> .el-tabs__active-bar {
background-color: #008aff;
box-shadow: 0 0 6px rgba(0, 138, 255, 0.5);
background-color: #165dff;
box-shadow: 0 0 6px rgba(22, 93, 255, 0.5);
}
.blue-tabs >>> .el-tabs__nav-wrap::after {
background-color: rgba(0, 138, 255, 0.3);
background-color: rgba(22, 93, 255, 0.3);
}
.blue-success {

481
ruoyi-ui/src/views/childRoom/ScreenshotGalleryPanel.vue

@ -0,0 +1,481 @@
<template>
<!-- 截图展示 4T 相同的全屏透明层 + 可拖动/缩放面板不阻挡地图操作 -->
<div v-show="visible" class="screenshot-gallery-root" :class="{ 'sg-ready': layoutReady }">
<div class="sg-panel" :style="panelStyle">
<div class="sg-toolbar" @mousedown="onDragStart">
<span class="sg-title">截图展示</span>
<div class="sg-toolbar-btns" @mousedown.stop>
<input
ref="fileInput"
type="file"
accept="image/*"
multiple
class="sg-hidden-file"
@change="onFilesSelected"
/>
<button type="button" class="sg-txt-btn" @click="triggerPick">添加</button>
<button type="button" class="sg-txt-btn" :disabled="!hasImages" @click="removeCurrent">删除</button>
<button type="button" class="sg-icon-btn" :disabled="!canPrev" title="上一张" @click="prev"></button>
<span class="sg-counter">{{ pageLabel }}</span>
<button type="button" class="sg-icon-btn" :disabled="!canNext" title="下一张" @click="next"></button>
<button type="button" class="sg-close" title="关闭" @click="$emit('update:visible', false)">×</button>
</div>
</div>
<div class="sg-body">
<div v-if="!hasImages" class="sg-empty">点击添加插入图片多图可使用两侧箭头翻页</div>
<div v-else class="sg-img-frame">
<img :key="currentIndex" :src="currentSrc" alt="" class="sg-img" draggable="false" />
</div>
</div>
<div class="sg-resize" @mousedown="onResizeStart" title="拖动调整大小"></div>
</div>
</div>
</template>
<script>
import { saveScreenshotGalleryData, getScreenshotGalleryData } from '@/api/system/routes'
const MAX_IMAGES = 80
export default {
name: 'ScreenshotGalleryPanel',
props: {
visible: { type: Boolean, default: false },
roomId: { type: [String, Number], default: null }
},
data() {
return {
images: [],
currentIndex: 0,
layoutReady: false,
isDragging: false,
dragStartX: 0,
dragStartY: 0,
panelLeft: null,
panelTop: null,
panelWidth: 520,
panelHeight: 400,
isResizing: false,
resizeStartX: 0,
resizeStartY: 0,
resizeStartW: 0,
resizeStartH: 0
}
},
computed: {
panelStyle() {
const left = this.panelLeft != null ? this.panelLeft : 24
const top = this.panelTop != null ? this.panelTop : 100
return {
left: `${left}px`,
top: `${top}px`,
width: `${this.panelWidth}px`,
height: `${this.panelHeight}px`
}
},
hasImages() {
return Array.isArray(this.images) && this.images.length > 0
},
currentSrc() {
if (!this.hasImages) return ''
return this.images[this.currentIndex] || ''
},
canPrev() {
return this.hasImages && this.currentIndex > 0
},
canNext() {
return this.hasImages && this.currentIndex < this.images.length - 1
},
pageLabel() {
if (!this.hasImages) return '0 / 0'
return `${this.currentIndex + 1} / ${this.images.length}`
}
},
watch: {
visible: {
handler(val) {
if (val && this.roomId) {
this.loadData()
}
},
immediate: true
},
roomId: {
handler(val) {
if (val && this.visible) {
this.loadData()
}
},
immediate: true
}
},
beforeDestroy() {
document.removeEventListener('mousemove', this.onDragMove)
document.removeEventListener('mouseup', this.onDragEnd)
document.removeEventListener('mousemove', this.onResizeMove)
document.removeEventListener('mouseup', this.onResizeEnd)
},
methods: {
async loadData() {
this.layoutReady = false
if (!this.roomId) {
this.layoutReady = true
return
}
try {
const res = await getScreenshotGalleryData({ roomId: this.roomId })
let d = res && res.data
if (d) {
if (typeof d === 'string') {
try {
d = JSON.parse(d)
} catch (e) {
this.layoutReady = true
return
}
}
const imgs = d.images
this.images = Array.isArray(imgs) ? imgs.filter(Boolean) : []
this.currentIndex = this.images.length > 0 ? Math.min(this.currentIndex, this.images.length - 1) : 0
if (d.panelSize) {
const w = Number(d.panelSize.width)
const h = Number(d.panelSize.height)
if (!isNaN(w) && w >= 280 && w <= 1000) this.panelWidth = w
if (!isNaN(h) && h >= 200 && h <= window.innerHeight - 40) this.panelHeight = h
}
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)
}
}
} catch (e) {
console.warn('加载截图展示失败:', e)
} finally {
this.layoutReady = true
}
},
triggerPick() {
this.$refs.fileInput && this.$refs.fileInput.click()
},
readFileAsDataURL(file) {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (ev) => resolve(ev.target.result)
reader.onerror = () => resolve(null)
reader.readAsDataURL(file)
})
},
async onFilesSelected(e) {
const files = e.target.files ? Array.from(e.target.files) : []
e.target.value = ''
const imageFiles = files.filter((f) => f.type && f.type.startsWith('image/'))
if (imageFiles.length === 0) return
const remain = MAX_IMAGES - this.images.length
if (remain <= 0) {
this.$message.warning(`最多保存 ${MAX_IMAGES} 张图片`)
return
}
const take = imageFiles.slice(0, remain)
if (take.length < imageFiles.length) {
this.$message.warning(`已达上限,本次仅添加 ${take.length}`)
}
const urls = []
for (const f of take) {
const u = await this.readFileAsDataURL(f)
if (u) urls.push(u)
}
if (urls.length === 0) return
this.images = [...this.images, ...urls]
this.currentIndex = this.images.length - 1
if (this.roomId) this.saveData().catch(() => {})
},
removeCurrent() {
if (!this.hasImages) return
this.images.splice(this.currentIndex, 1)
if (this.currentIndex >= this.images.length) {
this.currentIndex = Math.max(0, this.images.length - 1)
}
if (this.roomId) this.saveData().catch(() => {})
},
prev() {
if (this.canPrev) this.currentIndex -= 1
},
next() {
if (this.canNext) this.currentIndex += 1
},
async saveData() {
if (!this.roomId) {
this.$message.warning('请先进入任务房间后再保存')
return
}
try {
const payload = {
images: this.images,
panelSize: { width: this.panelWidth, height: this.panelHeight }
}
if (this.panelLeft != null && this.panelTop != null) {
payload.panelPosition = { left: this.panelLeft, top: this.panelTop }
}
await saveScreenshotGalleryData({
roomId: this.roomId,
data: JSON.stringify(payload)
})
} catch (e) {
console.error('保存截图展示失败:', e)
this.$message.error('保存截图展示失败,请检查网络或权限')
}
},
onDragStart(e) {
e.preventDefault()
this.isDragging = true
const currentLeft = this.panelLeft != null ? this.panelLeft : 24
const currentTop = this.panelTop != null ? this.panelTop : 100
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)
if (this.roomId) this.saveData()
},
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
let w = Math.max(280, Math.min(1000, this.resizeStartW + dx))
let h = Math.max(200, Math.min(window.innerHeight - 40, this.resizeStartH + dy))
this.panelWidth = w
this.panelHeight = h
},
onResizeEnd() {
this.isResizing = false
document.removeEventListener('mousemove', this.onResizeMove)
document.removeEventListener('mouseup', this.onResizeEnd)
if (this.roomId) this.saveData()
}
}
}
</script>
<style scoped>
.screenshot-gallery-root {
position: fixed;
inset: 0;
z-index: 200;
background: transparent;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease-out;
}
.screenshot-gallery-root.sg-ready {
opacity: 1;
pointer-events: none;
}
.screenshot-gallery-root.sg-ready .sg-panel {
pointer-events: auto;
}
.sg-panel {
position: fixed;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(22, 93, 255, 0.15);
border: 1px solid #e0edff;
overflow: hidden;
z-index: 201;
}
.sg-toolbar {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px 8px 14px;
background: linear-gradient(180deg, #f9fcff 0%, #eef5ff 100%);
border-bottom: 1px solid #ddeaff;
cursor: move;
user-select: none;
}
.sg-title {
font-size: 14px;
font-weight: 600;
color: #165dff;
}
.sg-toolbar-btns {
display: flex;
align-items: center;
gap: 6px;
cursor: default;
}
.sg-hidden-file {
display: none;
}
.sg-txt-btn {
background: #ebf3ff;
color: #165dff;
border: 1px solid #cce5ff;
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: background 0.15s;
}
.sg-txt-btn:hover:not(:disabled) {
background: #e0edff;
}
.sg-txt-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.sg-icon-btn {
width: 26px;
height: 26px;
padding: 0;
border: 1px solid #cce5ff;
background: #fff;
color: #165dff;
border-radius: 4px;
font-size: 16px;
line-height: 1;
cursor: pointer;
}
.sg-icon-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.sg-counter {
font-size: 12px;
color: #546e7a;
min-width: 52px;
text-align: center;
}
.sg-close {
width: 28px;
height: 28px;
margin-left: 4px;
border: 1px solid #cce5ff;
background: #ebf3ff;
color: #165dff;
border-radius: 50%;
font-size: 16px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.sg-close:hover {
background: #e0edff;
}
.sg-body {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
background: #f5f9ff;
}
.sg-empty {
font-size: 13px;
color: #78909c;
text-align: center;
padding: 24px 16px;
line-height: 1.6;
}
.sg-img-frame {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
min-height: 0;
}
.sg-img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
vertical-align: middle;
}
.sg-resize {
position: absolute;
right: 0;
bottom: 0;
width: 18px;
height: 18px;
cursor: nwse-resize;
user-select: none;
z-index: 5;
background: linear-gradient(to top left, transparent 50%, rgba(22, 93, 255, 0.25) 50%);
}
.sg-resize:hover {
background: linear-gradient(to top left, transparent 50%, rgba(22, 93, 255, 0.45) 50%);
}
</style>

16
ruoyi-ui/src/views/childRoom/SixStepsOverlay.vue

@ -827,7 +827,7 @@ export default {
}
.sidebar-step.active .sidebar-step-num {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
background: linear-gradient(135deg, #165dff 0%, #165dff 100%);
color: white;
}
@ -865,7 +865,7 @@ export default {
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: linear-gradient(135deg, rgba(0, 138, 255, 0.08) 0%, rgba(0, 138, 255, 0.02) 100%);
background: linear-gradient(135deg, rgba(22, 93, 255, 0.08) 0%, rgba(22, 93, 255, 0.02) 100%);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
cursor: default;
flex-shrink: 0;
@ -892,13 +892,13 @@ export default {
}
.header-sub-title:hover {
background: rgba(0, 138, 255, 0.08);
color: #008aff;
background: rgba(22, 93, 255, 0.08);
color: #165dff;
}
.header-sub-title.active {
background: rgba(0, 138, 255, 0.12);
color: #008aff;
background: rgba(22, 93, 255, 0.12);
color: #165dff;
font-weight: 600;
}
@ -985,8 +985,8 @@ export default {
}
.sub-title-context-menu .context-menu-item:hover {
background: rgba(0, 138, 255, 0.08);
color: #008aff;
background: rgba(22, 93, 255, 0.08);
color: #165dff;
}
.sub-title-context-menu .context-menu-item-danger:hover {

28
ruoyi-ui/src/views/childRoom/StepCanvasContent.vue

@ -802,12 +802,12 @@ export default {
border: none;
border-radius: 8px;
font-size: 12px;
color: #008aff;
color: #165dff;
transition: all 0.2s;
}
.pagination-btn:hover:not(.disabled) {
background: rgba(0, 138, 255, 0.1);
border-color: rgba(0, 138, 255, 0.3);
background: rgba(22, 93, 255, 0.1);
border-color: rgba(22, 93, 255, 0.3);
}
.pagination-btn.disabled {
color: #cbd5e1;
@ -847,8 +847,8 @@ export default {
}
.pagination-indicator-clickable:hover {
background: rgba(255, 255, 255, 1);
border-color: rgba(0, 138, 255, 0.3);
color: #008aff;
border-color: rgba(22, 93, 255, 0.3);
color: #165dff;
}
.step-canvas-content {
display: flex;
@ -876,7 +876,7 @@ export default {
}
.canvas-icon.selected {
border-color: #008aff;
border-color: #165dff;
z-index: 10;
}
@ -898,7 +898,7 @@ export default {
.icon-placeholder {
font-size: 24px;
color: #008aff;
color: #165dff;
}
.delete-handle {
@ -943,7 +943,7 @@ export default {
position: absolute;
width: 8px;
height: 8px;
background: #008aff;
background: #165dff;
border: 1px solid #fff;
border-radius: 2px;
pointer-events: auto;
@ -966,7 +966,7 @@ export default {
width: 12px;
height: 12px;
background: #fff;
border: 2px solid #008aff;
border: 2px solid #165dff;
border-radius: 50%;
cursor: grab;
pointer-events: auto;
@ -1105,8 +1105,8 @@ export default {
.drawing-textbox {
position: absolute;
border: 2px dashed #008aff;
background: rgba(0, 138, 255, 0.05);
border: 2px dashed #165dff;
background: rgba(22, 93, 255, 0.05);
pointer-events: none;
}
</style>
@ -1138,8 +1138,8 @@ export default {
background: #f1f5f9;
}
.page-preview-item.active {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
background: rgba(22, 93, 255, 0.1);
color: #165dff;
}
.page-preview-label {
flex: 1;
@ -1152,7 +1152,7 @@ export default {
color: #94a3b8;
}
.page-preview-item.active .page-preview-meta {
color: rgba(0, 138, 255, 0.7);
color: rgba(22, 93, 255, 0.7);
}
.page-delete-btn {
padding: 4px 8px;

12
ruoyi-ui/src/views/childRoom/TaskPageContent.vue

@ -764,7 +764,7 @@ export default {
}
.canvas-icon.selected {
border-color: #008aff;
border-color: #165dff;
z-index: 10;
}
@ -787,7 +787,7 @@ export default {
.icon-placeholder {
font-size: 24px;
color: #008aff;
color: #165dff;
}
.delete-handle {
@ -832,7 +832,7 @@ export default {
position: absolute;
width: 8px;
height: 8px;
background: #008aff;
background: #165dff;
border: 1px solid #fff;
border-radius: 2px;
pointer-events: auto;
@ -855,7 +855,7 @@ export default {
width: 12px;
height: 12px;
background: #fff;
border: 2px solid #008aff;
border: 2px solid #165dff;
border-radius: 50%;
cursor: grab;
pointer-events: auto;
@ -997,8 +997,8 @@ export default {
.drawing-textbox {
position: absolute;
border: 2px dashed #008aff;
background: rgba(0, 138, 255, 0.05);
border: 2px dashed #165dff;
background: rgba(22, 93, 255, 0.05);
pointer-events: none;
}

176
ruoyi-ui/src/views/childRoom/TopHeader.vue

@ -52,6 +52,7 @@
<el-dropdown-item @click.native="importATO">{{ $t('topHeader.file.importATO') }}</el-dropdown-item>
<el-dropdown-item @click.native="importLayer">{{ $t('topHeader.file.importLayer') }}</el-dropdown-item>
<el-dropdown-item @click.native="importRoute">{{ $t('topHeader.file.importRoute') }}</el-dropdown-item>
<el-dropdown-item @click.native="importPlatformGraphics">{{ $t('topHeader.file.importPlatformGraphics') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-dropdown-item>
@ -67,6 +68,7 @@
<el-dropdown-menu slot="dropdown" class="submenu">
<el-dropdown-item @click.native="exportRoute">{{ $t('topHeader.file.exportRoute') }}</el-dropdown-item>
<el-dropdown-item @click.native="exportPlan">{{ $t('topHeader.file.exportPlan') }}</el-dropdown-item>
<el-dropdown-item @click.native="exportPlatformGraphics">{{ $t('topHeader.file.exportPlatformGraphics') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-dropdown-item>
@ -269,11 +271,18 @@
<div
class="info-box combat-time-box"
:class="{ 'clickable': !(childRoomKTimes && childRoomKTimes.length > 0) && canSetKTime }"
:class="{
'combat-time-box--child': childRoomKTimes && childRoomKTimes.length > 0,
'combat-time-box--interactive': combatTimeBoxInteractive
}"
:role="combatTimeBoxInteractive ? 'button' : undefined"
:tabindex="combatTimeBoxInteractive ? 0 : -1"
@click="onCombatTimeBoxClick"
@keydown.enter.space.prevent="onCombatTimeBoxKey"
>
<i class="el-icon-timer info-icon"></i>
<div class="info-content combat-time-content">
<template v-if="childRoomKTimes && childRoomKTimes.length > 0">
<template v-if="childRoomKTimes && childRoomKTimes.length > 0">
<i class="el-icon-timer info-icon combat-time-box__icon"></i>
<div class="info-content combat-time-content">
<el-dropdown trigger="click" @command="handleSelectChildKTime" class="k-time-dropdown">
<div class="combat-k-time k-time-selectable">
<span class="k-time-label">{{ currentChildKTimeItem.name }}:</span>
@ -286,19 +295,30 @@
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<div v-else-if="kTimeDisplay" class="combat-k-time" @click="canSetKTime && $emit('set-k-time')">
<span class="k-time-label">K </span>
<span class="k-time-value">{{ kTimeDisplay }}</span>
<div class="combat-time-row combat-time-row--below-dropdown">
<span class="info-label">{{ $t('topHeader.info.combatTime') }}</span>
<span class="combat-time-value">
{{ combatTime }}
</span>
</div>
</div>
<div class="combat-time-row">
<span class="info-label">{{ $t('topHeader.info.combatTime') }}</span>
<span class="combat-time-value">
{{ combatTime }}
<i v-if="canSetKTime" class="el-icon-edit-outline set-k-hint" title="点击设定或修改 K 时(房主/管理员可随时更改)"></i>
</span>
</template>
<template v-else>
<i class="el-icon-timer info-icon combat-time-box__icon"></i>
<div class="info-content combat-time-content">
<div v-if="kTimeDisplay" class="combat-k-time">
<span class="k-time-label">K时</span>
<span class="k-time-value">{{ kTimeDisplay }}</span>
</div>
<div class="combat-time-row">
<span class="info-label">{{ $t('topHeader.info.combatTime') }}</span>
<span class="combat-time-value">
{{ combatTime }}
<i v-if="canSetKTime" class="el-icon-edit-outline set-k-hint" title="点击整块卡片(含留白边)设定或修改 K 时(房主/管理员)"></i>
</span>
</div>
</div>
</div>
</template>
</div>
<div class="info-box">
@ -431,6 +451,10 @@ export default {
const idx = Math.max(0, Math.min(this.selectedChildKTimeIndex, this.childRoomKTimes.length - 1));
return this.childRoomKTimes[idx];
},
/** 小房间且可编辑:整块信息卡(含 padding 外边)可点开 K 时弹窗 */
combatTimeBoxInteractive() {
return !!(this.canSetKTime && (!this.childRoomKTimes || this.childRoomKTimes.length === 0));
},
/** 显示数据库房间名,无则回退为房间编号;大房间时追加标识 */
roomDisplayName() {
const name = (this.roomDetail && this.roomDetail.name) ? this.roomDetail.name : this.roomCode;
@ -518,6 +542,14 @@ export default {
this.$emit('export-plan')
},
importPlatformGraphics() {
this.$emit('import-platform-graphics')
},
exportPlatformGraphics() {
this.$emit('export-platform-graphics')
},
//
routeEdit() {
this.$emit('route-edit')
@ -678,6 +710,20 @@ export default {
handleSelectChildKTime(idx) {
this.$emit('select-child-k-time', idx);
},
/** 小房间:整块「K时 + 作战时间」可点,避免只有窄条能触发 */
onSmallRoomKTimeAreaClick() {
if (!this.canSetKTime) return;
this.$emit('set-k-time');
},
/** 点击落在整张 combat-time 卡片上(含内边距留白);大房间时忽略 */
onCombatTimeBoxClick() {
if (this.childRoomKTimes && this.childRoomKTimes.length > 0) return;
this.onSmallRoomKTimeAreaClick();
},
onCombatTimeBoxKey() {
if (!this.combatTimeBoxInteractive) return;
this.onSmallRoomKTimeAreaClick();
},
formatChildKTime(val) {
if (!val) return '-'
const d = new Date(val)
@ -737,7 +783,7 @@ export default {
backdrop-filter: blur(15px);
/* 调整背景为更透明的白色 */
background: rgba(255, 255, 255, 0.3);
border-bottom: 1px solid rgba(0, 138, 255, 0.1);
border-bottom: 1px solid rgba(22, 93, 255, 0.1);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
}
@ -765,11 +811,11 @@ export default {
.system-title i {
font-size: 24px;
color: #008aff;
color: #165dff;
}
.blue-title {
color: #008aff !important;
color: #165dff !important;
}
/* 顶部导航菜单 - 优化为简洁文字效果 */
@ -788,12 +834,12 @@ export default {
}
.top-nav-menu::-webkit-scrollbar-track {
background: rgba(0, 138, 255, 0.1);
background: rgba(22, 93, 255, 0.1);
border-radius: 2px;
}
.top-nav-menu::-webkit-scrollbar-thumb {
background: rgba(0, 138, 255, 0.3);
background: rgba(22, 93, 255, 0.3);
border-radius: 2px;
}
@ -817,12 +863,12 @@ export default {
}
.top-nav-item:hover {
color: #008aff;
background: rgba(0, 138, 255, 0.05);
color: #165dff;
background: rgba(22, 93, 255, 0.05);
}
.top-nav-item.active {
color: #008aff;
color: #165dff;
font-weight: 700;
}
@ -874,7 +920,7 @@ export default {
backdrop-filter: blur(15px);
/* 下拉菜单也同步调整为更透明的白色 */
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 138, 255, 0.1);
border: 1px solid rgba(22, 93, 255, 0.1);
padding: 0;
min-width: auto;
width: fit-content;
@ -889,12 +935,12 @@ export default {
}
.file-dropdown-menu .el-dropdown-menu__item:hover {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
background: rgba(22, 93, 255, 0.1);
color: #165dff;
}
.file-dropdown-menu .el-dropdown-menu__item:not(:last-child) {
border-bottom: 1px solid rgba(0, 138, 255, 0.1);
border-bottom: 1px solid rgba(22, 93, 255, 0.1);
}
.file-dropdown-menu .el-dropdown-menu__item:last-child {
@ -931,7 +977,7 @@ export default {
backdrop-filter: blur(15px);
/* 子菜单同步调整透明度 */
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 138, 255, 0.1);
border: 1px solid rgba(22, 93, 255, 0.1);
padding: 0;
min-width: auto;
width: fit-content;
@ -946,12 +992,12 @@ export default {
}
.submenu .el-dropdown-menu__item:hover {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
background: rgba(22, 93, 255, 0.1);
color: #165dff;
}
.submenu .el-dropdown-menu__item:not(:last-child) {
border-bottom: 1px solid rgba(0, 138, 255, 0.1);
border-bottom: 1px solid rgba(22, 93, 255, 0.1);
}
.submenu .el-dropdown-menu__item:last-child {
@ -976,12 +1022,12 @@ export default {
transition: color 0.2s, background 0.2s;
}
.map-drag-toggle:hover {
color: #409eff;
background: rgba(64, 158, 255, 0.08);
color: #165dff;
background: rgba(22, 93, 255, 0.08);
}
.map-drag-toggle.active {
color: #409eff;
background: rgba(64, 158, 255, 0.12);
color: #165dff;
background: rgba(22, 93, 255, 0.12);
}
.map-drag-toggle .hand-icon {
flex-shrink: 0;
@ -1007,18 +1053,40 @@ export default {
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
border: 1px solid rgba(0, 138, 255, 0.05);
border: 1px solid rgba(22, 93, 255, 0.05);
}
.info-box:hover {
background: rgba(0, 138, 255, 0.1);
background: rgba(22, 93, 255, 0.1);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 138, 255, 0.1);
box-shadow: 0 4px 12px rgba(22, 93, 255, 0.1);
}
/* 作战时间区域:显示 K 时 + 当前作战时间 */
.combat-time-box {
min-width: 180px;
align-items: center;
}
.combat-time-box--child.info-box {
cursor: default;
}
.combat-time-box--child .k-time-selectable {
cursor: pointer;
}
/* 避免 K 时块因 hover 上移、内边距叠加而「长出」顶栏 */
.combat-time-box.info-box:hover {
transform: none;
}
.combat-time-box--interactive:focus {
outline: none;
}
.combat-time-box--interactive:focus-visible {
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.35);
}
/* 闹钟与两行字直接铺在 info-box 内,去掉内层可点区块,避免「框套框」 */
.combat-time-box__icon {
flex-shrink: 0;
align-self: center;
}
.k-time-dropdown {
width: 100%;
@ -1031,19 +1099,24 @@ export default {
padding: 2px 0;
}
.k-time-selectable:hover {
color: #008aff;
color: #165dff;
}
.k-time-arrow {
font-size: 12px;
margin-left: 4px;
color: #008aff;
color: #165dff;
}
.k-time-dropdown-menu .el-dropdown-menu__item {
padding: 8px 16px;
}
.combat-time-content {
gap: 6px;
flex-wrap: wrap;
gap: 2px;
flex-wrap: nowrap;
flex: 1;
min-width: 0;
}
.combat-time-row--below-dropdown {
width: 100%;
}
.combat-k-time {
display: flex;
@ -1051,9 +1124,10 @@ export default {
gap: 6px;
font-size: 11px;
color: #666;
line-height: 1.2;
}
.combat-k-time .k-time-label {
color: #008aff;
color: #165dff;
font-weight: 600;
flex-shrink: 0;
}
@ -1071,10 +1145,16 @@ export default {
flex-shrink: 0;
}
.combat-time-value {
font-size: 14px;
color: #008aff;
font-size: 13px;
color: #165dff;
font-weight: 700;
letter-spacing: 0.5px;
letter-spacing: 0.3px;
line-height: 1.25;
display: inline-flex;
align-items: center;
flex-wrap: nowrap;
gap: 4px;
white-space: nowrap;
}
.combat-info-group .info-box:nth-child(4) .info-value {
@ -1089,7 +1169,7 @@ export default {
.info-box .set-k-hint {
margin-left: 4px;
font-size: 12px;
color: #008aff;
color: #165dff;
vertical-align: middle;
}
.combat-time-box .set-k-hint {
@ -1101,7 +1181,7 @@ export default {
.info-icon {
font-size: 20px;
color: #008aff;
color: #165dff;
}
.info-content {

28
ruoyi-ui/src/views/childRoom/UnderstandingStepContent.vue

@ -931,12 +931,12 @@ export default {
border: none;
border-radius: 8px;
font-size: 12px;
color: #008aff;
color: #165dff;
transition: all 0.2s;
}
.pagination-btn:hover:not(.disabled) {
background: rgba(0, 138, 255, 0.1);
border-color: rgba(0, 138, 255, 0.3);
background: rgba(22, 93, 255, 0.1);
border-color: rgba(22, 93, 255, 0.3);
}
.pagination-btn.disabled {
color: #cbd5e1;
@ -976,8 +976,8 @@ export default {
}
.pagination-indicator-clickable:hover {
background: rgba(255, 255, 255, 1);
border-color: rgba(0, 138, 255, 0.3);
color: #008aff;
border-color: rgba(22, 93, 255, 0.3);
color: #165dff;
}
.understanding-step-content {
display: flex;
@ -1008,7 +1008,7 @@ export default {
}
.canvas-icon.selected {
border-color: #008aff;
border-color: #165dff;
z-index: 10;
}
@ -1031,7 +1031,7 @@ export default {
.icon-placeholder {
font-size: 24px;
color: #008aff;
color: #165dff;
}
.delete-handle {
@ -1076,7 +1076,7 @@ export default {
position: absolute;
width: 8px;
height: 8px;
background: #008aff;
background: #165dff;
border: 1px solid #fff;
border-radius: 2px;
pointer-events: auto;
@ -1099,7 +1099,7 @@ export default {
width: 12px;
height: 12px;
background: #fff;
border: 2px solid #008aff;
border: 2px solid #165dff;
border-radius: 50%;
cursor: grab;
pointer-events: auto;
@ -1238,8 +1238,8 @@ export default {
.drawing-textbox {
position: absolute;
border: 2px dashed #008aff;
background: rgba(0, 138, 255, 0.05);
border: 2px dashed #165dff;
background: rgba(22, 93, 255, 0.05);
pointer-events: none;
}
</style>
@ -1271,8 +1271,8 @@ export default {
background: #f1f5f9;
}
.page-preview-item.active {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
background: rgba(22, 93, 255, 0.1);
color: #165dff;
}
.page-preview-label {
flex: 1;
@ -1285,7 +1285,7 @@ export default {
color: #94a3b8;
}
.page-preview-item.active .page-preview-meta {
color: rgba(0, 138, 255, 0.7);
color: rgba(22, 93, 255, 0.7);
}
.page-delete-btn {
padding: 4px 8px;

70
ruoyi-ui/src/views/childRoom/WhiteboardPanel.vue

@ -29,12 +29,15 @@
</el-popover>
</div>
<!-- 绘制空域时间选择右侧 -->
<!-- 绘制空域 | 框选时间选择右侧 -->
<div class="wb-tools-section">
<span class="wb-label">绘制</span>
<el-button size="mini" :type="drawMode === 'airspace' ? 'primary' : 'default'" @click="toggleAirspaceDraw">
空域
</el-button>
<el-button size="mini" :type="platformBoxSelectActive ? 'primary' : 'default'" @click="togglePlatformBoxSelect">
框选
</el-button>
</div>
<!-- 白板方案选择新建退出与时间选择同高 -->
@ -83,15 +86,17 @@
</div>
</div>
<!-- 第二行平台空中 | 海上 | 地面 -->
<!-- 第二行平台空中 | 海上 | 地面网格独占一行避免 flex 挤压导致宽度为 0 -->
<div class="wb-row wb-row-platform">
<span class="wb-label">平台</span>
<el-radio-group v-model="platformFilter" size="mini" class="wb-platform-filter">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="air">空中</el-radio-button>
<el-radio-button label="sea">海上</el-radio-button>
<el-radio-button label="ground">地面</el-radio-button>
</el-radio-group>
<div class="wb-platform-row-head">
<span class="wb-label">平台</span>
<el-radio-group v-model="platformFilter" size="mini" class="wb-platform-filter">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="air">空中</el-radio-button>
<el-radio-button label="sea">海上</el-radio-button>
<el-radio-button label="ground">地面</el-radio-button>
</el-radio-group>
</div>
<div class="wb-platform-grid">
<div
v-for="p in filteredPlatforms"
@ -100,7 +105,7 @@
draggable="true"
@dragstart="onPlatformDragStart($event, p)"
>
<div class="wb-platform-icon" :style="{ color: p.color || '#008aff' }">
<div class="wb-platform-icon" :style="{ color: p.color || '#165dff' }">
<img v-if="isImg(p.imageUrl || p.iconUrl)" :src="formatImg(p.imageUrl || p.iconUrl)" class="wb-platform-img" />
<i v-else :class="p.icon || 'el-icon-picture-outline'"></i>
</div>
@ -182,6 +187,11 @@ export default {
type: Boolean,
default: false
},
/** 与房间左侧「框选平台」同步:高亮并触发与主地图相同的框选逻辑 */
platformBoxSelectActive: {
type: Boolean,
default: false
},
roomId: {
type: [String, Number],
default: null
@ -271,6 +281,12 @@ export default {
}
},
immediate: true
},
platformBoxSelectActive(val) {
if (val && this.drawMode === 'airspace') {
this.drawMode = null
this.$emit('draw-mode-change', null)
}
}
},
methods: {
@ -446,6 +462,9 @@ export default {
toggleAirspaceDraw() {
this.drawMode = this.drawMode === 'airspace' ? null : 'airspace'
this.$emit('draw-mode-change', this.drawMode)
},
togglePlatformBoxSelect() {
this.$emit('toggle-platform-box-select')
}
}
}
@ -459,11 +478,11 @@ export default {
right: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-top: 1px solid rgba(0, 138, 255, 0.2);
border-top: 1px solid rgba(22, 93, 255, 0.2);
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08);
z-index: 85;
padding: 10px 16px;
max-height: 200px;
max-height: min(38vh, 320px);
overflow-y: auto;
}
@ -479,8 +498,17 @@ export default {
}
.wb-row-platform {
align-items: flex-start;
flex-direction: column;
align-items: stretch;
width: 100%;
gap: 8px;
}
.wb-platform-row-head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.wb-time-section,
@ -532,12 +560,14 @@ export default {
.wb-platform-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(48px, 1fr));
gap: 6px;
flex: 1;
min-width: 0;
max-height: 70px;
grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
gap: 8px;
width: 100%;
min-height: 64px;
max-height: 140px;
overflow-x: hidden;
overflow-y: auto;
box-sizing: border-box;
}
.wb-platform-item {
@ -547,12 +577,12 @@ export default {
padding: 4px;
border-radius: 4px;
cursor: grab;
background: rgba(0, 138, 255, 0.06);
background: rgba(22, 93, 255, 0.06);
transition: background 0.2s;
}
.wb-platform-item:hover {
background: rgba(0, 138, 255, 0.15);
background: rgba(22, 93, 255, 0.15);
}
.wb-platform-icon {

503
ruoyi-ui/src/views/childRoom/index.vue

@ -45,6 +45,9 @@
@scale-click="handleScaleClick"
@platform-icon-updated="onPlatformIconUpdated"
@platform-icons-batch-updated="onPlatformIconsBatchUpdated"
@platform-icons-copy-placed="onPlatformIconsCopyPlaced"
@whiteboard-platforms-batch-updated="onWhiteboardPlatformsBatchUpdated"
@whiteboard-platforms-copy-placed="onWhiteboardPlatformsCopyPlaced"
@platform-icon-removed="onPlatformIconRemoved"
@viewer-ready="onViewerReady"
@drawing-entities-changed="onDrawingEntitiesChanged"
@ -52,6 +55,7 @@
@whiteboard-draw-complete="handleWhiteboardDrawComplete"
@whiteboard-platform-updated="handleWhiteboardPlatformUpdated"
@whiteboard-platform-style-updated="handleWhiteboardPlatformStyleUpdated"
@room-platform-icon-style-updated="handleRoomPlatformIconStyleUpdated"
@whiteboard-entity-deleted="handleWhiteboardEntityDeleted"
@whiteboard-drawing-updated="handleWhiteboardDrawingUpdated" />
<div v-show="!screenshotMode" class="map-overlay-text">
@ -127,8 +131,10 @@
@import-ato="importATO"
@import-layer="importLayer"
@import-route="importRoute"
@import-platform-graphics="importPlatformGraphics"
@export-routes="openExportRoutesDialog"
@export-plan="exportPlan"
@export-platform-graphics="exportPlatformGraphics"
@route-edit="routeEdit"
@military-marking="militaryMarking"
@icon-edit="iconEdit"
@ -438,6 +444,12 @@
:room-id="currentRoomId"
/>
<screenshot-gallery-panel
v-if="showScreenshotGalleryPanel && !screenshotMode"
:visible.sync="showScreenshotGalleryPanel"
:room-id="currentRoomId"
/>
<!-- 冲突列表弹窗可拖动可调整大小分页点击左侧冲突按钮即打开并自动检测 -->
<conflict-drawer
v-if="!screenshotMode"
@ -452,6 +464,7 @@
<whiteboard-panel
v-show="showWhiteboardPanel && !screenshotMode"
:visible="showWhiteboardPanel"
:platform-box-select-active="platformBoxSelectMode"
:room-id="currentRoomId"
:whiteboards="whiteboards"
:current-whiteboard="currentWhiteboard"
@ -468,6 +481,7 @@
@rename-time-block="handleWhiteboardRenameTimeBlock"
@delete-time-block="handleWhiteboardDeleteTimeBlock"
@draw-mode-change="handleWhiteboardDrawModeChange"
@toggle-platform-box-select="togglePlatformBoxSelectMenu"
@export-whiteboard="handleWhiteboardExport"
@import-whiteboard="handleWhiteboardImport"
/>
@ -533,11 +547,12 @@ 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';
import { listScenario, addScenario, delScenario } from "@/api/system/scenario";
import { listRoutes, getRoutes, addRoutes, updateRoutes, delRoutes, getPlatformStyle, getMissileParams, updateMissilePositions, saveMissileParams, deleteMissileParams } from "@/api/system/routes";
import { listRoutes, getRoutes, addRoutes, updateRoutes, delRoutes, getPlatformStyle, savePlatformStyle, getMissileParams, updateMissilePositions, saveMissileParams, deleteMissileParams } from "@/api/system/routes";
import { updateWaypoints, addWaypoints, delWaypoints } from "@/api/system/waypoints";
import { listLib,addLib,delLib} from "@/api/system/lib";
import { getRooms, updateRooms, listRooms } from "@/api/system/rooms";
@ -586,6 +601,7 @@ export default {
BottomLeftPanel,
TopHeader,
FourTPanel,
ScreenshotGalleryPanel,
ConflictDrawer,
WhiteboardPanel
},
@ -688,6 +704,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' },
@ -749,6 +766,8 @@ export default {
showKTimePopup: false,
// 4T4T/
show4TPanel: false,
/** 截图展示悬浮窗(多图翻页,与 4T 同属穿透交互) */
showScreenshotGalleryPanel: false,
/** 冲突列表弹窗(点击左侧冲突按钮即打开并自动执行检测) */
showConflictDrawer: false,
/** 冲突检测进行中(避免主线程长时间阻塞导致页面卡死) */
@ -985,7 +1004,7 @@ export default {
if (style && style.line && style.line.color) color = style.line.color;
else if (style && style.waypoint && style.waypoint.color) color = style.waypoint.color;
}
if (!color || this.isGanttColorTooDark(color)) color = '#409EFF';
if (!color || this.isGanttColorTooDark(color)) color = '#165dff';
bars.push({
id: `route-${route.id}`,
name: route.name || route.callSign || `航线${route.id}`,
@ -2387,33 +2406,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) {
@ -2486,7 +2514,7 @@ export default {
return null;
}
},
/** 甘特图用:判断颜色是否过深(黑/深灰),若是则改用图例蓝 #409EFF */
/** 甘特图用:判断颜色是否过深(黑/深灰),若是则改用图例蓝 #165dff */
isGanttColorTooDark(color) {
if (!color || typeof color !== 'string') return true;
const s = color.trim().toLowerCase();
@ -3540,6 +3568,117 @@ export default {
//
},
/** 导出当前房间已保存的摆放平台及探测区/威力区(JSON 文件) */
exportPlatformGraphics() {
if (!this.currentRoomId) {
this.$message.warning('请先进入任务房间');
return;
}
const map = this.$refs.cesiumMap;
if (!map || typeof map.getPlatformGraphicsExportData !== 'function') {
this.$message.error('地图未就绪');
return;
}
const exportObj = map.getPlatformGraphicsExportData(this.currentRoomId);
if (!exportObj || !exportObj.platforms || exportObj.platforms.length === 0) {
this.$message.info('当前房间没有可导出的摆放平台(需已从右侧拖入并保存到房间)');
return;
}
const blob = new Blob([JSON.stringify(exportObj, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `平台图形导出_${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
this.$message.success(`已导出 ${exportObj.platforms.length} 个平台图形`);
},
/** 从 JSON 导入平台图形到当前房间(新增实例,不覆盖已有平台) */
importPlatformGraphics() {
if (!this.currentRoomId) {
this.$message.warning('请先进入任务房间');
return;
}
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json,.json';
input.onchange = async () => {
const file = input.files && input.files[0];
if (!file) return;
try {
const text = await file.text();
const data = JSON.parse(text);
if (!data || !Array.isArray(data.platforms)) {
this.$message.error('文件格式不正确:需要包含 platforms 数组');
return;
}
let ok = 0;
for (const item of data.platforms) {
if (item == null || item.platformId == null || item.lat == null || item.lng == null) continue;
const payload = {
roomId: this.currentRoomId,
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
};
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(this.currentRoomId),
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++;
}
const rId = this.currentRoomId;
const listRes = await listRoomPlatformIcons(rId);
if (listRes.code === 200 && listRes.data && this.$refs.cesiumMap && typeof this.$refs.cesiumMap.loadRoomPlatformIcons === 'function') {
this.$refs.cesiumMap.loadRoomPlatformIcons(rId, listRes.data);
}
this.wsConnection?.sendSyncPlatformIcons?.();
this.wsConnection?.sendSyncPlatformStyles?.();
if (ok === 0) {
this.$message.warning('未能导入任何平台,请检查 platformId、经纬度等字段是否完整');
} else {
this.$message.success(`已导入 ${ok} 个平台图形`);
}
} catch (e) {
console.error('importPlatformGraphics', e);
this.$message.error('导入失败:' + (e.message || '请检查文件格式'));
}
};
input.click();
},
//
routeEdit() {
this.$message.success('航线编辑');
@ -3652,6 +3791,7 @@ export default {
} else {
this.drawDom = false
this.airspaceDrawDom = false
this.whiteboardAirspaceDraw = false
this.isRightPanelHidden = true
}
},
@ -3756,7 +3896,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 {
@ -3835,6 +3977,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()
@ -3902,6 +4052,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) {
@ -3968,7 +4126,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 }
@ -3978,18 +4141,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
@ -4033,10 +4229,13 @@ export default {
heading: e.heading != null ? e.heading : 0,
iconScale: e.iconScale != null ? e.iconScale : 1.5,
label: e.label || '',
color: e.color || '#008aff'
color:
e.color != null && String(e.color).trim() !== ''
? e.color
: null
}
}
const base = { type: e.type, id: e.id, color: e.color || '#008aff' }
const base = { type: e.type, id: e.id, color: e.color || '#165dff' }
const data = e.data ? { ...e.data } : {}
switch (e.type) {
case 'polygon': {
@ -4174,7 +4373,10 @@ export default {
heading: ent.heading != null ? ent.heading : 0,
iconScale: ent.iconScale != null ? ent.iconScale : 1.5,
label: ent.label || '平台',
color: ent.color || '#008aff'
color:
ent.color != null && String(ent.color).trim() !== ''
? ent.color
: null
}
}
if (ent.type === 'text' && ent.data) {
@ -4367,7 +4569,11 @@ export default {
lng: pos.lng,
heading: 0,
label: platform.name || '平台',
color: platform.color || '#008aff'
// null
color:
platform.color != null && String(platform.color).trim() !== ''
? platform.color
: null
}
const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) }
const currentContent = contentByTime[this.currentWhiteboardTimeBlock] || { entities: [] }
@ -4443,6 +4649,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
@ -4854,6 +5199,8 @@ export default {
} else if (item.id === '4t') {
// 4T4T
this.show4TPanel = !this.show4TPanel;
} else if (item.id === 'screenshotGallery') {
this.showScreenshotGalleryPanel = !this.showScreenshotGalleryPanel;
} else if (item.id === 'whiteboard') {
// /退
this.toggleWhiteboardMode();
@ -6843,51 +7190,51 @@ export default {
.blue-theme {
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 138, 255, 0.1);
box-shadow: 0 4px 12px rgba(0, 138, 255, 0.2);
border: 1px solid rgba(22, 93, 255, 0.1);
box-shadow: 0 4px 12px rgba(22, 93, 255, 0.2);
}
.blue-btn {
background: rgba(0, 138, 255, 0.8);
border: 1px solid rgba(0, 138, 255, 0.9);
background: rgba(22, 93, 255, 0.8);
border: 1px solid rgba(22, 93, 255, 0.9);
color: white;
transition: all 0.3s;
}
.blue-btn:hover {
background: rgba(0, 138, 255, 0.9);
border-color: rgba(0, 138, 255, 1);
background: rgba(22, 93, 255, 0.9);
border-color: rgba(22, 93, 255, 1);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.3);
box-shadow: 0 2px 8px rgba(22, 93, 255, 0.3);
}
.blue-text-btn {
color: #008aff;
color: #165dff;
}
.blue-text-btn:hover {
color: #0066cc;
color: #165dff;
}
.blue-badge {
background: rgba(0, 138, 255, 0.8);
background: rgba(22, 93, 255, 0.8);
color: white;
}
.blue-tag {
background: rgba(0, 138, 255, 0.2);
color: #008aff;
border: 1px solid rgba(0, 138, 255, 0.3);
background: rgba(22, 93, 255, 0.2);
color: #165dff;
border: 1px solid rgba(22, 93, 255, 0.3);
}
.blue-time {
background: rgba(0, 138, 255, 0.1);
border: 1px solid rgba(0, 138, 255, 0.3);
color: #008aff;
background: rgba(22, 93, 255, 0.1);
border: 1px solid rgba(22, 93, 255, 0.3);
color: #165dff;
}
.blue-success {
color: #008aff;
color: #165dff;
}
.blue-warning {
@ -6895,13 +7242,13 @@ export default {
}
.blue-mark {
color: #008aff;
color: #165dff;
}
.status-dot.operating {
background: #008aff;
background: #165dff;
animation: pulse 2s infinite;
box-shadow: 0 0 10px rgba(0, 138, 255, 0.8);
box-shadow: 0 0 10px rgba(22, 93, 255, 0.8);
}
/* 蓝色主题标签页 */
@ -6911,21 +7258,21 @@ export default {
}
.blue-tabs >>> .el-tabs__item:hover {
color: #008aff;
color: #165dff;
}
.blue-tabs >>> .el-tabs__item.is-active {
color: #008aff;
color: #165dff;
font-weight: 600;
}
.blue-tabs >>> .el-tabs__active-bar {
background-color: #008aff;
box-shadow: 0 0 6px rgba(0, 138, 255, 0.5);
background-color: #165dff;
box-shadow: 0 0 6px rgba(22, 93, 255, 0.5);
}
.blue-tabs >>> .el-tabs__nav-wrap::after {
background-color: rgba(0, 138, 255, 0.3);
background-color: rgba(22, 93, 255, 0.3);
}
/* 底部时间轴(最初版本的样式)- 蓝色主题 */
@ -6944,8 +7291,8 @@ export default {
pointer-events: none;
backdrop-filter: blur(15px);
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 8px 32px rgba(0, 138, 255, 0.25);
border: 1px solid rgba(0, 138, 255, 0.3);
box-shadow: 0 8px 32px rgba(22, 93, 255, 0.25);
border: 1px solid rgba(22, 93, 255, 0.3);
}
.floating-timeline.show {
@ -6970,22 +7317,22 @@ export default {
height: 28px;
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 138, 255, 0.4);
border: 1px solid rgba(22, 93, 255, 0.4);
border-bottom: none;
border-radius: 12px 12px 0 0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #008aff;
color: #165dff;
font-size: 18px;
transition: all 0.3s;
z-index: 10;
}
.popup-hide-btn:hover {
background: rgba(0, 138, 255, 0.2);
color: #0066cc;
background: rgba(22, 93, 255, 0.2);
color: #165dff;
height: 32px;
top: -32px;
}
@ -7001,9 +7348,9 @@ export default {
font-size: 12px;
padding: 4px 10px;
border-radius: 6px;
border: 1px solid rgba(0, 138, 255, 0.3);
color: #008aff;
background: rgba(0, 138, 255, 0.08);
border: 1px solid rgba(22, 93, 255, 0.3);
color: #165dff;
background: rgba(22, 93, 255, 0.08);
}
.timeline-mode-badge.single {
@ -7071,12 +7418,12 @@ export default {
}
.blue-slider >>> .el-slider__bar {
background-color: rgba(0, 138, 255, 0.8);
background-color: rgba(22, 93, 255, 0.8);
}
.blue-slider >>> .el-slider__button {
border-color: rgba(0, 138, 255, 0.8);
background-color: rgba(0, 138, 255, 0.8);
border-color: rgba(22, 93, 255, 0.8);
background-color: rgba(22, 93, 255, 0.8);
}
.playback-controls {
@ -7088,7 +7435,7 @@ export default {
.control-btn {
width: 26px;
height: 26px;
border: 1px solid rgba(0, 138, 255, 0.3);
border: 1px solid rgba(22, 93, 255, 0.3);
background: rgba(255, 255, 255, 0.9);
border-radius: 4px;
cursor: pointer;
@ -7096,12 +7443,12 @@ export default {
align-items: center;
justify-content: center;
transition: all 0.3s;
color: #008aff;
color: #165dff;
}
.control-btn:hover:not(:disabled) {
background: rgba(0, 138, 255, 0.1);
border-color: rgba(0, 138, 255, 0.5);
background: rgba(22, 93, 255, 0.1);
border-color: rgba(22, 93, 255, 0.5);
}
.control-btn:disabled {
@ -7119,14 +7466,14 @@ export default {
gap: 6px;
padding: 2px 8px;
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 138, 255, 0.3);
border: 1px solid rgba(22, 93, 255, 0.3);
border-radius: 4px;
}
.speed-text {
font-size: 11px;
font-weight: 600;
color: #008aff;
color: #165dff;
min-width: 24px;
text-align: center;
}
@ -7148,7 +7495,7 @@ export default {
font-size: 12px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(0, 138, 255, 0.4);
border-top: 1px solid rgba(22, 93, 255, 0.4);
}
.status-item {
@ -7175,19 +7522,19 @@ export default {
}
::-webkit-scrollbar-track {
background: rgba(0, 138, 255, 0.1);
background: rgba(22, 93, 255, 0.1);
border-radius: 3px;
backdrop-filter: blur(5px);
}
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, rgba(0, 138, 255, 0.5), rgba(0, 138, 255, 0.7));
background: linear-gradient(135deg, rgba(22, 93, 255, 0.5), rgba(22, 93, 255, 0.7));
border-radius: 3px;
box-shadow: 0 0 6px rgba(0, 138, 255, 0.4);
box-shadow: 0 0 6px rgba(22, 93, 255, 0.4);
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, rgba(0, 138, 255, 0.7), rgba(0, 138, 255, 0.9));
background: linear-gradient(135deg, rgba(22, 93, 255, 0.7), rgba(22, 93, 255, 0.9));
}
.ml-3 {

16
ruoyi-ui/src/views/dialogs/IconSelectDialog.vue

@ -132,15 +132,15 @@ export default {
}
.icon-item: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 4px 12px rgba(0, 138, 255, 0.15);
box-shadow: 0 4px 12px rgba(22, 93, 255, 0.15);
}
.icon-item.selected {
border-color: #008aff;
background: rgba(0, 138, 255, 0.1);
border-color: #165dff;
background: rgba(22, 93, 255, 0.1);
}
.icon-item i {
@ -152,7 +152,7 @@ export default {
.icon-item:hover i,
.icon-item.selected i {
color: #008aff;
color: #165dff;
}
.icon-name {
@ -168,12 +168,12 @@ export default {
right: 5px;
width: 20px;
height: 20px;
border: 2px solid #008aff;
border: 2px solid #165dff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #008aff;
color: #165dff;
font-size: 12px;
}

407
ruoyi-ui/src/views/dialogs/KTimeSetDialog.vue

@ -1,5 +1,4 @@
<template>
<!-- 功能与 4T 一致透明遮罩可拖动记录位置不阻挡地图风格保持原样 -->
<div v-if="value" class="k-time-set-dialog">
<div class="panel-container" :style="panelStyle">
<div class="dialog-header" @mousedown="onDragStart">
@ -8,21 +7,33 @@
</div>
<div class="dialog-body">
<el-form label-width="90px">
<el-form-item label="K 时(基准)">
<el-date-picker
:value="dateTime"
@input="$emit('update:dateTime', $event)"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="选择日期和时间"
style="width: 100%"
/>
</el-form-item>
</el-form>
<div class="dialog-footer">
<el-button @click="closeDialog"> </el-button>
<el-button type="primary" @click="handleSave"> </el-button>
<div class="k-time-picker-layout">
<div class="k-time-label-aside">
<span class="k-time-label-plain">K时</span>
</div>
<div class="wheels-wrap">
<div class="wheels-grid">
<div v-for="col in wheelColumns" :key="col.key === 'd' ? 'd-' + pick.y + '-' + pick.m : col.key" class="wheel-col">
<div class="wheel-head">{{ col.label }}</div>
<div class="wheel-viewport">
<div class="wheel-highlight" aria-hidden="true" />
<ul
:ref="'wheel_' + col.key"
class="wheel-list"
@scroll.passive="onWheelScroll(col.key)"
>
<li v-for="n in col.values" :key="col.key + '-' + n" class="wheel-item" :data-val="n">
{{ formatWheelCell(col.key, n) }}
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="dialog-footer k-time-dialog-footer">
<el-button size="small" class="k-time-pill-btn k-time-pill-btn--cancel" @click="closeDialog"> </el-button>
<el-button size="small" type="primary" class="k-time-pill-btn k-time-pill-btn--confirm" @click="handleSave"> </el-button>
</div>
</div>
</div>
@ -31,6 +42,18 @@
<script>
const STORAGE_KEY_PREFIX = 'kTimeSetPanel_'
const ITEM_H = 36
const VIEW_H = 180
const YEAR_MIN = 1970
const YEAR_MAX = 2100
function pad2(n) {
return String(n).padStart(2, '0')
}
function daysInMonth(y, m) {
return new Date(y, m, 0).getDate()
}
export default {
name: 'KTimeSetDialog',
@ -43,11 +66,21 @@ export default {
return {
panelLeft: null,
panelTop: null,
panelWidth: 420,
panelHeight: 250,
panelWidth: 560,
panelHeight: 392,
isDragging: false,
dragStartX: 0,
dragStartY: 0
dragStartY: 0,
pick: {
y: new Date().getFullYear(),
m: new Date().getMonth() + 1,
d: new Date().getDate(),
h: new Date().getHours(),
mi: new Date().getMinutes(),
s: new Date().getSeconds()
},
_snapTimer: null,
_scrollSilent: false
}
},
computed: {
@ -58,20 +91,147 @@ export default {
left: `${left}px`,
top: `${top}px`,
width: `${this.panelWidth}px`,
height: `${this.panelHeight}px`
minHeight: `${this.panelHeight}px`
}
},
dayValues() {
const max = daysInMonth(this.pick.y, this.pick.m)
const out = []
for (let d = 1; d <= max; d++) out.push(d)
return out
},
wheelColumns() {
const years = []
for (let y = YEAR_MIN; y <= YEAR_MAX; y++) years.push(y)
const months = []
for (let m = 1; m <= 12; m++) months.push(m)
const hours = []
for (let h = 0; h <= 23; h++) hours.push(h)
const mins = []
for (let i = 0; i <= 59; i++) mins.push(i)
const secs = []
for (let i = 0; i <= 59; i++) secs.push(i)
return [
{ key: 'y', label: '年', values: years },
{ key: 'm', label: '月', values: months },
{ key: 'd', label: '日', values: this.dayValues },
{ key: 'h', label: '时', values: hours },
{ key: 'mi', label: '分', values: mins },
{ key: 's', label: '秒', values: secs }
]
}
},
watch: {
value(val) {
if (val) this.loadPosition()
if (val) {
this.loadPosition()
this.syncPickFromProp()
this.$nextTick(() => this.scrollAllToPick(false))
}
},
dateTime() {
if (this.value) {
this.syncPickFromProp()
this.$nextTick(() => this.scrollAllToPick(false))
}
}
},
methods: {
formatWheelCell(key, n) {
if (key === 'y') return String(n)
return pad2(n)
},
parsePropDate(str) {
if (!str) return new Date()
const m = String(str).trim().match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})$/)
if (m) {
return new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6])
}
const d = new Date(str)
return isNaN(d.getTime()) ? new Date() : d
},
syncPickFromProp() {
const d = this.parsePropDate(this.dateTime)
this.pick = {
y: d.getFullYear(),
m: d.getMonth() + 1,
d: d.getDate(),
h: d.getHours(),
mi: d.getMinutes(),
s: d.getSeconds()
}
this.clampDay()
},
clampDay() {
const max = daysInMonth(this.pick.y, this.pick.m)
if (this.pick.d > max) this.pick.d = max
},
wheelEl(key) {
const r = this.$refs['wheel_' + key]
return Array.isArray(r) ? r[0] : r
},
scrollAllToPick(smooth) {
this._scrollSilent = true
const bh = (VIEW_H - ITEM_H) / 2
const snap = (el, index) => {
if (!el) return
const top = Math.max(0, index) * ITEM_H
el.scrollTo({ top, behavior: smooth ? 'smooth' : 'auto' })
}
snap(this.wheelEl('y'), this.pick.y - YEAR_MIN)
snap(this.wheelEl('m'), this.pick.m - 1)
snap(this.wheelEl('d'), this.pick.d - 1)
snap(this.wheelEl('h'), this.pick.h)
snap(this.wheelEl('mi'), this.pick.mi)
snap(this.wheelEl('s'), this.pick.s)
this.$nextTick(() => {
this._scrollSilent = false
})
},
onWheelScroll(key) {
if (this._scrollSilent) return
clearTimeout(this._snapTimer)
this._snapTimer = setTimeout(() => this.snapColumn(key), 120)
},
snapColumn(key) {
const el = this.wheelEl(key)
if (!el) return
const col = this.wheelColumns.find(c => c.key === key)
if (!col || !col.values.length) return
const maxIdx = col.values.length - 1
let idx = Math.round(el.scrollTop / ITEM_H)
idx = Math.max(0, Math.min(maxIdx, idx))
const exact = idx * ITEM_H
if (Math.abs(el.scrollTop - exact) > 1) {
this._scrollSilent = true
el.scrollTo({ top: exact, behavior: 'smooth' })
this.$nextTick(() => {
this._scrollSilent = false
})
}
const v = col.values[idx]
if (key === 'y') this.pick.y = v
else if (key === 'm') this.pick.m = v
else if (key === 'd') this.pick.d = v
else if (key === 'h') this.pick.h = v
else if (key === 'mi') this.pick.mi = v
else if (key === 's') this.pick.s = v
this.clampDay()
if (key === 'y' || key === 'm') {
this.$nextTick(() => {
this.scrollAllToPick(false)
})
}
},
formatPickString() {
return `${this.pick.y}-${pad2(this.pick.m)}-${pad2(this.pick.d)} ${pad2(this.pick.h)}:${pad2(this.pick.mi)}:${pad2(this.pick.s)}`
},
closeDialog() {
this.$emit('input', false)
},
handleSave() {
const str = this.formatPickString()
this.$emit('update:dateTime', str)
this.$emit('save')
},
getStorageKey() {
@ -135,75 +295,236 @@ export default {
</script>
<style scoped>
/* 透明遮罩、不阻挡地图 */
.k-time-set-dialog {
position: fixed;
inset: 0;
z-index: 1000;
/* 高于地图 DOM 标牌(≈36)与右下角信息(1000),低于右键菜单(9999) */
z-index: 5000;
background: transparent;
pointer-events: none;
}
/* 原 el-dialog 风格 */
.panel-container {
position: fixed;
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
background: linear-gradient(165deg, #ffffff 0%, #f8faff 55%, #f4f7fc 100%);
border-radius: 20px;
box-shadow:
0 12px 40px rgba(15, 23, 42, 0.12),
0 2px 8px rgba(22, 93, 255, 0.06),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
overflow: hidden;
z-index: 1001;
z-index: 5001;
pointer-events: auto;
display: flex;
flex-direction: column;
border: 1px solid rgba(22, 93, 255, 0.1);
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
padding: 16px 22px;
background: rgba(255, 255, 255, 0.72);
backdrop-filter: blur(8px);
border-bottom: 1px solid rgba(22, 93, 255, 0.08);
cursor: move;
border-radius: 20px 20px 0 0;
}
.dialog-header h3 {
margin: 0;
font-size: 16px;
font-size: 15px;
font-weight: 600;
color: #303133;
color: #1e293b;
letter-spacing: 0.02em;
}
.close-btn {
font-size: 20px;
color: #909399;
cursor: pointer;
transition: color 0.3s;
width: 24px;
height: 24px;
transition: color 0.2s;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.close-btn:hover {
color: #606266;
background: #f5f5f5;
border-radius: 50%;
background: rgba(0, 0, 0, 0.05);
}
.dialog-body {
padding: 20px 25px;
padding: 18px 20px 16px;
flex: 1;
overflow: hidden;
}
.k-time-picker-layout {
display: flex;
align-items: center;
gap: 14px;
min-height: 228px;
}
.k-time-label-aside {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 0 12px 0 4px;
margin-right: 4px;
border-right: 1px solid rgba(15, 23, 42, 0.08);
user-select: none;
}
.k-time-label-plain {
font-size: 15px;
font-weight: 700;
color: #165dff;
letter-spacing: 0.12em;
line-height: 1.3;/* 横排「K时」 */
white-space: nowrap;
}
.wheels-wrap {
flex: 1;
min-width: 0;
padding: 0;
background: transparent;
border: none;
border-radius: 0;
box-shadow: none;
}
.wheels-grid {
display: flex;
gap: 8px;
min-width: 0;
justify-content: space-between;
}
.wheel-col {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: stretch;
}
.wheel-head {
text-align: center;
font-size: 11px;
color: #64748b;
font-weight: 600;
margin-bottom: 6px;
letter-spacing: 0.06em;
}
.wheel-viewport {
position: relative;
height: 180px;
border-radius: 10px;
border: 1px solid rgba(15, 23, 42, 0.07);
background: linear-gradient(180deg, #fdfefe 0%, #f4f6fb 100%);
overflow: hidden;
box-shadow: inset 0 2px 6px rgba(15, 23, 42, 0.04);
}
.wheel-highlight {
pointer-events: none;
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 36px;
margin-top: -18px;
border-top: 1px solid rgba(22, 93, 255, 0.22);
border-bottom: 1px solid rgba(22, 93, 255, 0.22);
background: linear-gradient(180deg, rgba(22, 93, 255, 0.07) 0%, rgba(22, 93, 255, 0.04) 100%);
z-index: 1;
}
.wheel-list {
list-style: none;
margin: 0;
padding: 72px 0;
height: 180px;
overflow-y: auto;
overflow-x: hidden;
scroll-snap-type: y mandatory;
-webkit-overflow-scrolling: touch;
position: relative;
z-index: 2;
scrollbar-width: none;
-ms-overflow-style: none;
}
.wheel-list::-webkit-scrollbar {
width: 0;
height: 0;
display: none;
}
.wheel-item {
height: 36px;
line-height: 36px;
text-align: center;
font-size: 13px;
color: #334155;
font-variant-numeric: tabular-nums;
scroll-snap-align: center;
scroll-snap-stop: always;
user-select: none;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 10px 0 0 0;
margin-top: 10px;
border-top: 1px solid #ebeef5;
gap: 8px;
padding: 16px 0 0;
margin-top: 14px;
border-top: 1px solid rgba(15, 23, 42, 0.07);
}
/* 胶囊形按钮(与航线「已选择」等圆角矩形风格一致) */
.k-time-dialog-footer >>> .k-time-pill-btn {
border-radius: 999px;
padding: 6px 18px;
font-size: 12px;
font-weight: 500;
min-width: 72px;
line-height: 1.35;
transition: background 0.2s, border-color 0.2s, color 0.2s, box-shadow 0.2s;
}
.k-time-dialog-footer >>> .k-time-pill-btn--cancel {
background: #fff;
border: 1px solid #e2e8f0;
color: #617395;
}
.k-time-dialog-footer >>> .k-time-pill-btn--cancel:hover,
.k-time-dialog-footer >>> .k-time-pill-btn--cancel:focus {
color: #165dff;
border-color: rgba(22, 93, 255, 0.45);
background: #f8fafc;
}
.k-time-dialog-footer >>> .k-time-pill-btn--confirm {
background: #165dff;
border-color: #165dff;
color: #fff;
}
.k-time-dialog-footer >>> .k-time-pill-btn--confirm:hover,
.k-time-dialog-footer >>> .k-time-pill-btn--confirm:focus {
background: #0e52e0;
border-color: #0e52e0;
color: #fff;
}
</style>

993
ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue

File diff suppressed because it is too large

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

8
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%);
}
</style>

2
ruoyi-ui/src/views/dialogs/PlatformImportDialog.vue

@ -101,7 +101,7 @@ export default {
<style scoped>
/* 简单的上传样式 */
.icon-uploader {
border: 1px dashed #409EFF;
border: 1px dashed #165dff;
border-radius: 6px;
width: 80px;
height: 80px;

18
ruoyi-ui/src/views/dialogs/RouteEditDialog.vue

@ -378,7 +378,7 @@ export default {
line: { style: 'solid', width: 2, color: '#64748b', gapColor: '#cbd5e1', dashLength: 20 }
},
presetColors: [
'#f1f5f9', '#64748b', '#334155', '#94a3b8', '#e2e8f0', '#0078FF', '#409EFF', '#67C23A',
'#f1f5f9', '#64748b', '#334155', '#94a3b8', '#e2e8f0', '#165dff', '#67C23A',
'#E6A23C', '#F56C6C', '#909399', '#00CED1', '#FF1493', '#FFD700', '#4B0082', '#FF4500'
]
}
@ -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('加载平台列表失败')
@ -1116,7 +1124,7 @@ export default {
align-items: center;
justify-content: center;
margin-right: 12px;
background: #e0f0ff;
background: #ebf3ff;
border-radius: 50%;
}
.platform-card-icon .platform-img {

6
ruoyi-ui/src/views/ganttChart/index.vue

@ -88,7 +88,7 @@ export default {
id: 1,
name: '航线一',
type: 'route',
color: '#409EFF',
color: '#165dff',
startTime: new Date(2026, 1, 20, 6, 30),
endTime: new Date(2026, 1, 20, 8, 30),
startPercent: 3.33,
@ -138,7 +138,7 @@ export default {
id: 6,
name: '航线四',
type: 'route',
color: '#409EFF',
color: '#165dff',
startTime: new Date(2026, 1, 20, 16, 15),
endTime: new Date(2026, 1, 20, 18, 15),
startPercent: 63.33,
@ -166,7 +166,7 @@ export default {
}
],
legends: [
{ type: 'route', label: '航线', color: '#409EFF' },
{ type: 'route', label: '航线', color: '#165dff' },
{ type: 'missile', label: '导弹发射', color: '#F56C6C' },
{ type: 'refuel', label: '空中加油', color: '#909399' },
{ type: 'reconnaissance', label: '侦察任务', color: '#E6A23C' }

4
ruoyi-ui/src/views/index.vue

@ -127,8 +127,8 @@ export default {
margin-bottom: 10px;
&.primary {
background-color: rgba(64, 158, 255, 0.15);
color: #409EFF;
background-color: rgba(22, 93, 255, 0.15);
color: #165dff;
}
}

24
ruoyi-ui/src/views/login.vue

@ -322,7 +322,7 @@ export default {
display: inline-block;
width: 48px;
height: 48px;
background-color: #165DFF; /* 原 primary 色值 */
background-color: #165dff; /* 原 primary 色值 */
border-radius: 50%;
display: flex;
align-items: center;
@ -375,12 +375,12 @@ export default {
}
.tab-item:hover {
color: #165DFF; /* 原 primary 色值 */
color: #165dff; /* 原 primary 色值 */
}
.tab-active {
border-bottom: 2px solid #165DFF;
color: #165DFF;
border-bottom: 2px solid #165dff;
color: #165dff;
font-weight: 500;
}
@ -417,7 +417,7 @@ export default {
}
.form-select:focus {
border-color: #165DFF;
border-color: #165dff;
}
/* 输入框容器 */
@ -436,7 +436,7 @@ export default {
}
.form-input:focus {
border-color: #165DFF;
border-color: #165dff;
}
.input-icon {
@ -457,7 +457,7 @@ export default {
.forgot-pwd {
font-size: 12px;
color: #165DFF;
color: #165dff;
text-decoration: none;
}
@ -492,13 +492,13 @@ export default {
.checkbox {
margin-right: 4px;
color: #165DFF;
color: #165dff;
}
/* 登录按钮 */
.login-btn {
width: 100%;
background-color: #165DFF;
background-color: #165dff;
color: white;
padding: 8px 0;
border: none;
@ -540,9 +540,9 @@ export default {
width: 32px;
height: 32px;
border-radius: 50%;
background-color: #E8F3FF; /* 原 secondary 色值 */
background-color: #ebf3ff; /* 浅色底,与主色 #165dff 同系 */
border: none;
color: #165DFF;
color: #165dff;
display: flex;
align-items: center;
justify-content: center;
@ -551,7 +551,7 @@ export default {
}
.other-login-btn:hover {
background-color: #165DFF;
background-color: #165dff;
color: white;
}

34
ruoyi-ui/src/views/selectRoom/index.vue

@ -323,7 +323,7 @@ export default {
}
.page-container {
background: linear-gradient(135deg, #ffffff 0%, #f0f8ff 100%);
background: linear-gradient(135deg, #ffffff 0%, #f5f9ff 100%);
min-height: 100vh;
display: flex;
align-items: center;
@ -383,7 +383,7 @@ export default {
}
.back-login:hover {
color: #165DFF;
color: #165dff;
}
.panel-header h2 {
@ -421,8 +421,8 @@ export default {
}
.parent-room.active {
background-color: #f0f7ff;
border-left-color: #165DFF;
background-color: #ebf3ff;
border-left-color: #165dff;
}
.child-room {
@ -432,8 +432,8 @@ export default {
}
.child-room.active {
background-color: #f0f7ff;
border-left-color: #165DFF;
background-color: #ebf3ff;
border-left-color: #165dff;
}
.room-info {
@ -455,13 +455,13 @@ export default {
}
.parent-room .room-icon {
background: #e0f2fe;
color: #0369a1;
background: #ebf3ff;
color: #165dff;
}
.child-room .room-icon {
background: #f0f9ff;
color: #0ea5e9;
background: #ebf3ff;
color: #165dff;
}
.room-details h3 {
@ -492,7 +492,7 @@ export default {
}
.add-child-btn:hover {
color: #165DFF;
color: #165dff;
}
.delete-room-btn {
@ -531,7 +531,7 @@ export default {
border-radius: 10px;
font-size: 14px;
font-weight: 500;
background: #165DFF;
background: #165dff;
color: white;
border: none;
cursor: pointer;
@ -540,7 +540,7 @@ export default {
}
.btn-primary:hover:not(.disabled) {
background: #3370ff;
background: #165dff;
transform: translateY(-1px);
}
@ -598,7 +598,7 @@ export default {
.menu-item:hover {
background-color: #f8fafc;
color: #165DFF;
color: #165dff;
}
.menu-item-danger {
@ -641,7 +641,7 @@ export default {
}
::v-deep .el-input__inner:focus {
border-color: #165DFF !important;
border-color: #165dff !important;
box-shadow: 0 0 0 3px rgba(22, 93, 255, 0.1) !important;
}
@ -669,8 +669,8 @@ export default {
}
::v-deep .el-button--primary {
background-color: #165DFF !important;
border-color: #165DFF !important;
background-color: #165dff !important;
border-color: #165dff !important;
box-shadow: 0 4px 10px rgba(22, 93, 255, 0.2) !important;
}

2
ruoyi-ui/src/views/system/lib/index.vue

@ -69,7 +69,7 @@
<el-table-column label="平台名称" align="center" prop="name" min-width="150">
<template slot-scope="scope">
<span style="font-weight: bold; color: #409EFF">{{ scope.row.name }}</span>
<span style="font-weight: bold; color: #165dff">{{ scope.row.name }}</span>
</template>
</el-table-column>

2
ruoyi-ui/src/views/system/rooms/index.vue

@ -70,7 +70,7 @@
<el-table-column label="房间名称" align="center" prop="name" min-width="150">
<template slot-scope="scope">
<span style="font-weight: bold; color: #409EFF">{{ scope.row.name }}</span>
<span style="font-weight: bold; color: #165dff">{{ scope.row.name }}</span>
</template>
</el-table-column>

2
ruoyi-ui/src/views/system/routes/index.vue

@ -75,7 +75,7 @@
<el-table-column label="名称" align="center" prop="callSign" min-width="120">
<template slot-scope="scope">
<span style="font-weight: bold; color: #409EFF">{{ scope.row.callSign }}</span>
<span style="font-weight: bold; color: #165dff">{{ scope.row.callSign }}</span>
</template>
</el-table-column>

2
ruoyi-ui/src/views/system/scenario/index.vue

@ -70,7 +70,7 @@
<el-table-column label="方案名称" align="center" prop="name" min-width="150">
<template slot-scope="scope">
<span style="font-weight: bold; color: #409EFF">{{ scope.row.name }}</span>
<span style="font-weight: bold; color: #165dff">{{ scope.row.name }}</span>
</template>
</el-table-column>

2
ruoyi-ui/src/views/system/users/index.vue

@ -68,7 +68,7 @@
<el-table-column label="登录账号" align="center" prop="username">
<template slot-scope="scope">
<span style="font-weight: bold; color: #409EFF">{{ scope.row.username }}</span>
<span style="font-weight: bold; color: #165dff">{{ scope.row.username }}</span>
</template>
</el-table-column>

4
ruoyi-ui/src/views/tool/build/RightPanel.vue

@ -902,7 +902,7 @@ export default {
margin-top: 4px;
}
.select-item.sortable-chosen {
border: 1px dashed #409eff;
border: 1px dashed #165dff;
}
.select-line-icon {
line-height: 32px;
@ -929,7 +929,7 @@ export default {
top: 0;
left: 0;
cursor: pointer;
background: #409eff;
background: #165dff;
z-index: 1;
border-radius: 0 0 6px 0;
text-align: center;

6
ruoyi-ui/src/views/tool/build/index.vue

@ -482,7 +482,7 @@ export default {
margin-left: 6px;
}
.el-icon-plus{
color: #409EFF;
color: #165dff;
}
.el-icon-delete{
color: #157a0c;
@ -502,7 +502,7 @@ export default {
}
$selectedColor: #f6f7ff;
$lighterBlue: #409EFF;
$lighterBlue: #165dff;
.container {
position: relative;
@ -613,7 +613,7 @@ $lighterBlue: #409EFF;
left: 12px;
top: 6px;
line-height: 30px;
color: #00afff;
color: #165dff;
font-weight: 600;
font-size: 17px;
white-space: nowrap;

Loading…
Cancel
Save