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.

580 lines
18 KiB

3 weeks ago
<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>