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.
899 lines
24 KiB
899 lines
24 KiB
<template>
|
|
<div>
|
|
<div
|
|
class="right-external-hide-btn"
|
|
:class="{ hidden: isHidden }"
|
|
@click="handleHide"
|
|
:title="$t('rightPanel.hideRightPanel')"
|
|
>
|
|
<i class="el-icon-arrow-right"></i>
|
|
</div>
|
|
|
|
<div
|
|
class="floating-right-panel blue-theme"
|
|
:class="{ 'hidden': isHidden }"
|
|
>
|
|
<div v-if="activeTab === 'plan'" class="tab-content plan-content">
|
|
<div class="section">
|
|
<div class="section-header">
|
|
<div class="section-title">{{ $t('rightPanel.planList') }}</div>
|
|
<el-button
|
|
type="primary"
|
|
size="mini"
|
|
@click="handleCreatePlan"
|
|
class="create-route-btn-new"
|
|
>
|
|
{{ $t('rightPanel.createPlan') }}
|
|
</el-button>
|
|
</div>
|
|
<div class="tree-list">
|
|
<!-- 方案列表 -->
|
|
<div
|
|
v-for="plan in plans"
|
|
:key="plan.id"
|
|
class="tree-item plan-item"
|
|
:class="{ selected: selectedPlanId === plan.id }"
|
|
>
|
|
<div class="tree-item-header" @click="togglePlan(plan.id)">
|
|
<i :class="expandedPlans.includes(plan.id) ? 'el-icon-folder-opened' : 'el-icon-folder'" class="tree-icon"></i>
|
|
<div class="tree-item-info">
|
|
<div class="tree-item-name">{{ plan.name }}</div>
|
|
<div class="tree-item-meta">{{ routes.filter(r => r.scenarioId === plan.id).length }}个航线</div>
|
|
</div>
|
|
<div class="tree-item-actions">
|
|
<i class="el-icon-plus" title="新建航线" @click.stop="handleCreateRouteForPlan(plan)"></i>
|
|
<i class="el-icon-edit" title="编辑" @click.stop="handleOpenPlanDialog(plan)"></i>
|
|
<i class="el-icon-delete" title="删除" @click.stop="handleDeletePlan(plan)"></i>
|
|
</div>
|
|
</div>
|
|
<!-- 航线列表 -->
|
|
<div v-if="expandedPlans.includes(plan.id)" class="tree-children route-children">
|
|
<div
|
|
v-for="route in routes.filter(r => r.scenarioId === plan.id)"
|
|
:key="route.id"
|
|
class="tree-item route-item"
|
|
:class="getRouteClasses(route.id)"
|
|
>
|
|
<div class="tree-item-header" @click="toggleRoute(route.id)">
|
|
<i class="el-icon-map-location tree-icon"></i>
|
|
<div class="tree-item-info">
|
|
<div class="tree-item-name">{{ route.name }}</div>
|
|
<div class="tree-item-meta">{{ route.points }}{{ $t('rightPanel.points') }}</div>
|
|
<div v-if="routeLockedByOther(route.id)" class="route-locked-tag">
|
|
<i class="el-icon-lock"></i> {{ routeLockedByName(route.id) }} 正在编辑
|
|
</div>
|
|
</div>
|
|
<el-tag
|
|
v-if="route.conflict"
|
|
size="mini"
|
|
type="danger"
|
|
class="conflict-tag"
|
|
>
|
|
{{ $t('rightPanel.conflict') }}
|
|
</el-tag>
|
|
<div class="tree-item-actions">
|
|
<i class="el-icon-view" title="显示/隐藏" @click.stop="handleToggleRouteVisibility(route)"></i>
|
|
<i
|
|
v-if="!routeLockedByOther(route.id)"
|
|
:class="routeLocked[route.id] ? 'el-icon-lock' : 'el-icon-unlock'"
|
|
:title="routeLocked[route.id] ? '解锁' : '上锁'"
|
|
@click.stop="$emit('toggle-route-lock', route)"
|
|
></i>
|
|
<i
|
|
v-else
|
|
class="el-icon-lock route-locked-by-other-icon"
|
|
title="被他人锁定,无法操作"
|
|
></i>
|
|
<i
|
|
class="el-icon-edit"
|
|
:title="routeLockedByOther(route.id) ? '被他人锁定,无法编辑' : '编辑'"
|
|
:class="{ 'action-disabled': routeLockedByOther(route.id) }"
|
|
@click.stop="!routeLockedByOther(route.id) && handleOpenRouteDialog(route)"
|
|
></i>
|
|
<i
|
|
class="el-icon-delete"
|
|
:class="{ 'action-disabled': routeLockedByOther(route.id) }"
|
|
title="删除"
|
|
@click.stop="!routeLockedByOther(route.id) && $emit('delete-route', route)"
|
|
></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="activeTab === 'platform'" class="tab-content platform-content">
|
|
<div class="section-header" style="padding: 10px 0; display: flex; justify-content: space-between; align-items: center;">
|
|
<div class="section-title">平台列表</div>
|
|
<el-button
|
|
type="primary"
|
|
size="mini"
|
|
icon="el-icon-upload2"
|
|
@click="handleImportPlatform"
|
|
class="create-route-btn-new"
|
|
style="width: 90px;height: 25px;line-height: 25px;padding: 0;font-size: 14px;"
|
|
>
|
|
导入平台
|
|
</el-button>
|
|
</div>
|
|
<div class="platform-categories">
|
|
<el-tabs v-model="activePlatformTab" type="card" size="mini" class="blue-tabs">
|
|
<el-tab-pane :label="$t('rightPanel.air')" name="air">
|
|
<div class="platform-list">
|
|
<div
|
|
v-for="platform in airPlatforms"
|
|
:key="platform.id"
|
|
class="platform-item platform-item-draggable"
|
|
draggable="true"
|
|
@click="handlePlatformItemClick(platform, $event)"
|
|
@dragstart="onPlatformDragStart($event, platform)"
|
|
>
|
|
<div class="platform-icon" :style="{ color: platform.color }">
|
|
<img v-if="isImg(platform.imageUrl)"
|
|
:src="formatImg(platform.imageUrl)"
|
|
class="platform-img"
|
|
/>
|
|
<i v-else :class="platform.icon || 'el-icon-picture-outline'"></i>
|
|
</div>
|
|
<div class="platform-info">
|
|
<div class="platform-name">{{ platform.name }}</div>
|
|
<div class="platform-type">{{ platform.type }}</div>
|
|
</div>
|
|
<div class="platform-action" style="margin-left: auto; padding-right: 10px;">
|
|
<i
|
|
class="el-icon-delete"
|
|
title="删除平台"
|
|
style="color: #F56C6C; cursor: pointer;"
|
|
@click.stop="$emit('delete-platform', platform)"
|
|
></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</el-tab-pane>
|
|
|
|
<el-tab-pane :label="$t('rightPanel.sea')" name="sea">
|
|
<div class="platform-list">
|
|
<div
|
|
v-for="platform in seaPlatforms"
|
|
:key="platform.id"
|
|
class="platform-item platform-item-draggable"
|
|
draggable="true"
|
|
@click="handlePlatformItemClick(platform, $event)"
|
|
@dragstart="onPlatformDragStart($event, platform)"
|
|
>
|
|
<div class="platform-icon" :style="{ color: platform.color }">
|
|
<img v-if="isImg(platform.imageUrl)"
|
|
:src="formatImg(platform.imageUrl)"
|
|
class="platform-img"
|
|
/>
|
|
<i v-else :class="platform.icon || 'el-icon-picture-outline'"></i>
|
|
</div>
|
|
<div class="platform-info">
|
|
<div class="platform-name">{{ platform.name }}</div>
|
|
<div class="platform-type">{{ platform.type }}</div>
|
|
</div>
|
|
<div class="platform-action" style="margin-left: auto; padding-right: 10px;">
|
|
<i class="el-icon-delete" title="删除平台" style="color: #F56C6C; cursor: pointer;" @click.stop="$emit('delete-platform', platform)"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</el-tab-pane>
|
|
|
|
<el-tab-pane :label="$t('rightPanel.ground')" name="ground">
|
|
<div class="platform-list">
|
|
<div
|
|
v-for="platform in groundPlatforms"
|
|
:key="platform.id"
|
|
class="platform-item platform-item-draggable"
|
|
draggable="true"
|
|
@click="handlePlatformItemClick(platform, $event)"
|
|
@dragstart="onPlatformDragStart($event, platform)"
|
|
>
|
|
<div class="platform-icon" :style="{ color: platform.color }">
|
|
<img v-if="isImg(platform.imageUrl)"
|
|
:src="formatImg(platform.imageUrl)"
|
|
class="platform-img"
|
|
/>
|
|
<i v-else :class="platform.icon || 'el-icon-picture-outline'"></i>
|
|
</div>
|
|
<div class="platform-info">
|
|
<div class="platform-name">{{ platform.name }}</div>
|
|
<div class="platform-type">{{ platform.type }}</div>
|
|
</div>
|
|
<div class="platform-action" style="margin-left: auto; padding-right: 10px;">
|
|
<i class="el-icon-delete" title="删除平台" style="color: #F56C6C; cursor: pointer;" @click.stop="$emit('delete-platform', platform)"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</el-tab-pane>
|
|
</el-tabs>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
name: 'RightPanel',
|
|
props: {
|
|
isHidden: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
activeTab: {
|
|
type: String,
|
|
default: 'plan'
|
|
},
|
|
plans: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
routes: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
activeRouteIds: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
/** 航线上锁状态:routeId -> true 上锁,与地图右键上锁/解锁同步 */
|
|
routeLocked: {
|
|
type: Object,
|
|
default: () => ({})
|
|
},
|
|
/** 协作:谁正在编辑(锁定)该航线 routeId -> { userId, userName, nickName, sessionId } */
|
|
routeLockedBy: {
|
|
type: Object,
|
|
default: () => ({})
|
|
},
|
|
/** 当前用户 ID,用于判断“正在查看/锁定”是否为自己 */
|
|
currentUserId: {
|
|
type: [Number, String],
|
|
default: null
|
|
},
|
|
selectedPlanId: {
|
|
type: [String, Number],
|
|
default: null
|
|
},
|
|
selectedPlanDetails: {
|
|
type: Object,
|
|
default: null
|
|
},
|
|
selectedRouteId: {
|
|
type: [String, Number],
|
|
default: null
|
|
},
|
|
selectedRouteDetails: {
|
|
type: Object,
|
|
default: null
|
|
},
|
|
/** 父组件要求展开的方案树(如冲突定位时),会展开对应方案以便看到航线行 */
|
|
expandRouteIds: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
airPlatforms: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
seaPlatforms: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
groundPlatforms: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
activePlatformTab: 'air',
|
|
expandedPlans: [], // 展开的方案列表
|
|
platformJustDragged: false // 刚拖拽过,避免拖拽后误触点击打开弹窗
|
|
}
|
|
},
|
|
watch: {
|
|
expandRouteIds(newVal) {
|
|
if (newVal && newVal.length) {
|
|
newVal.forEach(routeId => {
|
|
const r = this.routes.find(route => route.id === routeId);
|
|
if (r && r.scenarioId != null && !this.expandedPlans.includes(r.scenarioId)) {
|
|
this.expandedPlans.push(r.scenarioId);
|
|
}
|
|
});
|
|
}
|
|
},
|
|
selectedPlanId(newId) {
|
|
if (newId) {
|
|
console.log('>>> [子组件同步] 检测到方案切换,自动展开 ID:', newId);
|
|
// 如果还没展开,就把它塞进展开数组里
|
|
if (!this.expandedPlans.includes(newId)) {
|
|
this.expandedPlans.push(newId);
|
|
}
|
|
}
|
|
},
|
|
activeRouteIds: {
|
|
handler(newVal) {
|
|
console.log('activeRouteIds updated:', newVal);
|
|
},
|
|
deep: true
|
|
}
|
|
},
|
|
methods: {
|
|
// 切换方案展开/折叠
|
|
togglePlan(planId) {
|
|
const index = this.expandedPlans.indexOf(planId)
|
|
if (index > -1) {
|
|
this.expandedPlans.splice(index, 1)
|
|
const planRoutes = this.routes.filter(r => r.scenarioId === planId)
|
|
planRoutes.forEach(route => {
|
|
if (this.activeRouteIds.includes(route.id)) {
|
|
this.handleToggleRouteVisibility(route)
|
|
}
|
|
})
|
|
this.$emit('select-plan', { id: null })
|
|
} else {
|
|
// 展开新方案:不再隐藏航线,保留已打开的航线显示;仅切换展开状态并选中方案
|
|
this.expandedPlans = [];
|
|
this.expandedPlans.push(planId)
|
|
const plan = this.plans.find(p => p.id === planId)
|
|
if (plan) {
|
|
this.$emit('select-plan', plan)
|
|
}
|
|
}
|
|
},
|
|
|
|
// 点击航线行:与父组件 selectRoute 联动(未选中则上图,已选中则取消上图);航点请在「编辑航线」中维护
|
|
toggleRoute(routeId) {
|
|
const route = this.routes.find(r => r.id === routeId)
|
|
if (!route) return
|
|
if (!route.waypoints) {
|
|
this.$set(route, 'waypoints', [])
|
|
}
|
|
this.handleSelectRoute(route)
|
|
this.$nextTick(() => {
|
|
if (route.scenarioId != null && !this.expandedPlans.includes(route.scenarioId)) {
|
|
this.expandedPlans.push(route.scenarioId)
|
|
}
|
|
})
|
|
},
|
|
|
|
// 判断是否是图片路径
|
|
isImg(path) {
|
|
if (!path) return false;
|
|
return path.includes('/') || path.includes('data:image');
|
|
},
|
|
|
|
// 格式化图片地址
|
|
formatImg(url) {
|
|
if (!url) return '';
|
|
// 清理路径中的双斜杠
|
|
const cleanPath = url.replace(/\/+/g, '/');
|
|
const backendUrl = process.env.VUE_APP_BACKEND_URL;
|
|
const finalUrl = backendUrl + cleanPath;
|
|
|
|
// 👈 核心排查日志:按 F12 在控制台看这个输出!
|
|
console.log('>>> [图片渲染调试] 最终拼接地址:', finalUrl);
|
|
|
|
return finalUrl;
|
|
},
|
|
|
|
handleHide() {
|
|
this.$emit('hide')
|
|
},
|
|
|
|
handleSelectRoute(route) {
|
|
// 确保航线有航点数据
|
|
this.$emit('select-route', route)
|
|
},
|
|
|
|
handleCreatePlan() {
|
|
this.$emit('create-plan')
|
|
},
|
|
|
|
handleCreateRouteForPlan(plan) {
|
|
this.$emit('create-route', plan)
|
|
},
|
|
|
|
handleDeletePlan(plan) {
|
|
this.$confirm(`是否确认删除方案 "${plan.name}"?这将同时删除该方案下的所有航线。`, "警告", {
|
|
confirmButtonText: "确定",
|
|
cancelButtonText: "取消",
|
|
type: "warning"
|
|
}).then(() => {
|
|
this.$emit('delete-plan', plan);
|
|
}).catch(() => {
|
|
});
|
|
},
|
|
|
|
handleOpenPlanDialog(plan) {
|
|
this.$emit('open-plan-dialog', plan)
|
|
},
|
|
|
|
handleImportPlatform() {
|
|
this.$emit('open-import-dialog');
|
|
},
|
|
|
|
handleOpenRouteDialog(route) {
|
|
this.$emit('open-route-dialog', route)
|
|
},
|
|
|
|
handleToggleRouteVisibility(route) {
|
|
this.$emit('toggle-route-visibility', route)
|
|
},
|
|
|
|
getRouteClasses(routeId) {
|
|
return {
|
|
active: this.activeRouteIds.includes(routeId),
|
|
'locked-by-other': this.routeLockedByOther(routeId)
|
|
}
|
|
},
|
|
/** 是否有其他成员正在编辑(锁定)该航线 */
|
|
routeLockedByOther(routeId) {
|
|
const e = this.routeLockedBy[routeId]
|
|
if (!e) return false
|
|
const myId = this.currentUserId != null ? Number(this.currentUserId) : null
|
|
const uid = e.userId != null ? Number(e.userId) : null
|
|
return myId !== uid
|
|
},
|
|
routeLockedByName(routeId) {
|
|
const e = this.routeLockedBy[routeId]
|
|
return (e && (e.nickName || e.userName)) || ''
|
|
},
|
|
handleOpenPlatformDialog(platform) {
|
|
this.$emit('open-platform-dialog', platform)
|
|
},
|
|
|
|
/** 平台项点击:若刚拖拽过则不打开弹窗,避免误触 */
|
|
handlePlatformItemClick(platform, ev) {
|
|
if (this.platformJustDragged) {
|
|
this.platformJustDragged = false
|
|
return
|
|
}
|
|
this.handleOpenPlatformDialog(platform)
|
|
},
|
|
|
|
/** 拖拽平台图标到地图时传递平台数据 */
|
|
onPlatformDragStart(ev, platform) {
|
|
this.platformJustDragged = true
|
|
setTimeout(() => { this.platformJustDragged = false }, 300)
|
|
try {
|
|
ev.dataTransfer.setData('application/json', JSON.stringify({
|
|
id: platform.id,
|
|
name: platform.name,
|
|
type: platform.type,
|
|
imageUrl: platform.imageUrl,
|
|
iconUrl: platform.iconUrl,
|
|
icon: platform.icon,
|
|
color: platform.color
|
|
}))
|
|
ev.dataTransfer.effectAllowed = 'copy'
|
|
} catch (e) {
|
|
console.warn('Platform drag start failed', e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* 右侧外部隐藏按钮 */
|
|
.right-external-hide-btn {
|
|
position: absolute;
|
|
top: 80px;
|
|
right: 20px;
|
|
width: 30px;
|
|
height: 30px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
color: #165dff;
|
|
font-size: 18px;
|
|
background: rgba(255, 255, 255, 0.5);
|
|
border-radius: 50%;
|
|
z-index: 85;
|
|
box-shadow: 0 2px 8px rgba(22, 93, 255, 0.3);
|
|
transition: all 0.3s;
|
|
backdrop-filter: blur(5px);
|
|
}
|
|
|
|
.right-external-hide-btn:hover {
|
|
color: #165dff;
|
|
background: rgba(22, 93, 255, 0.2);
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.right-external-hide-btn.hidden {
|
|
opacity: 0;
|
|
transform: translateX(100%);
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* 右侧浮动面板 - 蓝色主题 */
|
|
.floating-right-panel {
|
|
position: absolute;
|
|
top: 70px;
|
|
right: 20px;
|
|
width: 300px;
|
|
border-radius: 0 8px 8px 8px;
|
|
z-index: 90;
|
|
color: #333;
|
|
overflow: hidden;
|
|
box-shadow: 0 4px 20px rgba(22, 93, 255, 0.2);
|
|
background: rgba(255, 255, 255, 0.95);
|
|
backdrop-filter: blur(15px);
|
|
transition: all 0.3s ease;
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.floating-right-panel.hidden {
|
|
opacity: 0;
|
|
transform: translateX(100%);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.tab-content {
|
|
padding: 15px;
|
|
max-height: 500px;
|
|
overflow-y: auto;
|
|
background: rgba(255, 255, 255, 0.95);
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.section {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
/* 新增:标题栏容器样式 */
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
border-bottom: 2px solid rgba(22, 93, 255, 0.2);
|
|
margin-bottom: 15px;
|
|
padding-bottom: 10px;
|
|
padding-top: 10px;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #165dff;
|
|
}
|
|
|
|
.create-route-btn-new {
|
|
background-color: #165dff !important;
|
|
border-color: #165dff !important;
|
|
color: #ffffff !important;
|
|
padding: 4px 10px;
|
|
font-size: 12px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.create-route-btn-new:hover {
|
|
background-color: #285fd9 !important;
|
|
border-color: #285fd9 !important;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.tree-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.tree-item {
|
|
border-radius: 6px;
|
|
transition: all 0.3s;
|
|
border: 1px solid rgba(22, 93, 255, 0.1);
|
|
}
|
|
|
|
.tree-item-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px;
|
|
background: rgba(255, 255, 255, 0.8);
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.tree-item-header:hover {
|
|
background: rgba(22, 93, 255, 0.1);
|
|
transform: translateX(-2px);
|
|
box-shadow: 0 2px 8px rgba(22, 93, 255, 0.15);
|
|
}
|
|
|
|
.tree-item.plan-item .tree-item-header {
|
|
background: rgba(255, 255, 255, 0.9) !important;
|
|
}
|
|
|
|
.tree-item.route-item .tree-item-header {
|
|
background: rgba(255, 255, 255, 0.8) !important;
|
|
}
|
|
|
|
.tree-item.route-item:not(.active) .tree-item-header {
|
|
background: rgba(255, 255, 255, 0.8) !important;
|
|
}
|
|
|
|
.tree-item.active .tree-item-header {
|
|
background: rgba(22, 93, 255, 0.15) !important;
|
|
border-color: rgba(22, 93, 255, 0.3);
|
|
box-shadow: 0 2px 10px rgba(22, 93, 255, 0.25);
|
|
}
|
|
|
|
.tree-item.selected .tree-item-header {
|
|
background: rgba(22, 93, 255, 0.1) !important;
|
|
border-color: rgba(22, 93, 255, 0.2);
|
|
}
|
|
|
|
.tree-icon {
|
|
font-size: 16px;
|
|
color: #165dff;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.tree-item-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.tree-item-name {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #333;
|
|
}
|
|
|
|
.tree-item-meta {
|
|
font-size: 12px;
|
|
color: #999;
|
|
}
|
|
|
|
.route-locked-tag {
|
|
font-size: 11px;
|
|
color: #909399;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.route-locked-tag .el-icon-lock {
|
|
margin-right: 2px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.tree-item.route-item.locked-by-other .tree-item-header {
|
|
background: rgba(240, 242, 245, 0.95) !important;
|
|
border-left: 3px solid #c0c4cc;
|
|
}
|
|
|
|
.tree-item-actions i.action-disabled {
|
|
color: #c0c4cc;
|
|
cursor: not-allowed;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.tree-item-actions i.action-disabled:hover {
|
|
background: transparent;
|
|
transform: none;
|
|
}
|
|
|
|
.route-locked-by-other-icon {
|
|
color: #909399;
|
|
cursor: default;
|
|
}
|
|
|
|
.tree-item-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.tree-item-actions i {
|
|
cursor: pointer;
|
|
color: #165dff;
|
|
font-size: 14px;
|
|
padding: 4px;
|
|
border-radius: 4px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.tree-item-actions i:hover {
|
|
background: rgba(22, 93, 255, 0.1);
|
|
transform: scale(1.2);
|
|
}
|
|
|
|
.tree-children {
|
|
margin-left: 20px;
|
|
margin-top: 4px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.route-children {
|
|
margin-left: 25px;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
padding: 10px 0;
|
|
}
|
|
|
|
.platform-categories {
|
|
height: 100%;
|
|
}
|
|
|
|
.platform-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
max-height: 450px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.platform-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 12px;
|
|
background: rgba(255, 255, 255, 0.8);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
border: 1px solid rgba(22, 93, 255, 0.1);
|
|
}
|
|
|
|
.platform-item:hover {
|
|
background: rgba(22, 93, 255, 0.1);
|
|
transform: translateX(-2px);
|
|
box-shadow: 0 2px 8px rgba(22, 93, 255, 0.15);
|
|
}
|
|
|
|
.platform-item-draggable {
|
|
cursor: grab;
|
|
}
|
|
|
|
.platform-item-draggable:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.platform-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 20px;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
border-radius: 50%;
|
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.platform-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.platform-name {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #333;
|
|
}
|
|
|
|
.platform-type {
|
|
font-size: 12px;
|
|
color: #999;
|
|
}
|
|
|
|
.platform-status {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
}
|
|
|
|
.status-dot.online {
|
|
background: #67c23a;
|
|
box-shadow: 0 0 6px rgba(103, 194, 58, 0.6);
|
|
}
|
|
|
|
.status-dot.offline {
|
|
background: #999;
|
|
}
|
|
|
|
.status-dot.operating {
|
|
background: #165dff;
|
|
animation: pulse 2s infinite;
|
|
box-shadow: 0 0 10px rgba(22, 93, 255, 0.8);
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% {
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
opacity: 0.5;
|
|
}
|
|
}
|
|
|
|
.blue-btn {
|
|
background: rgba(22, 93, 255, 0.1);
|
|
color: #165dff;
|
|
border: 1px solid rgba(22, 93, 255, 0.3);
|
|
}
|
|
|
|
.blue-btn:hover {
|
|
background: rgba(22, 93, 255, 0.2);
|
|
border-color: rgba(22, 93, 255, 0.5);
|
|
}
|
|
|
|
.blue-text-btn {
|
|
color: #165dff;
|
|
}
|
|
|
|
.blue-text-btn:hover {
|
|
color: #165dff;
|
|
}
|
|
|
|
.blue-badge {
|
|
background: rgba(245, 108, 108, 0.1);
|
|
color: #f56c6c;
|
|
border: 1px solid rgba(245, 108, 108, 0.3);
|
|
}
|
|
|
|
.blue-tabs >>> .el-tabs__item {
|
|
color: #666;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.blue-tabs >>> .el-tabs__item:hover {
|
|
color: #165dff;
|
|
}
|
|
|
|
.blue-tabs >>> .el-tabs__item.is-active {
|
|
color: #165dff;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.blue-tabs >>> .el-tabs__active-bar {
|
|
background-color: #165dff;
|
|
box-shadow: 0 0 6px rgba(22, 93, 255, 0.5);
|
|
}
|
|
|
|
.blue-tabs >>> .el-tabs__nav-wrap::after {
|
|
background-color: rgba(22, 93, 255, 0.3);
|
|
}
|
|
|
|
.blue-success {
|
|
color: #67c23a;
|
|
}
|
|
|
|
.blue-warning {
|
|
color: #e6a23c;
|
|
}
|
|
|
|
.platform-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden; /* 裁剪超出部分 */
|
|
}
|
|
|
|
.platform-img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover; /* 关键:图片自动填充不拉伸 */
|
|
border-radius: 4px;
|
|
}
|
|
</style>
|
|
|