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.
 
 
 
 
 

461 lines
12 KiB

<template>
<!-- 冲突列表弹窗类似 4T可拖动可调整大小带分页 -->
<div v-show="visible" class="conflict-drawer-wrap">
<div class="conflict-drawer-container" :style="panelStyle">
<!-- 标题栏可拖动 -->
<div class="conflict-drawer-header" @mousedown="onDragStart">
<span class="conflict-drawer-title">冲突列表</span>
<button class="conflict-drawer-close" @mousedown.stop @click="handleClose">×</button>
</div>
<!-- 筛选与每页条数 -->
<div class="conflict-drawer-toolbar">
<div class="toolbar-row">
<span class="toolbar-label">{{ $t('rightPanel.conflictFilter') }}</span>
<el-select v-model="filterType" size="mini" class="filter-select">
<el-option :label="$t('rightPanel.conflictTypeAll')" value="all" />
<el-option :label="$t('rightPanel.conflictTypeTime')" value="time" />
<el-option :label="$t('rightPanel.conflictTypeSpace')" value="space" />
<el-option :label="$t('rightPanel.conflictTypeSpectrum')" value="spectrum" />
</el-select>
</div>
<div class="toolbar-row">
<span class="toolbar-label">每页条数:</span>
<el-select v-model="pageSize" size="mini" class="page-size-select">
<el-option :label="'5'" :value="5" />
<el-option :label="'10'" :value="10" />
<el-option :label="'20'" :value="20" />
<el-option :label="'50'" :value="50" />
</el-select>
</div>
</div>
<!-- 列表区域 -->
<div class="conflict-drawer-body">
<div v-if="loading" class="conflict-loading">
正在检测冲突…
</div>
<div v-else-if="paginatedList.length > 0" class="conflict-list">
<div
v-for="conflict in paginatedList"
:key="conflict.id"
class="conflict-item"
>
<div class="conflict-header">
<i class="el-icon-warning conflict-icon"></i>
<span class="conflict-title">{{ conflict.title }}</span>
<el-tag size="mini" :type="conflictTypeTagType(conflict.type)" class="conflict-type-tag">{{ conflictTypeLabel(conflict.type) }}</el-tag>
<el-tag v-if="conflict.severity === 'high'" size="mini" type="danger">{{ $t('rightPanel.serious') }}</el-tag>
</div>
<div class="conflict-details">
<div class="detail-item">
<span class="label">{{ $t('rightPanel.involvedRoutes') }}:</span>
<span class="value">{{ conflict.routeName || (conflict.routeNames && conflict.routeNames.join('、')) }}</span>
</div>
<div v-if="conflict.fromWaypoint && conflict.toWaypoint" class="detail-item">
<span class="label">问题航段:</span>
<span class="value">{{ conflict.fromWaypoint }} → {{ conflict.toWaypoint }}</span>
</div>
<div v-if="conflict.time" class="detail-item">
<span class="label">{{ $t('rightPanel.conflictTime') }}:</span>
<span class="value">{{ conflict.time }}</span>
</div>
<div v-if="conflict.position" class="detail-item">
<span class="label">{{ $t('rightPanel.conflictPosition') }}:</span>
<span class="value">{{ conflict.position }}</span>
</div>
<div v-if="conflict.suggestion" class="detail-item suggestion">
<span class="label">建议:</span>
<span class="value">{{ conflict.suggestion }}</span>
</div>
</div>
<div class="conflict-actions">
<el-button type="text" size="mini" class="blue-text-btn" @click="$emit('view-conflict', conflict)">
{{ $t('rightPanel.locate') }}
</el-button>
<el-button type="text" size="mini" class="blue-text-btn" @click="$emit('resolve-conflict', conflict)">
{{ $t('rightPanel.resolveConflict') }}
</el-button>
</div>
</div>
</div>
<div v-else-if="!loading" class="no-conflict">
<i class="el-icon-success no-conflict-icon"></i>
<p>{{ filteredConflicts.length > 0 ? $t('rightPanel.noMatchFilter') : $t('rightPanel.noConflict') }}</p>
</div>
</div>
<!-- 分页 -->
<div v-if="filteredConflicts.length > pageSize" class="conflict-drawer-footer">
<el-pagination
small
layout="prev, pager, next, total"
:total="filteredConflicts.length"
:page-size="pageSize"
:current-page.sync="currentPage"
:pager-count="5"
/>
</div>
<!-- 右下角调整大小手柄 -->
<div class="conflict-resize-handle" @mousedown="onResizeStart" title="拖动调整大小"></div>
</div>
</div>
</template>
<script>
export default {
name: 'ConflictDrawer',
props: {
visible: {
type: Boolean,
default: false
},
conflicts: {
type: Array,
default: () => []
},
loading: {
type: Boolean,
default: false
}
},
data() {
return {
filterType: 'all',
pageSize: 10,
currentPage: 1,
isDragging: false,
dragStartX: 0,
dragStartY: 0,
panelLeft: null,
panelTop: null,
panelWidth: 420,
panelHeight: 480,
isResizing: false,
resizeStartX: 0,
resizeStartY: 0,
resizeStartW: 0,
resizeStartH: 0
}
},
computed: {
filteredConflicts() {
const list = this.conflicts || []
if (this.filterType === 'all') return list
return list.filter(c => c.type === this.filterType)
},
paginatedList() {
const list = this.filteredConflicts
const start = (this.currentPage - 1) * this.pageSize
return list.slice(start, start + this.pageSize)
},
panelStyle() {
const left = this.panelLeft != null ? this.panelLeft : (window.innerWidth - this.panelWidth) / 2 - 20
const top = this.panelTop != null ? this.panelTop : 80
return {
left: `${Math.max(0, left)}px`,
top: `${Math.max(0, top)}px`,
width: `${this.panelWidth}px`,
height: `${this.panelHeight}px`
}
}
},
watch: {
filterType() {
this.currentPage = 1
},
pageSize() {
this.currentPage = 1
}
},
methods: {
handleClose() {
this.$emit('update:visible', false)
},
conflictTypeLabel(type) {
const t = type || ''
if (t === 'time') return this.$t('rightPanel.conflictTypeTime')
if (t === 'space') return this.$t('rightPanel.conflictTypeSpace')
if (t === 'spectrum') return this.$t('rightPanel.conflictTypeSpectrum')
return this.$t('rightPanel.conflictTypeAll')
},
conflictTypeTagType(type) {
if (type === 'time') return 'warning'
if (type === 'space') return 'danger'
if (type === 'spectrum') return 'primary'
return 'info'
},
onDragStart(e) {
e.preventDefault()
this.isDragging = true
const currentLeft = this.panelLeft != null ? this.panelLeft : (window.innerWidth - this.panelWidth) / 2 - 20
const currentTop = this.panelTop != null ? this.panelTop : 80
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)
},
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
this.panelWidth = Math.max(320, Math.min(800, this.resizeStartW + dx))
this.panelHeight = Math.max(300, Math.min(window.innerHeight - 60, this.resizeStartH + dy))
},
onResizeEnd() {
this.isResizing = false
document.removeEventListener('mousemove', this.onResizeMove)
document.removeEventListener('mouseup', this.onResizeEnd)
}
}
}
</script>
<style scoped>
.conflict-drawer-wrap {
position: fixed;
inset: 0;
background: transparent;
z-index: 200;
pointer-events: none;
}
.conflict-drawer-wrap .conflict-drawer-container {
pointer-events: auto;
position: fixed;
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
border: 1px solid #e0edff;
display: flex;
flex-direction: column;
overflow: hidden;
}
.conflict-drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #f0f7ff;
border-bottom: 1px solid #cce5ff;
cursor: move;
flex-shrink: 0;
}
.conflict-drawer-title {
font-weight: 600;
color: #1994fe;
font-size: 15px;
}
.conflict-drawer-close {
width: 28px;
height: 28px;
border: 1px solid #cce5ff;
background: #fff;
color: #1994fe;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.conflict-drawer-close:hover {
background: #e8f4ff;
}
.conflict-drawer-toolbar {
padding: 10px 16px;
border-bottom: 1px solid #eee;
flex-shrink: 0;
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.toolbar-row {
display: flex;
align-items: center;
gap: 6px;
}
.toolbar-label {
font-size: 12px;
color: #666;
}
.filter-select,
.page-size-select {
width: 100px;
}
.conflict-drawer-body {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.conflict-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
color: #909399;
font-size: 14px;
}
.conflict-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.conflict-item {
background: #fafafa;
border-radius: 8px;
padding: 12px;
border: 1px solid rgba(245, 108, 108, 0.2);
}
.conflict-item:hover {
background: #fff5f5;
}
.conflict-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
}
.conflict-icon {
color: #f56c6c;
font-size: 16px;
}
.conflict-title {
flex: 1;
font-weight: 600;
color: #f56c6c;
font-size: 13px;
}
.conflict-type-tag {
margin-left: 4px;
}
.conflict-details {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 8px;
}
.conflict-details .detail-item {
font-size: 12px;
display: flex;
gap: 6px;
}
.conflict-details .label {
color: #999;
min-width: 70px;
flex-shrink: 0;
}
.conflict-details .value {
color: #333;
}
.conflict-details .suggestion .value {
color: #e6a23c;
}
.conflict-actions {
display: flex;
gap: 8px;
}
.blue-text-btn {
color: #1994fe;
font-size: 12px;
}
.no-conflict {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: #67c23a;
}
.no-conflict-icon {
font-size: 48px;
margin-bottom: 12px;
}
.no-conflict p {
margin: 0;
font-size: 14px;
color: #666;
}
.conflict-drawer-footer {
padding: 8px 16px;
border-top: 1px solid #eee;
flex-shrink: 0;
display: flex;
justify-content: center;
}
.conflict-resize-handle {
position: absolute;
right: 0;
bottom: 0;
width: 20px;
height: 20px;
cursor: nwse-resize;
user-select: none;
z-index: 10;
background: linear-gradient(to top left, transparent 50%, rgba(25, 148, 254, 0.2) 50%);
}
.conflict-resize-handle:hover {
background: linear-gradient(to top left, transparent 50%, rgba(25, 148, 254, 0.4) 50%);
}
</style>