2026-04-03 02:05:29 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="statistics">
|
|
|
|
|
|
<div class="banner">
|
|
|
|
|
|
<div class="banner-select-text banner-static-title">{{ currentTitle }}</div>
|
|
|
|
|
|
<div class="numbers">
|
|
|
|
|
|
<div v-for="(item, index) in numbers" :key="index" class="item">
|
|
|
|
|
|
<div class="value">{{ item.value }}<span class="unit">{{ item.unit }}</span></div>
|
|
|
|
|
|
<div class="label">{{ item.label }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="unit-toolbar">
|
|
|
|
|
|
<div class="unit-toolbar-inner">
|
|
|
|
|
|
<div class="unit-filter-box">
|
|
|
|
|
|
<span class="banner-unit-label">统计单位</span>
|
2026-04-03 11:10:58 +08:00
|
|
|
|
<el-select v-model="selectedUnits" class="banner-unit-select" multiple filterable clearable size="small"
|
|
|
|
|
|
placeholder="请选择单位(不选表示全部)" value-key="unit_name" collapse-tags @change="onBannerUnitsChange">
|
|
|
|
|
|
<el-option v-for="unit in unitOptions" :key="unit.unit_id || unit.unit_name" :label="unit.unit_name"
|
|
|
|
|
|
:value="unit" />
|
2026-04-03 02:05:29 +08:00
|
|
|
|
</el-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-03 04:09:35 +08:00
|
|
|
|
<!-- 地图板块(与科考船汇总统计页 ships 一致:Mapbox + shipUnit + shipVoyage) -->
|
2026-04-03 02:05:29 +08:00
|
|
|
|
<div class="map-module">
|
|
|
|
|
|
<div class="bezel">
|
|
|
|
|
|
<div class="map-title">参航单位</div>
|
2026-04-03 04:09:35 +08:00
|
|
|
|
<div id="workUnitMap_uuv" ref="workUnitMap_uuv" style="height: 100%;width:100%;" />
|
2026-04-03 02:05:29 +08:00
|
|
|
|
</div>
|
2026-04-03 12:03:56 +08:00
|
|
|
|
<!-- <div class="bezel">
|
2026-04-03 02:05:29 +08:00
|
|
|
|
<div class="map-title">航次地图</div>
|
|
|
|
|
|
<div class="map-year-panel">
|
|
|
|
|
|
<span class="map-year-label">年份</span>
|
2026-04-03 11:10:58 +08:00
|
|
|
|
<el-date-picker v-model="voyageYear" type="year" size="small" placeholder="全部" class="map-year" clearable
|
|
|
|
|
|
format="yyyy" @change="selectVoyageYear" />
|
2026-04-03 02:05:29 +08:00
|
|
|
|
</div>
|
2026-04-03 04:09:35 +08:00
|
|
|
|
<div id="voyageMap_uuv" ref="voyageMap_uuv" style="height: 100%;width:100%;" />
|
2026-04-03 12:03:56 +08:00
|
|
|
|
</div> -->
|
2026-04-03 02:05:29 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<!-- echarts板块-->
|
|
|
|
|
|
<div class="echarts-module">
|
|
|
|
|
|
<div class="bezel">
|
2026-04-03 11:10:58 +08:00
|
|
|
|
<BarChart :title="barVoyageCount.title" :chart-data="barVoyageCount.salesData"
|
|
|
|
|
|
:categories="barVoyageCount.productCategories" chart-type="single" />
|
2026-04-03 02:05:29 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="bezel">
|
2026-04-03 11:10:58 +08:00
|
|
|
|
<BarChart :title="barVoyageDays.title" :chart-data="barVoyageDays.salesData"
|
|
|
|
|
|
:categories="barVoyageDays.productCategories" chart-type="single" />
|
2026-04-03 02:05:29 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="bezel">
|
2026-04-03 11:10:58 +08:00
|
|
|
|
<BarChart :title="barPeopleDiving.title" :chart-data="barPeopleDiving.salesData"
|
|
|
|
|
|
:categories="barPeopleDiving.productCategories" chart-type="multiple" />
|
2026-04-03 02:05:29 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="bezel">
|
2026-04-03 11:10:58 +08:00
|
|
|
|
<BarChart :title="barFirstPeopleDiving.title" :chart-data="barFirstPeopleDiving.salesData"
|
|
|
|
|
|
:categories="barFirstPeopleDiving.productCategories" chart-type="multiple" />
|
2026-04-03 02:05:29 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="bezel">
|
2026-04-03 11:10:58 +08:00
|
|
|
|
<BarChart :title="mixedMileage.title" :chart-data="mixedMileage.series" :categories="mixedMileage.categories"
|
|
|
|
|
|
chart-type="mixed" :dual-y-axis="true" left-axis-name="次数" right-axis-name="天数" />
|
2026-04-03 02:05:29 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2026-04-03 04:09:35 +08:00
|
|
|
|
import Vue from 'vue'
|
2026-04-03 02:05:29 +08:00
|
|
|
|
import BarChart from './components/BarChart.vue'
|
2026-04-03 04:09:35 +08:00
|
|
|
|
import { unitTotal, hovUnit, shipUnit, shipVoyage } from '../../api/statistics'
|
2026-04-03 02:05:29 +08:00
|
|
|
|
import { getYearRange } from '../../utils/index'
|
2026-04-03 04:09:35 +08:00
|
|
|
|
import { initMapbox, handleWheel } from '@/utils/mapbox-utils'
|
|
|
|
|
|
import { loadVectorLayer, loadJsonPointFeature } from '@/utils/vector-layer-utils'
|
|
|
|
|
|
import { ColorGenerator } from '@/utils/color-generator'
|
2026-04-03 02:05:29 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @param {*} v 原始值
|
|
|
|
|
|
* @returns {number}
|
|
|
|
|
|
*/
|
|
|
|
|
|
function num(v) {
|
|
|
|
|
|
if (v == null || v === '') return 0
|
|
|
|
|
|
const n = Number(v)
|
|
|
|
|
|
return Number.isFinite(n) ? n : 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
components: {
|
|
|
|
|
|
BarChart
|
|
|
|
|
|
},
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
currentTitle: '参航单位统计',
|
|
|
|
|
|
voyageYear: '',
|
|
|
|
|
|
unitOptions: [],
|
2026-04-03 04:09:35 +08:00
|
|
|
|
/** 与 Banner 多选绑定:筛选单位汇总图表(空表示全部) */
|
2026-04-03 02:05:29 +08:00
|
|
|
|
selectedUnits: [],
|
|
|
|
|
|
/** unitTotal 全量缓存,便于按 Banner 选择做前端过滤 */
|
|
|
|
|
|
unitTotalRawList: [],
|
|
|
|
|
|
numbers: [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '参航单位数量',
|
|
|
|
|
|
value: '',
|
|
|
|
|
|
unit: '家'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '参航人数',
|
|
|
|
|
|
value: '',
|
|
|
|
|
|
unit: '人'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '深潜次数',
|
|
|
|
|
|
value: '',
|
|
|
|
|
|
unit: '次'
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
/** 航行次数(柱状) */
|
|
|
|
|
|
barVoyageCount: { title: '航行次数', salesData: [], productCategories: [] },
|
|
|
|
|
|
/** 参航天数(柱状) */
|
|
|
|
|
|
barVoyageDays: { title: '参航天数', salesData: [], productCategories: [] },
|
|
|
|
|
|
/** 参航人员数、深潜次数(柱状,多系列) */
|
|
|
|
|
|
barPeopleDiving: {
|
|
|
|
|
|
title: '参航人员数、深潜次数',
|
|
|
|
|
|
salesData: [
|
|
|
|
|
|
{ name: '参航人员数', type: 'bar', data: [] },
|
|
|
|
|
|
{ name: '深潜次数', type: 'bar', data: [] }
|
|
|
|
|
|
],
|
|
|
|
|
|
productCategories: []
|
|
|
|
|
|
},
|
|
|
|
|
|
/** 首次参航、首次深潜(柱状,多系列) */
|
|
|
|
|
|
barFirstPeopleDiving: {
|
|
|
|
|
|
title: '首次参航人数、首次深潜人数',
|
|
|
|
|
|
salesData: [
|
|
|
|
|
|
{ name: '首次参航人数', type: 'bar', data: [] },
|
|
|
|
|
|
{ name: '首次深潜人数', type: 'bar', data: [] }
|
|
|
|
|
|
],
|
|
|
|
|
|
productCategories: []
|
|
|
|
|
|
},
|
|
|
|
|
|
/** 航行次数(柱)+ 参航天数(折) */
|
|
|
|
|
|
mixedMileage: {
|
|
|
|
|
|
title: '航行次数与参航天数',
|
|
|
|
|
|
categories: [],
|
|
|
|
|
|
series: [
|
|
|
|
|
|
{ name: '航行次数', type: 'bar', data: [], yAxisIndex: 0 },
|
|
|
|
|
|
{ name: '参航天数', type: 'line', data: [], yAxisIndex: 1 }
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
created() {
|
|
|
|
|
|
this.loadUnitTotalFromApi()
|
|
|
|
|
|
this.getHovUnit()
|
2026-04-03 04:09:35 +08:00
|
|
|
|
this.getShipUnit()
|
|
|
|
|
|
this.gitShipVoyage()
|
2026-04-03 02:05:29 +08:00
|
|
|
|
},
|
|
|
|
|
|
mounted() {
|
2026-04-03 04:09:35 +08:00
|
|
|
|
initMapbox('workUnitMap_uuv', {
|
|
|
|
|
|
tileKey: 'gaode',
|
|
|
|
|
|
zoom: 3,
|
2026-04-03 12:03:56 +08:00
|
|
|
|
center: [110, 33],
|
2026-04-03 04:09:35 +08:00
|
|
|
|
minZoom: 2,
|
|
|
|
|
|
maxZoom: 29
|
|
|
|
|
|
})
|
2026-04-03 12:03:56 +08:00
|
|
|
|
// initMapbox('voyageMap_uuv', {
|
|
|
|
|
|
// tileKey: 'GEBCO_basemap_NCEI',
|
|
|
|
|
|
// zoom: 3,
|
|
|
|
|
|
// center: [113, 15],
|
|
|
|
|
|
// minZoom: 2,
|
|
|
|
|
|
// maxZoom: 29
|
|
|
|
|
|
// })
|
2026-04-03 02:05:29 +08:00
|
|
|
|
},
|
|
|
|
|
|
beforeDestroy() {
|
2026-04-03 04:09:35 +08:00
|
|
|
|
const unitMap = Vue.config.maps && Vue.config.maps['workUnitMap_uuv']
|
|
|
|
|
|
if (unitMap) {
|
|
|
|
|
|
unitMap.remove()
|
|
|
|
|
|
delete Vue.config.maps['workUnitMap_uuv']
|
2026-04-03 02:05:29 +08:00
|
|
|
|
}
|
2026-04-03 04:09:35 +08:00
|
|
|
|
const voyageMap = Vue.config.maps && Vue.config.maps['voyageMap_uuv']
|
|
|
|
|
|
if (voyageMap) {
|
|
|
|
|
|
voyageMap.remove()
|
|
|
|
|
|
delete Vue.config.maps['voyageMap_uuv']
|
2026-04-03 02:05:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
/**
|
2026-04-03 04:09:35 +08:00
|
|
|
|
* Banner 统计单位多选变更:刷新汇总图表。
|
2026-04-03 02:05:29 +08:00
|
|
|
|
* @returns {void}
|
|
|
|
|
|
*/
|
|
|
|
|
|
onBannerUnitsChange() {
|
|
|
|
|
|
this.applyFilteredUnitTotal()
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 按 Banner 选中单位过滤 unitTotal 行,再写入 Banner 与图表。
|
|
|
|
|
|
* @returns {void}
|
|
|
|
|
|
*/
|
|
|
|
|
|
applyFilteredUnitTotal() {
|
|
|
|
|
|
const raw = this.unitTotalRawList || []
|
|
|
|
|
|
if (!raw.length) {
|
|
|
|
|
|
this.applyUnitTotalRows([])
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const sel = this.selectedUnits || []
|
|
|
|
|
|
if (!sel.length) {
|
|
|
|
|
|
this.applyUnitTotalRows(raw)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const nameSet = new Set(sel.map(u => u && u.unit_name).filter(Boolean))
|
|
|
|
|
|
const filtered = raw.filter(row => nameSet.has(row.unit_name))
|
|
|
|
|
|
this.applyUnitTotalRows(filtered)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 拉取「单位汇总」报表(/report/unit/total.htm),驱动 Banner 与各柱状图。
|
|
|
|
|
|
* @returns {void}
|
|
|
|
|
|
*/
|
|
|
|
|
|
loadUnitTotalFromApi() {
|
|
|
|
|
|
unitTotal({})
|
|
|
|
|
|
.then(res => {
|
|
|
|
|
|
const list = res && Array.isArray(res.array) ? res.array : []
|
|
|
|
|
|
this.unitTotalRawList = list
|
|
|
|
|
|
this.applyFilteredUnitTotal()
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
this.unitTotalRawList = []
|
|
|
|
|
|
this.applyUnitTotalRows([])
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将 unitTotal 返回的按单位行写入 Banner 与图表。
|
|
|
|
|
|
* @param {Object[]} list 接口 array
|
|
|
|
|
|
* @returns {void}
|
|
|
|
|
|
*/
|
|
|
|
|
|
applyUnitTotalRows(list) {
|
|
|
|
|
|
if (!list.length) {
|
|
|
|
|
|
this.numbers[0].value = ''
|
|
|
|
|
|
this.numbers[1].value = ''
|
|
|
|
|
|
this.numbers[2].value = ''
|
|
|
|
|
|
this.barVoyageCount = { title: '航行次数', salesData: [], productCategories: [] }
|
|
|
|
|
|
this.barVoyageDays = { title: '参航天数', salesData: [], productCategories: [] }
|
|
|
|
|
|
this.barPeopleDiving = {
|
|
|
|
|
|
title: '参航人员数、深潜次数',
|
|
|
|
|
|
salesData: [
|
|
|
|
|
|
{ name: '参航人员数', type: 'bar', data: [] },
|
|
|
|
|
|
{ name: '深潜次数', type: 'bar', data: [] }
|
|
|
|
|
|
],
|
|
|
|
|
|
productCategories: []
|
|
|
|
|
|
}
|
|
|
|
|
|
this.barFirstPeopleDiving = {
|
|
|
|
|
|
title: '首次参航人数、首次深潜人数',
|
|
|
|
|
|
salesData: [
|
|
|
|
|
|
{ name: '首次参航人数', type: 'bar', data: [] },
|
|
|
|
|
|
{ name: '首次深潜人数', type: 'bar', data: [] }
|
|
|
|
|
|
],
|
|
|
|
|
|
productCategories: []
|
|
|
|
|
|
}
|
|
|
|
|
|
this.mixedMileage = {
|
|
|
|
|
|
title: '航行次数与参航天数',
|
|
|
|
|
|
categories: [],
|
|
|
|
|
|
series: [
|
|
|
|
|
|
{ name: '航行次数', type: 'bar', data: [], yAxisIndex: 0 },
|
|
|
|
|
|
{ name: '参航天数', type: 'line', data: [], yAxisIndex: 1 }
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sorted = [...list].sort((a, b) => num(b.voyage_total) - num(a.voyage_total))
|
|
|
|
|
|
const names = sorted.map(r => r.unit_name || '—')
|
|
|
|
|
|
const voyage = sorted.map(r => num(r.voyage_total))
|
|
|
|
|
|
const days = sorted.map(r => num(r.voyage_days))
|
|
|
|
|
|
const member = sorted.map(r => num(r.member_total))
|
|
|
|
|
|
const drving = sorted.map(r => num(r.drving_total))
|
|
|
|
|
|
const fVoyage = sorted.map(r => num(r.f_voyage_total))
|
|
|
|
|
|
const fDrving = sorted.map(r => num(r.f_drving_total))
|
|
|
|
|
|
|
|
|
|
|
|
this.numbers[0].value = sorted.length
|
|
|
|
|
|
this.numbers[1].value = sorted.reduce((s, r) => s + num(r.member_total), 0)
|
|
|
|
|
|
this.numbers[2].value = sorted.reduce((s, r) => s + num(r.drving_total), 0)
|
|
|
|
|
|
|
|
|
|
|
|
this.barVoyageCount = {
|
|
|
|
|
|
title: '航行次数',
|
|
|
|
|
|
salesData: voyage,
|
|
|
|
|
|
productCategories: names
|
|
|
|
|
|
}
|
|
|
|
|
|
this.barVoyageDays = {
|
|
|
|
|
|
title: '参航天数',
|
|
|
|
|
|
salesData: days,
|
|
|
|
|
|
productCategories: names
|
|
|
|
|
|
}
|
|
|
|
|
|
this.barPeopleDiving = {
|
|
|
|
|
|
title: '参航人员数、深潜次数',
|
|
|
|
|
|
salesData: [
|
|
|
|
|
|
{ name: '参航人员数', type: 'bar', data: member },
|
|
|
|
|
|
{ name: '深潜次数', type: 'bar', data: drving }
|
|
|
|
|
|
],
|
|
|
|
|
|
productCategories: names
|
|
|
|
|
|
}
|
|
|
|
|
|
this.barFirstPeopleDiving = {
|
|
|
|
|
|
title: '首次参航人数、首次深潜人数',
|
|
|
|
|
|
salesData: [
|
|
|
|
|
|
{ name: '首次参航人数', type: 'bar', data: fVoyage },
|
|
|
|
|
|
{ name: '首次深潜人数', type: 'bar', data: fDrving }
|
|
|
|
|
|
],
|
|
|
|
|
|
productCategories: names
|
|
|
|
|
|
}
|
|
|
|
|
|
this.mixedMileage = {
|
|
|
|
|
|
title: '航行次数与参航天数',
|
|
|
|
|
|
categories: names,
|
|
|
|
|
|
series: [
|
|
|
|
|
|
{ name: '航行次数', type: 'bar', data: voyage, yAxisIndex: 0 },
|
|
|
|
|
|
{ name: '参航天数', type: 'line', data: days, yAxisIndex: 1 }
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-04-03 04:09:35 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 载人潜水器参航单位列表:仅用于 Banner 多选筛选图表。
|
|
|
|
|
|
* @returns {void}
|
|
|
|
|
|
*/
|
2026-04-03 02:05:29 +08:00
|
|
|
|
getHovUnit() {
|
|
|
|
|
|
hovUnit().then(res => {
|
|
|
|
|
|
const map = new Map()
|
2026-04-03 11:10:58 +08:00
|
|
|
|
; (res.array || []).forEach(unit => {
|
|
|
|
|
|
const k = unit.unit_id || unit.unitId || unit.unit_name
|
|
|
|
|
|
if (!map.has(k)) map.set(k, unit)
|
|
|
|
|
|
})
|
2026-04-03 02:05:29 +08:00
|
|
|
|
this.unitOptions = Array.from(map.values())
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-04-03 04:09:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 科考船侧参航单位点位(与 ships 页地图同源接口),通过 Mapbox GeoJSON 渲染。
|
|
|
|
|
|
* @returns {void}
|
|
|
|
|
|
*/
|
|
|
|
|
|
getShipUnit() {
|
|
|
|
|
|
shipUnit().then(res => {
|
|
|
|
|
|
const geojson = {
|
|
|
|
|
|
type: 'FeatureCollection',
|
|
|
|
|
|
features: []
|
|
|
|
|
|
}
|
2026-04-03 11:10:58 +08:00
|
|
|
|
; (res.array || []).forEach(element => {
|
|
|
|
|
|
geojson.features.push({
|
|
|
|
|
|
type: 'Feature',
|
|
|
|
|
|
geometry: {
|
|
|
|
|
|
type: 'Point',
|
|
|
|
|
|
coordinates: [element.unit_lon, element.unit_lat]
|
|
|
|
|
|
},
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
unit_name: element.unit_name,
|
|
|
|
|
|
unit_type: element.unit_type,
|
|
|
|
|
|
create_time: element.create_time,
|
|
|
|
|
|
tsy_id: element.tsy_id
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-04-03 04:09:35 +08:00
|
|
|
|
})
|
|
|
|
|
|
loadJsonPointFeature('workUnit', 'blue', geojson, 'unit_name', 'workUnitMap_uuv')
|
2026-04-03 02:05:29 +08:00
|
|
|
|
})
|
|
|
|
|
|
},
|
2026-04-03 04:09:35 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 科考船航次轨迹(与 ships 页地图同源接口),通过 Mapbox 矢量图层渲染。
|
|
|
|
|
|
* @param {Object} [params] 查询参数
|
|
|
|
|
|
* @returns {void}
|
|
|
|
|
|
*/
|
|
|
|
|
|
gitShipVoyage(params = {}) {
|
|
|
|
|
|
shipVoyage(params).then(res => {
|
|
|
|
|
|
let i = 0
|
|
|
|
|
|
const colors = ColorGenerator.generateDivergingColors((res.array || []).length)
|
2026-04-03 11:10:58 +08:00
|
|
|
|
; (res.array || []).forEach(item => {
|
|
|
|
|
|
loadVectorLayer(item.voyage_name, ['line', 'symbol'], [colors[i], '#FFFFFF'], 'visible', 'voyageMap_uuv', 'dsds')
|
|
|
|
|
|
i++
|
|
|
|
|
|
})
|
2026-04-03 04:09:35 +08:00
|
|
|
|
})
|
2026-04-03 02:05:29 +08:00
|
|
|
|
},
|
2026-04-03 04:09:35 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 选择航次地图年份。
|
|
|
|
|
|
* @param {*} val 年份值
|
|
|
|
|
|
* @returns {void}
|
|
|
|
|
|
*/
|
2026-04-03 02:05:29 +08:00
|
|
|
|
selectVoyageYear(val) {
|
|
|
|
|
|
if (val) {
|
|
|
|
|
|
const params = getYearRange(val)
|
2026-04-03 04:09:35 +08:00
|
|
|
|
this.gitShipVoyage({ ...params })
|
2026-04-03 02:05:29 +08:00
|
|
|
|
} else {
|
2026-04-03 04:09:35 +08:00
|
|
|
|
this.gitShipVoyage({})
|
2026-04-03 02:05:29 +08:00
|
|
|
|
}
|
2026-04-03 04:09:35 +08:00
|
|
|
|
}
|
2026-04-03 02:05:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.statistics {
|
|
|
|
|
|
background-color: #ebf1f7;
|
|
|
|
|
|
|
|
|
|
|
|
.banner {
|
2026-04-03 11:17:11 +08:00
|
|
|
|
background-color: #012458;
|
2026-04-03 02:05:29 +08:00
|
|
|
|
background-image: url(../../../static/images/bannerny1.jpg);
|
|
|
|
|
|
background-repeat: no-repeat;
|
2026-04-03 14:14:42 +08:00
|
|
|
|
// 超宽屏下 contain 会在左右留边;cover 铺满裁切边缘
|
2026-04-03 02:05:29 +08:00
|
|
|
|
background-position: center;
|
2026-04-03 14:14:42 +08:00
|
|
|
|
background-size: cover;
|
2026-04-03 02:05:29 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 480px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
|
|
|
|
|
|
.banner-topic {
|
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
margin-bottom: 18px;
|
|
|
|
|
|
text-shadow: 0 6px 22px rgba(0, 0, 0, 0.25);
|
|
|
|
|
|
letter-spacing: 0.02em;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.banner-type-dropdown {
|
|
|
|
|
|
margin-bottom: 40px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.banner-static-title {
|
|
|
|
|
|
margin-bottom: 40px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.banner-select-trigger {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
padding: 14px 24px 14px 28px;
|
|
|
|
|
|
border: 2px solid rgba(255, 255, 255, 0.88);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
background: rgba(8, 40, 72, 0.42);
|
|
|
|
|
|
backdrop-filter: blur(10px);
|
|
|
|
|
|
-webkit-backdrop-filter: blur(10px);
|
|
|
|
|
|
box-shadow:
|
|
|
|
|
|
0 4px 20px rgba(0, 0, 0, 0.28),
|
|
|
|
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.14);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition:
|
|
|
|
|
|
background 0.25s ease,
|
|
|
|
|
|
border-color 0.25s ease,
|
|
|
|
|
|
box-shadow 0.25s ease,
|
|
|
|
|
|
transform 0.25s ease;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: rgba(12, 55, 95, 0.58);
|
|
|
|
|
|
border-color: rgba(255, 255, 255, 0.98);
|
|
|
|
|
|
transform: translateY(-3px);
|
|
|
|
|
|
box-shadow:
|
|
|
|
|
|
0 12px 36px rgba(0, 0, 0, 0.38),
|
|
|
|
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.22);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:focus-visible {
|
|
|
|
|
|
outline: 3px solid rgba(255, 255, 255, 0.85);
|
|
|
|
|
|
outline-offset: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.banner-select-text {
|
|
|
|
|
|
font-size: 52px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
line-height: 1.15;
|
|
|
|
|
|
max-width: #{"min(90vw, 920px)"};
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.banner-select-chevron {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
opacity: 0.95;
|
|
|
|
|
|
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.35));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.numbers {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 80px;
|
|
|
|
|
|
|
|
|
|
|
|
.item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
2026-04-03 11:10:58 +08:00
|
|
|
|
|
2026-04-03 02:05:29 +08:00
|
|
|
|
.value {
|
|
|
|
|
|
font-size: 56px;
|
|
|
|
|
|
font-weight: bold;
|
2026-04-03 11:10:58 +08:00
|
|
|
|
|
2026-04-03 02:05:29 +08:00
|
|
|
|
.unit {
|
|
|
|
|
|
font-size: 26px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-03 11:10:58 +08:00
|
|
|
|
|
2026-04-03 02:05:29 +08:00
|
|
|
|
.label {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Banner 下方:统计单位筛选条,整行容器 + 右侧对齐独立框 */
|
|
|
|
|
|
.unit-toolbar {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 8px 50px 8px;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
background-color: #ebf1f7;
|
|
|
|
|
|
|
|
|
|
|
|
.unit-toolbar-inner {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.unit-filter-box {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 10px 18px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.92);
|
|
|
|
|
|
border: 1px solid rgba(0, 50, 90, 0.12);
|
|
|
|
|
|
box-shadow: 0 2px 10px rgba(0, 40, 80, 0.06);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.banner-unit-label {
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
|
color: rgba(28, 45, 63, 0.88);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.banner-unit-select {
|
|
|
|
|
|
width: #{"min(72vw, 520px)"};
|
|
|
|
|
|
min-width: 200px;
|
|
|
|
|
|
|
|
|
|
|
|
::v-deep .el-input__inner {
|
|
|
|
|
|
min-height: 34px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
border: 1px solid rgba(0, 50, 90, 0.14);
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #1c2d3f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
::v-deep .el-select__tags {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.map-module {
|
2026-04-03 04:09:35 +08:00
|
|
|
|
padding: 50px 50px;
|
2026-04-03 02:05:29 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 50px;
|
2026-04-03 11:10:58 +08:00
|
|
|
|
|
2026-04-03 02:05:29 +08:00
|
|
|
|
.bezel {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 520px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
border: 1px solid #ccc;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
|
|
.map-title {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 20px;
|
|
|
|
|
|
left: 20px;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
z-index: 8888;
|
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.map-year-panel {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 16px;
|
|
|
|
|
|
right: 16px;
|
|
|
|
|
|
z-index: 8888;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
padding: 6px 10px 6px 12px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.68);
|
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.55);
|
|
|
|
|
|
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.14);
|
|
|
|
|
|
backdrop-filter: blur(12px);
|
|
|
|
|
|
-webkit-backdrop-filter: blur(12px);
|
|
|
|
|
|
transition:
|
|
|
|
|
|
background 0.25s ease,
|
|
|
|
|
|
border-color 0.25s ease,
|
|
|
|
|
|
box-shadow 0.25s ease,
|
|
|
|
|
|
transform 0.25s ease;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.82);
|
|
|
|
|
|
border-color: rgba(255, 255, 255, 0.92);
|
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.map-year-label {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: rgba(28, 42, 58, 0.78);
|
|
|
|
|
|
letter-spacing: 0.04em;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.map-year {
|
|
|
|
|
|
width: 120px;
|
|
|
|
|
|
|
|
|
|
|
|
::v-deep .el-input__inner {
|
|
|
|
|
|
height: 34px;
|
|
|
|
|
|
line-height: 34px;
|
|
|
|
|
|
padding-left: 30px;
|
|
|
|
|
|
padding-right: 28px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
border: 1px solid rgba(0, 50, 90, 0.12);
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.55);
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #1c2d3f;
|
|
|
|
|
|
transition:
|
|
|
|
|
|
background 0.2s ease,
|
|
|
|
|
|
border-color 0.2s ease,
|
|
|
|
|
|
box-shadow 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
::v-deep .el-input__inner:hover,
|
|
|
|
|
|
::v-deep .el-input__inner:focus {
|
|
|
|
|
|
border-color: rgba(0, 90, 150, 0.42);
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.78);
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 50, 100, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
::v-deep .el-input__prefix {
|
|
|
|
|
|
left: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
::v-deep .el-input__suffix {
|
|
|
|
|
|
right: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.echarts-module {
|
|
|
|
|
|
padding: 0 50px 50px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
|
gap: 50px;
|
|
|
|
|
|
grid-auto-rows: 300px;
|
|
|
|
|
|
|
|
|
|
|
|
.bezel {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
border: 1px solid #ccc;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
|
|
.map-title {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 20px;
|
|
|
|
|
|
left: 20px;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
z-index: 8888;
|
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss">
|
|
|
|
|
|
.banner-type-dropdown-popper.el-dropdown-menu {
|
|
|
|
|
|
min-width: 240px;
|
|
|
|
|
|
max-height: #{"min(60vh, 420px)"};
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding: 6px 0;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.banner-type-dropdown-popper.el-dropdown-menu .el-dropdown-menu__item {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
line-height: 22px;
|
|
|
|
|
|
padding: 14px 22px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|