16 changed files with 2347 additions and 155 deletions
@ -0,0 +1,63 @@ |
|||
package com.ruoyi.web.controller; |
|||
|
|||
import java.util.List; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.web.bind.annotation.DeleteMapping; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.PathVariable; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.PutMapping; |
|||
import org.springframework.web.bind.annotation.RequestBody; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
import com.ruoyi.common.core.controller.BaseController; |
|||
import com.ruoyi.common.core.domain.AjaxResult; |
|||
import com.ruoyi.websocket.service.WhiteboardRoomService; |
|||
|
|||
/** |
|||
* 白板 Controller:房间维度的白板 CRUD,数据存 Redis |
|||
*/ |
|||
@RestController |
|||
@RequestMapping("/room") |
|||
public class WhiteboardController extends BaseController { |
|||
|
|||
@Autowired |
|||
private WhiteboardRoomService whiteboardRoomService; |
|||
|
|||
/** 获取房间下所有白板列表 */ |
|||
@GetMapping("/{roomId}/whiteboards") |
|||
public AjaxResult list(@PathVariable Long roomId) { |
|||
List<Object> list = whiteboardRoomService.listWhiteboards(roomId); |
|||
return success(list); |
|||
} |
|||
|
|||
/** 获取单个白板详情 */ |
|||
@GetMapping("/{roomId}/whiteboard/{whiteboardId}") |
|||
public AjaxResult get(@PathVariable Long roomId, @PathVariable String whiteboardId) { |
|||
Object wb = whiteboardRoomService.getWhiteboard(roomId, whiteboardId); |
|||
if (wb == null) return error("白板不存在"); |
|||
return success(wb); |
|||
} |
|||
|
|||
/** 创建白板 */ |
|||
@PostMapping("/{roomId}/whiteboard") |
|||
public AjaxResult create(@PathVariable Long roomId, @RequestBody Object whiteboard) { |
|||
Object created = whiteboardRoomService.createWhiteboard(roomId, whiteboard); |
|||
return success(created); |
|||
} |
|||
|
|||
/** 更新白板 */ |
|||
@PutMapping("/{roomId}/whiteboard/{whiteboardId}") |
|||
public AjaxResult update(@PathVariable Long roomId, @PathVariable String whiteboardId, |
|||
@RequestBody Object whiteboard) { |
|||
boolean ok = whiteboardRoomService.updateWhiteboard(roomId, whiteboardId, whiteboard); |
|||
return ok ? success() : error("更新失败"); |
|||
} |
|||
|
|||
/** 删除白板 */ |
|||
@DeleteMapping("/{roomId}/whiteboard/{whiteboardId}") |
|||
public AjaxResult delete(@PathVariable Long roomId, @PathVariable String whiteboardId) { |
|||
boolean ok = whiteboardRoomService.deleteWhiteboard(roomId, whiteboardId); |
|||
return ok ? success() : error("删除失败"); |
|||
} |
|||
} |
|||
@ -0,0 +1,124 @@ |
|||
package com.ruoyi.websocket.service; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.TimeUnit; |
|||
import com.alibaba.fastjson2.JSON; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.beans.factory.annotation.Qualifier; |
|||
import org.springframework.data.redis.core.RedisTemplate; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
/** |
|||
* 白板房间服务:按房间维度将白板数据存储于 Redis |
|||
*/ |
|||
@Service |
|||
public class WhiteboardRoomService { |
|||
|
|||
private static final String ROOM_WHITEBOARDS_PREFIX = "room:"; |
|||
private static final String ROOM_WHITEBOARDS_SUFFIX = ":whiteboards"; |
|||
private static final int EXPIRE_HOURS = 24; |
|||
|
|||
@Autowired |
|||
@Qualifier("stringObjectRedisTemplate") |
|||
private RedisTemplate<String, Object> redisTemplate; |
|||
|
|||
private String whiteboardsKey(Long roomId) { |
|||
return ROOM_WHITEBOARDS_PREFIX + roomId + ROOM_WHITEBOARDS_SUFFIX; |
|||
} |
|||
|
|||
/** 获取房间下所有白板列表 */ |
|||
@SuppressWarnings("unchecked") |
|||
public List<Object> listWhiteboards(Long roomId) { |
|||
if (roomId == null) return new ArrayList<>(); |
|||
String key = whiteboardsKey(roomId); |
|||
Object raw = redisTemplate.opsForValue().get(key); |
|||
if (raw == null) return new ArrayList<>(); |
|||
if (raw instanceof List) return (List<Object>) raw; |
|||
if (raw instanceof String) { |
|||
try { |
|||
return JSON.parseArray((String) raw); |
|||
} catch (Exception e) { |
|||
return new ArrayList<>(); |
|||
} |
|||
} |
|||
return new ArrayList<>(); |
|||
} |
|||
|
|||
/** 获取单个白板详情 */ |
|||
public Object getWhiteboard(Long roomId, String whiteboardId) { |
|||
List<Object> list = listWhiteboards(roomId); |
|||
for (Object item : list) { |
|||
if (item instanceof java.util.Map) { |
|||
Object id = ((java.util.Map<?, ?>) item).get("id"); |
|||
if (whiteboardId.equals(String.valueOf(id))) return item; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/** 创建白板 */ |
|||
public Object createWhiteboard(Long roomId, Object whiteboard) { |
|||
if (roomId == null || whiteboard == null) return null; |
|||
List<Object> list = listWhiteboards(roomId); |
|||
String id = UUID.randomUUID().toString(); |
|||
Object wb = whiteboard; |
|||
if (wb instanceof java.util.Map) { |
|||
((java.util.Map<String, Object>) wb).put("id", id); |
|||
if (!((java.util.Map<String, Object>) wb).containsKey("name")) { |
|||
((java.util.Map<String, Object>) wb).put("name", "草稿"); |
|||
} |
|||
if (!((java.util.Map<String, Object>) wb).containsKey("timeBlocks")) { |
|||
((java.util.Map<String, Object>) wb).put("timeBlocks", new ArrayList<String>()); |
|||
} |
|||
if (!((java.util.Map<String, Object>) wb).containsKey("contentByTime")) { |
|||
((java.util.Map<String, Object>) wb).put("contentByTime", new java.util.HashMap<String, Object>()); |
|||
} |
|||
} |
|||
list.add(wb); |
|||
saveWhiteboards(roomId, list); |
|||
return wb; |
|||
} |
|||
|
|||
/** 更新白板 */ |
|||
public boolean updateWhiteboard(Long roomId, String whiteboardId, Object whiteboard) { |
|||
if (roomId == null || whiteboardId == null || whiteboard == null) return false; |
|||
List<Object> list = listWhiteboards(roomId); |
|||
for (int i = 0; i < list.size(); i++) { |
|||
Object item = list.get(i); |
|||
if (item instanceof java.util.Map) { |
|||
Object id = ((java.util.Map<?, ?>) item).get("id"); |
|||
if (whiteboardId.equals(String.valueOf(id))) { |
|||
if (whiteboard instanceof java.util.Map) { |
|||
((java.util.Map<String, Object>) whiteboard).put("id", whiteboardId); |
|||
} |
|||
list.set(i, whiteboard); |
|||
saveWhiteboards(roomId, list); |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/** 删除白板 */ |
|||
public boolean deleteWhiteboard(Long roomId, String whiteboardId) { |
|||
if (roomId == null || whiteboardId == null) return false; |
|||
List<Object> list = listWhiteboards(roomId); |
|||
boolean removed = list.removeIf(item -> { |
|||
if (item instanceof java.util.Map) { |
|||
Object id = ((java.util.Map<?, ?>) item).get("id"); |
|||
return whiteboardId.equals(String.valueOf(id)); |
|||
} |
|||
return false; |
|||
}); |
|||
if (removed) saveWhiteboards(roomId, list); |
|||
return removed; |
|||
} |
|||
|
|||
private void saveWhiteboards(Long roomId, List<Object> list) { |
|||
String key = whiteboardsKey(roomId); |
|||
redisTemplate.opsForValue().set(key, list, EXPIRE_HOURS, TimeUnit.HOURS); |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
import request from '@/utils/request' |
|||
|
|||
/** 获取房间下所有白板列表 */ |
|||
export function listWhiteboards(roomId) { |
|||
return request({ |
|||
url: `/room/${roomId}/whiteboards`, |
|||
method: 'get' |
|||
}) |
|||
} |
|||
|
|||
/** 获取单个白板详情 */ |
|||
export function getWhiteboard(roomId, whiteboardId) { |
|||
return request({ |
|||
url: `/room/${roomId}/whiteboard/${whiteboardId}`, |
|||
method: 'get' |
|||
}) |
|||
} |
|||
|
|||
/** 创建白板 */ |
|||
export function createWhiteboard(roomId, data) { |
|||
return request({ |
|||
url: `/room/${roomId}/whiteboard`, |
|||
method: 'post', |
|||
data: data || {} |
|||
}) |
|||
} |
|||
|
|||
/** 更新白板 */ |
|||
export function updateWhiteboard(roomId, whiteboardId, data) { |
|||
return request({ |
|||
url: `/room/${roomId}/whiteboard/${whiteboardId}`, |
|||
method: 'put', |
|||
data: data || {} |
|||
}) |
|||
} |
|||
|
|||
/** 删除白板 */ |
|||
export function deleteWhiteboard(roomId, whiteboardId) { |
|||
return request({ |
|||
url: `/room/${roomId}/whiteboard/${whiteboardId}`, |
|||
method: 'delete' |
|||
}) |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,579 @@ |
|||
<template> |
|||
<div class="whiteboard-panel" v-if="visible"> |
|||
<!-- 第一行:时间选择 | 绘制:空域 | 白板方案 | 新建 | 退出 --> |
|||
<div class="wb-row wb-row-main"> |
|||
<!-- 时间选择(左侧) --> |
|||
<div class="wb-time-section"> |
|||
<span class="wb-label">时间选择:</span> |
|||
<div class="wb-time-blocks"> |
|||
<el-tag |
|||
v-for="tb in sortedTimeBlocks" |
|||
:key="tb" |
|||
:type="currentTimeBlock === tb ? 'primary' : 'info'" |
|||
size="small" |
|||
class="wb-time-tag" |
|||
@click="selectTimeBlock(tb)" |
|||
> |
|||
{{ tb }} |
|||
</el-tag> |
|||
<el-button type="text" size="mini" @click="showAddTimeBlock = true" title="添加时间"> |
|||
<i class="el-icon-plus"></i> |
|||
</el-button> |
|||
</div> |
|||
<el-popover placement="bottom" width="200" trigger="click" v-if="currentTimeBlock"> |
|||
<div class="wb-time-edit"> |
|||
<el-button type="text" size="small" @click="openModifyTimeBlock">修改时间</el-button> |
|||
<el-button type="text" size="small" style="color: #F56C6C;" @click="deleteCurrentTimeBlock">删除</el-button> |
|||
</div> |
|||
<el-button slot="reference" type="text" size="mini"><i class="el-icon-more"></i></el-button> |
|||
</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> |
|||
</div> |
|||
|
|||
<!-- 白板方案选择、新建、退出(与时间选择同高) --> |
|||
<div class="wb-draft-actions"> |
|||
<span class="wb-label">白板方案:</span> |
|||
<div class="wb-draft-select-wrap"> |
|||
<el-select |
|||
v-model="currentWhiteboardId" |
|||
placeholder="选择白板方案" |
|||
size="small" |
|||
filterable |
|||
allow-create |
|||
default-first-option |
|||
class="wb-draft-select" |
|||
@change="onWhiteboardChange" |
|||
@create="(name) => $emit('create-whiteboard', name)" |
|||
> |
|||
<el-option |
|||
v-for="wb in whiteboards" |
|||
:key="wb.id" |
|||
:label="wb.name || '未命名'" |
|||
:value="wb.id" |
|||
/> |
|||
</el-select> |
|||
<el-dropdown v-if="currentWhiteboard" trigger="click" @command="onDraftCommand"> |
|||
<el-button type="text" size="mini" class="wb-draft-more-btn"><i class="el-icon-more"></i></el-button> |
|||
<el-dropdown-menu slot="dropdown"> |
|||
<el-dropdown-item command="rename"><i class="el-icon-edit"></i> 重命名</el-dropdown-item> |
|||
<el-dropdown-item command="delete" divided><i class="el-icon-delete"></i> 删除白板方案</el-dropdown-item> |
|||
</el-dropdown-menu> |
|||
</el-dropdown> |
|||
</div> |
|||
<el-button type="text" size="small" @click="$emit('create-whiteboard')" title="新建白板"> |
|||
<i class="el-icon-plus"></i> 新建 |
|||
</el-button> |
|||
<el-button type="text" size="small" @click="exitWhiteboard" title="退出白板"> |
|||
<i class="el-icon-close"></i> 退出 |
|||
</el-button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 第二行:平台(空中 | 海上 | 地面) --> |
|||
<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-grid"> |
|||
<div |
|||
v-for="p in filteredPlatforms" |
|||
:key="p.id" |
|||
class="wb-platform-item" |
|||
draggable="true" |
|||
@dragstart="onPlatformDragStart($event, p)" |
|||
> |
|||
<div class="wb-platform-icon" :style="{ color: p.color || '#008aff' }"> |
|||
<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> |
|||
<span class="wb-platform-name">{{ p.name }}</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 添加时间块弹窗 --> |
|||
<el-dialog title="添加时间块" :visible.sync="showAddTimeBlock" width="400px" append-to-body @close="newTimeBlockValue = null; newTimeBlockInput = ''"> |
|||
<el-form label-width="100px" size="small"> |
|||
<el-form-item label="快捷选择"> |
|||
<div class="time-presets"> |
|||
<el-tag |
|||
v-for="preset in timeBlockPresets" |
|||
:key="preset.value" |
|||
class="preset-tag" |
|||
@click="selectTimePreset(preset.value)" |
|||
> |
|||
{{ preset.label }} |
|||
</el-tag> |
|||
</div> |
|||
</el-form-item> |
|||
<el-form-item label="选择时间"> |
|||
<el-time-picker |
|||
v-model="newTimeBlockValue" |
|||
format="HH:mm:ss" |
|||
value-format="HH:mm:ss" |
|||
placeholder="选择 K+ 后的时间" |
|||
style="width: 100%;" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="或手动输入"> |
|||
<el-input v-model="newTimeBlockInput" placeholder="如 K+00:05:00" size="small" /> |
|||
</el-form-item> |
|||
</el-form> |
|||
<span slot="footer" class="dialog-footer"> |
|||
<el-button @click="showAddTimeBlock = false">取消</el-button> |
|||
<el-button type="primary" @click="addTimeBlock">确定</el-button> |
|||
</span> |
|||
</el-dialog> |
|||
|
|||
<!-- 重命名白板方案弹窗 --> |
|||
<el-dialog title="重命名白板方案" :visible.sync="showRenameWhiteboardDialog" width="400px" append-to-body @open="initRenameWhiteboardDialog" @close="renameWhiteboardName = ''"> |
|||
<el-form label-width="80px" size="small"> |
|||
<el-form-item label="方案名称"> |
|||
<el-input v-model="renameWhiteboardName" placeholder="请输入白板方案名称" @keyup.enter.native="commitRenameWhiteboardDialog" /> |
|||
</el-form-item> |
|||
</el-form> |
|||
<span slot="footer" class="dialog-footer"> |
|||
<el-button @click="showRenameWhiteboardDialog = false">取消</el-button> |
|||
<el-button type="primary" @click="commitRenameWhiteboardDialog">确定</el-button> |
|||
</span> |
|||
</el-dialog> |
|||
|
|||
<!-- 修改时间弹窗(与新建时间块同结构) --> |
|||
<el-dialog title="修改时间" :visible.sync="showRenameTimeBlock" width="400px" append-to-body @open="initModifyTimeBlock" @close="renameTimeBlockValue = null; renameTimeBlockInput = ''"> |
|||
<el-form label-width="100px" size="small"> |
|||
<el-form-item label="快捷选择"> |
|||
<div class="time-presets"> |
|||
<el-tag |
|||
v-for="preset in timeBlockPresets" |
|||
:key="'rename-' + preset.value" |
|||
class="preset-tag" |
|||
@click="selectRenamePreset(preset.value)" |
|||
> |
|||
{{ preset.label }} |
|||
</el-tag> |
|||
</div> |
|||
</el-form-item> |
|||
<el-form-item label="选择时间"> |
|||
<el-time-picker |
|||
v-model="renameTimeBlockValue" |
|||
format="HH:mm:ss" |
|||
value-format="HH:mm:ss" |
|||
placeholder="选择 K+ 后的时间" |
|||
style="width: 100%;" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="或手动输入"> |
|||
<el-input v-model="renameTimeBlockInput" placeholder="如 K+00:10:00" size="small" /> |
|||
</el-form-item> |
|||
</el-form> |
|||
<span slot="footer" class="dialog-footer"> |
|||
<el-button @click="showRenameTimeBlock = false">取消</el-button> |
|||
<el-button type="primary" @click="renameTimeBlock">确定</el-button> |
|||
</span> |
|||
</el-dialog> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'WhiteboardPanel', |
|||
props: { |
|||
visible: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
roomId: { |
|||
type: [String, Number], |
|||
default: null |
|||
}, |
|||
whiteboards: { |
|||
type: Array, |
|||
default: () => [] |
|||
}, |
|||
currentWhiteboard: { |
|||
type: Object, |
|||
default: null |
|||
}, |
|||
airPlatforms: { |
|||
type: Array, |
|||
default: () => [] |
|||
}, |
|||
seaPlatforms: { |
|||
type: Array, |
|||
default: () => [] |
|||
}, |
|||
groundPlatforms: { |
|||
type: Array, |
|||
default: () => [] |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
currentWhiteboardId: null, |
|||
currentTimeBlock: null, |
|||
drawMode: null, |
|||
platformFilter: 'all', |
|||
showRenameWhiteboardDialog: false, |
|||
renameWhiteboardName: '', |
|||
showAddTimeBlock: false, |
|||
showRenameTimeBlock: false, |
|||
newTimeBlockValue: null, |
|||
newTimeBlockInput: '', |
|||
renameTimeBlockValue: null, |
|||
renameTimeBlockInput: '' |
|||
} |
|||
}, |
|||
computed: { |
|||
timeBlockPresets() { |
|||
return [ |
|||
{ label: 'K+0', value: 'K+00:00:00' }, |
|||
{ label: 'K+5', value: 'K+00:05:00' }, |
|||
{ label: 'K+10', value: 'K+00:10:00' }, |
|||
{ label: 'K+15', value: 'K+00:15:00' }, |
|||
{ label: 'K+30', value: 'K+00:30:00' }, |
|||
{ label: 'K+60', value: 'K+01:00:00' }, |
|||
{ label: 'K+2h', value: 'K+02:00:00' } |
|||
] |
|||
}, |
|||
sortedTimeBlocks() { |
|||
const wb = this.currentWhiteboard |
|||
if (!wb || !Array.isArray(wb.timeBlocks)) return [] |
|||
return [...wb.timeBlocks].sort((a, b) => this.compareTimeBlock(a, b)) |
|||
}, |
|||
allPlatforms() { |
|||
return [ |
|||
...(this.airPlatforms || []), |
|||
...(this.seaPlatforms || []), |
|||
...(this.groundPlatforms || []) |
|||
] |
|||
}, |
|||
filteredPlatforms() { |
|||
if (this.platformFilter === 'all') return this.allPlatforms |
|||
if (this.platformFilter === 'air') return this.airPlatforms || [] |
|||
if (this.platformFilter === 'sea') return this.seaPlatforms || [] |
|||
if (this.platformFilter === 'ground') return this.groundPlatforms || [] |
|||
return this.allPlatforms |
|||
} |
|||
}, |
|||
watch: { |
|||
currentWhiteboard: { |
|||
handler(wb) { |
|||
if (wb) { |
|||
this.currentWhiteboardId = wb.id |
|||
const blocks = wb.timeBlocks || [] |
|||
if (blocks.length > 0 && !blocks.includes(this.currentTimeBlock)) { |
|||
this.currentTimeBlock = this.sortedTimeBlocks[0] || blocks[0] |
|||
} else if (blocks.length === 0) { |
|||
this.currentTimeBlock = null |
|||
} |
|||
} else { |
|||
this.currentWhiteboardId = null |
|||
this.currentTimeBlock = null |
|||
} |
|||
}, |
|||
immediate: true |
|||
} |
|||
}, |
|||
methods: { |
|||
/** 与 RightPanel 一致:判断是否为图片路径(支持 /profile/upload/ 等相对路径) */ |
|||
isImg(url) { |
|||
if (!url || typeof url !== 'string') return false |
|||
return url.includes('/') || url.includes('data:image') || /\.(png|jpg|jpeg|gif|webp|svg)(\?|$)/i.test(url) |
|||
}, |
|||
/** 与 RightPanel 一致:拼接后端地址,图片需通过完整 URL 加载 */ |
|||
formatImg(url) { |
|||
if (!url) return '' |
|||
if (url.startsWith('http') || url.startsWith('//') || url.startsWith('data:')) return url |
|||
const cleanPath = (url || '').replace(/\/+/g, '/') |
|||
const backendUrl = process.env.VUE_APP_BACKEND_URL || process.env.VUE_APP_BASE_API || '' |
|||
return backendUrl ? backendUrl + cleanPath : url |
|||
}, |
|||
compareTimeBlock(a, b) { |
|||
const parse = (s) => { |
|||
const m = /K\+(\d+):(\d+):(\d+)/.exec(s) |
|||
if (!m) return 0 |
|||
return parseInt(m[1], 10) * 3600 + parseInt(m[2], 10) * 60 + parseInt(m[3], 10) |
|||
} |
|||
return parse(a) - parse(b) |
|||
}, |
|||
onWhiteboardChange(id) { |
|||
this.$emit('select-whiteboard', id) |
|||
}, |
|||
initRenameWhiteboardDialog() { |
|||
this.renameWhiteboardName = this.currentWhiteboard ? (this.currentWhiteboard.name || '白板方案') : '' |
|||
}, |
|||
commitRenameWhiteboardDialog() { |
|||
const name = (this.renameWhiteboardName || '').trim() |
|||
if (!name || !this.currentWhiteboard) { |
|||
this.$message.warning('请输入方案名称') |
|||
return |
|||
} |
|||
this.$emit('rename-whiteboard', this.currentWhiteboard.id, name) |
|||
this.showRenameWhiteboardDialog = false |
|||
this.renameWhiteboardName = '' |
|||
}, |
|||
onDraftCommand(cmd) { |
|||
if (cmd === 'rename' && this.currentWhiteboard) { |
|||
this.showRenameWhiteboardDialog = true |
|||
} else if (cmd === 'delete' && this.currentWhiteboard) { |
|||
this.$confirm('确定删除该白板方案吗?', '提示', { |
|||
type: 'warning' |
|||
}).then(() => this.$emit('delete-whiteboard', this.currentWhiteboard.id)).catch(() => {}) |
|||
} |
|||
}, |
|||
createNewWhiteboard() { |
|||
this.$emit('create-whiteboard') |
|||
}, |
|||
exitWhiteboard() { |
|||
this.$emit('exit-whiteboard') |
|||
}, |
|||
selectTimeBlock(tb) { |
|||
this.currentTimeBlock = tb |
|||
this.$emit('select-time-block', tb) |
|||
}, |
|||
selectTimePreset(value) { |
|||
this.$emit('add-time-block', value) |
|||
this.newTimeBlockValue = null |
|||
this.newTimeBlockInput = '' |
|||
this.showAddTimeBlock = false |
|||
}, |
|||
addTimeBlock() { |
|||
let timeStr = '' |
|||
if (this.newTimeBlockValue) { |
|||
timeStr = 'K+' + this.newTimeBlockValue |
|||
} else { |
|||
const input = (this.newTimeBlockInput || '').trim() |
|||
if (!input) { |
|||
this.$message.warning('请选择时间或输入格式,如 K+00:05:00') |
|||
return |
|||
} |
|||
if (!/^K\+\d+:\d{2}:\d{2}$/.test(input)) { |
|||
this.$message.warning('格式应为 K+HH:MM:SS,如 K+00:05:00') |
|||
return |
|||
} |
|||
timeStr = input |
|||
} |
|||
this.$emit('add-time-block', timeStr) |
|||
this.newTimeBlockValue = null |
|||
this.newTimeBlockInput = '' |
|||
this.showAddTimeBlock = false |
|||
}, |
|||
openModifyTimeBlock() { |
|||
this.showRenameTimeBlock = true |
|||
}, |
|||
initModifyTimeBlock() { |
|||
if (!this.currentTimeBlock) return |
|||
const m = /K\+(\d+):(\d{2}):(\d{2})/.exec(this.currentTimeBlock) |
|||
if (m) { |
|||
this.renameTimeBlockValue = `${String(m[1]).padStart(2, '0')}:${m[2]}:${m[3]}` |
|||
this.renameTimeBlockInput = this.currentTimeBlock |
|||
} else { |
|||
this.renameTimeBlockValue = null |
|||
this.renameTimeBlockInput = this.currentTimeBlock |
|||
} |
|||
}, |
|||
selectRenamePreset(value) { |
|||
this.$emit('rename-time-block', this.currentTimeBlock, value) |
|||
this.renameTimeBlockValue = null |
|||
this.renameTimeBlockInput = '' |
|||
this.showRenameTimeBlock = false |
|||
}, |
|||
renameTimeBlock() { |
|||
let timeStr = '' |
|||
if (this.renameTimeBlockValue) { |
|||
timeStr = 'K+' + this.renameTimeBlockValue |
|||
} else { |
|||
const input = (this.renameTimeBlockInput || '').trim() |
|||
if (!input) { |
|||
this.$message.warning('请选择时间或输入格式,如 K+00:05:00') |
|||
return |
|||
} |
|||
if (!/^K\+\d+:\d{2}:\d{2}$/.test(input)) { |
|||
this.$message.warning('格式应为 K+HH:MM:SS,如 K+00:05:00') |
|||
return |
|||
} |
|||
timeStr = input |
|||
} |
|||
if (!this.currentTimeBlock) return |
|||
this.$emit('rename-time-block', this.currentTimeBlock, timeStr) |
|||
this.renameTimeBlockValue = null |
|||
this.renameTimeBlockInput = '' |
|||
this.showRenameTimeBlock = false |
|||
}, |
|||
deleteCurrentTimeBlock() { |
|||
if (!this.currentTimeBlock) return |
|||
this.$emit('delete-time-block', this.currentTimeBlock) |
|||
}, |
|||
onPlatformDragStart(evt, platform) { |
|||
evt.dataTransfer.setData('application/json', JSON.stringify({ |
|||
type: 'whiteboardPlatform', |
|||
platform: platform |
|||
})) |
|||
evt.dataTransfer.effectAllowed = 'copy' |
|||
}, |
|||
toggleAirspaceDraw() { |
|||
this.drawMode = this.drawMode === 'airspace' ? null : 'airspace' |
|||
this.$emit('draw-mode-change', this.drawMode) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.whiteboard-panel { |
|||
position: fixed; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
background: rgba(255, 255, 255, 0.95); |
|||
backdrop-filter: blur(10px); |
|||
border-top: 1px solid rgba(0, 138, 255, 0.2); |
|||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08); |
|||
z-index: 85; |
|||
padding: 10px 16px; |
|||
max-height: 200px; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
.wb-row { |
|||
display: flex; |
|||
align-items: center; |
|||
flex-wrap: wrap; |
|||
gap: 8px 16px; |
|||
} |
|||
|
|||
.wb-row-main { |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.wb-row-platform { |
|||
align-items: flex-start; |
|||
width: 100%; |
|||
} |
|||
|
|||
.wb-time-section, |
|||
.wb-tools-section, |
|||
.wb-draft-actions { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 6px; |
|||
} |
|||
|
|||
.wb-draft-actions { |
|||
margin-left: auto; |
|||
} |
|||
|
|||
.wb-draft-select-wrap { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 2px; |
|||
} |
|||
|
|||
.wb-draft-select { |
|||
width: 140px; |
|||
} |
|||
|
|||
.wb-draft-more-btn { |
|||
padding: 4px; |
|||
} |
|||
|
|||
.wb-label { |
|||
font-size: 12px; |
|||
color: #606266; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.wb-time-blocks { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 4px; |
|||
align-items: center; |
|||
} |
|||
|
|||
.wb-time-tag { |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.wb-platform-filter { |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.wb-platform-grid { |
|||
display: grid; |
|||
grid-template-columns: repeat(auto-fill, minmax(48px, 1fr)); |
|||
gap: 6px; |
|||
flex: 1; |
|||
min-width: 0; |
|||
max-height: 70px; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
.wb-platform-item { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
padding: 4px; |
|||
border-radius: 4px; |
|||
cursor: grab; |
|||
background: rgba(0, 138, 255, 0.06); |
|||
transition: background 0.2s; |
|||
} |
|||
|
|||
.wb-platform-item:hover { |
|||
background: rgba(0, 138, 255, 0.15); |
|||
} |
|||
|
|||
.wb-platform-icon { |
|||
width: 28px; |
|||
height: 28px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
font-size: 18px; |
|||
} |
|||
|
|||
.wb-platform-img { |
|||
width: 24px; |
|||
height: 24px; |
|||
object-fit: contain; |
|||
} |
|||
|
|||
.wb-platform-name { |
|||
font-size: 10px; |
|||
color: #606266; |
|||
max-width: 44px; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.wb-time-edit { |
|||
display: flex; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.time-presets { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.preset-tag { |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.preset-tag:hover { |
|||
opacity: 0.85; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue