<template>
<div class="content" :class="isDark ? 'is-dark' : ''">
<div class="total-info">
<span class="name" :title="titleText">{{ titleText }}</span>
<span class="count">
{{ consumablesInfo.totalNum !== null ? consumablesInfo.totalNum : '--' }}
</span>
<span class="contrasts">
<img v-if="consumablesInfo.totalNumChange < 0" :src="arrowBottom" alt="" />
<img v-else :src="arrowTop" alt="" />
{{ consumablesInfo.totalNumChange !== null ? consumablesInfo.totalNumChange : '--' }}
</span>
</div>
<!-- 引导线开始 -->
<div class="out-line"></div>
<div class="out-line2"></div>
<div class="out-line3"></div>
<div class="out-line4"></div>
<div class="out-line5"></div>
<div class="out-line6"></div>
<div class="out-line7"></div>
<div class="out-line8"></div>
<div class="out-line10"></div>
<!-- 引导线结束 -->
<div class="online-info">
<div class="online-servers">
<span class="label" :title="$t('在线资产数')">
<span class="icon"></span>
{{ $t('在线资产数') }}
</span>
<span class="num">
{{ consumablesInfo.onlineNum !== null ? consumablesInfo.onlineNum : '--' }}
</span>
</div>
<div class="online-asset">
<span class="label" :title="$t('资产在线率')">{{ $t('资产在线率') }}</span>
<span class="num">
{{
consumablesInfo.onlineRate !== null
? (consumablesInfo.onlineRate * 100).toFixed(2)
: '--'
}}
<span v-if="consumablesInfo.onlineRate !== null">%</span>
</span>
</div>
</div>
<div class="offline-info">
<div class="offline-servers">
<span class="label" :title="$t('非在线资产数')">
<span class="icon"></span>
{{ $t('非在线资产数') }}
</span>
<span class="num">
{{
consumablesInfo.totalNum !== null
? consumablesInfo.totalNum - consumablesInfo.onlineNum
: '--'
}}
</span>
<span class="contrasts">
<img v-if="consumablesInfo.offlineAssetCntAdd < 0" :src="arrowBottom" alt="" />
<img v-else :src="arrowTop" alt="" />
{{
consumablesInfo.offlineAssetCntAdd !== null
? Math.abs(consumablesInfo.offlineAssetCntAdd)
: '--'
}}
</span>
</div>
<div class="offline-asset">
<span class="label" :title="$t('资产非在线率')">{{ $t('资产非在线率') }}</span>
<span class="num">
{{
consumablesInfo.onlineRate !== null
? ((1 - consumablesInfo.onlineRate) * 100).toFixed(2)
: '--'
}}
<span v-if="consumablesInfo.onlineRate !== null">%</span>
</span>
<span class="contrasts">
<img v-if="consumablesInfo.offlineAssetRateAdd < 0" :src="arrowBottom" alt="" />
<img v-else :src="arrowTop" alt="" />
{{
consumablesInfo.offlineAssetRateAdd !== null
? Math.abs(consumablesInfo.offlineAssetRateAdd)
: '--'
}}
<span v-if="consumablesInfo.offlineAssetRateAdd !== null">%</span>
</span>
</div>
</div>
<div class="status-info">
<div class="legend">
<div>
<span class="icon icon1"></span>
{{ $t('历史') }}
</div>
<div>
<span class="icon icon2"></span>
{{ $t('本月新增') }}
</div>
<div>
<span class="icon icon3"></span>
{{ $t('本月流出') }}
</div>
</div>
<div class="list-info">
<div v-for="(item, index) in statusList" :key="index" class="list" :class="item.nameEn">
<div class="top">
<div class="left">
<div
class="name"
:title="$t(item.name)"
:class="{ 'is-link': item.nameEn === 'invigorating' }"
@click="handleClick(item)"
>
{{ $t(item.name) }}
</div>
</div>
<div class="right">
<div class="count">
<span class="his-count" :style="getHisWidth(item)"></span>
<span
v-if="consumablesInfo[item.netWorth] > 0"
class="cur-add"
:style="getAddWidth(item)"
></span>
<span v-else class="outflow" :style="getOutWidth(item)"></span>
<span class="num">
{{ consumablesInfo[item.netWorth] > 0 ? '+' : '' }}
{{
consumablesInfo[item.netWorth] !== null ? consumablesInfo[item.netWorth] : '--'
}}
</span>
</div>
<div class="bottom">
<span class="total-count">
{{ $t('总量') }}:
{{ consumablesInfo[item.total] !== null ? consumablesInfo[item.total] : '--' }}
</span>
<span class="his">
{{ $t('历史') }}:
{{
consumablesInfo[item.total] !== null && consumablesInfo[item.netWorth] !== null
? Math.max(0, consumablesInfo[item.total] - consumablesInfo[item.netWorth])
: '--'
}}
</span>
<span class="net-worth" :title="$t('本月净值')">
{{ $t('本月净值') }}:
<i :style="{ color: consumablesInfo[item.netWorth] < 0 ? '#50d4ab' : '' }">
{{
consumablesInfo[item.netWorth] !== null
? consumablesInfo[item.netWorth]
: '--'
}}
</i>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, defineEmits, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import arrowTop from '@/assets/images/01.png';
import arrowBottom from '@/assets/images/02.png';
import { getConsumablesInfoAPI } from '@/apis/dcPlanned.js';
import { ElMessage } from 'element-plus';
import { useMapLevelDataManager } from '@/application/core/infra-layout/composables/useMapLevelDataManager';
import { usePropertyMetricsManager } from '@/application/core/infra-layout/composables/usePropertyMetricsManager';
const propertyMetricsManager = usePropertyMetricsManager();
const mapLevelDataManager = useMapLevelDataManager();
const { mapLevelCode } = mapLevelDataManager;
const { t } = useI18n();
interface StatusItem {
name: string;
nameEn: string;
total: keyof consumablesInfoVO;
netWorth: keyof consumablesInfoVO;
}
interface consumablesInfoVO {
onlineRate: number | null;
onlineRateChange: number | null;
totalNum: number | null;
totalNumChange: number | null;
onlineNum: number | null;
onlineNumChange: number | null;
vitalizeNum: number | null;
vitalizeNumChange: number | null;
transmitNum: number | null;
transmitNumChange: number | null;
constructingNum: number | null;
constructingNumChange: number | null;
exitNum: number | null;
exitNumChange: number | null;
[key: string]: number; // 动态访问兼容
}
const props = defineProps<{
isDark: boolean;
activeName: {
type: string;
default: 'opticalModules';
};
}>();
const store = useStore();
const maxCount = ref(0);
const consumablesInfo = ref<Partial<consumablesInfoVO>>({});
const titleText = computed(() => {
return props.activeName === 'cables' ? t('线缆数量') : t('低端光模块数量');
});
const statusList = ref<StatusItem[]>([
{
name: '在途',
nameEn: 'inTransit',
total: 'transmitNum',
netWorth: 'transmitNumChange',
},
{
name: '在建',
nameEn: 'underConstruction',
total: 'constructingNum',
netWorth: 'constructingNumChange',
},
{
name: '盘活中',
nameEn: 'invigorating',
total: 'vitalizeNum',
netWorth: 'vitalizeNumChange',
},
{
name: '退出',
nameEn: 'withdrawal',
total: 'exitNum',
netWorth: 'exitNumChange',
},
]);
// 获取概览信息
const getConsumablesInfo = () => {
const params = {
type: props.activeName === 'opticalModules' ? 'optical' : 'cables',
date: propertyMetricsManager.timeValue,
area: mapLevelCode.areaCode ? mapLevelCode.areaCode : '',
city: mapLevelCode.cityCode ? mapLevelCode.cityCode : '',
};
getConsumablesInfoAPI(params)
.then(res => {
if (res.success) {
if (res.data && Object.keys(res.data).length > 0) {
consumablesInfo.value = res.data;
gemaxData();
}
} else {
ElMessage.warning('暂无数据');
}
})
.catch((err: Error) => {
ElMessage.error(err.message);
});
};
// 计算最大值用于条形图比例
const gemaxData = () => {
const arr: number[] = [];
statusList.value.forEach(item => {
const total = consumablesInfo.value[item.total];
const netWorth = consumablesInfo.value[item.netWorth];
if (total !== null && netWorth !== null) {
const historyCount = total - netWorth;
arr.push(Math.abs(historyCount));
}
});
maxCount.value = Math.max(...arr, 1); // 至少为1避免除以0
};
// 条形图宽度计算
const getHisWidth = (item: StatusItem) => {
const total = consumablesInfo.value[item.total];
const netWorth = consumablesInfo.value[item.netWorth];
if (total === null || netWorth === null) {
return { width: '0%' };
}
const historyCount = total - netWorth;
const value = Math.abs(historyCount);
return { width: `${((value / maxCount.value) * 100).toFixed(2)}%` };
};
const getAddWidth = (item: StatusItem) => {
const value = Math.abs(Number(consumablesInfo.value[item.netWorth] ?? 0));
return { width: `${((value / maxCount.value) * 100).toFixed(2)}%` };
};
const getOutWidth = (item: StatusItem) => {
const value = Math.abs(Number(consumablesInfo.value[item.netWorth] ?? 0));
return { width: `${((value / maxCount.value) * 100).toFixed(2)}%` };
};
const emits = defineEmits(['detail-show']);
// 处理点击“盘活中”
const handleClick = (item: StatusItem) => {
if (item.nameEn === 'invigorating') {
emits('detail-show', true);
}
};
onMounted(() => {
getConsumablesInfo();
});
watch(
() => mapLevelCode,
() => getConsumablesInfo(),
{ deep: true }
);
watch(
() => propertyMetricsManager.timeValue,
() => getConsumablesInfo(),
{ deep: true }
);
watch(
() => props.activeName,
() => getConsumablesInfo(),
{ deep: true }
);
</script>
<style lang="less" scoped>
@lineImg: '@/assets/images/line-move.png';
:deep(.el-overlay-dialog) {
.el-dialog.is-dark {
.el-dialog__header {
margin-right: 0 !important;
}
.search-box {
.label {
color: #fff;
}
.el-select {
.select-trigger {
.el-input__wrapper {
background-color: #282b33;
color: #dfe1e6;
}
}
}
.el-input {
.el-input__wrapper {
background-color: #282b33;
.el-input__inner {
color: #dfe1e6;
}
}
}
}
.el-popper.el-select__popper {
background-color: #282b33;
}
.el-dialog__header,
.el-dialog__body {
background: #12151f;
}
.el-dialog__title {
color: #fff;
}
.dialog-box {
.label {
color: #fff;
}
}
.el-table.is-dark {
.el-table__inner-wrapper {
border-right: 1px solid #fff;
.el-table__header-wrapper {
.el-table__header {
th {
background: #3e4148;
color: #dfe1e6;
}
}
}
}
:deep(.el-scrollbar) {
.el-table__empty-block {
background: #33363d;
}
}
.el-table__body-wrapper {
.el-table__body tr:hover > td.el-table__cell {
background-color: #2e3445 !important;
}
.el-table__body {
tr {
background: #33363d;
.cell {
color: #dfe1e6;
}
}
}
}
}
}
}
.el-overlay-dialog {
:deep(.el-dialog) {
.el-dialog__header {
margin-right: 0;
}
}
}
.content.is-dark {
background: #282b33;
.left-content {
.bg-box {
background: #2e3445 !important;
border-bottom: 1px solid #505050;
color: #fff;
&.is-active {
background: #5e7ce0 !important;
filter: none !important;
}
}
}
.middle-content {
.type,
.num {
color: #fff;
}
}
.total-info {
background: #445266;
.name,
.contrasts {
color: #dfd8c4;
}
.count {
color: #fff;
}
}
.online-info,
.offline-info {
background: #3c4551;
.num {
color: #fff;
}
.label {
color: #adb0b8;
}
}
.status-info {
background: #2f343d;
border-radius: 3px;
}
}
.content {
font-size: 14px;
background: #fff;
padding: 20px;
position: relative;
margin-bottom: 50px;
border-radius: 8px;
.label {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
white-space: nowrap;
}
.name {
max-width: 60px;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
white-space: nowrap;
}
.net-worth {
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
white-space: nowrap;
}
.out-line {
background: url('@{lineImg}') left top no-repeat;
transform: translate(-16px, 111px) rotate(90deg);
width: 86px;
height: 3px;
position: absolute;
top: -3px;
left: -1px;
}
.out-line2 {
background: url('@{lineImg}') left top;
transform: translate(-16px, 111px) rotate(90deg);
width: 242px;
height: 3px;
position: absolute;
top: 35.5%;
left: -10.2%;
}
.out-line3 {
background: url('@{lineImg}') left top;
width: 24px;
height: 2px;
position: absolute;
top: 20.4%;
left: 5.5%;
}
.out-line4 {
background: url('@{lineImg}') left top;
width: 24px;
height: 2px;
position: absolute;
top: 32.3%;
left: 5.5%;
}
.out-line5 {
background: url('@{lineImg}') left top;
width: 18px;
height: 2px;
position: absolute;
top: 49.8%;
left: 10.6%;
}
.out-line6 {
background: url('@{lineImg}') left top;
width: 18px;
height: 2px;
position: absolute;
top: 61.6%;
left: 10.6%;
}
.out-line7 {
background: url('@{lineImg}') left top;
width: 18px;
height: 2px;
position: absolute;
top: 73.6%;
left: 10.6%;
}
.out-line8 {
background: url('@{lineImg}') left top;
width: 18px;
height: 2px;
position: absolute;
top: 85%;
left: 10.6%;
}
.out-line10 {
width: 1px;
background: #adb0b8;
height: 214px;
position: absolute;
top: 46%;
left: 24.2%;
}
.total-info {
background: linear-gradient(90deg, #e8f5ff, #e9effd);
padding: 14px;
margin-bottom: 10px;
display: flex;
align-items: center;
.name {
font-weight: bold;
color: #252b3a;
max-width: 120px;
}
.count {
color: #5e7ce0;
font-weight: bold;
margin: 0 10px;
}
.contrasts {
color: #575d6c;
font-size: 10px;
}
}
.online-info,
.offline-info {
background: #f6faff;
padding: 14px;
display: flex;
width: 97%;
margin-bottom: 12px;
margin-left: 15px;
> div {
width: 50%;
display: flex;
align-items: center;
}
.num {
font-weight: bold;
color: #252b3a;
margin: 0 10px;
}
.contrasts {
font-size: 10px;
color: #575d6c;
}
.icon {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 2px;
margin-right: 4px;
}
.online-servers {
.icon {
background: linear-gradient(90deg, #92dd99, #6bc374);
}
}
.offline-servers {
.icon {
background: linear-gradient(90deg, #afc5d3, #778e9e);
}
}
}
.status-info {
margin-left: 30px;
padding: 10px;
background: #f6faff;
.legend {
display: flex;
margin-bottom: 14px;
color: #959aa8;
margin-left: 15px;
> div {
margin-right: 20px;
font-size: 10px;
display: flex;
align-items: center;
.icon {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 2px;
margin-right: 4px;
&.icon1 {
background: linear-gradient(90deg, #76c0ff, #69a0ff);
}
&.icon2 {
background: linear-gradient(90deg, #d8c1ff, #c89eff);
}
&.icon3 {
background: #f0f5ff;
border: 1px dashed #69a0ff;
box-sizing: border-box;
}
}
}
}
}
.list-info {
margin-left: 15px;
.list {
margin-bottom: 14px;
&.invigorating {
cursor: pointer;
}
.top {
display: flex;
margin-bottom: 6px;
width: 100%;
.left {
width: 70px;
.name {
margin-top: 5px;
&.is-link {
color: #5e7ce0;
text-decoration: underline;
}
}
.days {
color: #959aa8;
font-size: 10px;
margin-top: 8px;
}
}
.right {
font-size: 12px;
color: #959aa8;
width: 100%;
.count {
color: #252b3a;
font-weight: bold;
display: flex;
width: 100%;
> span {
display: inline-block;
height: 24px;
width: 100px;
line-height: 24px;
padding-left: 8px;
}
.his-count {
background: linear-gradient(90deg, #76c0ff, #69a0ff);
}
.cur-add {
background: linear-gradient(90deg, #d8c1ff, #c89eff);
}
.outflow {
background: #f0f5ff;
color: #252b3a;
border: 1px dashed #69a0ff;
box-sizing: border-box;
border-left: 0;
}
}
.bottom {
margin-top: 5px;
font-size: 10px;
padding-left: 10px;
display: flex;
> span {
flex: 1;
}
i {
color: #de5076;
}
.total-count {
display: inline-block;
width: 40%;
font-style: normal;
}
.net-worth {
display: inline-block;
width: 40%;
}
}
}
}
}
}
}
.tooltip-info {
font-size: 12px !important;
h3 {
display: flex;
font-size: 12px;
justify-content: space-between;
padding-bottom: 4px;
}
.list {
p {
display: flex;
justify-content: space-between;
line-height: 24px;
color: #575d6c;
}
}
.outflow {
display: flex;
justify-content: space-between;
border-top: 1px solid #e4eaf3;
padding: 10px 0;
font-weight: bold;
}
}
.dialog-body {
.search-box {
margin-bottom: 15px;
.label {
margin-right: 10px;
}
}
.content {
display: flex;
min-height: 220px;
margin-bottom: 0;
.left-content {
width: 160px;
.list-box {
margin-top: 15px;
border: 1px solid #5e7ce0;
background: #5e7ce01a;
padding: 10px;
}
.list {
.bg-box {
background: #fff;
height: 30px;
padding: 0 10px;
margin-top: 3px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
position: relative;
.name {
font-size: 12px;
}
.icon {
font-family: fangsong;
font-size: 12px;
}
&.is-active {
width: 152px;
height: 32px;
color: #fff;
background-color: rgb(92, 153, 251);
filter: drop-shadow(rgba(179, 186, 210, 0.44) 0px 4px 2px);
border-radius: 4.5px 0px 0px 4.5px;
&::after {
content: '';
position: absolute;
top: 0;
right: -32px;
display: inline-block;
width: 0;
height: 0;
border-width: 16.2px;
border-style: solid;
border-color: transparent transparent transparent rgb(92, 153, 251);
}
}
}
}
}
.middle-content {
flex: 1;
position: relative;
.label-box {
position: absolute;
z-index: 1;
width: 100%;
right: 0;
top: -10px;
span {
display: inline-block;
font-size: 12px;
&.type {
position: absolute;
top: 0;
right: 27%;
z-index: 1;
}
&.num {
position: absolute;
top: 0;
right: 0;
z-index: 1;
}
}
}
}
}
}
</style>
帮我检查一下代码,看一下有什么不合理的地方
最新发布