Browse Source

六步法任务的持久化

mh
cuitw 4 weeks ago
parent
commit
4b97960ad9
  1. 39
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java
  2. 19
      ruoyi-ui/src/api/system/routes.js
  3. 24
      ruoyi-ui/src/views/cesiumMap/index.vue
  4. 1
      ruoyi-ui/src/views/childRoom/BottomLeftPanel.vue
  5. 5
      ruoyi-ui/src/views/childRoom/SixStepsOverlay.vue
  6. 179
      ruoyi-ui/src/views/childRoom/TaskPageContent.vue
  7. 73
      ruoyi-ui/src/views/childRoom/UnderstandingStepContent.vue
  8. 2
      ruoyi-ui/src/views/childRoom/index.vue
  9. 23
      ruoyi-ui/src/views/dialogs/RouteEditDialog.vue

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

@ -124,6 +124,45 @@ public class RoutesController extends BaseController
}
/**
* 保存六步法任务页数据到 Redis背景图标文本框
*/
@PreAuthorize("@ss.hasPermi('system:routes:edit')")
@PostMapping("/saveTaskPageData")
public AjaxResult saveTaskPageData(@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) + ":task_page";
fourTRedisTemplate.opsForValue().set(key, data.toString());
return success();
}
/**
* Redis 获取六步法任务页数据
*/
@PreAuthorize("@ss.hasPermi('system:routes:query')")
@GetMapping("/getTaskPageData")
public AjaxResult getTaskPageData(Long roomId)
{
if (roomId == null) {
return AjaxResult.error("房间ID不能为空");
}
String key = "room:" + String.valueOf(roomId) + ":task_page";
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();
}
/**
* 获取导弹发射参数列表Redis房间+航线+平台为 key值为数组每项含 angle/distance/launchTimeMinutesFromK/startLng/startLat/platformHeadingDeg
*/
@PreAuthorize("@ss.hasPermi('system:routes:query')")

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

@ -81,6 +81,25 @@ export function get4TData(params) {
})
}
// 保存六步法任务页数据到 Redis(背景、图标、文本框)
export function saveTaskPageData(data) {
return request({
url: '/system/routes/saveTaskPageData',
method: 'post',
data,
headers: { repeatSubmit: false }
})
}
// 从 Redis 获取六步法任务页数据
export function getTaskPageData(params) {
return request({
url: '/system/routes/getTaskPageData',
method: 'get',
params
})
}
// 获取导弹发射参数(Redis:房间+航线+平台为 key)
export function getMissileParams(params) {
return request({

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

@ -1031,7 +1031,7 @@ export default {
this.tempEntity = this.viewer.entities.add({
polyline: {
positions: solidPositions,
width: 3,
width: 2,
material: Cesium.Color.fromCssColorString('#64748b'),
arcType: Cesium.ArcType.NONE
}
@ -1052,7 +1052,7 @@ export default {
}
return last ? [last, last] : [];
}, false),
width: 3,
width: 2,
material: new Cesium.PolylineDashMaterialProperty({
color: Cesium.Color.fromCssColorString('#64748b'),
dashLength: 16
@ -1194,7 +1194,7 @@ export default {
if (last && this.activeCursorPosition) return [last, this.activeCursorPosition];
return last ? [last, last] : [];
}, false),
width: 3,
width: 2,
material: new Cesium.PolylineDashMaterialProperty({
color: Cesium.Color.fromCssColorString('#64748b'),
dashLength: 16
@ -1255,7 +1255,7 @@ export default {
this.tempEntity = this.viewer.entities.add({
polyline: {
positions: solidPositions,
width: 3,
width: 2,
material: Cesium.Color.fromCssColorString('#64748b'),
arcType: Cesium.ArcType.NONE
}
@ -1268,7 +1268,7 @@ export default {
if (last && this.activeCursorPosition) return [last, this.activeCursorPosition];
return last ? [last, last] : [];
}, false),
width: 3,
width: 2,
material: new Cesium.PolylineDashMaterialProperty({
color: Cesium.Color.fromCssColorString('#64748b'),
dashLength: 16
@ -1421,7 +1421,7 @@ export default {
this.tempEntity = this.viewer.entities.add({
polyline: {
positions: solidPositions,
width: 3,
width: 2,
material: Cesium.Color.fromCssColorString('#64748b'),
arcType: Cesium.ArcType.NONE
}
@ -2013,8 +2013,8 @@ export default {
const defaultWpColor = wpStyle.color || '#f1f5f9';
const defaultWpOutline = wpStyle.outlineColor || '#64748b';
const wpOutlineW = wpStyle.outlineWidth != null ? wpStyle.outlineWidth : 1.5;
// 线线 3线线线
const lineWidth = lineStyle.width != null ? lineStyle.width : 3;
// 线线 2线线线
const lineWidth = lineStyle.width != null ? lineStyle.width : 2;
const lineColor = lineStyle.color || '#64748b';
const gapColor = lineStyle.gapColor != null ? lineStyle.gapColor : '#cbd5e1';
const dashLen = lineStyle.dashLength != null ? lineStyle.dashLength : 20;
@ -2511,7 +2511,7 @@ export default {
id: hitLineId,
polyline: {
positions: routePositionsCallback,
width: 48,
width: 12,
material: Cesium.Color.TRANSPARENT,
arcType: Cesium.ArcType.NONE,
zIndex: 0
@ -2521,7 +2521,7 @@ export default {
if (this.allEntities) {
this.allEntities.push({ id: hitLineId, entity: hitEntity, type: 'route', routeId });
}
const displayWidth = Math.max(lineWidth, 5);
const displayWidth = Math.max(lineWidth, 2);
// 线
const routeEntity = this.viewer.entities.add({
id: lineId,
@ -6793,7 +6793,7 @@ export default {
this.copyPreviewEntity = this.viewer.entities.add({
polyline: {
positions: positions,
width: 4,
width: 2,
material: Cesium.Color.fromCssColorString('rgba(0, 120, 255, 0.75)'),
arcType: Cesium.ArcType.NONE
}
@ -6875,7 +6875,7 @@ export default {
this.addWaypointPreviewEntity = this.viewer.entities.add({
polyline: {
positions: positions,
width: 3,
width: 2,
material: new Cesium.PolylineDashMaterialProperty({
color: Cesium.Color.fromCssColorString('#64748b'),
dashLength: 16

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

@ -29,6 +29,7 @@
<six-steps-overlay
v-if="showSixStepsBar"
:visible="showSixStepsOverlay"
:room-id="roomId"
:current-step-index="activeStepIndex"
:step-titles="sixStepsData.map(s => s.title)"
:override-title="taskBlockActive ? '任务' : null"

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

@ -61,6 +61,7 @@
<task-page-content
v-if="overrideTitle === '任务'"
ref="taskPage"
:room-id="roomId"
:background-image="taskPageBackground"
@background-change="taskPageBackground = $event"
class="task-page-body"
@ -116,6 +117,10 @@ export default {
name: 'SixStepsOverlay',
components: { TaskPageContent, UnderstandingStepContent },
props: {
roomId: {
type: [Number, String],
default: null
},
visible: {
type: Boolean,
default: false

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

@ -64,12 +64,23 @@
@mousedown="onTextBoxMouseDown(box, $event)"
>
<div class="textbox-drag-bar" @mousedown.stop="selectElement(box.id, $event)"></div>
<!-- Office 风格格式工具栏选中时显示在文本框上方 -->
<div v-if="selectedId === box.id" class="textbox-format-toolbar" @mousedown.stop>
<el-select v-model="box.fontFamily" size="mini" placeholder="字体" class="format-font" @change="debouncedSave">
<el-option v-for="f in fontOptions" :key="f" :label="f" :value="f" />
</el-select>
<el-select v-model="box.fontSize" size="mini" placeholder="字号" class="format-size" @change="debouncedSave">
<el-option v-for="s in fontSizeOptions" :key="s" :label="String(s)" :value="s" />
</el-select>
<el-color-picker v-model="box.color" class="format-color" @change="debouncedSave" />
</div>
<div
class="textbox-input"
contenteditable="true"
:style="getTextBoxInputStyle(box)"
@blur="box.text = $event.target.innerText"
@mousedown.stop="selectedId = box.id"
>{{ box.text }}</div>
></div>
<div class="textbox-resize-handle" v-if="selectedId === box.id">
<div
v-for="pos in resizeHandles"
@ -96,14 +107,24 @@
</template>
<script>
import { getTaskPageData, saveTaskPageData } from '@/api/system/routes'
let idCounter = 0
function genId() {
return 'el_' + (++idCounter) + '_' + Date.now()
}
const FONT_OPTIONS = ['宋体', '黑体', '微软雅黑', '楷体', '仿宋', 'Arial', 'Times New Roman', 'Verdana', 'Georgia']
const FONT_SIZE_OPTIONS = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]
const DEFAULT_FONT = { fontSize: 14, fontFamily: '微软雅黑', color: '#333333' }
export default {
name: 'TaskPageContent',
props: {
roomId: {
type: [Number, String],
default: null
},
backgroundImage: {
type: String,
default: null
@ -111,6 +132,8 @@ export default {
},
data() {
return {
fontOptions: FONT_OPTIONS,
fontSizeOptions: FONT_SIZE_OPTIONS,
insertMode: null,
pendingIconImage: null,
icons: [],
@ -127,12 +150,27 @@ export default {
drawCurrentY: 0
}
},
watch: {
backgroundImage: { handler() { this.debouncedSave() }, immediate: false },
icons: { handler() { this.debouncedSave() }, deep: true },
textBoxes: { handler() { this.debouncedSave() }, deep: true }
},
mounted() {
this._keydownHandler = (e) => this.onKeydown(e)
document.addEventListener('keydown', this._keydownHandler)
this.loadFromRedis()
this.$nextTick(() => this.syncTextBoxContent())
},
updated() {
this.$nextTick(() => this.syncTextBoxContent())
},
beforeDestroy() {
document.removeEventListener('keydown', this._keydownHandler)
if (this._saveTimer) {
clearTimeout(this._saveTimer)
this._saveTimer = null
}
this.saveToRedis()
},
computed: {
canvasStyle() {
@ -248,7 +286,10 @@ export default {
width: w,
height: h,
text: '',
rotation: 0
rotation: 0,
fontSize: DEFAULT_FONT.fontSize,
fontFamily: DEFAULT_FONT.fontFamily,
color: DEFAULT_FONT.color
})
this.drawingTextBox = false
this.insertMode = null
@ -419,11 +460,114 @@ export default {
transform: `rotate(${box.rotation || 0}deg)`
}
},
getTextBoxInputStyle(box) {
return {
fontSize: (box.fontSize || DEFAULT_FONT.fontSize) + 'px',
fontFamily: box.fontFamily || DEFAULT_FONT.fontFamily,
color: box.color || DEFAULT_FONT.color
}
},
deleteIcon(id) {
const idx = this.icons.findIndex(i => i.id === id)
if (idx >= 0) this.icons.splice(idx, 1)
if (this.selectedId === id) this.selectedId = null
},
syncTextBoxContent() {
if (!this.$refs.canvas) return
const inputs = this.$refs.canvas.querySelectorAll('.textbox-input')
this.textBoxes.forEach((box, idx) => {
const el = inputs[idx]
if (el && document.activeElement !== el && el.innerText !== box.text) {
el.innerText = box.text
}
})
},
async loadFromRedis() {
if (this.roomId == null) return
try {
const res = await getTaskPageData({ roomId: this.roomId })
let data = res && res.data
if (!data) return
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch (_) {
return
}
}
const raw = data
if (raw.icons && Array.isArray(raw.icons)) {
this.icons = raw.icons.map(i => ({
id: i.id || genId(),
x: Number(i.x) || 0,
y: Number(i.y) || 0,
width: Number(i.width) || 60,
height: Number(i.height) || 60,
rotation: Number(i.rotation) || 0,
src: i.src || ''
}))
}
if (raw.textBoxes && Array.isArray(raw.textBoxes)) {
this.textBoxes = raw.textBoxes.map(t => ({
id: t.id || genId(),
x: Number(t.x) || 0,
y: Number(t.y) || 0,
width: Number(t.width) || 100,
height: Number(t.height) || 60,
text: String(t.text || ''),
rotation: Number(t.rotation) || 0,
fontSize: Number(t.fontSize) || DEFAULT_FONT.fontSize,
fontFamily: t.fontFamily || DEFAULT_FONT.fontFamily,
color: t.color || DEFAULT_FONT.color
}))
}
if (raw.background) {
this.$emit('background-change', raw.background)
}
} catch (e) {
console.warn('TaskPage loadFromRedis failed:', e)
}
},
saveToRedis() {
if (this.roomId == null) return
const payload = {
background: this.backgroundImage || null,
icons: this.icons.map(i => ({
id: i.id,
x: i.x,
y: i.y,
width: i.width,
height: i.height,
rotation: i.rotation || 0,
src: i.src
})),
textBoxes: this.textBoxes.map(t => ({
id: t.id,
x: t.x,
y: t.y,
width: t.width,
height: t.height,
text: t.text || '',
rotation: t.rotation || 0,
fontSize: t.fontSize || DEFAULT_FONT.fontSize,
fontFamily: t.fontFamily || DEFAULT_FONT.fontFamily,
color: t.color || DEFAULT_FONT.color
}))
}
saveTaskPageData({
roomId: this.roomId,
data: JSON.stringify(payload)
}).catch(e => {
console.warn('TaskPage saveToRedis failed:', e)
})
},
debouncedSave() {
if (this._saveTimer) clearTimeout(this._saveTimer)
this._saveTimer = setTimeout(() => {
this._saveTimer = null
this.saveToRedis()
}, 300)
},
deleteTextBox(id) {
const idx = this.textBoxes.findIndex(t => t.id === id)
if (idx >= 0) this.textBoxes.splice(idx, 1)
@ -649,6 +793,37 @@ export default {
}
/* Office 风格格式工具栏:选中文本框时显示在上方 */
.textbox-format-toolbar {
position: absolute;
bottom: 100%;
left: 0;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: #fff;
border: 1px solid #ddd;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 20;
}
.format-font {
width: 120px;
}
.format-size {
width: 70px;
}
.format-color {
vertical-align: middle;
}
.format-color ::v-deep .el-color-picker__trigger {
width: 24px;
height: 24px;
padding: 2px;
}
.textbox-drag-bar {
position: absolute;
top: 0;

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

@ -54,7 +54,7 @@
</div>
</div>
<!-- 文本框元素 -->
<!-- 文本框元素Office 风格支持字体字号颜色 -->
<div
v-for="box in currentTextBoxes"
:key="box.id"
@ -64,12 +64,23 @@
@mousedown="onTextBoxMouseDown(box, $event)"
>
<div class="textbox-drag-bar" @mousedown.stop="selectElement(box.id, $event)"></div>
<!-- Office 风格格式工具栏 -->
<div v-if="selectedId === box.id" class="textbox-format-toolbar" @mousedown.stop>
<el-select v-model="box.fontFamily" size="mini" placeholder="字体" class="format-font">
<el-option v-for="f in fontOptions" :key="f" :label="f" :value="f" />
</el-select>
<el-select v-model="box.fontSize" size="mini" placeholder="字号" class="format-size">
<el-option v-for="s in fontSizeOptions" :key="s" :label="String(s)" :value="s" />
</el-select>
<el-color-picker v-model="box.color" class="format-color" />
</div>
<div
class="textbox-input"
contenteditable="true"
:style="getTextBoxInputStyle(box)"
@blur="box.text = $event.target.innerText"
@mousedown.stop="selectedId = box.id"
>{{ box.text }}</div>
></div>
<div class="textbox-resize-handle" v-if="selectedId === box.id">
<div
v-for="pos in resizeHandles"
@ -101,6 +112,10 @@ function genId() {
return 'el_' + (++idCounter) + '_' + Date.now()
}
const FONT_OPTIONS = ['宋体', '黑体', '微软雅黑', '楷体', '仿宋', 'Arial', 'Times New Roman', 'Verdana', 'Georgia']
const FONT_SIZE_OPTIONS = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]
const DEFAULT_FONT = { fontSize: 14, fontFamily: '微软雅黑', color: '#333333' }
export default {
name: 'UnderstandingStepContent',
props: {
@ -115,6 +130,8 @@ export default {
},
data() {
return {
fontOptions: FONT_OPTIONS,
fontSizeOptions: FONT_SIZE_OPTIONS,
subContents: [
{ icons: [], textBoxes: [] },
{ icons: [], textBoxes: [] },
@ -171,6 +188,10 @@ export default {
mounted() {
this._keydownHandler = (e) => this.onKeydown(e)
document.addEventListener('keydown', this._keydownHandler)
this.$nextTick(() => this.syncTextBoxContent())
},
updated() {
this.$nextTick(() => this.syncTextBoxContent())
},
beforeDestroy() {
document.removeEventListener('keydown', this._keydownHandler)
@ -267,7 +288,10 @@ export default {
width: w,
height: h,
text: '',
rotation: 0
rotation: 0,
fontSize: DEFAULT_FONT.fontSize,
fontFamily: DEFAULT_FONT.fontFamily,
color: DEFAULT_FONT.color
})
this.drawingTextBox = false
this.insertMode = null
@ -438,12 +462,30 @@ export default {
transform: `rotate(${box.rotation || 0}deg)`
}
},
getTextBoxInputStyle(box) {
return {
fontSize: (box.fontSize || DEFAULT_FONT.fontSize) + 'px',
fontFamily: box.fontFamily || DEFAULT_FONT.fontFamily,
color: box.color || DEFAULT_FONT.color
}
},
deleteIcon(id) {
const sub = this.subContents[this.activeSubIndex]
const idx = sub.icons.findIndex(i => i.id === id)
if (idx >= 0) sub.icons.splice(idx, 1)
if (this.selectedId === id) this.selectedId = null
},
syncTextBoxContent() {
if (!this.$refs.canvas) return
const inputs = this.$refs.canvas.querySelectorAll('.textbox-input')
const boxes = this.currentTextBoxes
boxes.forEach((box, idx) => {
const el = inputs[idx]
if (el && document.activeElement !== el && el.innerText !== box.text) {
el.innerText = box.text
}
})
},
deleteTextBox(id) {
const sub = this.subContents[this.activeSubIndex]
const idx = sub.textBoxes.findIndex(t => t.id === id)
@ -667,6 +709,31 @@ export default {
pointer-events: auto;
}
/* Office 风格格式工具栏 */
.textbox-format-toolbar {
position: absolute;
bottom: 100%;
left: 0;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: #fff;
border: 1px solid #ddd;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 20;
}
.format-font { width: 120px; }
.format-size { width: 70px; }
.format-color { vertical-align: middle; }
.format-color ::v-deep .el-color-picker__trigger {
width: 24px;
height: 24px;
padding: 2px;
}
.textbox-drag-bar {
position: absolute;
top: 0;

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

@ -1691,7 +1691,7 @@ export default {
getDefaultRouteAttributes() {
const defaultAttrs = {
waypointStyle: { pixelSize: 10, color: '#f1f5f9', outlineColor: '#64748b', outlineWidth: 1.5 },
lineStyle: { style: 'solid', width: 3, color: '#64748b', gapColor: '#cbd5e1', dashLength: 20 }
lineStyle: { style: 'solid', width: 2, color: '#64748b', gapColor: '#cbd5e1', dashLength: 20 }
};
return JSON.stringify(defaultAttrs);
},

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

@ -274,7 +274,7 @@
<el-table-column label="标记大小" width="80">
<template slot-scope="scope">
<span v-if="!waypointsEditMode">{{ formatNum(scope.row.pixelSize) }}</span>
<el-input v-else v-model.number="scope.row.pixelSize" size="mini" placeholder="12" :min="4" :max="24" />
<el-input v-else v-model.number="scope.row.pixelSize" size="mini" placeholder="10" :min="4" :max="24" />
</template>
</el-table-column>
<el-table-column label="标记颜色" width="100">
@ -352,14 +352,14 @@ export default {
id: '',
name: ''
},
// 线 attributes 退
// 线线 attributes 退
defaultStyle: {
waypoint: { pixelSize: 10, color: '#f1f5f9', outlineColor: '#64748b', outlineWidth: 1.5 },
line: { style: 'solid', width: 3, color: '#64748b', gapColor: '#cbd5e1', dashLength: 20 }
line: { style: 'solid', width: 2, color: '#64748b', gapColor: '#cbd5e1', dashLength: 20 }
},
styleForm: {
waypoint: { pixelSize: 10, color: '#f1f5f9', outlineColor: '#64748b', outlineWidth: 1.5 },
line: { style: 'solid', width: 3, color: '#64748b', gapColor: '#cbd5e1', dashLength: 20 }
line: { style: 'solid', width: 2, color: '#64748b', gapColor: '#cbd5e1', dashLength: 20 }
},
presetColors: [
'#f1f5f9', '#64748b', '#334155', '#94a3b8', '#e2e8f0', '#0078FF', '#409EFF', '#67C23A',
@ -1065,6 +1065,21 @@ export default {
padding: 12px 0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 4px 12px rgba(0, 0, 0, 0.05);
}
/* 航点表格底部水平滚动条加宽,便于拖动 */
.waypoints-table-wrap .el-table__body-wrapper::-webkit-scrollbar {
height: 12px;
}
.waypoints-table-wrap .el-table__body-wrapper::-webkit-scrollbar-track {
background: #f5f5f5;
border-radius: 6px;
}
.waypoints-table-wrap .el-table__body-wrapper::-webkit-scrollbar-thumb {
background: #c0c4cc;
border-radius: 6px;
}
.waypoints-table-wrap .el-table__body-wrapper::-webkit-scrollbar-thumb:hover {
background: #909399;
}
.waypoints-table {
width: 100%;
}

Loading…
Cancel
Save