You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

508 lines
13 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="dashboard-container">
<div class="bg-grid"></div>
<div class="bg-scan"><div class="scan-line"></div></div>
<DashboardHeader />
<main>
<div class="layout">
<el-row :gutter="10" class="main-row">
<el-col :span="5" class="col">
<div class="col-item col-item-overview">
<!-- 设备概况 -->
<DeviceOverview />
</div>
<div class="col-item col-item-payment">
<!-- Payment method -->
<PaymentMethod />
</div>
<div class="col-item col-item-extra">
<!-- 产量趋势 -->
<ProductionTrend />
</div>
</el-col>
<el-col :span="14" class="col">
<div class="center-shell">
<img class="dashboard-center-image" src="@/assets/imgs/dashboard_img.png" alt="dashboard" />
<!-- 上面4个 -->
<div
v-for="(item, index) in topDevices"
:key="'top-' + index"
class="device-card top-card"
:class="`device-card--${getDeviceStatusType(item.operatingStatus)}`"
:style="{ left: getTopLeft(index) }"
>
<div class="device-header">
<div class="header-left">
<span class="device-dot"></span>
<span class="device-name" :title="item.deviceName">{{ item.deviceName }}</span>
<el-tag
size="small"
effect="plain"
class="device-status-tag"
:class="`device-status-tag--${getDeviceStatusType(item.operatingStatus)}`"
>
{{ getDeviceStatusText(item.operatingStatus) }}
</el-tag>
</div>
<div class="header-right">
<span class="device-id">ID: {{ item.deviceId }}</span>
</div>
</div>
<div class="device-body">
<div v-for="(attr, aIndex) in item.attributes" :key="aIndex" class="device-row">
<span class="label" :title="attr.attributeName">{{ attr.attributeName }}:</span>
<span class="value">{{ attr.addressValue ?? '-' }} {{ attr.dataUnit || '' }}</span>
</div>
</div>
</div>
<!-- 下面4个 -->
<div
v-for="(item, index) in bottomDevices"
:key="'bottom-' + index"
class="device-card bottom-card"
:class="`device-card--${getDeviceStatusType(item.operatingStatus)}`"
:style="{ left: getBottomLeft(index) }"
>
<div class="device-header">
<div class="header-left">
<span class="device-dot"></span>
<span class="device-name" :title="item.deviceName">{{ item.deviceName }}</span>
<el-tag
size="small"
effect="plain"
class="device-status-tag"
:class="`device-status-tag--${getDeviceStatusType(item.operatingStatus)}`"
>
{{ getDeviceStatusText(item.operatingStatus) }}
</el-tag>
</div>
<div class="header-right">
<span class="device-id">ID: {{ item.deviceId }}</span>
</div>
</div>
<div class="device-body">
<div v-for="(attr, aIndex) in item.attributes" :key="aIndex" class="device-row">
<span class="label" :title="attr.attributeName">{{ attr.attributeName }}:</span>
<span class="value">{{ attr.addressValue ?? '-' }} {{ attr.dataUnit || '' }}</span>
</div>
</div>
</div>
</div>
</el-col>
<el-col :span="5" class="col">
<div class="col-item col-item-event">
<!-- 事件提醒 -->
<EventReminder />
</div>
<div class="col-item col-item-task">
<!-- 任务列表 -->
<TaskList />
</div>
<div class="col-item col-item-energy">
<!-- 能耗监测 -->
<EnergyMonitor />
</div>
</el-col>
</el-row>
</div>
</main>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { DeviceApi } from '@/api/iot/device'
import DashboardHeader from './components/DashboardHeader.vue'
import DeviceOverview from './components/DeviceOverview.vue'
import PaymentMethod from './components/PaymentMethod.vue'
import EventReminder from './components/EventReminder.vue'
import TaskList from './components/TaskList.vue'
import EnergyMonitor from './components/EnergyMonitor.vue'
import ProductionTrend from './components/ProductionTrend.vue'
const route = useRoute()
const goviewId = route.query.goviewId as string
const orgId = route.query.orgId
interface DeviceAttribute {
attributeName: string
addressValue?: number | string | null
dataUnit?: string | null
}
interface DeviceCardData {
deviceName: string
deviceId: number
attributes: DeviceAttribute[]
operatingStatus?: string
}
const topDevices = ref<DeviceCardData[]>([])
const bottomDevices = ref<DeviceCardData[]>([])
const getTopLeft = (index: number) => {
// 简单均匀分布,可根据实际需求微调
const positions = ['5%', '30%', '55%', '80%']
return positions[index] || '0'
}
const getBottomLeft = (index: number) => {
const positions = ['5%', '30%', '55%', '80%']
return positions[index] || '0'
}
const loadDeviceAttributes = async () => {
if (!goviewId) return
try {
const res = await DeviceApi.getDeviceAttributeBatchList({ goviewId, orgId })
const list = (res && Array.isArray(res) ? res : []) as any[]
const cards: DeviceCardData[] = list.map((d: any) => {
return {
deviceName: d.deviceName || 'Device',
deviceId: d.deviceId,
operatingStatus: d.operatingStatus,
attributes: (d.attributes || []).map((attr: any) => ({
attributeName: attr.attributeName || '-',
addressValue: attr.addressValue,
dataUnit: attr.dataUnit
}))
}
})
topDevices.value = cards.slice(0, 4)
bottomDevices.value = cards.slice(4, 8)
} catch (e) {
console.error(e)
}
}
const getDeviceStatusType = (status?: string) => {
if (!status) return 'info'
const s = String(status).toLowerCase()
if (s.includes('故障') || s === 'fault') return 'danger'
if (s.includes('待机') || s === 'standby') return 'primary'
if (s.includes('运行') || s === 'run' || s === 'running') return 'success'
if (s.includes('离线') || s === 'offline') return 'info'
return 'info'
}
const getDeviceStatusText = (status?: string) => {
if (!status) return '离线'
return status
}
onMounted(() => {
loadDeviceAttributes()
})
</script>
<style scoped>
@keyframes scanDown {
0% { transform: translateY(-100%); }
100% { transform: translateY(260%); }
}
.dashboard-container {
--bg: #050816;
--bg-deep: #020617;
--card-bg: rgb(15 23 42 / 86%);
--border: rgb(56 189 248 / 35%);
--text: #e5f0ff;
--muted: #94a3b8;
--primary: #38bdf8;
--accent: #22d3ee;
--blue: #1e90ff;
--green: #22c55e;
--purple: #8b5cf6;
--warn: #f59e0b;
--danger: #ef4444;
--gap: 10px;
--header-h: 86px;
position: relative;
display: flex;
width: 100%;
min-height: 100vh;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft Yahei", Arial, sans-serif;
color: var(--text);
background-color: var(--bg-deep);
background-image:
radial-gradient(circle at 20% 0, rgb(56 189 248 / 26%) 0, transparent 48%),
radial-gradient(circle at 80% 110%, rgb(129 140 248 / 22%) 0, transparent 52%),
linear-gradient(135deg, #020617 0%, #020617 45%, #020617 100%);
flex-direction: column;
}
.bg-grid {
position: absolute;
z-index: 0;
pointer-events: none;
background-image: linear-gradient(rgb(15 23 42 / 80%) 1px, transparent 1px),
linear-gradient(90deg, rgb(15 23 42 / 80%) 1px, transparent 1px);
background-size: 70px 70px;
opacity: 0.55;
inset: 0;
}
.bg-scan {
position: absolute;
z-index: 0;
overflow: hidden;
pointer-events: none;
inset: 0;
}
.scan-line {
position: absolute;
top: -40%;
left: 0;
width: 100%;
height: 40%;
background: radial-gradient(circle at 50% 0, rgb(56 189 248 / 38%), transparent 70%);
opacity: 0.5;
filter: blur(32px);
animation: scanDown 16s linear infinite;
}
main {
position: relative;
z-index: 1;
display: flex;
height: calc(100vh - var(--header-h));
min-height: 0;
padding: var(--gap);
box-sizing: border-box;
flex-direction: column;
gap: var(--gap);
}
.layout {
display: flex;
width: 100%;
height: 100%;
min-height: 0;
flex: 1;
flex-direction: column;
gap: var(--gap);
}
.main-row {
flex: 1;
height: 100%;
min-height: 0;
}
.col {
display: flex;
height: 100%;
min-height: 0;
flex-direction: column;
gap: var(--gap);
}
.col-item {
display: flex;
min-height: 0;
flex: 1;
}
.col-item :deep(> *) {
width: 100%;
height: 100%;
}
.center-shell {
position: relative; /* 用于定位内部绝对定位的设备卡片 */
display: flex;
min-height: 0;
padding: 10px;
background: rgb(2 6 23 / 18%);
border: 1px solid rgb(30 64 175 / 55%);
border-radius: 8px;
flex: 1;
align-items: center;
justify-content: center;
}
.device-card {
position: absolute;
width: 18%; /* 约占宽度的1/5留出间隔 */
min-width: 120px;
padding: 8px;
color: #fff;
background: rgb(15 23 42 / 85%);
border: 1px solid rgb(56 189 248 / 40%);
border-radius: 6px;
box-shadow: 0 0 10px rgb(0 0 0 / 50%);
}
.device-card--success {
border-color: var(--green);
box-shadow: 0 0 10px rgb(34 197 94 / 45%);
}
.device-card--primary {
border-color: var(--primary);
box-shadow: 0 0 10px rgb(56 189 248 / 45%);
}
.device-card--danger {
border-color: var(--danger);
box-shadow: 0 0 10px rgb(239 68 68 / 45%);
}
.device-card--info {
border-color: var(--muted);
box-shadow: 0 0 10px rgb(148 163 184 / 35%);
}
.top-card {
top: 5%;
}
.bottom-card {
bottom: 4%;
}
.device-header {
display: flex;
padding-bottom: 4px;
margin-bottom: 6px;
border-bottom: 1px solid rgb(56 189 248 / 30%);
align-items: center;
justify-content: space-between;
}
.header-left {
display: flex;
margin-right: 8px;
overflow: hidden;
align-items: center;
flex: 1;
}
.header-right {
flex-shrink: 0;
}
.device-dot {
width: 0;
height: 0;
margin-right: 6px;
border-color: transparent transparent transparent #38bdf8;
border-style: solid;
border-width: 4px 0 4px 6px;
flex-shrink: 0;
}
.device-name {
overflow: hidden;
font-size: 14px;
font-weight: bold;
color: #e5f0ff;
text-overflow: ellipsis;
white-space: nowrap;
}
.device-status-tag {
margin-left: 6px;
padding: 0 6px;
font-size: 11px;
border-radius: 999px;
}
.device-status-tag--success {
--el-tag-text-color: var(--green);
--el-tag-border-color: rgb(34 197 94 / 60%);
--el-tag-bg-color: rgb(22 163 74 / 15%);
}
.device-status-tag--primary {
--el-tag-text-color: var(--primary);
--el-tag-border-color: rgb(56 189 248 / 60%);
--el-tag-bg-color: rgb(37 99 235 / 18%);
}
.device-status-tag--danger {
--el-tag-text-color: var(--danger);
--el-tag-border-color: rgb(239 68 68 / 60%);
--el-tag-bg-color: rgb(185 28 28 / 18%);
}
.device-status-tag--info {
--el-tag-text-color: var(--muted);
--el-tag-border-color: rgb(148 163 184 / 60%);
--el-tag-bg-color: rgb(30 41 59 / 60%);
}
.device-id {
font-size: 12px;
color: #94a3b8;
}
.device-body {
min-height: 60px; /* 固定高度,内容不足留白,内容多则滚动 */
height: 7vh;
padding-right: 4px; /* 防止滚动条遮挡内容 */
overflow-y: auto; /* 启用垂直滚动 */
font-size: 12px;
}
/* 滚动条样式 */
.device-body::-webkit-scrollbar {
width: 4px;
}
.device-body::-webkit-scrollbar-thumb {
background: rgb(56 189 248 / 30%);
border-radius: 2px;
}
.device-body::-webkit-scrollbar-track {
background: transparent;
}
.device-row {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
line-height: 1.4;
}
.device-row .label {
max-width: 60%;
margin-right: 8px;
overflow: hidden;
color: #94a3b8;
text-overflow: ellipsis;
white-space: nowrap;
}
.device-row .value {
overflow: hidden;
color: #cbd5e1;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.dashboard-center-image {
max-width: 100%;
max-height: 75%;
object-fit: contain;
}
/* Define CSS Variables locally for this dashboard */
</style>