565 lines
15 KiB
Vue
565 lines
15 KiB
Vue
<template>
|
||
<div class="statistics">
|
||
<div class="banner">
|
||
<div class="banner-title-picker">
|
||
<el-dropdown class="banner-type-dropdown" trigger="click" @command="handleCommand">
|
||
<span class="banner-select-trigger">
|
||
<span class="banner-select-text">{{ currentTitle }}</span>
|
||
<svg class="banner-select-chevron" viewBox="0 0 24 24" width="40" height="40" aria-hidden="true"
|
||
focusable="false">
|
||
<path fill="#ffffff" d="M7 10l5 5 5-5H7z" />
|
||
</svg>
|
||
</span>
|
||
<el-dropdown-menu slot="dropdown" class="banner-type-dropdown-popper">
|
||
<el-dropdown-item command="全部科考船">全部科考船</el-dropdown-item>
|
||
<el-dropdown-item v-for="item in titles" :key="item.tsy_id" :command="item.report_name">
|
||
{{ item.report_name }}
|
||
</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</el-dropdown>
|
||
<p class="banner-title-picker__hint">点击选择</p>
|
||
</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="map-module">
|
||
<div class="bezel">
|
||
<div class="map-title">参航单位</div>
|
||
<div id="workUnitMap_ship" ref="workUnitMap_ship" style="height: 100%;width:100%;" />
|
||
</div>
|
||
<div class="bezel">
|
||
<div class="map-title">航次地图</div>
|
||
<div class="map-year-panel">
|
||
<span class="map-year-label">年份</span>
|
||
<el-date-picker v-model="voyageYear" type="year" size="small" placeholder="全部" class="map-year" clearable
|
||
format="yyyy" @change="selectVoyageYear" />
|
||
</div>
|
||
<div id="voyageMap_ship" ref="voyageMap_ship" style="height: 100%;width:100%;" />
|
||
</div>
|
||
</div>
|
||
<!-- echarts板块-->
|
||
<div class="echarts-module">
|
||
<div class="bezel">
|
||
<BarChart :title="barData1.title" :chart-data="barData1.salesData" :categories="barData1.productCategories"
|
||
chart-type="single" />
|
||
</div>
|
||
<div class="bezel">
|
||
<BarChart :title="barDataDays.title" :chart-data="barDataDays.salesData"
|
||
:categories="barDataDays.productCategories" chart-type="single" />
|
||
</div>
|
||
<div class="bezel">
|
||
<BarChart :title="barData2.title" :chart-data="barData2.salesData" :categories="barData2.productCategories"
|
||
chart-type="stacked" stack-name="新增单位数" />
|
||
</div>
|
||
<div class="bezel">
|
||
<BarChart :title="barData3.title" :chart-data="barData3.salesData" :categories="barData3.productCategories" />
|
||
</div>
|
||
<div class="bezel">
|
||
<BarChart :title="barData4.title" :chart-data="barData4.salesData" :categories="barData4.productCategories" />
|
||
</div>
|
||
<div class="bezel">
|
||
<PieChart :chart-data="pieData" title="参航单位类型" chart-type="ring" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import BarChart from './components/BarChart.vue'
|
||
import PieChart from './components/PieChart.vue'
|
||
import { reportList, shipTotal, shipUnit, shipUnitYear, shipVoyage } from '../../api/statistics'
|
||
import { getYearRange, filterDictItems } from '../../utils/index'
|
||
import { initMapbox } from '@/utils/mapbox-utils'
|
||
import { loadVectorLayer, loadJsonPointFeature } from '@/utils/vector-layer-utils'
|
||
import { ColorGenerator } from '@/utils/color-generator'
|
||
|
||
export default {
|
||
components: {
|
||
BarChart,
|
||
PieChart
|
||
},
|
||
data() {
|
||
return {
|
||
currentTitle: '全部科考船',
|
||
titles: [],
|
||
numbers: [
|
||
{
|
||
label: '航次',
|
||
value: '',
|
||
unit: '次'
|
||
},
|
||
{
|
||
label: '航次天数',
|
||
value: '',
|
||
unit: '天'
|
||
},
|
||
{
|
||
label: '参航单位',
|
||
value: '',
|
||
unit: '家'
|
||
},
|
||
{
|
||
label: '数据样品个数',
|
||
value: '',
|
||
unit: '个'
|
||
},
|
||
{
|
||
label: '累计里程',
|
||
value: '',
|
||
unit: 'km'
|
||
}
|
||
],
|
||
voyageYear: '',
|
||
barData1: {},
|
||
/** 年度航次天数(与 barData1 同源接口按年) */
|
||
barDataDays: {},
|
||
barData2: {},
|
||
barData3: {},
|
||
barData4: {},
|
||
pieData: []
|
||
}
|
||
},
|
||
created() {
|
||
this.getTypeList()
|
||
this.getShipList()
|
||
this.getShipUnit()
|
||
this.gitShipVoyage()
|
||
this.gitShipUnitYear()
|
||
this.getChartBar({ year: '2025' })
|
||
},
|
||
mounted() {
|
||
initMapbox('workUnitMap_ship', {
|
||
tileKey: 'gaode',
|
||
zoom: 3,
|
||
center: [110, 31],
|
||
minZoom: 2,
|
||
maxZoom: 29
|
||
})
|
||
initMapbox('voyageMap_ship', {
|
||
tileKey: 'GEBCO_basemap_NCEI',
|
||
zoom: 3,
|
||
center: [113, 15],
|
||
minZoom: 2,
|
||
maxZoom: 29
|
||
})
|
||
},
|
||
beforeDestroy() {
|
||
const unitMap = Vue.config.maps && Vue.config.maps['workUnitMap_ship']
|
||
if (unitMap) {
|
||
unitMap.remove()
|
||
delete Vue.config.maps['workUnitMap_ship']
|
||
}
|
||
const voyageMap = Vue.config.maps && Vue.config.maps['voyageMap_ship']
|
||
if (voyageMap) {
|
||
voyageMap.remove()
|
||
delete Vue.config.maps['voyageMap_ship']
|
||
}
|
||
},
|
||
methods: {
|
||
// 科考船数据
|
||
getTypeList() {
|
||
reportList().then(res => {
|
||
this.titles = filterDictItems(res.array, '科考船')
|
||
})
|
||
},
|
||
// 选择科考船
|
||
handleCommand(command) {
|
||
this.currentTitle = command
|
||
// this.removeSingleLineImg()
|
||
if (command === '全部科考船') {
|
||
this.getShipList()
|
||
this.gitShipVoyage()
|
||
this.gitShipUnitYear()
|
||
this.getChartBar({ year: '2025' })
|
||
} else {
|
||
this.getShipList({ ship_name: command })
|
||
this.gitShipVoyage({ ship_name: command })
|
||
this.gitShipUnitYear({ ship_name: command })
|
||
this.getChartBar({ year: '2025', ship_name: command })
|
||
}
|
||
},
|
||
// 汇总统计数据
|
||
getShipList(params = {}) {
|
||
shipTotal(params).then(res => {
|
||
const obj = res.array[0]
|
||
this.numbers[0].value = obj.member_total
|
||
this.numbers[1].value = obj.days_total
|
||
this.numbers[2].value = obj.unit_total
|
||
this.numbers[3].value = obj.dataset_total
|
||
this.numbers[4].value = obj.mileage_total
|
||
})
|
||
},
|
||
// 参航单位统计
|
||
getShipUnit() {
|
||
shipUnit().then(res => {
|
||
const geojson = {
|
||
type: 'FeatureCollection',
|
||
features: []
|
||
}
|
||
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
|
||
}
|
||
})
|
||
})
|
||
loadJsonPointFeature('workUnit', 'blue', geojson, 'unit_name', 'workUnitMap_ship')
|
||
|
||
const typeCount = res.array.reduce((acc, unit) => {
|
||
const type = unit.unit_type
|
||
acc[type] = (acc[type] || 0) + 1 // 如果类型已存在则计数+1,否则初始化为1
|
||
return acc
|
||
}, {})
|
||
this.pieData = Object.entries(typeCount).map(([type, count]) => ({
|
||
name: type,
|
||
value: count
|
||
}))
|
||
})
|
||
},
|
||
|
||
// 航次统计
|
||
gitShipVoyage(params = {}) {
|
||
shipVoyage(params).then(res => {
|
||
let i = 0
|
||
const colors = ColorGenerator.generateDivergingColors(res.array.length)
|
||
// 添加轨迹图层
|
||
res.array.forEach(item => {
|
||
// 加载轨迹图层
|
||
loadVectorLayer(item.voyage_name, ['line', 'symbol'], [colors[i], '#FFFFFF'], 'visible', 'voyageMap_ship', 'dsds')
|
||
i++
|
||
})
|
||
})
|
||
},
|
||
|
||
// 选择航次地图年份
|
||
selectVoyageYear(val) {
|
||
const shipName = this.currentTitle === '全部科考船' ? '' : this.currentTitle
|
||
if (val) {
|
||
// this.removeSingleLineImg()
|
||
const params = getYearRange(val)
|
||
this.gitShipVoyage({ ...params, ship_name: shipName })
|
||
} else {
|
||
this.gitShipVoyage({ ship_name: shipName })
|
||
}
|
||
},
|
||
// 参航单位年度
|
||
gitShipUnitYear(params = {}) {
|
||
shipUnitYear(params).then(res => {
|
||
const originalData = res.array
|
||
const sortedData = originalData.sort((a, b) => a.voyage_year - b.voyage_year)
|
||
this.barData2 = {
|
||
title: '年度参航单位',
|
||
salesData: [
|
||
{
|
||
name: '参航单位',
|
||
data: sortedData.map(item => item.unit_total),
|
||
stack: '新增单位数'
|
||
},
|
||
{
|
||
name: '新增单位',
|
||
data: sortedData.map(item => item.year_unit_total),
|
||
stack: '新增单位数'
|
||
}
|
||
],
|
||
productCategories: sortedData.map(item => item.voyage_year.toString())
|
||
}
|
||
})
|
||
},
|
||
// 柱状图数据
|
||
getChartBar(params) {
|
||
shipTotal(params).then(res => {
|
||
const originalData = res.array
|
||
const sortedData = originalData.sort((a, b) => a.year - b.year)
|
||
this.barData1 = {
|
||
title: '年度航次数',
|
||
salesData: sortedData.map(item => item.member_total),
|
||
productCategories: sortedData.map(item => item.year.toString())
|
||
}
|
||
this.barDataDays = {
|
||
title: '年度航次天数',
|
||
salesData: sortedData.map(item => item.days_total),
|
||
productCategories: sortedData.map(item => item.year.toString())
|
||
}
|
||
this.barData3 = {
|
||
title: '样品集个数',
|
||
salesData: sortedData.map(item => item.dataset_total),
|
||
productCategories: sortedData.map(item => item.year.toString())
|
||
}
|
||
this.barData4 = {
|
||
title: '里程数',
|
||
salesData: sortedData.map(item => item.mileage_total),
|
||
productCategories: sortedData.map(item => item.year.toString())
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.statistics {
|
||
background-color: #ebf1f7;
|
||
|
||
.banner {
|
||
background-color: #012458;
|
||
background-image: url(../../../static/images/bannerny1.jpg);
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
background-size: contain;
|
||
width: 100%;
|
||
height: 480px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
color: #fff;
|
||
|
||
.banner-title-picker {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin-bottom: 40px;
|
||
padding: 12px 16px 0;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.banner-type-dropdown {
|
||
display: inline-block;
|
||
width: auto;
|
||
max-width: 100%;
|
||
vertical-align: top;
|
||
}
|
||
|
||
.banner-select-trigger {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 2px 0;
|
||
border: none;
|
||
border-radius: 0;
|
||
background: transparent;
|
||
box-shadow: none;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
outline: none;
|
||
|
||
&:hover .banner-select-text {
|
||
opacity: 0.92;
|
||
}
|
||
|
||
&:focus-visible {
|
||
outline: 2px solid rgba(255, 255, 255, 0.65);
|
||
outline-offset: 6px;
|
||
}
|
||
}
|
||
|
||
.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;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.banner-title-picker__hint {
|
||
margin: 14px 0 0;
|
||
padding: 0;
|
||
font-size: 14px;
|
||
font-weight: 400;
|
||
letter-spacing: 0.12em;
|
||
color: rgba(255, 255, 255, 0.78);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.numbers {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 80px;
|
||
|
||
.item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
|
||
.value {
|
||
font-size: 56px;
|
||
font-weight: bold;
|
||
|
||
.unit {
|
||
font-size: 26px;
|
||
}
|
||
}
|
||
|
||
.label {
|
||
font-size: 18px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.map-module {
|
||
padding: 50px 50px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 50px;
|
||
|
||
.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;
|
||
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>
|