Compare commits

...

2 Commits

Author SHA1 Message Date
80937518651731347 f0608a28b0 Merge branch 'master' of http://124.222.8.13:3000/revisit/DSDSWeb 2026-04-03 03:20:12 +08:00
80937518651731347 fa5a6f2b63 更新修复 2026-04-03 02:05:29 +08:00
23 changed files with 7956 additions and 3785 deletions

11
.gitignore vendored
View File

@ -1,4 +1,10 @@
.DS_Store .DS_Store
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
node_modules/ node_modules/
/dist/ /dist/
npm-debug.log* npm-debug.log*
@ -8,9 +14,14 @@ yarn-error.log*
/test/e2e/reports/ /test/e2e/reports/
selenium-debug.log selenium-debug.log
# 根目录本地环境变量(避免把密钥提交入库);构建仍依赖的 config/*.env.js 保持跟踪
.env.local
.env.*.local
# Editor directories and files # Editor directories and files
.idea .idea
.vscode .vscode
*.code-workspace
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj

View File

@ -17,6 +17,8 @@ module.exports = merge(prodEnv, {
// VUE_APP_API_URL: '"http://10.10.150.237/"' // VUE_APP_API_URL: '"http://10.10.150.237/"'
VUE_APP_BASE_API: '"/api"', VUE_APP_BASE_API: '"/api"',
/** 仅参航科学家报表;与生产一致直连 https服务端 CORS 已放开,避免本地代理 404 */
VUE_APP_UNIT_MEMBER_REPORT_BASE: '"https://weixin.bdsmart.cn/ds1"',
// VUE_APP_BASE_APIURL: '"http://ds2.hzzxq.com/api2"', // VUE_APP_BASE_APIURL: '"http://ds2.hzzxq.com/api2"',
VUE_APP_BASE_APIURL: '"http://test.gopmas.com/ds"', VUE_APP_BASE_APIURL: '"http://test.gopmas.com/ds"',
VUE_APP_FULL_API: '"http://10.10.151.5:5555"', VUE_APP_FULL_API: '"http://10.10.151.5:5555"',

View File

@ -12,6 +12,18 @@ module.exports = {
assetsSubDirectory: 'static', assetsSubDirectory: 'static',
assetsPublicPath: '/', assetsPublicPath: '/',
proxyTable: { proxyTable: {
/**
* 参航科学家接口单独转发到 weixin须写在通用 /api 之前避免被旧后端吞掉
* unitMember 使用 /ds1-report-proxy /api 时的兜底一致
*/
'/api/report/unit/member.htm': {
target: 'https://weixin.bdsmart.cn',
changeOrigin: true,
secure: true,
pathRewrite: {
'^/api': '/ds1'
}
},
'/api': { '/api': {
// target: 'http://120.48.105.88', // 线上 // target: 'http://120.48.105.88', // 线上
// target: 'http://10.0.90.70', // 测试环境 // target: 'http://10.0.90.70', // 测试环境

View File

@ -16,6 +16,8 @@ module.exports = {
// VUE_APP_BASE_API: '"/api"', // VUE_APP_BASE_API: '"/api"',
// VUE_APP_BASE_APIURL: '"http://ds.hzzxq.com/api"', // VUE_APP_BASE_APIURL: '"http://ds.hzzxq.com/api"',
VUE_APP_BASE_API: '"./"', VUE_APP_BASE_API: '"./"',
/** 仅参航科学家报表 report/unit/member.htm 使用,与全局 VUE_APP_BASE_API 解耦 */
VUE_APP_UNIT_MEMBER_REPORT_BASE: '"https://weixin.bdsmart.cn/ds1"',
VUE_APP_BASE_APIURL: '"./"', VUE_APP_BASE_APIURL: '"./"',
VUE_APP_FULL_API: '"http://10.10.151.3:5555"', VUE_APP_FULL_API: '"http://10.10.151.3:5555"',
VUE_APP_BASE_API_FRONT: '"http://10.10.150.128:8080/satellitePub/pub/index.html"', VUE_APP_BASE_API_FRONT: '"http://10.10.150.128:8080/satellitePub/pub/index.html"',

8411
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
diff --git a/node_modules/http-deceiver/lib/deceiver.js b/node_modules/http-deceiver/lib/deceiver.js
index 33e14cb..cec712f 100644
--- a/node_modules/http-deceiver/lib/deceiver.js
+++ b/node_modules/http-deceiver/lib/deceiver.js
@@ -19,12 +19,17 @@ var kOnHeadersComplete;
var kOnMessageComplete;
var kOnBody;
if (mode === 'normal' || mode === 'modern') {
- HTTPParser = process.binding('http_parser').HTTPParser;
- methods = HTTPParser.methods;
-
- // v6
- if (!methods)
- methods = process.binding('http_parser').methods;
+ try {
+ var binding = process.binding('http_parser');
+ HTTPParser = binding.HTTPParser;
+ methods = HTTPParser.methods;
+ if (!methods) {
+ methods = binding.methods;
+ }
+ } catch (err) {
+ HTTPParser = require('http-parser-js').HTTPParser;
+ methods = HTTPParser.methods;
+ }
reverseMethods = {};

View File

@ -156,7 +156,7 @@ export function platformAuv(data) {
}) })
} }
// 遥控潜水器潜水器统计 // 遥控潜水器汇总统计
export function platformRov(data) { export function platformRov(data) {
return request({ return request({
url: '/report/platform/rov.htm', url: '/report/platform/rov.htm',
@ -165,3 +165,48 @@ export function platformRov(data) {
data data
}) })
} }
/**
* ds1 报表独立域名参航科学家/单位汇总等与全局 VUE_APP_BASE_API 解耦
* @returns {string}
*/
function getDs1ReportBase() {
return (
process.env.VUE_APP_UNIT_MEMBER_REPORT_BASE ||
'https://weixin.bdsmart.cn/ds1'
)
}
/**
* 单位维度参航科学家明细按科学家汇总
* @param {Object} data 请求体需包含 unit单位标识
* @returns {Promise}
*/
export function unitMember(data) {
return request({
baseURL: getDs1ReportBase(),
url: '/report/unit/member.htm',
method: 'post',
type: 'application/x-www-form-urlencoded',
/** 跨域且服务端返回 Access-Control-Allow-Origin: * 时不可带 cookie否则浏览器会拦截 */
withCredentials: false,
data
})
}
/**
* 单位维度汇总统计按单位一行含航次天数人数深潜等
* unitMember 共用 ds1 baseURL
* @param {Object} [data] 可选 unit 等筛选参数空对象表示全部单位
* @returns {Promise}
*/
export function unitTotal(data = {}) {
return request({
baseURL: getDs1ReportBase(),
url: '/report/unit/total.htm',
method: 'post',
type: 'application/x-www-form-urlencoded',
withCredentials: false,
data
})
}

View File

@ -388,6 +388,30 @@ export const constantRoutes = [
loginAuthority: true loginAuthority: true
} }
}, },
{
path: 'uuv',
redirect: '/container/auv'
},
{
path: 'uuvMember',
name: 'uuvMember',
component: () => import('@/views/statistics/UuvMemberStatistics.vue'),
meta: {
title: '参航科学家统计',
icon: 'peoples',
loginAuthority: true
}
},
{
path: 'uuvUnit',
name: 'uuvUnit',
component: () => import('@/views/statistics/UuvUnitStatistics.vue'),
meta: {
title: '参航单位统计',
icon: 'peoples',
loginAuthority: true
}
},
{ {
path: 'AboutUs', path: 'AboutUs',
name: 'AboutUs', name: 'AboutUs',

View File

@ -25,26 +25,23 @@
<el-radio label="航次">航次</el-radio> <el-radio label="航次">航次</el-radio>
</el-radio-group> </el-radio-group>
</div> </div>
<div class="left4" v-if="radio == '数据样品集'"> <div class="left4-sample-wrap" v-if="radio == '数据样品集'">
<div v-for="(item, index) in dataList" :key="index" :id="item.tsy_id" class="left4_1">
<div class="left4_top">
<p class="rule-text">编号规则:航次号_潜次号_设备类型_数据样品类型</p> <p class="rule-text">编号规则:航次号_潜次号_设备类型_数据样品类型</p>
<p @click="openList(item, 'list')" <div class="left4">
style="font-weight: 400;font-size: 14px;cursor: pointer;color: #409eff">{{ item.dataset_name }} <span <div v-for="(item, index) in dataList" :key="index" :id="item.tsy_id" class="left4_1" >
class="mileage">{{ item.file_total }}</span></p> <div class="left4_top">
<p @click="openList(item, 'list')" style="font-weight: 400;font-size: 14px;cursor: pointer;color: #409eff" >{{ item.dataset_name }} <span class="mileage">{{ item.file_total }}</span></p>
<span class="org">{{ item.create_time }}</span> <span class="org">{{ item.create_time }}</span>
</div> </div>
<ul v-show="currenId == item.tsy_id && fold"> <ul v-show="currenId == item.tsy_id && fold">
<li :class="sample.tsy_id == currenSample ? 'bg-color' : ''" class="sample" :id="sample.tsy_id" <li :class="sample.tsy_id == currenSample ? 'bg-color' : ''" class="sample" :id="sample.tsy_id"
v-for="(sample, i) in item.sampleArray" :key="i" @click.stop="onSample(sample, item.job_type)">{{ v-for="(sample, i) in item.sampleArray" :key="i" @click.stop="onSample(sample, item.job_type)">{{ sample.sample_name }}
sample.sample_name }}
</li> </li>
</ul> </ul>
<div class="left4_list"> <div class="left4_list">
<div class="list1"> <div class="list1">
<p>航次</p> <p>航次</p>
<span class="hover-color" style="color: #409eff" @click="turnDeatilPage(item)">{{ item.voyage_name <span class="hover-color" style="color: #409eff" @click="turnDeatilPage(item)">{{ item.voyage_name }}</span>
}}</span>
</div> </div>
<div class="list1"> <div class="list1">
<p>平台类型</p> <p>平台类型</p>
@ -79,7 +76,7 @@
</div> </div>
</div> </div>
</div>
</div> </div>
<div class="left4" v-if="radio == '航次'"> <div class="left4" v-if="radio == '航次'">
<div v-for="(item, index) in dataList" :key="index" class="left4_1" style="cursor: pointer" :id="item.tsy_id"> <div v-for="(item, index) in dataList" :key="index" class="left4_1" style="cursor: pointer" :id="item.tsy_id">
@ -916,6 +913,7 @@ export default {
.left1 { .left1 {
width: 100%; width: 100%;
height: 40px; height: 40px;
flex-shrink: 0;
display: flex; display: flex;
align-items: center; align-items: center;
@ -956,27 +954,60 @@ export default {
padding: 10px 0; padding: 10px 0;
width: 100%; width: 100%;
margin-top: 10px; margin-top: 10px;
flex-shrink: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-bottom: 1px solid #e5e5e5; border-bottom: 1px solid #e5e5e5;
} }
/**
* 数据样品集编号说明固定在滚动区外仅列表在 .left4 内滚动
*/
.left4-sample-wrap {
width: 100%;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
> .rule-text {
flex-shrink: 0;
margin-bottom: 10px;
}
> .left4 {
flex: 1;
min-height: 0;
}
}
/** 筛选结果列表:占满左侧剩余高度并独立滚动 */
.left4 { .left4 {
width: 100%; width: 100%;
height: 100%; flex: 1;
overflow-y: scroll; min-height: 0;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: #c0c4cc #f0f0f0;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 5px; width: 6px;
/* 保持一个最小的宽度 */ }
background-color: transparent;
/* 透明背景 */ &::-webkit-scrollbar-track {
background: #f0f0f0;
border-radius: 3px;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background-color: transparent; background-color: #c0c4cc;
/* 滚动条滑块也设置为透明 */ border-radius: 3px;
&:hover {
background-color: #909399;
}
} }
.left4_1 { .left4_1 {
@ -1099,6 +1130,7 @@ export default {
color: #333333; color: #333333;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
flex-shrink: 0;
.tag { .tag {
color: #333333; color: #333333;

View File

@ -18,14 +18,14 @@
<el-button type="primary" size="medium" @click="handleAdd">数据样品类型录入</el-button> <el-button type="primary" size="medium" @click="handleAdd">数据样品类型录入</el-button>
</div> </div>
</div> </div>
<el-table ref="tableData" v-loading="loading" stripe border :data="tableData" :header-cell-style="{ background: '#dddfe6',color: '#555960',textAlign: 'center' }" :cell-style="{ textAlign: 'center' }"> <el-table ref="tableData" v-loading="loading" stripe border :data="tableData" :header-cell-style="{ background: '#dddfe6',color: '#555960',textAlign: 'center' }" :cell-style="{ textAlign: 'center' }" @sort-change="handleSortChange">
<el-table-column type="index" label="#" width="50"/> <el-table-column type="index" label="#" width="50"/>
<el-table-column prop="data_name" label="数据样品类型名称" /> <el-table-column prop="data_name" label="数据样品类型名称" sortable="custom" />
<el-table-column prop="data_eng" label="英文名称" /> <el-table-column prop="data_eng" label="英文名称" sortable="custom" />
<el-table-column prop="data_alias" label="别名" width="100" /> <el-table-column prop="data_alias" label="别名" width="100" sortable="custom" />
<el-table-column prop="data_order" label="排序" width="100" /> <el-table-column prop="data_order" label="排序" width="100" sortable="custom" />
<el-table-column prop="data_group" label="分组" width="120" /> <el-table-column prop="data_group" label="分组" width="120" sortable="custom" />
<el-table-column prop="create_time" label="创建时间" width="160" /> <el-table-column prop="create_time" label="创建时间" width="160" sortable="custom" />
<el-table-column label="操作" width="120"> <el-table-column label="操作" width="120">
<template slot-scope="scope"> <template slot-scope="scope">
<el-tooltip class="item" effect="dark" content="修改" placement="top-end"> <el-tooltip class="item" effect="dark" content="修改" placement="top-end">
@ -91,7 +91,9 @@ export default {
start: 0, start: 0,
limit: 10, limit: 10,
group: 10, group: 10,
query: '' query: '',
order: undefined,
type: undefined
}, },
pageNum: 1, pageNum: 1,
// //
@ -133,6 +135,24 @@ export default {
this.getData() this.getData()
}, },
methods: { methods: {
/**
* 表头排序分页请求携带 order字段名typeASC|DESC
* @param {{ column?: object, prop?: string, order?: string | null }} sort
*/
handleSortChange(sort) {
const prop = sort.prop
const order = sort.order
if (!order || !prop) {
this.$delete(this.params, 'order')
this.$delete(this.params, 'type')
} else {
this.params.order = prop
this.params.type = order === 'ascending' ? 'ASC' : 'DESC'
}
this.pageNum = 1
this.params.start = 0
this.getData()
},
// //
getData() { getData() {
this.loading = true this.loading = true
@ -211,6 +231,8 @@ export default {
}, },
handleSizeChange(val) { handleSizeChange(val) {
this.params.limit = val this.params.limit = val
this.pageNum = 1
this.params.start = 0
this.getData() this.getData()
}, },
} }

View File

@ -18,14 +18,14 @@
<el-button type="primary" size="medium" @click="handleAdd">设备类型录入</el-button> <el-button type="primary" size="medium" @click="handleAdd">设备类型录入</el-button>
</div> </div>
</div> </div>
<el-table ref="tableData" v-loading="loading" stripe border :data="tableData" :header-cell-style="{ background: '#dddfe6',color: '#555960',textAlign: 'center' }" :cell-style="{ textAlign: 'center' }"> <el-table ref="tableData" v-loading="loading" stripe border :data="tableData" :header-cell-style="{ background: '#dddfe6',color: '#555960',textAlign: 'center' }" :cell-style="{ textAlign: 'center' }" @sort-change="handleSortChange">
<el-table-column type="index" label="#" width="50"/> <el-table-column type="index" label="#" width="50"/>
<el-table-column prop="equipment_name" label="设备类型名称" /> <el-table-column prop="equipment_name" label="设备类型名称" sortable="custom" />
<el-table-column prop="equipment_eng" label="英文名称" /> <el-table-column prop="equipment_eng" label="英文名称" sortable="custom" />
<el-table-column prop="equipment_alias" label="别名" width="100" /> <el-table-column prop="equipment_alias" label="别名" width="100" sortable="custom" />
<el-table-column prop="equipment_order" label="排序" width="100" /> <el-table-column prop="equipment_order" label="排序" width="100" sortable="custom" />
<el-table-column prop="equipment_group" label="分组" width="120" /> <el-table-column prop="equipment_group" label="分组" width="120" sortable="custom" />
<el-table-column prop="create_time" label="创建时间" width="160" /> <el-table-column prop="create_time" label="创建时间" width="160" sortable="custom" />
<el-table-column label="操作" width="120"> <el-table-column label="操作" width="120">
<template slot-scope="scope"> <template slot-scope="scope">
<el-tooltip class="item" effect="dark" content="修改" placement="top-end"> <el-tooltip class="item" effect="dark" content="修改" placement="top-end">
@ -91,7 +91,9 @@ export default {
start: 0, start: 0,
limit: 10, limit: 10,
group: 10, group: 10,
query: '' query: '',
order: undefined,
type: undefined
}, },
pageNum: 1, pageNum: 1,
// //
@ -133,6 +135,24 @@ export default {
this.getData() this.getData()
}, },
methods: { methods: {
/**
* 表头排序分页请求携带 order字段名typeASC|DESC
* @param {{ column?: object, prop?: string, order?: string | null }} sort
*/
handleSortChange(sort) {
const prop = sort.prop
const order = sort.order
if (!order || !prop) {
this.$delete(this.params, 'order')
this.$delete(this.params, 'type')
} else {
this.params.order = prop
this.params.type = order === 'ascending' ? 'ASC' : 'DESC'
}
this.pageNum = 1
this.params.start = 0
this.getData()
},
// //
getData() { getData() {
this.loading = true this.loading = true
@ -211,6 +231,8 @@ export default {
}, },
handleSizeChange(val) { handleSizeChange(val) {
this.params.limit = val this.params.limit = val
this.pageNum = 1
this.params.start = 0
this.getData() this.getData()
}, },
} }

View File

@ -18,12 +18,12 @@
<el-button type="primary" size="medium" @click="handleAdd">资助机构录入</el-button> <el-button type="primary" size="medium" @click="handleAdd">资助机构录入</el-button>
</div> </div>
</div> </div>
<el-table ref="tableData" v-loading="loading" stripe border :data="tableData" :header-cell-style="{ background: '#dddfe6',color: '#555960',textAlign: 'center' }" :cell-style="{ textAlign: 'center' }"> <el-table ref="tableData" v-loading="loading" stripe border :data="tableData" :header-cell-style="{ background: '#dddfe6',color: '#555960',textAlign: 'center' }" :cell-style="{ textAlign: 'center' }" @sort-change="handleSortChange">
<el-table-column type="index" label="#" width="50"/> <el-table-column type="index" label="#" width="50"/>
<el-table-column prop="funding_name" label="资助机构名称" /> <el-table-column prop="funding_name" label="资助机构名称" sortable="custom" />
<el-table-column prop="funding_eng" label="英文名称" /> <el-table-column prop="funding_eng" label="英文名称" sortable="custom" />
<el-table-column prop="funding_order" label="排序" width="100" /> <el-table-column prop="funding_order" label="排序" width="100" sortable="custom" />
<el-table-column prop="create_time" label="创建时间" width="160" /> <el-table-column prop="create_time" label="创建时间" width="160" sortable="custom" />
<el-table-column label="操作" width="120"> <el-table-column label="操作" width="120">
<template slot-scope="scope"> <template slot-scope="scope">
<el-tooltip class="item" effect="dark" content="修改" placement="top-end"> <el-tooltip class="item" effect="dark" content="修改" placement="top-end">
@ -80,7 +80,9 @@ export default {
start: 0, start: 0,
limit: 10, limit: 10,
group: 10, group: 10,
query: '' query: '',
order: undefined,
type: undefined
}, },
pageNum: 1, pageNum: 1,
// //
@ -116,6 +118,24 @@ export default {
this.getData() this.getData()
}, },
methods: { methods: {
/**
* 表头排序分页请求携带 order字段名typeASC|DESC
* @param {{ column?: object, prop?: string, order?: string | null }} sort
*/
handleSortChange(sort) {
const prop = sort.prop
const order = sort.order
if (!order || !prop) {
this.$delete(this.params, 'order')
this.$delete(this.params, 'type')
} else {
this.params.order = prop
this.params.type = order === 'ascending' ? 'ASC' : 'DESC'
}
this.pageNum = 1
this.params.start = 0
this.getData()
},
// //
getData() { getData() {
this.loading = true this.loading = true
@ -191,6 +211,8 @@ export default {
}, },
handleSizeChange(val) { handleSizeChange(val) {
this.params.limit = val this.params.limit = val
this.pageNum = 1
this.params.start = 0
this.getData() this.getData()
}, },
} }

View File

@ -18,14 +18,14 @@
<el-button type="primary" size="medium" @click="handleAdd">平台类型录入</el-button> <el-button type="primary" size="medium" @click="handleAdd">平台类型录入</el-button>
</div> </div>
</div> </div>
<el-table ref="tableData" v-loading="loading" stripe border :data="tableData" :header-cell-style="{ background: '#dddfe6',color: '#555960',textAlign: 'center' }" :cell-style="{ textAlign: 'center' }"> <el-table ref="tableData" v-loading="loading" stripe border :data="tableData" :header-cell-style="{ background: '#dddfe6',color: '#555960',textAlign: 'center' }" :cell-style="{ textAlign: 'center' }" @sort-change="handleSortChange">
<el-table-column type="index" label="#" width="50"/> <el-table-column type="index" label="#" width="50"/>
<el-table-column prop="platform_name" label="平台类型名称" /> <el-table-column prop="platform_name" label="平台类型名称" sortable="custom" />
<el-table-column prop="platform_eng" label="英文名称" /> <el-table-column prop="platform_eng" label="英文名称" sortable="custom" />
<el-table-column prop="platform_alias" label="别名" width="100" /> <el-table-column prop="platform_alias" label="别名" width="100" sortable="custom" />
<el-table-column prop="platform_order" label="排序" width="100" /> <el-table-column prop="platform_order" label="排序" width="100" sortable="custom" />
<el-table-column prop="platform_group" label="分组" width="120" /> <el-table-column prop="platform_group" label="分组" width="120" sortable="custom" />
<el-table-column prop="create_time" label="创建时间" width="160" /> <el-table-column prop="create_time" label="创建时间" width="160" sortable="custom" />
<el-table-column label="操作" width="120"> <el-table-column label="操作" width="120">
<template slot-scope="scope"> <template slot-scope="scope">
<el-tooltip class="item" effect="dark" content="修改" placement="top-end"> <el-tooltip class="item" effect="dark" content="修改" placement="top-end">
@ -91,7 +91,9 @@ export default {
start: 0, start: 0,
limit: 10, limit: 10,
group: 10, group: 10,
query: '' query: '',
order: undefined,
type: undefined
}, },
pageNum: 1, pageNum: 1,
// //
@ -133,6 +135,24 @@ export default {
this.getData() this.getData()
}, },
methods: { methods: {
/**
* 表头排序分页请求携带 order字段名typeASC|DESC
* @param {{ column?: object, prop?: string, order?: string | null }} sort
*/
handleSortChange(sort) {
const prop = sort.prop
const order = sort.order
if (!order || !prop) {
this.$delete(this.params, 'order')
this.$delete(this.params, 'type')
} else {
this.params.order = prop
this.params.type = order === 'ascending' ? 'ASC' : 'DESC'
}
this.pageNum = 1
this.params.start = 0
this.getData()
},
// //
getData() { getData() {
this.loading = true this.loading = true
@ -211,6 +231,8 @@ export default {
}, },
handleSizeChange(val) { handleSizeChange(val) {
this.params.limit = val this.params.limit = val
this.pageNum = 1
this.params.start = 0
this.getData() this.getData()
}, },
} }

View File

@ -18,17 +18,17 @@
<el-button type="primary" size="medium" @click="handleAdd">共享单位录入</el-button> <el-button type="primary" size="medium" @click="handleAdd">共享单位录入</el-button>
</div> </div>
</div> </div>
<el-table ref="tableData" v-loading="loading" stripe border :data="tableData" :header-cell-style="{ background: '#dddfe6',color: '#555960',textAlign: 'center' }" :cell-style="{ textAlign: 'center' }"> <el-table ref="tableData" v-loading="loading" stripe border :data="tableData" :header-cell-style="{ background: '#dddfe6',color: '#555960',textAlign: 'center' }" :cell-style="{ textAlign: 'center' }" @sort-change="handleSortChange">
<el-table-column type="index" label="#" width="50"/> <el-table-column type="index" label="#" width="50"/>
<el-table-column prop="unit_name" label="共享单位名称" width="300" /> <el-table-column prop="unit_name" label="共享单位名称" width="300" sortable="custom" />
<el-table-column prop="unit_type" label="单位类型" width="200"> <el-table-column prop="unit_type" label="单位类型" width="200" sortable="custom">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ scope.row.unit_type ? '单位类型' + scope.row.unit_type : '' }}</span> <span>{{ scope.row.unit_type ? '单位类型' + scope.row.unit_type : '' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="link_man" label="单位负责人" width="100" /> <el-table-column prop="link_man" label="单位负责人" width="100" sortable="custom" />
<el-table-column prop="link_tel" label="联系电话" width="150" /> <el-table-column prop="link_tel" label="联系电话" width="150" sortable="custom" />
<el-table-column prop="unit_addr" label="单位地址" /> <el-table-column prop="unit_addr" label="单位地址" sortable="custom" />
<el-table-column label="操作" width="120"> <el-table-column label="操作" width="120">
<template slot-scope="scope"> <template slot-scope="scope">
<el-tooltip class="item" effect="dark" content="修改" placement="top-end"> <el-tooltip class="item" effect="dark" content="修改" placement="top-end">
@ -107,7 +107,9 @@ export default {
start: 0, start: 0,
limit: 10, limit: 10,
group: 10, group: 10,
query: '' query: '',
order: undefined,
type: undefined
}, },
pageNum: 1, pageNum: 1,
// //
@ -156,6 +158,24 @@ export default {
this.getData() this.getData()
}, },
methods: { methods: {
/**
* 表头排序分页请求携带 order字段名typeASC|DESC
* @param {{ column?: object, prop?: string, order?: string | null }} sort
*/
handleSortChange(sort) {
const prop = sort.prop
const order = sort.order
if (!order || !prop) {
this.$delete(this.params, 'order')
this.$delete(this.params, 'type')
} else {
this.params.order = prop
this.params.type = order === 'ascending' ? 'ASC' : 'DESC'
}
this.pageNum = 1
this.params.start = 0
this.getData()
},
// //
getData() { getData() {
this.loading = true this.loading = true
@ -236,6 +256,8 @@ export default {
}, },
handleSizeChange(val) { handleSizeChange(val) {
this.params.limit = val this.params.limit = val
this.pageNum = 1
this.params.start = 0
this.getData() this.getData()
}, },
} }

View File

@ -18,12 +18,12 @@
<el-button type="primary" size="medium" @click="handleAdd">新增参航单位</el-button> <el-button type="primary" size="medium" @click="handleAdd">新增参航单位</el-button>
</div> </div>
</div> </div>
<el-table ref="tableData" v-loading="loading" stripe border :data="tableData" :header-cell-style="{ background: '#dddfe6',color: '#555960',textAlign: 'center' }" :cell-style="{ textAlign: 'center' }"> <el-table ref="tableData" v-loading="loading" stripe border :data="tableData" :header-cell-style="{ background: '#dddfe6',color: '#555960',textAlign: 'center' }" :cell-style="{ textAlign: 'center' }" @sort-change="handleSortChange">
<el-table-column type="index" label="#" width="50"/> <el-table-column type="index" label="#" width="50"/>
<el-table-column prop="unit_name" label="参航单位名称" /> <el-table-column prop="unit_name" label="参航单位名称" sortable="custom" />
<el-table-column prop="unit_type" label="参航单位类型" width="200" /> <el-table-column prop="unit_type" label="参航单位类型" width="200" sortable="custom" />
<el-table-column prop="unit_lon" label="经度" width="200" /> <el-table-column prop="unit_lon" label="经度" width="200" sortable="custom" />
<el-table-column prop="unit_lat" label="纬度" width="200" /> <el-table-column prop="unit_lat" label="纬度" width="200" sortable="custom" />
<el-table-column label="操作" width="120"> <el-table-column label="操作" width="120">
<template slot-scope="scope"> <template slot-scope="scope">
<el-tooltip class="item" effect="dark" content="修改" placement="top-end"> <el-tooltip class="item" effect="dark" content="修改" placement="top-end">
@ -89,7 +89,9 @@ export default {
start: 0, start: 0,
limit: 10, limit: 10,
group: 10, group: 10,
query: '' query: '',
order: undefined,
type: undefined
}, },
pageNum: 1, pageNum: 1,
// //
@ -131,6 +133,24 @@ export default {
this.getData() this.getData()
}, },
methods: { methods: {
/**
* 表头排序请求 `/dict/unit/sail/page.htm` 时携带 order字段名typeASC|DESC
* @param {{ column?: object, prop?: string, order?: string | null }} sort
*/
handleSortChange(sort) {
const prop = sort.prop
const order = sort.order
if (!order || !prop) {
this.$delete(this.params, 'order')
this.$delete(this.params, 'type')
} else {
this.params.order = prop
this.params.type = order === 'ascending' ? 'ASC' : 'DESC'
}
this.pageNum = 1
this.params.start = 0
this.getData()
},
// //
getData() { getData() {
this.loading = true this.loading = true
@ -208,6 +228,8 @@ export default {
}, },
handleSizeChange(val) { handleSizeChange(val) {
this.params.limit = val this.params.limit = val
this.pageNum = 1
this.params.start = 0
this.getData() this.getData()
}, },
} }

View File

@ -1,15 +1,38 @@
<template> <template>
<div class="statistics"> <div class="statistics">
<div class="banner"> <div class="banner">
<el-dropdown @command="handleCommand"> <div class="banner-title-picker">
<span class="titles">{{ currentTitle }}<i class="el-icon-arrow-down el-icon--right" /></span> <el-dropdown
<el-dropdown-menu slot="dropdown"> class="banner-type-dropdown"
trigger="click"
@command="handleCommand"
>
<span class="banner-select-trigger">
<span class="banner-select-text">{{ currentTitle }}</span>
<svg
class="banner-select-chevron"
viewBox="0 0 24 24"
width="40"
height="40"
aria-hidden="true"
focusable="false"
>
<path fill="#ffffff" d="M7 10l5 5 5-5H7z" />
</svg>
</span>
<el-dropdown-menu slot="dropdown" class="banner-type-dropdown-popper">
<el-dropdown-item command="全部无人航行潜水器">全部无人航行潜水器</el-dropdown-item> <el-dropdown-item command="全部无人航行潜水器">全部无人航行潜水器</el-dropdown-item>
<el-dropdown-item v-for="item in titles" :key="item.tsy_id" :command="item.report_name"> <el-dropdown-item
v-for="item in titles"
:key="item.tsy_id"
:command="item.report_name"
>
{{ item.report_name }} {{ item.report_name }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
<p class="banner-title-picker__hint">点击选择</p>
</div>
<div class="numbers"> <div class="numbers">
<div v-for="(item, index) in numbers" :key="index" class="item"> <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="value">{{ item.value }}<span class="unit">{{ item.unit }}</span></div>
@ -55,6 +78,9 @@
</template> </template>
<script> <script>
/**
* 无人航行潜水器汇总统计页请求 `/report/platform/auv` 系列接口交互与遥控潜水器汇总页一致Cesium + ECharts
*/
import BarChart from './components/BarChart.vue' import BarChart from './components/BarChart.vue'
import PieChart from './components/PieChart.vue' import PieChart from './components/PieChart.vue'
import { setImageryViewModels } from '../../utils/common' import { setImageryViewModels } from '../../utils/common'
@ -287,7 +313,7 @@ export default {
}, },
// Cesium - // Cesium -
initBaiduCesium() { initBaiduCesium() {
this.baiduCesiumViewer = new Cesium.Viewer(this.$refs.baiduCesiumContainer, { this.baiduCesiumViewer = new Cesium.Viewer(this.$refs.workUnitMap_auv, {
sceneMode: Cesium.SceneMode.SCENE2D, sceneMode: Cesium.SceneMode.SCENE2D,
shadows: false, shadows: false,
timeline: false, timeline: false,
@ -323,7 +349,7 @@ export default {
// cesium // cesium
initCesium() { initCesium() {
Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkY2Y5NWY2Ni1mODQ1LTQ3YTUtYjc4Zi1jYzhjMGI2YzcxMWYiLCJpZCI6MTI5ODMwLCJpYXQiOjE2Nzk0Njk5NTd9.DTH54ioOH-HLqeNIetBe9hFyrPOX2Vp1AQmZzw8TIZ4' Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkY2Y5NWY2Ni1mODQ1LTQ3YTUtYjc4Zi1jYzhjMGI2YzcxMWYiLCJpZCI6MTI5ODMwLCJpYXQiOjE2Nzk0Njk5NTd9.DTH54ioOH-HLqeNIetBe9hFyrPOX2Vp1AQmZzw8TIZ4'
this.viewer = new Cesium.Viewer('cesiumContainer', { this.viewer = new Cesium.Viewer(this.$refs.voyageMap_auv, {
sceneMode: Cesium.SceneMode.SCENE2D, sceneMode: Cesium.SceneMode.SCENE2D,
shadows: false, shadows: false,
timeline: false, timeline: false,
@ -442,6 +468,9 @@ export default {
.banner { .banner {
background-image: url(../../../static/images/bannerny1.jpg); background-image: url(../../../static/images/bannerny1.jpg);
background-repeat: no-repeat;
background-position: center;
background-size: 100% 100%;
width: 100%; width: 100%;
height: 480px; height: 480px;
display: flex; display: flex;
@ -450,12 +479,69 @@ export default {
align-items: center; align-items: center;
color: #fff; color: #fff;
.titles { .banner-title-picker {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40px; margin-bottom: 40px;
font-size: 56px; padding: 12px 16px 0;
color: #fff; max-width: 100%;
font-weight: bold; }
.banner-type-dropdown {
display: inline-block;
width: auto;
max-width: 100%;
vertical-align: top;
}
.banner-select-trigger {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 2px 0;
border: none;
border-radius: 0;
background: transparent;
box-shadow: none;
cursor: pointer; cursor: pointer;
user-select: none;
outline: none;
&:hover .banner-select-text {
opacity: 0.92;
}
&:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.65);
outline-offset: 6px;
}
}
.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;
color: #ffffff;
}
.banner-title-picker__hint {
margin: 14px 0 0;
padding: 0;
font-size: 14px;
font-weight: 400;
letter-spacing: 0.12em;
color: rgba(255, 255, 255, 0.78);
line-height: 1.4;
} }
.numbers { .numbers {
@ -546,8 +632,21 @@ export default {
} }
} }
} }
</style>
.el-icon-arrow-down { <style lang="scss">
font-size: 12px .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> </style>

View File

@ -1,9 +1,26 @@
<template> <template>
<div class="statistics"> <div class="statistics">
<div class="banner"> <div class="banner">
<el-dropdown @command="handleCommand"> <div class="banner-title-picker">
<span class="titles">{{ currentTitle }}<i class="el-icon-arrow-down el-icon--right" /></span> <el-dropdown
<el-dropdown-menu slot="dropdown"> class="banner-type-dropdown"
trigger="click"
@command="handleCommand"
>
<span class="banner-select-trigger">
<span class="banner-select-text">{{ currentTitle }}</span>
<svg
class="banner-select-chevron"
viewBox="0 0 24 24"
width="40"
height="40"
aria-hidden="true"
focusable="false"
>
<path fill="#ffffff" d="M7 10l5 5 5-5H7z" />
</svg>
</span>
<el-dropdown-menu slot="dropdown" class="banner-type-dropdown-popper">
<el-dropdown-item command="全部载人潜水器">全部载人潜水器</el-dropdown-item> <el-dropdown-item command="全部载人潜水器">全部载人潜水器</el-dropdown-item>
<el-dropdown-item <el-dropdown-item
v-for="item in titles" v-for="item in titles"
@ -14,6 +31,8 @@
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
<p class="banner-title-picker__hint">点击选择</p>
</div>
<div class="numbers"> <div class="numbers">
<div v-for="(item, index) in numbers" :key="index" class="item"> <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="value">{{ item.value }}<span class="unit">{{ item.unit }}</span></div>
@ -471,6 +490,9 @@ export default {
.banner { .banner {
background-image: url(../../../static/images/bannerny1.jpg); background-image: url(../../../static/images/bannerny1.jpg);
background-repeat: no-repeat;
background-position: center;
background-size: 100% 100%;
width: 100%; width: 100%;
height: 480px; height: 480px;
display: flex; display: flex;
@ -479,12 +501,69 @@ export default {
align-items: center; align-items: center;
color: #fff; color: #fff;
.titles { .banner-title-picker {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40px; margin-bottom: 40px;
font-size: 56px; padding: 12px 16px 0;
color: #fff; max-width: 100%;
font-weight: bold; }
.banner-type-dropdown {
display: inline-block;
width: auto;
max-width: 100%;
vertical-align: top;
}
.banner-select-trigger {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 2px 0;
border: none;
border-radius: 0;
background: transparent;
box-shadow: none;
cursor: pointer; cursor: pointer;
user-select: none;
outline: none;
&:hover .banner-select-text {
opacity: 0.92;
}
&:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.65);
outline-offset: 6px;
}
}
.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;
color: #ffffff;
}
.banner-title-picker__hint {
margin: 14px 0 0;
padding: 0;
font-size: 14px;
font-weight: 400;
letter-spacing: 0.12em;
color: rgba(255, 255, 255, 0.78);
line-height: 1.4;
} }
.numbers { .numbers {
@ -568,8 +647,21 @@ export default {
} }
} }
} }
</style>
.el-icon-arrow-down { <style lang="scss">
font-size: 12px .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> </style>

View File

@ -1,15 +1,38 @@
<template> <template>
<div class="statistics"> <div class="statistics">
<div class="banner"> <div class="banner">
<el-dropdown @command="handleCommand"> <div class="banner-title-picker">
<span class="titles">{{ currentTitle }}<i class="el-icon-arrow-down el-icon--right" /></span> <el-dropdown
<el-dropdown-menu slot="dropdown"> class="banner-type-dropdown"
trigger="click"
@command="handleCommand"
>
<span class="banner-select-trigger">
<span class="banner-select-text">{{ currentTitle }}</span>
<svg
class="banner-select-chevron"
viewBox="0 0 24 24"
width="40"
height="40"
aria-hidden="true"
focusable="false"
>
<path fill="#ffffff" d="M7 10l5 5 5-5H7z" />
</svg>
</span>
<el-dropdown-menu slot="dropdown" class="banner-type-dropdown-popper">
<el-dropdown-item command="全部遥控潜水器">全部遥控潜水器</el-dropdown-item> <el-dropdown-item command="全部遥控潜水器">全部遥控潜水器</el-dropdown-item>
<el-dropdown-item v-for="item in titles" :key="item.tsy_id" :command="item.report_name"> <el-dropdown-item
v-for="item in titles"
:key="item.tsy_id"
:command="item.report_name"
>
{{ item.report_name }} {{ item.report_name }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
<p class="banner-title-picker__hint">点击选择</p>
</div>
<div class="numbers"> <div class="numbers">
<div v-for="(item, index) in numbers" :key="index" class="item"> <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="value">{{ item.value }}<span class="unit">{{ item.unit }}</span></div>
@ -55,6 +78,9 @@
</template> </template>
<script> <script>
/**
* 遥控潜水器汇总统计页请求 `/report/platform/rov` 系列接口
*/
import BarChart from './components/BarChart.vue' import BarChart from './components/BarChart.vue'
import PieChart from './components/PieChart.vue' import PieChart from './components/PieChart.vue'
import { setImageryViewModels } from '../../utils/common' import { setImageryViewModels } from '../../utils/common'
@ -323,7 +349,7 @@ export default {
// cesium // cesium
initCesium() { initCesium() {
Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkY2Y5NWY2Ni1mODQ1LTQ3YTUtYjc4Zi1jYzhjMGI2YzcxMWYiLCJpZCI6MTI5ODMwLCJpYXQiOjE2Nzk0Njk5NTd9.DTH54ioOH-HLqeNIetBe9hFyrPOX2Vp1AQmZzw8TIZ4' Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkY2Y5NWY2Ni1mODQ1LTQ3YTUtYjc4Zi1jYzhjMGI2YzcxMWYiLCJpZCI6MTI5ODMwLCJpYXQiOjE2Nzk0Njk5NTd9.DTH54ioOH-HLqeNIetBe9hFyrPOX2Vp1AQmZzw8TIZ4'
this.viewer = new Cesium.Viewer('cesiumContainer', { this.viewer = new Cesium.Viewer(this.$refs.voyageMap_rov, {
sceneMode: Cesium.SceneMode.SCENE2D, sceneMode: Cesium.SceneMode.SCENE2D,
shadows: false, shadows: false,
timeline: false, timeline: false,
@ -442,6 +468,9 @@ export default {
.banner { .banner {
background-image: url(../../../static/images/bannerny1.jpg); background-image: url(../../../static/images/bannerny1.jpg);
background-repeat: no-repeat;
background-position: center;
background-size: 100% 100%;
width: 100%; width: 100%;
height: 480px; height: 480px;
display: flex; display: flex;
@ -450,12 +479,69 @@ export default {
align-items: center; align-items: center;
color: #fff; color: #fff;
.titles { .banner-title-picker {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40px; margin-bottom: 40px;
font-size: 56px; padding: 12px 16px 0;
color: #fff; max-width: 100%;
font-weight: bold; }
.banner-type-dropdown {
display: inline-block;
width: auto;
max-width: 100%;
vertical-align: top;
}
.banner-select-trigger {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 2px 0;
border: none;
border-radius: 0;
background: transparent;
box-shadow: none;
cursor: pointer; cursor: pointer;
user-select: none;
outline: none;
&:hover .banner-select-text {
opacity: 0.92;
}
&:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.65);
outline-offset: 6px;
}
}
.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;
color: #ffffff;
}
.banner-title-picker__hint {
margin: 14px 0 0;
padding: 0;
font-size: 14px;
font-weight: 400;
letter-spacing: 0.12em;
color: rgba(255, 255, 255, 0.78);
line-height: 1.4;
} }
.numbers { .numbers {
@ -546,8 +632,21 @@ export default {
} }
} }
} }
</style>
.el-icon-arrow-down { <style lang="scss">
font-size: 12px .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> </style>

View File

@ -1,15 +1,38 @@
<template> <template>
<div class="statistics"> <div class="statistics">
<div class="banner"> <div class="banner">
<el-dropdown @command="handleCommand"> <div class="banner-title-picker">
<span class="titles">{{ currentTitle }}<i class="el-icon-arrow-down el-icon--right" /></span> <el-dropdown
<el-dropdown-menu slot="dropdown"> class="banner-type-dropdown"
trigger="click"
@command="handleCommand"
>
<span class="banner-select-trigger">
<span class="banner-select-text">{{ currentTitle }}</span>
<svg
class="banner-select-chevron"
viewBox="0 0 24 24"
width="40"
height="40"
aria-hidden="true"
focusable="false"
>
<path fill="#ffffff" d="M7 10l5 5 5-5H7z" />
</svg>
</span>
<el-dropdown-menu slot="dropdown" class="banner-type-dropdown-popper">
<el-dropdown-item command="全部科考船">全部科考船</el-dropdown-item> <el-dropdown-item command="全部科考船">全部科考船</el-dropdown-item>
<el-dropdown-item v-for="item in titles" :key="item.tsy_id" :command="item.report_name"> <el-dropdown-item
v-for="item in titles"
:key="item.tsy_id"
:command="item.report_name"
>
{{ item.report_name }} {{ item.report_name }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
<p class="banner-title-picker__hint">点击选择</p>
</div>
<div class="numbers"> <div class="numbers">
<div v-for="(item, index) in numbers" :key="index" class="item"> <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="value">{{ item.value }}<span class="unit">{{ item.unit }}</span></div>
@ -25,8 +48,19 @@
</div> </div>
<div class="bezel"> <div class="bezel">
<div class="map-title">航次地图</div> <div class="map-title">航次地图</div>
<el-date-picker v-model="voyageYear" type="year" size="mini" placeholder="选择年" class="map-year" format="yyyy" <div class="map-year-panel">
@change="selectVoyageYear" /> <span class="map-year-label">年份</span>
<el-date-picker
v-model="voyageYear"
type="year"
size="small"
placeholder="全部"
class="map-year"
clearable
format="yyyy"
@change="selectVoyageYear"
/>
</div>
<div id="voyageMap_ship" ref="voyageMap_ship" style="height: 100%;width:100%;" /> <div id="voyageMap_ship" ref="voyageMap_ship" style="height: 100%;width:100%;" />
</div> </div>
</div> </div>
@ -37,8 +71,21 @@
chart-type="single" /> chart-type="single" />
</div> </div>
<div class="bezel"> <div class="bezel">
<BarChart :title="barData2.title" :chart-data="barData2.salesData" :categories="barData2.productCategories" <BarChart
chart-type="stacked" stack-name="新增单位数" /> :title="barDataDays.title"
:chart-data="barDataDays.salesData"
:categories="barDataDays.productCategories"
chart-type="single"
/>
</div>
<div class="bezel">
<BarChart
:title="barData2.title"
:chart-data="barData2.salesData"
:categories="barData2.productCategories"
chart-type="stacked"
stack-name="新增单位数"
/>
</div> </div>
<div class="bezel"> <div class="bezel">
<BarChart :title="barData3.title" :chart-data="barData3.salesData" :categories="barData3.productCategories" /> <BarChart :title="barData3.title" :chart-data="barData3.salesData" :categories="barData3.productCategories" />
@ -99,7 +146,7 @@ export default {
{ {
label: '累计里程', label: '累计里程',
value: '', value: '',
unit: 'KM' unit: 'km'
} }
], ],
// Cesium - // Cesium -
@ -111,6 +158,8 @@ export default {
trackLineLayers: [], trackLineLayers: [],
voyageYear: '', voyageYear: '',
barData1: {}, barData1: {},
/** 年度航次天数(与 barData1 同源接口按年) */
barDataDays: {},
barData2: {}, barData2: {},
barData3: {}, barData3: {},
barData4: {}, barData4: {},
@ -330,7 +379,7 @@ export default {
// cesium // cesium
initCesium() { initCesium() {
Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkY2Y5NWY2Ni1mODQ1LTQ3YTUtYjc4Zi1jYzhjMGI2YzcxMWYiLCJpZCI6MTI5ODMwLCJpYXQiOjE2Nzk0Njk5NTd9.DTH54ioOH-HLqeNIetBe9hFyrPOX2Vp1AQmZzw8TIZ4' Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkY2Y5NWY2Ni1mODQ1LTQ3YTUtYjc4Zi1jYzhjMGI2YzcxMWYiLCJpZCI6MTI5ODMwLCJpYXQiOjE2Nzk0Njk5NTd9.DTH54ioOH-HLqeNIetBe9hFyrPOX2Vp1AQmZzw8TIZ4'
this.viewer = new Cesium.Viewer('voyageMap_ship', { this.viewer = new Cesium.Viewer(this.$refs.voyageMap_ship, {
sceneMode: Cesium.SceneMode.SCENE2D, sceneMode: Cesium.SceneMode.SCENE2D,
shadows: false, shadows: false,
timeline: false, timeline: false,
@ -434,6 +483,11 @@ export default {
salesData: sortedData.map(item => item.member_total), salesData: sortedData.map(item => item.member_total),
productCategories: sortedData.map(item => item.year.toString()) productCategories: sortedData.map(item => item.year.toString())
} }
this.barDataDays = {
title: '年度航次天数',
salesData: sortedData.map(item => item.days_total),
productCategories: sortedData.map(item => item.year.toString())
}
this.barData3 = { this.barData3 = {
title: '样品集个数', title: '样品集个数',
salesData: sortedData.map(item => item.dataset_total), salesData: sortedData.map(item => item.dataset_total),
@ -456,6 +510,9 @@ export default {
.banner { .banner {
background-image: url(../../../static/images/bannerny1.jpg); background-image: url(../../../static/images/bannerny1.jpg);
background-repeat: no-repeat;
background-position: center;
background-size: 100% 100%;
width: 100%; width: 100%;
height: 480px; height: 480px;
display: flex; display: flex;
@ -464,12 +521,69 @@ export default {
align-items: center; align-items: center;
color: #fff; color: #fff;
.titles { .banner-title-picker {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40px; margin-bottom: 40px;
font-size: 56px; padding: 12px 16px 0;
color: #fff; max-width: 100%;
font-weight: bold; }
.banner-type-dropdown {
display: inline-block;
width: auto;
max-width: 100%;
vertical-align: top;
}
.banner-select-trigger {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 2px 0;
border: none;
border-radius: 0;
background: transparent;
box-shadow: none;
cursor: pointer; cursor: pointer;
user-select: none;
outline: none;
&:hover .banner-select-text {
opacity: 0.92;
}
&:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.65);
outline-offset: 6px;
}
}
.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;
color: #ffffff;
}
.banner-title-picker__hint {
margin: 14px 0 0;
padding: 0;
font-size: 14px;
font-weight: 400;
letter-spacing: 0.12em;
color: rgba(255, 255, 255, 0.78);
line-height: 1.4;
} }
.numbers { .numbers {
@ -523,13 +637,77 @@ export default {
font-size: 22px; font-size: 22px;
} }
.map-year { .map-year-panel {
position: absolute; position: absolute;
top: 20px; top: 16px;
right: 20px; right: 16px;
color: white;
z-index: 8888; z-index: 8888;
width: 100px; display: flex;
align-items: center;
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;
}
} }
} }
} }
@ -560,8 +738,21 @@ export default {
} }
} }
} }
</style>
.el-icon-arrow-down { <style lang="scss">
font-size: 12px .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> </style>

View File

@ -0,0 +1,377 @@
<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 {
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>

View File

@ -0,0 +1,881 @@
<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="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>
<!-- 地图板块-->
<div class="map-module">
<div class="bezel">
<div class="map-title">参航单位</div>
<div id="baiduCesiumContainer" ref="baiduCesiumContainer" style="height: 100%;width:100%;" />
</div>
<div class="bezel">
<div class="map-title">航次地图</div>
<div class="map-year-panel">
<span class="map-year-label">年份</span>
<el-date-picker
v-model="voyageYear"
type="year"
size="small"
placeholder="全部"
class="map-year"
clearable
format="yyyy"
@change="selectVoyageYear"
/>
</div>
<div id="cesiumContainer" ref="cesiumContainer" style="height: 100%;width:100%;" />
</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 BarChart from './components/BarChart.vue'
import { setImageryViewModels } from '../../utils/common'
import { unitTotal, hovUnit, hovDiving } from '../../api/statistics'
import { getYearRange } from '../../utils/index'
const Cesium = window.Cesium
/**
* @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: [],
// hovDiving 便
hovDivingItems: [],
numbers: [
{
label: '参航单位数量',
value: '',
unit: '家'
},
{
label: '参航人数',
value: '',
unit: '人'
},
{
label: '深潜次数',
value: '',
unit: '次'
}
],
// Cesium -
baiduCesiumViewer: null,
baiduCesiumEntities: [], //
viewer: null,
//
CesiumHostAddr: `${window.location.protocol}//${window.location.hostname}:${window.location.port}/`,
trackLineLayers: [],
/** 航行次数(柱状) */
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.gitHovDiving()
},
mounted() {
this.imageryViewModels = setImageryViewModels(this.CesiumHostAddr)
this.initCesium() //
this.initBaiduCesium() //
this.refreshTrackLines()
},
beforeDestroy() {
// Cesium
if (this.baiduCesiumViewer) {
this.baiduCesiumViewer.destroy()
}
if (this.viewer) {
this.viewer.destroy()
}
},
methods: {
/**
* 刷新航次地图轨迹图层按单位多选进行前端过滤
* @returns {void}
*/
refreshTrackLines() {
if (!this.viewer) return
this.removeSingleLineImg()
const selectedNameSet = new Set(
(this.selectedUnits || [])
.map(u => u && u.unit_name)
.filter(Boolean)
)
const selectedIdSet = new Set(
(this.selectedUnits || [])
.map(u => (u && (u.unit_id || u.unitId)) || null)
.filter(Boolean)
)
const unitFilterEnabled = selectedNameSet.size > 0 || selectedIdSet.size > 0
const itemsToShow = unitFilterEnabled
? (this.hovDivingItems || []).filter(item => {
const itemName = item.unit_name || item.unitName || item.unit_name_cn || null
const itemId = item.unit_id || item.unitId || item.unit_code || null
return selectedNameSet.has(itemName) || selectedIdSet.has(itemId)
})
: (this.hovDivingItems || [])
itemsToShow.forEach(item => {
this.addSingleLineImg(item)
})
},
/**
* Banner 统计单位多选变更刷新汇总图表与航次地图轨迹
* @returns {void}
*/
onBannerUnitsChange() {
this.applyFilteredUnitTotal()
this.refreshTrackLines()
},
/**
* 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)
},
/**
* 拉取单位汇总报表/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 }
]
}
},
//
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())
//
this.clearBaiduCesiumMarkers()
//
this.createBaiduCesiumMarkers(res.array || [])
})
},
// Cesium
createBaiduCesiumMarkers(units) {
if (!this.baiduCesiumViewer) return
const entities = []
units.forEach((item, index) => {
//
const entity = this.baiduCesiumViewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(item.unit_lon, item.unit_lat),
billboard: {
image: '../../../static/img/01.png', //
width: 32,
height: 32,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER
},
label: {
text: item.unit_name,
font: '14pt monospace',
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
outlineWidth: 2,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0, -40), //
fillColor: Cesium.Color.WHITE,
outlineColor: Cesium.Color.BLACK
},
unitData: item, //
index: index //
})
entities.push(entity)
})
this.baiduCesiumEntities = entities
//
if (entities.length > 0) {
this.baiduCesiumViewer.zoomTo(entities)
}
},
//
clearBaiduCesiumMarkers() {
if (this.baiduCesiumViewer && this.baiduCesiumEntities.length > 0) {
this.baiduCesiumEntities.forEach(entity => {
this.baiduCesiumViewer.entities.remove(entity)
})
this.baiduCesiumEntities = []
}
},
//
gitHovDiving(params = {}) {
hovDiving(params).then(res => {
this.hovDivingItems = res.array || []
this.refreshTrackLines()
})
},
// Cesium -
initBaiduCesium() {
this.baiduCesiumViewer = new Cesium.Viewer(this.$refs.baiduCesiumContainer, {
sceneMode: Cesium.SceneMode.SCENE2D,
shadows: false,
timeline: false,
baseLayerPicker: false,
imageryProviderViewModels: this.imageryViewModels,
terrainProviderViewModels: [],
fullscreenButton: false,
selectionIndicator: false,
homeButton: false,
sceneModePicker: false,
animation: false,
infoBox: false,
geocoder: false,
navigationHelpButton: false
})
this.baiduCesiumViewer._cesiumWidget._creditContainer.style.display = 'none'
this.baiduCesiumViewer.scene.sun.show = false
this.baiduCesiumViewer.scene.moon.show = false
this.baiduCesiumViewer.scene.fog.enabled = false
this.baiduCesiumViewer.scene.skyAtmosphere.show = false
this.baiduCesiumViewer.scene.sun.show = false
this.baiduCesiumViewer.scene.skyBox.show = false
this.baiduCesiumViewer.scene.globe.enableLighting = false
this.baiduCesiumViewer.shadowMap.darkness = 0.8
this.baiduCesiumViewer.scene._sunBloom = false
this.baiduCesiumViewer.scene.globe.showGroundAtmosphere = false
this.baiduCesiumViewer.scene.postProcessStages.fxaa.enabled = true
this.baiduCesiumViewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(114.4040, 30.5196, 4000000)
})
},
// cesium
initCesium() {
Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkY2Y5NWY2Ni1mODQ1LTQ3YTUtYjc4Zi1jYzhjMGI2YzcxMWYiLCJpZCI6MTI5ODMwLCJpYXQiOjE2Nzk0Njk5NTd9.DTH54ioOH-HLqeNIetBe9hFyrPOX2Vp1AQmZzw8TIZ4'
this.viewer = new Cesium.Viewer('cesiumContainer', {
sceneMode: Cesium.SceneMode.SCENE2D,
shadows: false,
timeline: false,
baseLayerPicker: false,
//
imageryProviderViewModels: this.imageryViewModels,
//
terrainProviderViewModels: [],
fullscreenButton: false,
selectionIndicator: false,
homeButton: false,
sceneModePicker: false,
animation: false,
infoBox: false,
geocoder: false,
navigationHelpButton: false
})
this.viewer._cesiumWidget._creditContainer.style.display = 'none'
this.viewer.scene.sun.show = false
this.viewer.scene.moon.show = false
this.viewer.scene.fog.enabled = false
this.viewer.scene.skyAtmosphere.show = false
this.viewer.scene.sun.show = false
this.viewer.scene.skyBox.show = false
this.viewer.scene.globe.enableLighting = false
this.viewer.shadowMap.darkness = 0.8
this.viewer.scene._sunBloom = false
this.viewer.scene.globe.showGroundAtmosphere = false
this.viewer.scene.postProcessStages.fxaa.enabled = true
this.viewer.camera.setView({
//
destination: Cesium.Cartesian3.fromDegrees(112, 18, 4000000)
})
},
//
addSingleLineImg(item) {
const trackLine = new Cesium.WebMapTileServiceImageryProvider(
{
url: `${this.CesiumHostAddr}geoserver/gwc/service/wmts/rest/dsds:${item.diving_sn}/{TileMatrixSet}/{TileMatrixSet}:{TileMatrix}/{TileRow}/{TileCol}?format=image/png`,
layer: `dsds:${item.diving_sn}`,
style: 'default',
format: 'image/png',
tileMatrixSetID: 'EPSG:4326',
tilingScheme: new Cesium.GeographicTilingScheme()
}
)
const trackLineLayer = this.viewer.imageryLayers.addImageryProvider(trackLine)
this.trackLineLayers.push(trackLineLayer)
},
//
removeSingleLineImg() {
if (this.viewer && this.trackLineLayers.length) {
this.trackLineLayers.forEach(line => {
this.viewer.imageryLayers.remove(line)
})
}
// remove
this.trackLineLayers = []
},
//
selectVoyageYear(val) {
this.removeSingleLineImg()
if (val) {
const params = getYearRange(val)
this.gitHovDiving({ ...params })
} else {
this.gitHovDiving({})
}
},
}
}
</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-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: 20px 50px 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>

View File

@ -1,5 +1,9 @@
<template> <template>
<div ref="chartContainer" style="width: 100%; height: 300px;"></div> <div
ref="chartContainer"
class="bar-chart-host"
:style="chartHostStyle"
/>
</template> </template>
<script> <script>
@ -34,6 +38,21 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
/** 双 Y 轴时左侧轴名称 */
leftAxisName: {
type: String,
default: ''
},
/** 双 Y 轴时右侧轴名称 */
rightAxisName: {
type: String,
default: ''
},
/** 图表区域高度(像素) */
height: {
type: Number,
default: 300
},
// stacked // stacked
stackName: { stackName: {
type: String, type: String,
@ -45,6 +64,12 @@ export default {
myChart: null myChart: null
}; };
}, },
computed: {
chartHostStyle() {
const h = typeof this.height === 'number' ? `${this.height}px` : '300px';
return { width: '100%', height: h };
}
},
mounted() { mounted() {
this.initChart(); this.initChart();
}, },
@ -99,7 +124,10 @@ export default {
name: series.name || (series.type === 'line' ? '折线' : '柱状') + (index + 1), name: series.name || (series.type === 'line' ? '折线' : '柱状') + (index + 1),
type: series.type || (index === 0 ? 'bar' : 'line'), type: series.type || (index === 0 ? 'bar' : 'line'),
data: series.data || [], data: series.data || [],
yAxisIndex: series.yAxisIndex || (this.dualYAxis && index > 0 ? 1 : 0) yAxisIndex:
series.yAxisIndex !== undefined && series.yAxisIndex !== null
? series.yAxisIndex
: (this.dualYAxis && index > 0 ? 1 : 0)
})); }));
} }
} else if (this.chartType === 'stacked') { } else if (this.chartType === 'stacked') {
@ -162,7 +190,7 @@ export default {
return [ return [
{ {
type: 'value', type: 'value',
name: '左轴', name: this.leftAxisName || '左轴',
position: 'left', position: 'left',
axisLine: { axisLine: {
show: true, show: true,
@ -180,7 +208,7 @@ export default {
}, },
{ {
type: 'value', type: 'value',
name: '右轴', name: this.rightAxisName || '右轴',
position: 'right', position: 'right',
axisLine: { axisLine: {
show: true, show: true,
@ -201,6 +229,13 @@ export default {
// //
getSeriesConfig(processedSeries) { getSeriesConfig(processedSeries) {
const firstBarIndex = processedSeries.findIndex(s => s.type === 'bar');
const barSeriesCount = processedSeries.filter(s => s.type === 'bar').length;
const tightBarGroup =
this.chartType !== 'stacked' &&
barSeriesCount >= 2 &&
(this.chartType === 'mixed' || this.chartType === 'multiple');
return processedSeries.map((series, index) => { return processedSeries.map((series, index) => {
const isLine = series.type === 'line'; const isLine = series.type === 'line';
const isStacked = this.chartType === 'stacked'; const isStacked = this.chartType === 'stacked';
@ -252,6 +287,11 @@ export default {
barConfig.itemStyle.borderRadius = [2, 2, 0, 0]; // barConfig.itemStyle.borderRadius = [2, 2, 0, 0]; //
} }
// 0使barGap bar
if (tightBarGroup && index === firstBarIndex) {
barConfig.barGap = '0%';
}
return barConfig; return barConfig;
} }
}); });
@ -425,6 +465,23 @@ export default {
this.renderChart(); this.renderChart();
} }
}, },
leftAxisName() {
if (this.myChart) {
this.renderChart();
}
},
rightAxisName() {
if (this.myChart) {
this.renderChart();
}
},
height() {
this.$nextTick(() => {
if (this.myChart) {
this.myChart.resize();
}
});
},
stackName() { stackName() {
if (this.myChart && this.chartType === 'stacked') { if (this.myChart && this.chartType === 'stacked') {
this.renderChart(); this.renderChart();

View File

@ -9,96 +9,224 @@
<!-- 科考船汇总统计 --> <!-- 科考船汇总统计 -->
<el-col :xs="24" :sm="12" :md="8"> <el-col :xs="24" :sm="12" :md="8">
<el-card <el-card
class="stat-card" class="stat-card stat-card--nav stat-card--ship"
shadow="hover" shadow="never"
@click.native="goToDetail('ships')" @click.native="goToDetail('ships')"
> >
<div class="card-content"> <div class="stat-card__inner">
<div class="card-icon"> <div class="stat-card__top">
<i class="el-icon-ship"></i> <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>
<div class="card-info"> <div class="stat-card__head-text">
<h3 class="card-title">科考船汇总统计</h3> <h3 class="stat-card__title">科考船汇总统计</h3>
<p class="card-count">{{ ship.length }}</p> <p class="stat-card__meta">
<p class="card-desc"><span v-for="(item, index) in ship" :key="index">{{ item.report_name }}<span v-if="index !== ship.length - 1"></span></span></p> <span class="stat-card__badge">{{ ship.length }} 艘在册</span>
</p>
</div> </div>
</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-card>
</el-col> </el-col>
<!-- 载人潜水器汇总统计 --> <!-- 载人潜水器汇总统计 -->
<el-col :xs="24" :sm="12" :md="8"> <el-col :xs="24" :sm="12" :md="8">
<el-card <el-card
class="stat-card" class="stat-card stat-card--nav stat-card--hov"
shadow="hover" shadow="never"
@click.native="goToDetail('hov')" @click.native="goToDetail('hov')"
> >
<div class="card-content"> <div class="stat-card__inner">
<div class="card-icon"> <div class="stat-card__top">
<i class="el-icon-watermelon"></i> <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>
<div class="card-info"> <div class="stat-card__head-text">
<h3 class="card-title">载人潜水器汇总统计</h3> <h3 class="stat-card__title">载人潜水器汇总统计</h3>
<p class="card-count">{{ hov.length }}</p> <p class="stat-card__meta">
<p class="card-desc"><span v-for="(item, index) in hov" :key="index">{{ item.report_name }}<span v-if="index !== hov.length - 1"></span></span> </p> <span class="stat-card__badge">{{ hov.length }} 艘在册</span>
</p>
</div> </div>
</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-card>
</el-col> </el-col>
<!-- 遥控潜水器汇总统计 --> <!-- 遥控潜水器汇总统计 -->
<el-col :xs="24" :sm="12" :md="8"> <el-col :xs="24" :sm="12" :md="8">
<el-card <el-card
class="stat-card" class="stat-card stat-card--nav stat-card--rov"
shadow="hover" shadow="never"
@click.native="goToDetail('rov')" @click.native="goToDetail('rov')"
> >
<div class="card-content"> <div class="stat-card__inner">
<div class="card-icon"> <div class="stat-card__top">
<i class="el-icon-cpu"></i> <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>
<div class="card-info"> <div class="stat-card__head-text">
<h3 class="card-title">遥控潜水器汇总统计</h3> <h3 class="stat-card__title">遥控潜水器汇总统计</h3>
<p class="card-count">{{ rov.length }}</p> <p class="stat-card__meta">
<p class="card-desc"><span v-for="(item, index) in rov" :key="index">{{ item.report_name }}<span v-if="index !== rov.length - 1"></span></span> </p> <span class="stat-card__badge">{{ rov.length }} 艘在册</span>
</p>
</div> </div>
</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-card>
</el-col> </el-col>
<!-- 无人航行潜水器汇总统计 --> <!-- 无人航行潜水器汇总统计 -->
<el-col :xs="24" :sm="12" :md="8"> <el-col :xs="24" :sm="12" :md="8">
<el-card <el-card
class="stat-card" class="stat-card stat-card--nav stat-card--uuv"
shadow="hover" shadow="never"
@click.native="goToDetail('auv')" @click.native="goToDetail('auv')"
> >
<div class="card-content"> <div class="stat-card__inner">
<div class="card-icon"> <div class="stat-card__top">
<i class="el-icon-help"></i> <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>
<div class="card-info"> <div class="stat-card__head-text">
<h3 class="card-title">无人航行潜水器汇总统计</h3> <h3 class="stat-card__title">无人航行潜水器汇总统计</h3>
<p class="card-count">{{ auv.length }}</p> <p class="stat-card__meta">
<p class="card-desc"><span v-for="(item, index) in auv" :key="index">{{ item.report_name }}<span v-if="index !== auv.length - 1"></span></span> </p> <span class="stat-card__badge">{{ uuv.length }} 艘在册</span>
</p>
</div> </div>
</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>
<p class="stat-card__fleet" :title="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-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>
<p class="stat-card__fleet" :title="uuvNamesJoined">默认单位中国科学院深海科学与工程研究所 · 点击查看详情</p>
</div>
</el-card> </el-card>
</el-col> </el-col>
<!-- 基站汇总统计 --> <!-- 基站汇总统计 -->
<el-col :xs="24" :sm="12" :md="8"> <el-col :xs="24" :sm="12" :md="8">
<el-card <el-card class="stat-card stat-card--placeholder stat-card--station" shadow="never">
class="stat-card" <div class="stat-card__inner">
shadow="hover" <div class="stat-card__top">
> <div class="stat-card__icon-wrap" aria-hidden="true">
<div class="card-content"> <svg class="stat-card__icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<div class="card-icon"> <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" />
<i class="el-icon-office-building"></i> <path d="M12 8v5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
</div> </div>
<div class="card-info"> <div class="stat-card__head-text">
<h3 class="card-title">基站汇总统计</h3> <h3 class="stat-card__title">基站汇总统计</h3>
<p class="card-count no-data">暂无数据</p> <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>
</div> </div>
</el-card> </el-card>
@ -106,18 +234,24 @@
<!-- 着陆器汇总统计 --> <!-- 着陆器汇总统计 -->
<el-col :xs="24" :sm="12" :md="8"> <el-col :xs="24" :sm="12" :md="8">
<el-card <el-card class="stat-card stat-card--placeholder stat-card--lander" shadow="never">
class="stat-card" <div class="stat-card__inner">
shadow="hover" <div class="stat-card__top">
> <div class="stat-card__icon-wrap" aria-hidden="true">
<div class="card-content"> <svg class="stat-card__icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<div class="card-icon"> <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" />
<i class="el-icon-place"></i> <circle cx="12" cy="10" r="2.5" stroke="currentColor" stroke-width="1.5" />
</svg>
</div> </div>
<div class="card-info"> <div class="stat-card__head-text">
<h3 class="card-title">着陆器汇总统计</h3> <h3 class="stat-card__title">着陆器汇总统计</h3>
<p class="card-count no-data">暂无数据</p> <p class="stat-card__meta">
<p class="card-desc"></p> <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>
</div> </div>
</el-card> </el-card>
@ -127,9 +261,56 @@
</template> </template>
<script> <script>
import { reportList } from '../../api/statistics' import {
reportList,
shipTotal,
platformHov,
platformRov,
platformAuv,
hovUnit,
unitMember,
unitTotal
} from '../../api/statistics'
import { filterDictItems } from '../../utils/index' 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 { export default {
name: 'MainDashboard', name: 'MainDashboard',
data() { data() {
@ -137,15 +318,125 @@ export default {
ship: [], ship: [],
hov: [], hov: [],
rov: [], rov: [],
auv: [] 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.member_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
},
/**
* 参航科学家卡片三项指标 Uuv 详情页 banner 一致
* @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() { created() {
this.getTypeList() this.getTypeList()
this.loadSummaries()
}, },
methods: { methods: {
goToDetail(type) { goToDetail(type) {
this.$router.push(`/container/${type}`); this.$router.push(`/container/${type}`)
}, },
getTypeList() { getTypeList() {
reportList().then(res => { reportList().then(res => {
@ -155,32 +446,125 @@ export default {
this.auv = 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> </script>
<style scoped> <style scoped lang="scss">
.main-dashboard { .main-dashboard {
padding: 20px; padding: 24px 20px 32px;
background-color: #f0f7ff;
min-height: calc(100vh - 130px); min-height: calc(100vh - 130px);
background: #e9eef4;
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 28px;
} }
.header h1 { .header h1 {
color: #1a365d; color: #0f172a;
font-size: 28px; font-size: 28px;
margin-bottom: 10px; margin: 0 0 10px;
font-weight: 600; font-weight: 600;
letter-spacing: 0.02em;
} }
.header p { .header p {
color: #4a5568; color: #475569;
font-size: 16px; font-size: 16px;
margin: 0;
line-height: 1.5;
} }
.stats-grid { .stats-grid {
@ -189,98 +573,213 @@ export default {
.stat-card { .stat-card {
margin-bottom: 20px; margin-bottom: 20px;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 8px;
border: none;
height: 140px;
display: flex;
align-items: center;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}
.card-content {
display: flex;
align-items: center;
padding: 15px;
width: 100%;
}
.card-icon {
width: 70px;
height: 70px;
border-radius: 12px; border-radius: 12px;
background-color: #ebf8ff; 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; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-right: 20px;
flex-shrink: 0; flex-shrink: 0;
} }
.card-icon i { .stat-card--ship .stat-card__icon-wrap {
font-size: 36px; background: #eff6ff;
color: #3182ce; color: #1d4ed8;
} }
.card-info { .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; flex: 1;
} }
.card-title { .stat-metrics--loading .stat-metric {
font-size: 18px; opacity: 0.55;
font-weight: 600;
color: #2d3748;
margin-bottom: 8px;
} }
.card-count { .stat-metric {
font-size: 28px; padding: 12px 14px;
font-weight: 700; border-radius: 8px;
color: #3182ce; background: #f8fafc;
margin-bottom: 5px; border: 1px solid #f1f5f9;
} display: flex;
.card-desc {
font-size: 14px;
color: #718096;
line-height: 1.4;
}
.loading {
color: #a0aec0;
}
.no-data {
color: #e53e3e;
}
/* 响应式设计 */
@media (max-width: 768px) {
.stat-card {
height: auto;
}
.card-content {
flex-direction: column; flex-direction: column;
text-align: center; gap: 4px;
} }
.card-icon { .stat-metric__value {
margin-right: 0; font-size: 20px;
margin-bottom: 15px; 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 { @keyframes fadeInUp {
from { from {
opacity: 0; opacity: 0;
transform: translateY(20px); transform: translateY(12px);
} }
to { to {
opacity: 1; opacity: 1;
@ -289,13 +788,42 @@ export default {
} }
.stat-card { .stat-card {
animation: fadeInUp 0.5s ease forwards; animation: fadeInUp 0.45s ease forwards;
} }
.el-col:nth-child(1) .stat-card { animation-delay: 0.1s; } .el-col:nth-child(1) .stat-card {
.el-col:nth-child(2) .stat-card { animation-delay: 0.2s; } animation-delay: 0.05s;
.el-col:nth-child(3) .stat-card { animation-delay: 0.3s; } }
.el-col:nth-child(4) .stat-card { animation-delay: 0.4s; } .el-col:nth-child(2) .stat-card {
.el-col:nth-child(5) .stat-card { animation-delay: 0.5s; } animation-delay: 0.1s;
.el-col:nth-child(6) .stat-card { animation-delay: 0.6s; } }
.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> </style>