You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
579 lines
18 KiB
579 lines
18 KiB
<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>
|
|
|