DSDSWeb/src/views/statistics/ShipStatistics.vue

609 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>
</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;
// 超宽屏下 contain 会在左右留边cover 铺满裁切边缘
background-position: center;
background-size: cover;
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;
}
.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: 260px;
max-height: #{"min(60vh, 420px)"};
overflow-x: hidden;
overflow-y: auto;
padding: 8px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.38);
background: linear-gradient(
155deg,
rgba(255, 255, 255, 0.48) 0%,
rgba(248, 252, 255, 0.28) 50%,
rgba(255, 255, 255, 0.16) 100%
);
backdrop-filter: blur(22px) saturate(175%);
-webkit-backdrop-filter: blur(22px) saturate(175%);
box-shadow:
0 4px 6px rgba(0, 0, 0, 0.05),
0 18px 48px rgba(10, 40, 90, 0.16),
inset 0 1px 0 rgba(255, 255, 255, 0.42);
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
margin: 6px 0;
background: rgba(255, 255, 255, 0.14);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: rgba(10, 132, 193, 0.38);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(10, 132, 193, 0.55);
}
}
.banner-type-dropdown-popper.el-dropdown-menu .el-dropdown-menu__item {
font-size: 18px;
font-weight: 500;
letter-spacing: 0.03em;
line-height: 1.45;
padding: 12px 18px;
margin: 2px 0;
border-radius: 10px;
color: rgba(28, 45, 63, 0.95);
transition:
background 0.2s ease,
color 0.2s ease;
}
.banner-type-dropdown-popper.el-dropdown-menu .el-dropdown-menu__item:not(.is-disabled):hover {
background: rgba(10, 132, 193, 0.2);
color: #0a84c1;
}
.banner-type-dropdown-popper.el-dropdown-menu .el-dropdown-menu__item:focus {
background: rgba(10, 132, 193, 0.17);
color: #0a84c1;
}
.banner-type-dropdown-popper.el-dropdown-menu .el-dropdown-menu__item.is-disabled {
color: rgba(28, 45, 63, 0.35);
}
</style>