8 changed files with 632 additions and 59 deletions
@ -0,0 +1,237 @@ |
|||||
|
<template> |
||||
|
<el-dialog |
||||
|
title="导出航线" |
||||
|
:visible.sync="visible" |
||||
|
width="720px" |
||||
|
top="52vh" |
||||
|
append-to-body |
||||
|
class="export-routes-dialog" |
||||
|
@close="handleClose" |
||||
|
> |
||||
|
<div v-if="routes.length === 0" class="empty-tip"> |
||||
|
<i class="el-icon-warning-outline"></i> |
||||
|
<p>暂无航线可导出,请先创建航线。</p> |
||||
|
</div> |
||||
|
<div v-else> |
||||
|
<div class="select-actions"> |
||||
|
<el-button type="text" size="small" @click="selectAll">全选</el-button> |
||||
|
<el-button type="text" size="small" @click="selectNone">全不选</el-button> |
||||
|
</div> |
||||
|
<div class="tree-list"> |
||||
|
<div |
||||
|
v-for="plan in plansWithRoutes" |
||||
|
:key="plan.id" |
||||
|
class="tree-item plan-item" |
||||
|
> |
||||
|
<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">{{ planRoutes(plan.id).length }} 个航线</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div v-if="expandedPlans.includes(plan.id)" class="tree-children route-children"> |
||||
|
<div |
||||
|
v-for="route in planRoutes(plan.id)" |
||||
|
:key="route.id" |
||||
|
class="tree-item route-item" |
||||
|
:class="{ selected: selectedIds.includes(route.id) }" |
||||
|
@click.stop="toggleRouteSelect(route.id)" |
||||
|
> |
||||
|
<el-checkbox |
||||
|
:value="selectedIds.includes(route.id)" |
||||
|
@change="(v) => setRouteSelected(route.id, v)" |
||||
|
@click.native.stop |
||||
|
> |
||||
|
<span class="route-name">{{ route.name }}</span> |
||||
|
<span class="route-meta">{{ route.points || (route.waypoints && route.waypoints.length) || 0 }} 个航点</span> |
||||
|
</el-checkbox> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div slot="footer" class="dialog-footer"> |
||||
|
<el-button @click="visible = false">取 消</el-button> |
||||
|
<el-button type="primary" :disabled="selectedIds.length === 0" @click="handleExport"> |
||||
|
导出 {{ selectedIds.length > 0 ? `(${selectedIds.length} 条)` : '' }} |
||||
|
</el-button> |
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: 'ExportRoutesDialog', |
||||
|
props: { |
||||
|
value: { |
||||
|
type: Boolean, |
||||
|
default: false |
||||
|
}, |
||||
|
routes: { |
||||
|
type: Array, |
||||
|
default: () => [] |
||||
|
}, |
||||
|
plans: { |
||||
|
type: Array, |
||||
|
default: () => [] |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
selectedIds: [], |
||||
|
expandedPlans: [] |
||||
|
}; |
||||
|
}, |
||||
|
computed: { |
||||
|
visible: { |
||||
|
get() { |
||||
|
return this.value; |
||||
|
}, |
||||
|
set(v) { |
||||
|
this.$emit('input', v); |
||||
|
} |
||||
|
}, |
||||
|
/** 有航线的方案列表(用于展示) */ |
||||
|
plansWithRoutes() { |
||||
|
return this.plans.filter(p => this.planRoutes(p.id).length > 0); |
||||
|
} |
||||
|
}, |
||||
|
watch: { |
||||
|
value(v) { |
||||
|
if (v) { |
||||
|
this.selectedIds = this.routes.map(r => r.id); |
||||
|
this.expandedPlans = this.plansWithRoutes.map(p => p.id); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
planRoutes(planId) { |
||||
|
return this.routes.filter(r => r.scenarioId === planId); |
||||
|
}, |
||||
|
togglePlan(planId) { |
||||
|
const idx = this.expandedPlans.indexOf(planId); |
||||
|
if (idx >= 0) { |
||||
|
this.expandedPlans.splice(idx, 1); |
||||
|
} else { |
||||
|
this.expandedPlans.push(planId); |
||||
|
} |
||||
|
}, |
||||
|
toggleRouteSelect(routeId) { |
||||
|
const idx = this.selectedIds.indexOf(routeId); |
||||
|
if (idx >= 0) { |
||||
|
this.selectedIds.splice(idx, 1); |
||||
|
} else { |
||||
|
this.selectedIds.push(routeId); |
||||
|
} |
||||
|
}, |
||||
|
setRouteSelected(routeId, selected) { |
||||
|
if (selected) { |
||||
|
if (!this.selectedIds.includes(routeId)) this.selectedIds.push(routeId); |
||||
|
} else { |
||||
|
this.selectedIds = this.selectedIds.filter(id => id !== routeId); |
||||
|
} |
||||
|
}, |
||||
|
selectAll() { |
||||
|
this.selectedIds = this.routes.map(r => r.id); |
||||
|
}, |
||||
|
selectNone() { |
||||
|
this.selectedIds = []; |
||||
|
}, |
||||
|
handleExport() { |
||||
|
this.$emit('export', this.selectedIds); |
||||
|
}, |
||||
|
handleClose() { |
||||
|
this.selectedIds = []; |
||||
|
this.expandedPlans = []; |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.export-routes-dialog .empty-tip { |
||||
|
text-align: center; |
||||
|
padding: 32px 0; |
||||
|
color: #909399; |
||||
|
} |
||||
|
.export-routes-dialog .empty-tip i { |
||||
|
font-size: 48px; |
||||
|
margin-bottom: 12px; |
||||
|
display: block; |
||||
|
} |
||||
|
.export-routes-dialog .select-actions { |
||||
|
margin-bottom: 12px; |
||||
|
} |
||||
|
.export-routes-dialog .tree-list { |
||||
|
max-height: 360px; |
||||
|
overflow-y: auto; |
||||
|
border: 1px solid #ebeef5; |
||||
|
border-radius: 4px; |
||||
|
padding: 8px; |
||||
|
} |
||||
|
.export-routes-dialog .tree-item { |
||||
|
user-select: none; |
||||
|
} |
||||
|
.export-routes-dialog .tree-item-header { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
padding: 8px 12px; |
||||
|
border-radius: 4px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
.export-routes-dialog .tree-item-header:hover { |
||||
|
background: #f5f7fa; |
||||
|
} |
||||
|
.export-routes-dialog .plan-item .tree-item-header { |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
.export-routes-dialog .tree-icon { |
||||
|
margin-right: 8px; |
||||
|
color: #909399; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
.export-routes-dialog .tree-item-info { |
||||
|
flex: 1; |
||||
|
min-width: 0; |
||||
|
} |
||||
|
.export-routes-dialog .tree-item-name { |
||||
|
font-size: 14px; |
||||
|
color: #303133; |
||||
|
} |
||||
|
.export-routes-dialog .tree-item-meta { |
||||
|
font-size: 12px; |
||||
|
color: #909399; |
||||
|
margin-top: 2px; |
||||
|
} |
||||
|
.export-routes-dialog .tree-children { |
||||
|
padding-left: 24px; |
||||
|
} |
||||
|
.export-routes-dialog .route-children { |
||||
|
margin-bottom: 4px; |
||||
|
} |
||||
|
.export-routes-dialog .route-item { |
||||
|
padding: 6px 12px; |
||||
|
border-radius: 4px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
.export-routes-dialog .route-item:hover { |
||||
|
background: #f5f7fa; |
||||
|
} |
||||
|
.export-routes-dialog .route-item .route-name { |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
.export-routes-dialog .route-item .route-meta { |
||||
|
margin-left: 8px; |
||||
|
font-size: 12px; |
||||
|
color: #909399; |
||||
|
} |
||||
|
.export-routes-dialog >>> .route-item .el-checkbox { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
width: 100%; |
||||
|
} |
||||
|
.export-routes-dialog >>> .route-item .el-checkbox__label { |
||||
|
flex: 1; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,240 @@ |
|||||
|
<template> |
||||
|
<el-dialog |
||||
|
title="导入航线" |
||||
|
:visible.sync="visible" |
||||
|
width="520px" |
||||
|
append-to-body |
||||
|
class="import-routes-dialog" |
||||
|
:modal-append-to-body="true" |
||||
|
@close="handleClose" |
||||
|
> |
||||
|
<div v-if="!parsedData" class="empty-tip"> |
||||
|
<i class="el-icon-upload"></i> |
||||
|
<p>请选择要导入的航线 JSON 文件</p> |
||||
|
<el-button type="primary" size="small" @click="triggerFileInput">选择文件</el-button> |
||||
|
<input |
||||
|
ref="fileInput" |
||||
|
type="file" |
||||
|
accept=".json" |
||||
|
style="display:none" |
||||
|
@change="onFileChange" |
||||
|
/> |
||||
|
</div> |
||||
|
<div v-else> |
||||
|
<div class="import-preview"> |
||||
|
<div class="preview-header"> |
||||
|
<i class="el-icon-document"></i> |
||||
|
<span>共 {{ routeItems.length }} 条航线待导入</span> |
||||
|
</div> |
||||
|
<div class="route-preview-list"> |
||||
|
<div v-for="(item, idx) in routeItems" :key="idx" class="route-preview-item"> |
||||
|
<span class="route-name">{{ item.callSign || item.name || `航线${idx + 1}` }}</span> |
||||
|
<span class="route-meta">{{ (item.waypoints || []).length }} 个航点</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<el-form label-width="100px" size="small" class="import-form"> |
||||
|
<el-form-item label="目标方案" required> |
||||
|
<el-select v-model="targetScenarioId" placeholder="请选择方案" style="width:100%" clearable> |
||||
|
<el-option v-for="p in plans" :key="p.id" :label="p.name" :value="p.id" /> |
||||
|
</el-select> |
||||
|
<div v-if="plans.length === 0" class="el-form-item__error" style="margin-top:4px;">暂无方案,请先创建方案后再导入。</div> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="默认平台"> |
||||
|
<el-select v-model="targetPlatformId" placeholder="导入时使用的平台(可后续在编辑中修改)" style="width:100%" clearable> |
||||
|
<el-option |
||||
|
v-for="p in allPlatforms" |
||||
|
:key="p.id" |
||||
|
:label="p.name" |
||||
|
:value="p.id" |
||||
|
/> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
</div> |
||||
|
<div slot="footer" class="dialog-footer"> |
||||
|
<el-button v-if="parsedData" @click="resetFile">重新选择</el-button> |
||||
|
<el-button @click="visible = false">取 消</el-button> |
||||
|
<el-button |
||||
|
type="primary" |
||||
|
:disabled="!canImport" |
||||
|
:loading="importing" |
||||
|
@click="handleImport" |
||||
|
> |
||||
|
导入 |
||||
|
</el-button> |
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: 'ImportRoutesDialog', |
||||
|
props: { |
||||
|
value: { |
||||
|
type: Boolean, |
||||
|
default: false |
||||
|
}, |
||||
|
plans: { |
||||
|
type: Array, |
||||
|
default: () => [] |
||||
|
}, |
||||
|
allPlatforms: { |
||||
|
type: Array, |
||||
|
default: () => [] |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
parsedData: null, |
||||
|
targetScenarioId: null, |
||||
|
targetPlatformId: null, |
||||
|
importing: false |
||||
|
}; |
||||
|
}, |
||||
|
computed: { |
||||
|
visible: { |
||||
|
get() { |
||||
|
return this.value; |
||||
|
}, |
||||
|
set(v) { |
||||
|
this.$emit('input', v); |
||||
|
} |
||||
|
}, |
||||
|
routeItems() { |
||||
|
if (!this.parsedData) return []; |
||||
|
const d = this.parsedData; |
||||
|
if (Array.isArray(d.routes)) return d.routes; |
||||
|
if (d.route && d.waypoints) return [{ ...d.route, waypoints: d.waypoints }]; |
||||
|
return []; |
||||
|
}, |
||||
|
canImport() { |
||||
|
return this.parsedData && this.targetScenarioId && this.routeItems.length > 0; |
||||
|
} |
||||
|
}, |
||||
|
watch: { |
||||
|
value(v) { |
||||
|
if (v && !this.parsedData) { |
||||
|
this.targetScenarioId = this.plans[0] && this.plans[0].id; |
||||
|
this.targetPlatformId = this.allPlatforms[0] && this.allPlatforms[0].id; |
||||
|
} |
||||
|
}, |
||||
|
plans: { |
||||
|
immediate: true, |
||||
|
handler(plans) { |
||||
|
if (plans.length > 0 && !this.targetScenarioId) { |
||||
|
this.targetScenarioId = plans[0].id; |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
allPlatforms: { |
||||
|
immediate: true, |
||||
|
handler(platforms) { |
||||
|
if (platforms.length > 0 && !this.targetPlatformId) { |
||||
|
this.targetPlatformId = platforms[0].id; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
triggerFileInput() { |
||||
|
this.$refs.fileInput && this.$refs.fileInput.click(); |
||||
|
}, |
||||
|
onFileChange(e) { |
||||
|
const file = e.target.files && e.target.files[0]; |
||||
|
if (!file) return; |
||||
|
const reader = new FileReader(); |
||||
|
reader.onload = (ev) => { |
||||
|
try { |
||||
|
const text = ev.target.result; |
||||
|
const data = JSON.parse(text); |
||||
|
if (!data.routes && !(data.route && data.waypoints)) { |
||||
|
this.$message.error('文件格式不正确,缺少 routes 或 route/waypoints 数据'); |
||||
|
return; |
||||
|
} |
||||
|
this.parsedData = data; |
||||
|
if (this.plans.length > 0 && !this.targetScenarioId) { |
||||
|
this.targetScenarioId = this.plans[0].id; |
||||
|
} |
||||
|
if (this.allPlatforms.length > 0 && !this.targetPlatformId) { |
||||
|
this.targetPlatformId = this.allPlatforms[0].id; |
||||
|
} |
||||
|
} catch (err) { |
||||
|
this.$message.error('JSON 解析失败:' + (err.message || '格式错误')); |
||||
|
} |
||||
|
e.target.value = ''; |
||||
|
}; |
||||
|
reader.readAsText(file, 'UTF-8'); |
||||
|
}, |
||||
|
resetFile() { |
||||
|
this.parsedData = null; |
||||
|
this.triggerFileInput(); |
||||
|
}, |
||||
|
handleImport() { |
||||
|
if (!this.canImport) return; |
||||
|
this.$emit('import', { |
||||
|
routeItems: this.routeItems, |
||||
|
targetScenarioId: this.targetScenarioId, |
||||
|
targetPlatformId: this.targetPlatformId |
||||
|
}); |
||||
|
}, |
||||
|
handleClose() { |
||||
|
this.parsedData = null; |
||||
|
this.targetScenarioId = null; |
||||
|
this.targetPlatformId = null; |
||||
|
}, |
||||
|
setImporting(v) { |
||||
|
this.importing = v; |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.import-routes-dialog >>> .el-dialog__body { |
||||
|
min-height: 180px; |
||||
|
} |
||||
|
.import-routes-dialog .empty-tip { |
||||
|
text-align: center; |
||||
|
padding: 32px 0; |
||||
|
color: #606266; |
||||
|
min-height: 120px; |
||||
|
} |
||||
|
.import-routes-dialog .empty-tip i { |
||||
|
font-size: 48px; |
||||
|
margin-bottom: 12px; |
||||
|
display: block; |
||||
|
} |
||||
|
.import-routes-dialog .empty-tip p { |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
.import-routes-dialog .import-preview { |
||||
|
margin-bottom: 16px; |
||||
|
border: 1px solid #ebeef5; |
||||
|
border-radius: 4px; |
||||
|
padding: 12px; |
||||
|
} |
||||
|
.import-routes-dialog .preview-header { |
||||
|
margin-bottom: 8px; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
.import-routes-dialog .preview-header i { |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
.import-routes-dialog .route-preview-list { |
||||
|
max-height: 160px; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
.import-routes-dialog .route-preview-item { |
||||
|
padding: 6px 0; |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
} |
||||
|
.import-routes-dialog .route-preview-item .route-name { |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
.import-routes-dialog .route-preview-item .route-meta { |
||||
|
font-size: 12px; |
||||
|
color: #909399; |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue