378 lines
10 KiB
Vue
378 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-image: url(../../../static/images/bannerny1.jpg);
|
|||
|
|
background-repeat: no-repeat;
|
|||
|
|
background-position: center;
|
|||
|
|
background-size: 100% 100%;
|
|||
|
|
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>
|
|||
|
|
|