DSDSWeb/src/views/statistics/UuvUnitStatistics.vue

657 lines
18 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-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>
<!-- 地图板块(与科考船汇总统计页 ships 一致Mapbox + shipUnit -->
<div class="map-module">
<div class="bezel">
<div class="map-title">参航单位</div>
<div id="workUnitMap_uuv" ref="workUnitMap_uuv" style="height: 100%;width:100%;" />
</div>
</div>
<div class="unit-toolbar">
<div class="unit-toolbar-inner">
<div class="unit-filter-box">
<span class="banner-unit-label">单位名称</span>
<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" />
</el-select>
</div>
</div>
</div>
<!-- echarts板块-->
<div class="echarts-module">
<div class="bezel">
<BarChart :title="barVoyageCount.title" :chart-data="barVoyageCount.salesData"
:categories="barVoyageCount.productCategories" chart-type="single" />
</div>
<div class="bezel">
<BarChart :title="barVoyageDays.title" :chart-data="barVoyageDays.salesData"
:categories="barVoyageDays.productCategories" chart-type="single" />
</div>
<div class="bezel">
<BarChart :title="barPeopleDiving.title" :chart-data="barPeopleDiving.salesData"
:categories="barPeopleDiving.productCategories" chart-type="multiple" />
</div>
<div class="bezel">
<BarChart :title="barFirstPeopleDiving.title" :chart-data="barFirstPeopleDiving.salesData"
:categories="barFirstPeopleDiving.productCategories" chart-type="multiple" />
</div>
<div class="bezel">
<BarChart :title="mixedMileage.title" :chart-data="mixedMileage.series" :categories="mixedMileage.categories"
chart-type="mixed" :dual-y-axis="true" left-axis-name="次数" right-axis-name="天数" />
</div>
</div>
</div>
</template>
<script>
import Vue from 'vue'
import BarChart from './components/BarChart.vue'
import { unitTotal, hovUnit, shipUnit, shipVoyage } from '../../api/statistics'
import { getYearRange } from '../../utils/index'
import { initMapbox, handleWheel } from '@/utils/mapbox-utils'
import { loadVectorLayer, loadJsonPointFeature, showPopupDetails,showUnitReport } from '@/utils/vector-layer-utils'
import { ColorGenerator } from '@/utils/color-generator'
/**
* @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: [],
/** 与 Banner 多选绑定:筛选单位汇总图表(空表示全部) */
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()
this.getShipUnit()
},
mounted() {
initMapbox('workUnitMap_uuv', {
tileKey: 'gaode',
zoom: 3,
center: [110, 33],
minZoom: 2,
maxZoom: 29
})
},
beforeDestroy() {
const unitMap = Vue.config.maps && Vue.config.maps['workUnitMap_uuv']
if (unitMap) {
unitMap.remove()
delete Vue.config.maps['workUnitMap_uuv']
}
},
methods: {
/**
* Banner 统计单位多选变更:刷新汇总图表。
* @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)
// 地图显示最后一个选中单位的相关信息
showUnitReport('workUnitMap_uuv', this.selectedUnits.at(-1).unit_name, [this.selectedUnits.at(-1).unit_lon, this.selectedUnits.at(-1).unit_lat])
},
/**
* 拉取「单位汇总」报表(/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 }
]
}
},
/**
* 载人潜水器参航单位列表:仅用于 Banner 多选筛选图表。
* @returns {void}
*/
getHovUnit() {
hovUnit().then(res => {
const map = new Map()
; (res.array || []).forEach(unit => {
const k = unit.unit_id || unit.unitId || unit.unit_name
if (!map.has(k)) map.set(k, unit)
})
this.unitOptions = Array.from(map.values())
})
},
/**
* 科考船侧参航单位点位(与 ships 页地图同源接口),通过 Mapbox GeoJSON 渲染。
* @returns {void}
*/
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_uuv')
})
},
}
}
</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-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;
.value {
font-size: 56px;
font-weight: bold;
.unit {
font-size: 26px;
}
}
.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 {
padding: 10px 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;
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>