830 lines
26 KiB
Vue
830 lines
26 KiB
Vue
<template>
|
||
<div class="main-dashboard">
|
||
<div class="header">
|
||
<h1>海洋科考数据统计分析</h1>
|
||
<p>全面展示各类科考设备与平台的运行数据统计</p>
|
||
</div>
|
||
|
||
<el-row :gutter="20" class="stats-grid">
|
||
<!-- 科考船汇总统计 -->
|
||
<el-col :xs="24" :sm="12" :md="8">
|
||
<el-card
|
||
class="stat-card stat-card--nav stat-card--ship"
|
||
shadow="never"
|
||
@click.native="goToDetail('ships')"
|
||
>
|
||
<div class="stat-card__inner">
|
||
<div class="stat-card__top">
|
||
<div class="stat-card__icon-wrap" aria-hidden="true">
|
||
<svg class="stat-card__icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M3 18h18v2H3v-2zm1-2h16l-2-9H6l-2 9z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
|
||
<path d="M8 7V5a2 2 0 012-2h4a2 2 0 012 2v2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||
</svg>
|
||
</div>
|
||
<div class="stat-card__head-text">
|
||
<h3 class="stat-card__title">科考船汇总统计</h3>
|
||
<p class="stat-card__meta">
|
||
<span class="stat-card__badge">{{ ship.length }} 艘在册</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="stat-metrics" :class="{ 'stat-metrics--loading': summaryLoading }">
|
||
<div v-for="(m, idx) in shipMetrics" :key="'s'+idx" class="stat-metric">
|
||
<span class="stat-metric__value">{{ m.display }}</span>
|
||
<span class="stat-metric__label">{{ m.label }}</span>
|
||
</div>
|
||
</div>
|
||
<p class="stat-card__fleet" :title="shipNamesJoined">{{ shipNamesJoined }}</p>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 载人潜水器汇总统计 -->
|
||
<el-col :xs="24" :sm="12" :md="8">
|
||
<el-card
|
||
class="stat-card stat-card--nav stat-card--hov"
|
||
shadow="never"
|
||
@click.native="goToDetail('hov')"
|
||
>
|
||
<div class="stat-card__inner">
|
||
<div class="stat-card__top">
|
||
<div class="stat-card__icon-wrap" aria-hidden="true">
|
||
<svg class="stat-card__icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<ellipse cx="12" cy="12" rx="7" ry="4" stroke="currentColor" stroke-width="1.5" />
|
||
<path d="M5 12h14M12 8v8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||
<circle cx="12" cy="12" r="2" stroke="currentColor" stroke-width="1.5" />
|
||
</svg>
|
||
</div>
|
||
<div class="stat-card__head-text">
|
||
<h3 class="stat-card__title">载人潜水器汇总统计</h3>
|
||
<p class="stat-card__meta">
|
||
<span class="stat-card__badge">{{ hov.length }} 艘在册</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="stat-metrics" :class="{ 'stat-metrics--loading': summaryLoading }">
|
||
<div v-for="(m, idx) in platformMetrics(hovSummary)" :key="'h'+idx" class="stat-metric">
|
||
<span class="stat-metric__value">{{ m.display }}</span>
|
||
<span class="stat-metric__label">{{ m.label }}</span>
|
||
</div>
|
||
</div>
|
||
<p class="stat-card__fleet" :title="hovNamesJoined">{{ hovNamesJoined }}</p>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 遥控潜水器汇总统计 -->
|
||
<el-col :xs="24" :sm="12" :md="8">
|
||
<el-card
|
||
class="stat-card stat-card--nav stat-card--rov"
|
||
shadow="never"
|
||
@click.native="goToDetail('rov')"
|
||
>
|
||
<div class="stat-card__inner">
|
||
<div class="stat-card__top">
|
||
<div class="stat-card__icon-wrap" aria-hidden="true">
|
||
<svg class="stat-card__icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<rect x="5" y="9" width="14" height="10" rx="2" stroke="currentColor" stroke-width="1.5" />
|
||
<path d="M9 14h6M12 11v6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||
<path d="M19 13v4M3 13v4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||
</svg>
|
||
</div>
|
||
<div class="stat-card__head-text">
|
||
<h3 class="stat-card__title">遥控潜水器汇总统计</h3>
|
||
<p class="stat-card__meta">
|
||
<span class="stat-card__badge">{{ rov.length }} 艘在册</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="stat-metrics" :class="{ 'stat-metrics--loading': summaryLoading }">
|
||
<div v-for="(m, idx) in platformMetrics(rovSummary)" :key="'r'+idx" class="stat-metric">
|
||
<span class="stat-metric__value">{{ m.display }}</span>
|
||
<span class="stat-metric__label">{{ m.label }}</span>
|
||
</div>
|
||
</div>
|
||
<p class="stat-card__fleet" :title="rovNamesJoined">{{ rovNamesJoined }}</p>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 水下自主式无人潜水器汇总统计 -->
|
||
<el-col :xs="24" :sm="12" :md="8">
|
||
<el-card
|
||
class="stat-card stat-card--nav stat-card--uuv"
|
||
shadow="never"
|
||
@click.native="goToDetail('auv')"
|
||
>
|
||
<div class="stat-card__inner">
|
||
<div class="stat-card__top">
|
||
<div class="stat-card__icon-wrap" aria-hidden="true">
|
||
<svg class="stat-card__icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M4 12c2-4 6-6 8-6s6 2 8 6c-2 4-6 6-8 6s-6-2-8-6z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
|
||
<circle cx="12" cy="12" r="2" stroke="currentColor" stroke-width="1.5" />
|
||
</svg>
|
||
</div>
|
||
<div class="stat-card__head-text">
|
||
<h3 class="stat-card__title">水下自主式无人潜水器汇总统计</h3>
|
||
<p class="stat-card__meta">
|
||
<span class="stat-card__badge">{{ uuv.length }} 艘在册</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="stat-metrics" :class="{ 'stat-metrics--loading': summaryLoading }">
|
||
<div v-for="(m, idx) in platformMetrics(uuvSummary)" :key="'u'+idx" class="stat-metric">
|
||
<span class="stat-metric__value">{{ m.display }}</span>
|
||
<span class="stat-metric__label">{{ m.label }}</span>
|
||
</div>
|
||
</div>
|
||
<p class="stat-card__fleet" :title="uuvNamesJoined">{{ uuvNamesJoined }}</p>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 参航单位统计(无人航行潜水器) -->
|
||
<el-col :xs="24" :sm="12" :md="8">
|
||
<el-card
|
||
class="stat-card stat-card--nav stat-card--uuv-unit"
|
||
shadow="never"
|
||
@click.native="goToDetail('uuvUnit')"
|
||
>
|
||
<div class="stat-card__inner">
|
||
<div class="stat-card__top">
|
||
<div class="stat-card__icon-wrap" aria-hidden="true">
|
||
<svg class="stat-card__icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M3 8l9-5 9 5-9 5-9-5z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
|
||
<path d="M3 8v8l9 5 9-5V8" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
|
||
</svg>
|
||
</div>
|
||
<div class="stat-card__head-text">
|
||
<h3 class="stat-card__title">参航单位统计</h3>
|
||
<p class="stat-card__meta">
|
||
<span class="stat-card__badge">{{ uuvUnitBadge }} 家</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="stat-metrics" :class="{ 'stat-metrics--loading': summaryLoading }">
|
||
<div v-for="(m, idx) in uuvUnitCardMetrics" :key="'uu'+idx" class="stat-metric">
|
||
<span class="stat-metric__value">{{ m.display }}</span>
|
||
<span class="stat-metric__label">{{ m.label }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 参行科学家统计(无人航行潜水器) -->
|
||
<el-col :xs="24" :sm="12" :md="8">
|
||
<el-card
|
||
class="stat-card stat-card--nav stat-card--uuv-scientist"
|
||
shadow="never"
|
||
@click.native="goToDetail('uuvMember')"
|
||
>
|
||
<div class="stat-card__inner">
|
||
<div class="stat-card__top">
|
||
<div class="stat-card__icon-wrap" aria-hidden="true">
|
||
<svg class="stat-card__icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M9 21h6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||
<path d="M10 17l-2-2 2-6h4l2 6-2 2" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
|
||
<path d="M9 9V7a3 3 0 016 0v2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||
</svg>
|
||
</div>
|
||
<div class="stat-card__head-text">
|
||
<h3 class="stat-card__title">参航科学家统计</h3>
|
||
<p class="stat-card__meta">
|
||
<span class="stat-card__badge">{{ uuvScientistBadge }} 人</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="stat-metrics" :class="{ 'stat-metrics--loading': summaryLoading }">
|
||
<div v-for="(m, idx) in uuvScientistMetrics" :key="'ms'+idx" class="stat-metric">
|
||
<span class="stat-metric__value">{{ m.display }}</span>
|
||
<span class="stat-metric__label">{{ m.label }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 基站汇总统计 -->
|
||
<el-col :xs="24" :sm="12" :md="8">
|
||
<el-card class="stat-card stat-card--placeholder stat-card--station" shadow="never">
|
||
<div class="stat-card__inner">
|
||
<div class="stat-card__top">
|
||
<div class="stat-card__icon-wrap" aria-hidden="true">
|
||
<svg class="stat-card__icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M12 3l8 4v6c0 5-3.5 9-8 10-4.5-1-8-5-8-10V7l8-4z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
|
||
<path d="M12 8v5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||
</svg>
|
||
</div>
|
||
<div class="stat-card__head-text">
|
||
<h3 class="stat-card__title">基站汇总统计</h3>
|
||
<p class="stat-card__meta">
|
||
<span class="stat-card__badge stat-card__badge--muted">敬请期待</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="stat-placeholder">
|
||
<span class="stat-placeholder__text">暂无数据</span>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 着陆器汇总统计 -->
|
||
<el-col :xs="24" :sm="12" :md="8">
|
||
<el-card class="stat-card stat-card--placeholder stat-card--lander" shadow="never">
|
||
<div class="stat-card__inner">
|
||
<div class="stat-card__top">
|
||
<div class="stat-card__icon-wrap" aria-hidden="true">
|
||
<svg class="stat-card__icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M12 21s7-4.5 7-11a7 7 0 10-14 0c0 6.5 7 11 7 11z" stroke="currentColor" stroke-width="1.5" />
|
||
<circle cx="12" cy="10" r="2.5" stroke="currentColor" stroke-width="1.5" />
|
||
</svg>
|
||
</div>
|
||
<div class="stat-card__head-text">
|
||
<h3 class="stat-card__title">着陆器汇总统计</h3>
|
||
<p class="stat-card__meta">
|
||
<span class="stat-card__badge stat-card__badge--muted">敬请期待</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="stat-placeholder">
|
||
<span class="stat-placeholder__text">暂无数据</span>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import {
|
||
reportList,
|
||
shipTotal,
|
||
platformHov,
|
||
platformRov,
|
||
platformAuv,
|
||
hovUnit,
|
||
unitMember,
|
||
unitTotal
|
||
} from '../../api/statistics'
|
||
import { filterDictItems } from '../../utils/index'
|
||
|
||
/**
|
||
* 与 UuvMemberStatistics 默认统计单位一致
|
||
* @type {string}
|
||
*/
|
||
const DEFAULT_MEMBER_STAT_UNIT_NAME = '中国科学院深海科学与工程研究所'
|
||
|
||
/**
|
||
* @param {*} v 原始值
|
||
* @returns {number}
|
||
*/
|
||
function num(v) {
|
||
if (v == null || v === '') return 0
|
||
const n = Number(v)
|
||
return Number.isFinite(n) ? n : 0
|
||
}
|
||
|
||
/**
|
||
* 将 unitTotal 按单位行聚合为卡片用汇总(与 UuvUnitStatistics 全量口径一致)。
|
||
* @param {Object[]} list - report/unit/total.htm 的 array
|
||
* @returns {{ unitCount: number, voyageSum: number, daysSum: number, memberSum: number, drvingSum: number }|null}
|
||
*/
|
||
function aggregateUnitTotalForCard(list) {
|
||
if (!Array.isArray(list) || !list.length) return null
|
||
return list.reduce(
|
||
(acc, row) => ({
|
||
unitCount: acc.unitCount + 1,
|
||
voyageSum: acc.voyageSum + num(row.voyage_total),
|
||
daysSum: acc.daysSum + num(row.voyage_days),
|
||
memberSum: acc.memberSum + num(row.member_total),
|
||
drvingSum: acc.drvingSum + num(row.drving_total)
|
||
}),
|
||
{ unitCount: 0, voyageSum: 0, daysSum: 0, memberSum: 0, drvingSum: 0 }
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 统计分析入口:展示各平台类型入口卡片及与子页一致的汇总指标
|
||
*/
|
||
export default {
|
||
name: 'MainDashboard',
|
||
data() {
|
||
return {
|
||
ship: [],
|
||
hov: [],
|
||
rov: [],
|
||
uuv: [],
|
||
/** 科考船汇总(与 ShipStatistics 顶部 banner 同源) */
|
||
shipSummary: null,
|
||
/** 载人潜水器汇总(与 HovStatistics 同源接口) */
|
||
hovSummary: null,
|
||
/** 遥控潜水器汇总 */
|
||
rovSummary: null,
|
||
/** 无人航行潜水器汇总 */
|
||
uuvSummary: null,
|
||
/** 参航科学家卡片:unitMember 聚合(默认单位) */
|
||
memberScientistCard: null,
|
||
/** 参航单位统计卡片:unitTotal 全表聚合(与 UuvUnitStatistics 同源) */
|
||
uuvUnitCardAgg: null,
|
||
summaryLoading: false
|
||
}
|
||
},
|
||
computed: {
|
||
shipNamesJoined() {
|
||
return this.joinReportNames(this.ship)
|
||
},
|
||
hovNamesJoined() {
|
||
return this.joinReportNames(this.hov)
|
||
},
|
||
rovNamesJoined() {
|
||
return this.joinReportNames(this.rov)
|
||
},
|
||
uuvNamesJoined() {
|
||
return this.joinReportNames(this.uuv)
|
||
},
|
||
/** 科考船四项核心指标展示行 */
|
||
shipMetrics() {
|
||
const s = this.shipSummary
|
||
if (!s) {
|
||
return [
|
||
{ label: '航次', display: '—' },
|
||
{ label: '航次天数', display: '—' },
|
||
{ label: '参航单位', display: '—' },
|
||
{ label: '数据样品', display: '—' }
|
||
]
|
||
}
|
||
return [
|
||
{ label: '航次', display: this.formatMetric(s.voyage_total) + ' 次' },
|
||
{ label: '航次天数', display: this.formatMetric(s.days_total) + ' 天' },
|
||
{ label: '参航单位', display: this.formatMetric(s.unit_total) + ' 家' },
|
||
{ label: '数据样品', display: this.formatMetric(s.dataset_total) + ' 个' }
|
||
]
|
||
},
|
||
/** 无人航行潜水器:参航单位数量(汇总接口兜底) */
|
||
uuvUnitValue() {
|
||
if (!this.uuvSummary) return '—'
|
||
return this.formatMetric(this.uuvSummary.unit_total)
|
||
},
|
||
/** 参航单位卡片角标:优先 unitTotal 家数,否则 platformAuv 汇总 */
|
||
uuvUnitBadge() {
|
||
if (this.uuvUnitCardAgg != null) {
|
||
return this.formatMetric(this.uuvUnitCardAgg.unitCount)
|
||
}
|
||
return this.uuvUnitValue
|
||
},
|
||
/**
|
||
* 参航单位统计卡片四项指标(与参航单位详情页 Banner/汇总一致)
|
||
* @returns {Array<{ label: string, display: string }>}
|
||
*/
|
||
uuvUnitCardMetrics() {
|
||
const a = this.uuvUnitCardAgg
|
||
if (!a) {
|
||
return [
|
||
{ label: '参航单位', display: '—' },
|
||
{ label: '参航人数', display: '—' },
|
||
{ label: '深潜次数', display: '—' },
|
||
{ label: '航行次数', display: '—' }
|
||
]
|
||
}
|
||
return [
|
||
{ label: '参航单位', display: this.formatMetric(a.unitCount) + ' 家' },
|
||
{ label: '参航人数', display: this.formatMetric(a.memberSum) + ' 人' },
|
||
{ label: '深潜次数', display: this.formatMetric(a.drvingSum) + ' 次' },
|
||
{ label: '航行次数', display: this.formatMetric(a.voyageSum) + ' 次' }
|
||
]
|
||
},
|
||
/** 无人航行潜水器:参航科学家数量(汇总接口兜底) */
|
||
uuvScientistValue() {
|
||
if (!this.uuvSummary) return '—'
|
||
return this.formatMetric(this.uuvSummary.driver_member_total)
|
||
},
|
||
/** 卡片角标:优先 unitMember 科学家人数,否则汇总接口人数 */
|
||
uuvScientistBadge() {
|
||
if (this.memberScientistCard && this.memberScientistCard.scientistCount != null) {
|
||
return this.formatMetric(this.memberScientistCard.scientistCount)
|
||
}
|
||
return this.uuvScientistValue
|
||
},
|
||
/**
|
||
* 参航科学家卡片三项指标
|
||
* @returns {Array<{ label: string, display: string }>}
|
||
*/
|
||
uuvScientistMetrics() {
|
||
const c = this.memberScientistCard
|
||
if (!c) {
|
||
return [
|
||
{ label: '科学家人数', display: '—' },
|
||
{ label: '累计参加航次', display: '—' },
|
||
{ label: '累计参加人·天', display: '—' }
|
||
]
|
||
}
|
||
return [
|
||
{ label: '科学家人数', display: this.formatMetric(c.scientistCount) + ' 人' },
|
||
{ label: '累计参加航次', display: this.formatMetric(c.sumVoyage) + ' 次' },
|
||
{ label: '累计参加人·天', display: this.formatMetric(c.sumDays) + ' 人·天' }
|
||
]
|
||
}
|
||
},
|
||
created() {
|
||
this.getTypeList()
|
||
this.loadSummaries()
|
||
},
|
||
methods: {
|
||
goToDetail(type) {
|
||
this.$router.push(`/container/${type}`)
|
||
},
|
||
getTypeList() {
|
||
reportList().then(res => {
|
||
this.ship = filterDictItems(res.array, '科考船')
|
||
this.hov = filterDictItems(res.array, '载人潜水器')
|
||
this.rov = filterDictItems(res.array, '遥控潜水器')
|
||
this.auv = filterDictItems(res.array, '无人航行潜水器')
|
||
})
|
||
},
|
||
/**
|
||
* 并行拉取各子页汇总接口,用于卡片内核心指标展示
|
||
*/
|
||
loadSummaries() {
|
||
this.summaryLoading = true
|
||
const pMemberCard = hovUnit()
|
||
.then(res => {
|
||
const list = res.array || []
|
||
const unit =
|
||
list.find(u => u.unit_name === DEFAULT_MEMBER_STAT_UNIT_NAME) || list[0]
|
||
if (!unit) return null
|
||
const key = unit.unit_id != null && unit.unit_id !== '' ? unit.unit_id : unit.unit_name
|
||
return unitMember({ unit: key })
|
||
})
|
||
.then(memberRes => {
|
||
if (!memberRes || !Array.isArray(memberRes.array)) return null
|
||
const arr = memberRes.array
|
||
return {
|
||
scientistCount: arr.length,
|
||
sumVoyage: arr.reduce((s, m) => s + num(m.voyage_total), 0),
|
||
sumDays: arr.reduce((s, m) => s + num(m.days_total), 0)
|
||
}
|
||
})
|
||
.catch(() => null)
|
||
|
||
const pUnitTotalCard = unitTotal({})
|
||
.then(res => aggregateUnitTotalForCard(res && res.array ? res.array : []))
|
||
.catch(() => null)
|
||
|
||
Promise.all([
|
||
shipTotal({}),
|
||
platformHov({}),
|
||
platformRov({}),
|
||
platformAuv({}),
|
||
pMemberCard,
|
||
pUnitTotalCard
|
||
])
|
||
.then(([shipRes, hovRes, rovRes, uuvRes, memberAgg, unitAgg]) => {
|
||
this.shipSummary = (shipRes.array && shipRes.array[0]) || null
|
||
this.hovSummary = (hovRes.array && hovRes.array[0]) || null
|
||
this.rovSummary = (rovRes.array && rovRes.array[0]) || null
|
||
this.uuvSummary = (uuvRes.array && uuvRes.array[0]) || null
|
||
this.memberScientistCard = memberAgg
|
||
this.uuvUnitCardAgg = unitAgg
|
||
})
|
||
.catch(() => {
|
||
this.shipSummary = null
|
||
this.hovSummary = null
|
||
this.rovSummary = null
|
||
this.uuvSummary = null
|
||
this.memberScientistCard = null
|
||
this.uuvUnitCardAgg = null
|
||
})
|
||
.finally(() => {
|
||
this.summaryLoading = false
|
||
})
|
||
},
|
||
joinReportNames(list) {
|
||
if (!list || !list.length) return '—'
|
||
return list.map(i => i.report_name).join('、')
|
||
},
|
||
/**
|
||
* 载人/遥控/无人平台卡片指标(与子页 banner 字段一致)
|
||
* @param {object|null} row - 接口 array[0]
|
||
* @returns {Array<{ label: string, display: string }>}
|
||
*/
|
||
platformMetrics(row) {
|
||
if (!row) {
|
||
return [
|
||
{ label: '潜次数', display: '—' },
|
||
{ label: '下潜人数', display: '—' },
|
||
{ label: '下潜单位', display: '—' },
|
||
{ label: '数据样品', display: '—' }
|
||
]
|
||
}
|
||
return [
|
||
{ label: '潜次数', display: this.formatMetric(row.diving_total) + ' 次' },
|
||
{ label: '下潜人数', display: this.formatMetric(row.driver_member_total) + ' 人' },
|
||
{ label: '下潜单位', display: this.formatMetric(row.unit_total) + ' 家' },
|
||
{ label: '数据样品', display: this.formatMetric(row.dataset_total) + ' 个' }
|
||
]
|
||
},
|
||
/**
|
||
* @param {string|number|null|undefined} v
|
||
* @returns {string}
|
||
*/
|
||
formatMetric(v) {
|
||
if (v === null || v === undefined || v === '') return '—'
|
||
return String(v)
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.main-dashboard {
|
||
padding: 24px 20px 32px;
|
||
min-height: calc(100vh - 130px);
|
||
background: #e9eef4;
|
||
}
|
||
|
||
.header {
|
||
text-align: center;
|
||
margin-bottom: 28px;
|
||
}
|
||
|
||
.header h1 {
|
||
color: #0f172a;
|
||
font-size: 28px;
|
||
margin: 0 0 10px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.header p {
|
||
color: #475569;
|
||
font-size: 16px;
|
||
margin: 0;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.stats-grid {
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.stat-card {
|
||
margin-bottom: 20px;
|
||
border-radius: 12px;
|
||
border: 1px solid #e2e8f0;
|
||
background: #fff;
|
||
min-height: 280px;
|
||
transition:
|
||
box-shadow 0.22s ease,
|
||
border-color 0.22s ease,
|
||
transform 0.22s ease;
|
||
|
||
::v-deep .el-card__body {
|
||
padding: 0;
|
||
height: 100%;
|
||
}
|
||
}
|
||
|
||
.stat-card--nav {
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
transform: translateY(-3px);
|
||
border-color: #cbd5e1;
|
||
box-shadow: 0 14px 32px -12px rgba(15, 23, 42, 0.18);
|
||
}
|
||
}
|
||
|
||
.stat-card--placeholder {
|
||
cursor: default;
|
||
opacity: 0.92;
|
||
|
||
&:hover {
|
||
transform: none;
|
||
box-shadow: none;
|
||
border-color: #e2e8f0;
|
||
}
|
||
}
|
||
|
||
.stat-card__inner {
|
||
padding: 18px 18px 16px;
|
||
position: relative;
|
||
min-height: 280px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* 移除顶部描边(border-top) */
|
||
|
||
.stat-card__top {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 14px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.stat-card__icon-wrap {
|
||
width: 60px;
|
||
height: 60px;
|
||
border-radius: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.stat-card--ship .stat-card__icon-wrap {
|
||
background: #eff6ff;
|
||
color: #1d4ed8;
|
||
}
|
||
|
||
.stat-card--hov .stat-card__icon-wrap {
|
||
background: #f0fdfa;
|
||
color: #0d9488;
|
||
}
|
||
|
||
.stat-card--rov .stat-card__icon-wrap {
|
||
background: #f5f3ff;
|
||
color: #6d28d9;
|
||
}
|
||
|
||
.stat-card--uuv .stat-card__icon-wrap {
|
||
background: #fffbeb;
|
||
color: #b45309;
|
||
}
|
||
|
||
.stat-card--uuv-unit .stat-card__icon-wrap {
|
||
background: #eff6ff;
|
||
color: #1d4ed8;
|
||
}
|
||
|
||
.stat-card--uuv-scientist .stat-card__icon-wrap {
|
||
background: #f5f3ff;
|
||
color: #6d28d9;
|
||
}
|
||
|
||
.stat-card--station .stat-card__icon-wrap,
|
||
.stat-card--lander .stat-card__icon-wrap {
|
||
background: #f1f5f9;
|
||
color: #64748b;
|
||
}
|
||
|
||
.stat-card__icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
}
|
||
|
||
.stat-card__head-text {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.stat-card__title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #0f172a;
|
||
margin: 0 0 6px;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
.stat-card__meta {
|
||
margin: 0;
|
||
}
|
||
|
||
.stat-card__badge {
|
||
display: inline-block;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #475569;
|
||
padding: 2px 8px;
|
||
border-radius: 6px;
|
||
background: #f1f5f9;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.stat-card__badge--muted {
|
||
color: #94a3b8;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.stat-metrics {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 10px;
|
||
margin-top: 14px;
|
||
flex: 1;
|
||
}
|
||
|
||
.stat-metrics--loading .stat-metric {
|
||
opacity: 0.55;
|
||
}
|
||
|
||
.stat-metric {
|
||
padding: 12px 14px;
|
||
border-radius: 8px;
|
||
background: #f8fafc;
|
||
border: 1px solid #f1f5f9;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.stat-metric__value {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #0f172a;
|
||
line-height: 1.2;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.stat-metric__label {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #64748b;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.stat-card__fleet {
|
||
margin: 12px 0 0;
|
||
font-size: 12px;
|
||
color: #64748b;
|
||
line-height: 1.45;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.stat-placeholder {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 160px;
|
||
margin-top: 8px;
|
||
border-radius: 8px;
|
||
border: 1px dashed #cbd5e1;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.stat-placeholder__text {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
@keyframes fadeInUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(12px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.stat-card {
|
||
animation: fadeInUp 0.45s ease forwards;
|
||
}
|
||
|
||
.el-col:nth-child(1) .stat-card {
|
||
animation-delay: 0.05s;
|
||
}
|
||
.el-col:nth-child(2) .stat-card {
|
||
animation-delay: 0.1s;
|
||
}
|
||
.el-col:nth-child(3) .stat-card {
|
||
animation-delay: 0.15s;
|
||
}
|
||
.el-col:nth-child(4) .stat-card {
|
||
animation-delay: 0.2s;
|
||
}
|
||
.el-col:nth-child(5) .stat-card {
|
||
animation-delay: 0.25s;
|
||
}
|
||
.el-col:nth-child(6) .stat-card {
|
||
animation-delay: 0.3s;
|
||
}
|
||
|
||
.el-col:nth-child(7) .stat-card {
|
||
animation-delay: 0.35s;
|
||
}
|
||
.el-col:nth-child(8) .stat-card {
|
||
animation-delay: 0.4s;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.stat-card {
|
||
min-height: auto;
|
||
}
|
||
|
||
.stat-card__inner {
|
||
min-height: auto;
|
||
}
|
||
}
|
||
</style>
|