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