380 lines
10 KiB
Vue
380 lines
10 KiB
Vue
<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_name、voyage_total、days_total、drving_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 {
|
||
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-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>
|
||
|