DSDSWeb/src/views/statistics/UuvMemberStatistics.vue

380 lines
10 KiB
Vue
Raw Normal View History

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="echarts-module echarts-module--single">
<div class="bezel bezel--single chart-panel-with-unit">
<div class="chart-unit-toolbar">
<span class="chart-unit-label">统计单位</span>
<el-select
v-model="currentUnitKey"
class="chart-unit-select"
filterable
clearable
size="small"
placeholder="请选择单位"
@change="onUnitChange"
>
<el-option
v-for="unit in unitOptions"
:key="unitOptionKey(unit)"
:label="unit.unit_name"
:value="unitOptionKey(unit)"
/>
</el-select>
</div>
<BarChart
:title="memberMixChart.title"
:chart-data="memberMixChart.series"
:categories="memberMixChart.categories"
chart-type="mixed"
:dual-y-axis="true"
left-axis-name="次数"
right-axis-name="天数"
:height="420"
/>
</div>
</div>
</div>
</template>
<script>
import BarChart from './components/BarChart.vue'
import { hovUnit, unitMember } from '../../api/statistics'
/**
* @param {*} v 原始值
* @returns {number}
*/
function num(v) {
if (v == null || v === '') return 0
const n = Number(v)
return Number.isFinite(n) ? n : 0
}
/**
* 深潜次数字段接口可能为 drving_total diving_total
* @param {Object} row 行数据
* @returns {number}
*/
function diveTotal(row) {
if (!row || typeof row !== 'object') return 0
const raw = row.drving_total != null ? row.drving_total : row.diving_total
return num(raw)
}
/**
* 无路由 query.unit 时默认选中的统计单位名称 hovUnit 返回的 unit_name 一致
* @type {string}
*/
const DEFAULT_STAT_UNIT_NAME = '中国科学院深海科学与工程研究所'
export default {
components: {
BarChart
},
data() {
return {
currentTitle: '参航科学家统计',
unitOptions: [],
/** 当前统计单位键(与 report/unit/member.htm 的 unit 参数一致) */
currentUnitKey: '',
numbers: [
{
label: '科学家人数',
value: '',
unit: '人'
},
{
label: '累计参加航次',
value: '',
unit: '次'
},
{
label: '累计参航天数',
value: '',
unit: '天'
}
],
/**
* 混合图横轴科学家姓名左轴次数参航次数深潜次数柱右轴天数参航天数折线
* @type {{ title: string, categories: string[], series: Array<{ name: string, type: string, data: number[], yAxisIndex: number }> }}
*/
memberMixChart: {
title: '参航科学家统计',
categories: [],
series: [
{ name: '参航次数', type: 'bar', data: [], yAxisIndex: 0 },
{ name: '深潜次数', type: 'bar', data: [], yAxisIndex: 0 },
{ name: '参航天数', type: 'line', data: [], yAxisIndex: 1 }
]
}
}
},
watch: {
'$route.query.unit': {
handler() {
this.syncUnitFromRoute()
}
}
},
created() {
this.getHovUnit()
},
methods: {
/**
* 下拉选项 value优先 unit_id否则 unit_name
* @param {Object} unit 单位行
* @returns {string|number}
*/
unitOptionKey(unit) {
if (!unit) return ''
return unit.unit_id != null && unit.unit_id !== '' ? unit.unit_id : unit.unit_name
},
/**
* 根据路由 query.unit 同步当前选中单位
* @returns {void}
*/
syncUnitFromRoute() {
const q = this.$route.query.unit
if (q == null || q === '') return
const match = (this.unitOptions || []).find(
u => String(this.unitOptionKey(u)) === String(q) || u.unit_name === q
)
if (match) {
this.currentUnitKey = this.unitOptionKey(match)
this.loadUnitMemberData()
}
},
/**
* 单位变更时重新拉取科学家统计
* @returns {void}
*/
onUnitChange() {
this.loadUnitMemberData()
},
/**
* 调用参航科学家接口并刷新 Banner 与图表
* @returns {void}
*/
loadUnitMemberData() {
if (this.currentUnitKey === '' || this.currentUnitKey == null) {
this.resetMemberStats()
return
}
unitMember({ unit: this.currentUnitKey })
.then(res => {
const list = res && Array.isArray(res.array) ? res.array : []
this.applyMemberStats(list)
})
.catch(() => {
this.resetMemberStats()
})
},
/**
* 无单位或未选单位时的空态
* @returns {void}
*/
resetMemberStats() {
this.numbers[0].value = ''
this.numbers[1].value = ''
this.numbers[2].value = ''
this.memberMixChart = {
title: '参航科学家统计',
categories: [],
series: [
{ name: '参航次数', type: 'bar', data: [], yAxisIndex: 0 },
{ name: '深潜次数', type: 'bar', data: [], yAxisIndex: 0 },
{ name: '参航天数', type: 'line', data: [], yAxisIndex: 1 }
]
}
},
/**
* 根据 report/unit/member.htm 返回列表更新 Banner 与混合图
* @param {Object[]} list 科学家行列表member_namevoyage_totaldays_totaldrving_total
* @returns {void}
*/
applyMemberStats(list) {
const n = list.length
const sumVoyage = list.reduce((s, m) => s + num(m.voyage_total), 0)
const sumDays = list.reduce((s, m) => s + num(m.days_total), 0)
this.numbers[0].value = n
this.numbers[1].value = sumVoyage
this.numbers[2].value = sumDays
const sorted = [...list].sort((a, b) => num(b.voyage_total) - num(a.voyage_total))
const names = sorted.map(m => m.member_name || '—')
const voyage = sorted.map(m => num(m.voyage_total))
const days = sorted.map(m => num(m.days_total))
const dives = sorted.map(m => diveTotal(m))
this.memberMixChart = {
title: '参航科学家统计',
categories: names,
series: [
{ name: '参航次数', type: 'bar', data: voyage, yAxisIndex: 0 },
{ name: '深潜次数', type: 'bar', data: dives, yAxisIndex: 0 },
{ name: '参航天数', type: 'line', data: days, yAxisIndex: 1 }
]
}
},
// 参航单位列表(用于统计单位下拉)
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())
const q = this.$route.query.unit
let initial = null
if (q != null && q !== '') {
initial = this.unitOptions.find(
u => String(this.unitOptionKey(u)) === String(q) || u.unit_name === q
)
}
if (!initial && this.unitOptions.length) {
initial =
this.unitOptions.find(u => u.unit_name === DEFAULT_STAT_UNIT_NAME) ||
this.unitOptions[0]
}
this.currentUnitKey = initial ? this.unitOptionKey(initial) : ''
this.loadUnitMemberData()
})
}
}
}
</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-static-title {
margin-bottom: 40px;
}
.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;
}
.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;
}
}
}
}
.echarts-module {
padding: 50px 50px 50px;
&.echarts-module--single {
max-width: 1400px;
margin: 0 auto;
}
.bezel {
width: 100%;
border-radius: 10px;
overflow: hidden;
border: 1px solid #ccc;
position: relative;
background: #fff;
padding: 12px 8px 8px;
box-sizing: border-box;
&.bezel--single {
min-height: 460px;
}
&.chart-panel-with-unit {
display: flex;
flex-direction: column;
}
.chart-unit-toolbar {
display: flex;
align-items: center;
align-self: flex-end;
flex-shrink: 0;
gap: 10px;
margin: 0 4px 6px;
padding: 0 4px;
}
.chart-unit-label {
font-size: 14px;
font-weight: 600;
color: #303133;
letter-spacing: 0.04em;
white-space: nowrap;
}
.chart-unit-select {
width: #{"min(56vw, 360px)"};
::v-deep .el-input__inner {
border-radius: 6px;
font-size: 13px;
font-weight: 500;
}
}
}
}
}
</style>