This commit is contained in:
xiaozhiyong
2026-06-10 09:39:21 +08:00
parent 93ccdace0f
commit f006955a77
10 changed files with 853 additions and 252 deletions

View File

@@ -1,5 +1,13 @@
import service from '@/utils/request'
// 网关
export const getDeviceGatewayListByPage = (data) => {
return service({
url: '/device/getDeviceGatewayListByPage',
method: 'post',
data: data
})
}
// 设备
export const getDeviceListByPage = (data) => {
return service({
url: '/device/getDeviceListByPage',
@@ -15,7 +23,7 @@ export const deviceOperation = (data) => {
data: data
})
}
// 曲线
export const getDeviceDetailsListByPage = (data) => {
return service({
url: '/device/getDeviceDetailsListByPage',
@@ -23,3 +31,20 @@ export const getDeviceDetailsListByPage = (data) => {
data: data
})
}
// 报警
export const getAlarmRecordListByPage = (data) => {
return service({
url: '/device/getAlarmRecordListByPage',
method: 'post',
data: data
})
}
// 报警
export const getCircuitBreakerDetail = (data) => {
return service({
url: '/device/getCircuitBreakerDetail',
method: 'post',
data: data
})
}

View File

@@ -1,9 +0,0 @@
import service from '@/utils/request'
export const getDeviceDetailsListByPage = (data) => {
return service({
url: '/device/getDeviceDetailsListByPage',
method: 'post',
data: data
})
}

View File

@@ -15,6 +15,7 @@
"/src/view/equipment/index.vue": "Equipment",
"/src/view/equipment/list/components/detail/components/alarm/index.vue": "Index",
"/src/view/equipment/list/components/detail/components/info/index.vue": "Index",
"/src/view/equipment/list/components/detail/components/line/index.vue": "Index",
"/src/view/equipment/list/components/detail/components/trend/index.vue": "Index",
"/src/view/equipment/list/components/detail/index.vue": "Index",
"/src/view/equipment/list/components/list/index.vue": "Index",

View File

@@ -90,7 +90,7 @@
})
if (table.code === 0) {
tableData.value = table.data.list || []
total.value = table.data.total || 0
// total.value = table.data.total || 0
return
}
} catch (e) {

View File

@@ -1,79 +1,185 @@
<template>
<div class="detail-content">
<div class="detail-card">
<h3>基本信息</h3>
<div class="detail-row">
<span class="detail-label">设备ID</span>
<span class="detail-value">{{ device?.ID || '未设置' }}</span>
<!-- <div class="cards">
<div class="detail-card">
<h3>基本信息</h3>
<div class="detail-row">
<span class="detail-label">设备号</span>
<span class="detail-value">{{ device?.gatewayId || '未设置' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">经度</span>
<span class="detail-value">{{ device?.gatewayLong || '未设置' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">纬度</span>
<span class="detail-value">{{ device?.gatewayLat || '未设置' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">imei</span>
<span class="detail-value">{{ device?.imei || '未设置' }}</span>
</div>
</div>
<div class="detail-row">
<span class="detail-label">网关ID</span>
<span class="detail-value">{{ device?.gatewayId || '未设置' }}</span>
<div class="detail-card">
<h3>状态信息</h3>
<div class="detail-row">
<span class="detail-label">网络状态</span>
<span class="detail-value">
<el-tag :type="device?.netStatus == 1 ? 'primary' : 'info'">
{{ device?.netStatus == 1 ? '在线' : '离线' }}
</el-tag>
</span>
</div>
</div>
<div class="detail-row">
<span class="detail-label">设备类型名称</span>
<span class="detail-value">{{ device?.cbTypeName || '未设置' }}</span>
<div class="detail-card">
<h3>其他信息</h3>
<div class="detail-row">
<span class="detail-label">安装时间</span>
<span class="detail-value">{{ device?.createTime || '未设置' }}</span>
</div>
</div>
<div class="detail-row">
<span class="detail-label">网关mac</span>
<span class="detail-value">{{ device?.gatewayMac || '未设置' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">协议</span>
<span class="detail-value">{{ device?.protocol || '未设置' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">品牌</span>
<span class="detail-value">{{ device?.brand || '未设置' }}</span>
</div> -->
<div class="info-box">
<p class="title">基础信息</p>
<div class="details">
<el-image src="https://v5.snd02.com/upload/icon/UN2.png" style="width: 150px; height: 150px; cursor: pointer" />
<div>
<p>
<span>设备号</span>
<span>{{ device?.gatewayId || '未设置' }}</span>
</p>
<p>
<span>网络状态</span>
<span
><el-tag :type="device?.netStatus == 1 ? 'primary' : 'info'">
{{ device?.netStatus == 1 ? '在线' : '离线' }}
</el-tag></span
>
</p>
<p>
<span>经度</span>
<span>{{ device?.gatewayLong || '未设置' }}</span>
</p>
<p>
<span>纬度</span>
<span>{{ device?.gatewayLat || '未设置' }}</span>
</p>
</div>
<div>
<p>
<span>imei</span>
<span>{{ device?.imei || '未设置' }}</span>
</p>
<p>
<span>地址</span>
<span>{{ device?.gatewayAddress || '未设置' }}</span>
</p>
</div>
</div>
</div>
<div class="detail-card">
<h3>状态信息</h3>
<div class="detail-row">
<span class="detail-label">设备状态</span>
<span class="detail-value">
<el-tag v-if="device?.cbTypeName !== 'T30'" :type="device?.deviceStatus == 1 ? 'success' : 'danger'">
{{ device?.deviceStatus == 1 ? '合闸' : '分闸' }}
</el-tag>
<span v-else>-</span>
</span>
</div>
<div class="detail-row">
<span class="detail-label">锁定状态</span>
<span class="detail-value">
<el-tag :type="['success', 'info', 'danger'][device?.lockStatus]">
{{ ['正常', '远程合闸禁止', '锁定'][device?.lockStatus] }}
</el-tag>
</span>
</div>
<div class="detail-row">
<span class="detail-label">网络状态</span>
<span class="detail-value">
<el-tag :type="device?.netStatus == 1 ? 'primary' : 'info'">
{{ device?.netStatus == 1 ? '在线' : '离线' }}
</el-tag>
</span>
</div>
</div>
<div class="detail-card">
<h3>其他信息</h3>
<div class="detail-row">
<span class="detail-label">安装时间</span>
<span class="detail-value">{{ device?.createTime || '未设置' }}</span>
<div class="alarms-box">
<p class="title">报警列表</p>
<div>
<el-table :data="tableData" style="width: 100%" stripe>
<el-table-column prop="CreatedAt" label="报警时间" width="180" />
<el-table-column prop="type" label="报警类型" width="120">
<template #default="{ row }">
<el-tag :type="typeMap.find((item) => item.value == row.warnType)?.type || 'info'">{{
typeMap.find((item) => item.value == row.warnType)?.label || row.warnType
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="level" label="报警等级" width="100">
<template #default="{ row }">
<el-tag :type="levelMap.find((item) => item.value == row.alarmLevel)?.type || 'info'" effect="dark">{{
levelMap.find((item) => item.value == row.alarmLevel)?.label || row.alarmLevel
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="originalContent" label="报警描述">
<template #default="{ row }">
{{ row.remark || '-' }}
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
import { ref, onMounted } from 'vue'
import * as serve from '@/api/equipment/list'
const props = defineProps({
device: {
type: Object,
default: () => null
}
})
const typeMap = [
{
value: '45',
type: 'danger',
label: '设备事件'
},
{
value: '4f',
type: 'danger',
label: '操作事件'
}
]
const levelMap = [
{
value: '0',
type: 'primary',
label: '记录'
},
{
value: '1',
type: 'warning',
label: '提醒'
},
{
value: '2',
type: 'danger',
label: '警告'
}
]
const params = ref({
page: 1,
pageSize: 10,
sortBy: 'CreatedAt',
desc: true
})
const tableData = ref([])
onMounted(() => {
// console.log('123123')
// getAlarmRecordListByPage()
getAlarmRecordListByPage()
})
const getAlarmRecordListByPage = async () => {
// console.log(123)
const gatewayMac = props.device?.gatewayMac
if (gatewayMac) {
try {
const table = await serve.getAlarmRecordListByPage({ gatewayMac, ...params.value })
if (table.code === 0) {
tableData.value = table.data.list || []
// total.value = table.data.total || 0
return
}
} catch (e) {
console.error('获取报警记录失败', e)
}
} else {
console.warn('[Alarm] device 为空或没有 ID 字段')
}
}
</script>
<style lang="scss" scoped>
@@ -83,43 +189,94 @@
gap: 20px;
}
.detail-card {
background-color: var(--el-bg-color-overlay, #ffffff);
border-radius: 8px;
padding: 24px;
border: 1px solid var(--el-border-color, #e4e7ed);
// .cards {
// display: flex;
// // flex-direction: column;
// gap: 5px;
// }
h3 {
// .detail-card {
// background-color: var(--el-bg-color-overlay, #ffffff);
// border-radius: 8px;
// padding: 24px;
// border: 1px solid var(--el-border-color, #e4e7ed);
// flex: 1;
// h3 {
// font-size: 16px;
// font-weight: 600;
// color: var(--el-text-color-primary, #303133);
// margin: 0 0 16px 0;
// padding-bottom: 12px;
// border-bottom: 1px solid var(--el-border-color-lighter, #ebeef5);
// }
// }
// .detail-row {
// display: flex;
// align-items: center;
// padding: 12px 0;
// border-bottom: 1px dashed var(--el-border-color-lighter, #ebeef5);
// &:last-child {
// border-bottom: none;
// }
// }
// .detail-label {
// width: 120px;
// color: var(--el-text-color-regular, #606266);
// font-size: 14px;
// flex-shrink: 0;
// }
// .detail-value {
// flex: 1;
// color: var(--el-text-color-primary, #303133);
// font-size: 14px;
// }
.alarms-box,
.info-box {
@apply p-4 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded my-2 rounded-lg;
padding-bottom: 40px;
.title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary, #303133);
margin: 0 0 16px 0;
// margin: 0 0 16px 0;
padding-bottom: 12px;
border-bottom: 1px solid var(--el-border-color-lighter, #ebeef5);
// border-bottom: 1px solid var(--el-border-color-lighter, #ebeef5);
}
}
.detail-row {
.alarms-box {
> div {
margin-top: 15px;
padding-left: 25px;
}
}
.info-box .details {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px dashed var(--el-border-color-lighter, #ebeef5);
&:last-child {
border-bottom: none;
margin-top: 15px;
padding-left: 25px;
// flex-direction: column;
gap: 40px;
> div {
display: flex;
flex-direction: column;
width: 30%;
gap: 20px;
> p {
> span {
font-size: 14px;
&:nth-child(1) {
font-weight: 600;
color: var(--el-text-color-regular, #606266);
}
&:nth-child(2) {
color: var(--el-text-color-regular, #303133);
}
}
}
// justify-content: space-around;
}
}
.detail-label {
width: 120px;
color: var(--el-text-color-regular, #606266);
font-size: 14px;
flex-shrink: 0;
}
.detail-value {
flex: 1;
color: var(--el-text-color-primary, #303133);
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,142 @@
<template>
<div class="line-content">
<div class="detail-card">
<h3>线路列表</h3>
<el-table :data="tableData" style="width: 100%" height="400" stripe>
<el-table-column type="selection" width="55" />
<el-table-column prop="lineName" label="线路名称" width="120">
<template #default="{ row }">
{{ row.nodeNumber == 0 ? '总路' : `线路${row.nodeNumber}` }}
</template>
</el-table-column>
<el-table-column prop="lineCode" label="接线方式" width="120">
<template #default="{ row }">
{{ row.nodeNumber == 0 ? '进线直连' : '总路' }}
</template>
</el-table-column>
<el-table-column prop="nodeNumber" label="地址码" width="120" />
<el-table-column prop="status" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="row.deviceStatus === 1 ? 'danger' : 'success'">
{{ row.deviceStatus === 1 ? '合闸' : '分闸' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="['success', 'info', 'danger'][row.lockStatus]">
{{ ['正常', '远程合闸禁止', '锁定'][row.lockStatus] }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" />
<el-table-column label="操作" :min-width="appStore.operateMinWith" fixed="right">
<template #default="{ row }">
<el-button v-if="row.deviceStatus == 1" type="success" link icon="SortUp" @click="changeStatus(row)"
>分闸</el-button
>
<el-button v-else type="danger" link icon="SortDown" @click="changeStatus(row)">合闸</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script setup>
import * as serve from '@/api/equipment/list.js'
import { ref, watch, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useAppStore } from '@/pinia'
const appStore = useAppStore()
const props = defineProps({
device: {
type: Object,
default: () => null
}
})
const tableData = ref([])
const params = ref({
page: 1,
pageSize: 50,
sortBy: 'CreatedAt',
desc: true
})
onMounted(() => {
getDeviceListByPage()
})
const getDeviceListByPage = async () => {
const gatewayMac = props.device?.gatewayMac
if (gatewayMac) {
try {
const res = await serve.getDeviceListByPage({
gatewayMac,
...params.value
})
if (res.code === 0) {
tableData.value = res.data.list || []
// total.value = res.data.total || 0
}
} catch (e) {
console.error('获取线路列表失败', e)
}
}
}
// 合分闸
const changeStatus = (row) => {
ElMessageBox.confirm(row.deviceStatus == 1 ? '确认分闸吗?' : '确认合闸吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await serve.deviceOperation({
id: row.ID,
gatewayId: row.gatewayId,
para: row.deviceStatus == 1 ? '0xA2' : '0xA1'
})
if (res.code === 0) {
ElMessage.success('操作成功')
getDeviceListByPage()
}
})
}
</script>
<style lang="scss" scoped>
.detail-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.detail-card {
background-color: var(--el-bg-color-overlay, #ffffff);
border-radius: 8px;
padding: 24px;
border: 1px solid var(--el-border-color, #e4e7ed);
h3 {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary, #303133);
margin: 0 0 16px 0;
padding-bottom: 12px;
border-bottom: 1px solid var(--el-border-color-lighter, #ebeef5);
}
}
.gva-pagination {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div ref="chartContainerRef" class="trend-content">
<div class="trend-toolbar">
<!-- <div class="trend-toolbar">
<span class="toolbar-label">时间范围</span>
<el-date-picker
v-model="dateRange"
@@ -12,31 +12,140 @@
value-format="YYYY-MM-DD HH:mm:ss"
@change="handleDateChange"
/>
<!-- <el-button type="primary" icon="Refresh" @click="resetDateRange">重置</el-button> -->
</div>
<el-row v-if="isReady" :gutter="16">
<el-col v-for="(item, index) in chartList" :key="item.key" :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
<div class="chart-card">
<div class="chart-header">
<span class="chart-title">{{ item.title }}</span>
<span class="chart-unit">{{ item.unit }}</span>
</div> -->
<div class="left">
<div class="title">
<span>线路列表</span>
<el-button :disabled="!checkedDevices.length" plain size="small" type="success">一键分闸</el-button>
<el-button style="margin-left: 3px" :disabled="!checkedDevices.length" plain size="small" type="danger"
>一键合闸</el-button
>
</div>
<div>
<el-checkbox v-model="checkAll" :indeterminate="isIndeterminate" @change="handleCheckAllChange">
全选
</el-checkbox>
<el-checkbox-group v-model="checkedDevices" @change="handleCheckedDevicesChange">
<div
class="ouliy"
:class="{ active: currentCheck == index }"
v-for="(item, index) in deviceList"
:key="item.id"
>
<el-checkbox :value="item.id">
<div class="line-item" @click.stop.prevent="handleClick(item, index)">
<span>{{ item.nodeNumber == 0 ? '总路' : `线路${index + 1}` }}</span>
<span>{{ `${item.gatewayMac}` }}</span>
<el-tag v-if="item.status == 1" type="danger" size="small" effect="light"> 合闸 </el-tag>
<el-tag v-else type="success" size="small" effect="light"> 分闸 </el-tag>
</div>
</el-checkbox>
</div>
</el-checkbox-group>
</div>
</div>
<div class="right">
<div class="top">
<p class="title">基本信息</p>
<div class="info">
<div class="item">
<div>
<span>设备名称</span>
<span>{{
circuitBreaker.device?.nodeNumber == 0 ? '总路' : `线路${circuitBreaker.device?.nodeNumber}`
}}</span>
</div>
<div>
<span>线路ID</span>
<span>{{ `${circuitBreaker.device?.gatewayMac}-${circuitBreaker.device?.nodeNumber}` }}</span>
</div>
<div>
<span>总功率</span>
<span>{{ circuitBreaker.latestTelemetry?.power || 0 }}</span>
</div>
<div>
<span>机身温度()</span>
<span>{{ circuitBreaker.latestTelemetry?.internalTemperature || 0 }}</span>
</div>
<div>
<span>漏保档位</span>
<span></span>
</div>
<div>
<span>电气子节点</span>
<span>点击展开</span>
</div>
<div>
<span>警情状态</span>
<span>--</span>
</div>
</div>
<div class="item">
<div>
<span>线路状态</span>
<span
><el-tag v-if="circuitBreaker.device?.deviceStatus == 1" type="danger" size="small" effect="light">
合闸
</el-tag>
<el-tag v-else type="success" size="small" effect="light"> 分闸 </el-tag></span
>
</div>
<div>
<span>地址码</span>
<span>{{ circuitBreaker.latestTelemetry?.nodeNumber || '--' }}</span>
</div>
<div>
<span>环境温度</span>
<span>--</span>
</div>
</div>
<div class="other" style="width: 100%">
<div>
<el-button :disabled="circuitBreaker.device?.deviceStatus == 1" type="danger" plain @click="changeStatus"
>合闸</el-button
>
<el-button :disabled="circuitBreaker.device?.deviceStatus == 0" type="success" plain @click="changeStatus"
>分闸</el-button
>
</div>
<el-table :data="realTimeData" border>
<el-table-column prop="text" label="实时数据" />
<el-table-column prop="value" label="相线" />
</el-table>
</div>
<v-chart class="chart" :option="item.option" autoresize />
</div>
</el-col>
</el-row>
</div>
<div class="bottom">
<div>
<p class="title">运行趋势</p>
</div>
<el-row v-if="isReady">
<el-col v-for="(item, index) in chartList" :key="item.key" :span="24">
<div class="chart-card">
<div class="chart-header">
<span class="chart-title">{{ item.title }}</span>
<span class="chart-unit">{{ item.unit }}</span>
</div>
<v-chart class="chart" :option="item.option" autoresize />
</div>
</el-col>
</el-row>
</div>
</div>
</div>
</template>
<script setup>
import * as serve from '@/api/equipment/list.js'
import { ref, nextTick, onMounted, onBeforeUnmount, watch } from 'vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart } from 'echarts/charts'
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
import VChart from 'vue-echarts'
import * as serve from '@/api/equipment/list.js'
import { ElMessage, ElMessageBox } from 'element-plus'
use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
const props = defineProps({
@@ -46,12 +155,55 @@
}
})
const currentCheck = ref(0)
const deviceList = ref([])
// const circuitBreaker = ref({})
const handleClick = (item, index) => {
currentCheck.value = index
console.log(item, index)
getDeviceDetailsListByPage()
getCircuitBreakerDetail()
}
// 容器 ref
const chartContainerRef = ref(null)
// 容器就绪状态:父级用 v-show 切换时,初始为 display:None宽度为 0需要等容器有尺寸后再渲染图表
const isReady = ref(false)
let resizeObserver = null
onMounted(async () => {
if (chartContainerRef.value) {
nextTick(checkContainerReady)
resizeObserver = new ResizeObserver(() => {
checkContainerReady()
})
resizeObserver.observe(chartContainerRef.value)
}
await getDeviceListByPage()
getCircuitBreakerDetail()
getDeviceDetailsListByPage()
})
// 左侧设备(线路)列表
const getDeviceListByPage = async () => {
const gatewayMac = props.device?.gatewayMac
if (gatewayMac) {
const res = await serve.getDeviceListByPage({
gatewayMac,
page: 1,
pageSize: 50,
sortBy: 'CreatedAt',
desc: true
})
if (res.code === 0) {
deviceList.value = res.data.list || []
// total.value = res.data.total || 0
}
}
}
// 检测容器是否已具备尺寸
const checkContainerReady = () => {
if (isReady.value || !chartContainerRef.value) return
@@ -88,25 +240,41 @@
}
return ''
}
const circuitBreaker = ref([])
const realTimeData = ref([])
const getTableData = async () => {
// 如果没有 device 或 ID不请求
if (!props.device) {
console.warn('[Trend] device 为空,跳过请求')
return
}
const deviceId = props.device.ID || props.device.id
if (!deviceId) {
console.warn('[Trend] device 没有 ID 字段')
return
// 基本信息
const getCircuitBreakerDetail = async () => {
const ID = deviceList.value[currentCheck.value].ID
// console.log(deviceId)
const res = await serve.getCircuitBreakerDetail({
ID
})
if (res.code === 0) {
const arr = [
{ label: 'current', text: '电流(A)' },
{ label: 'voltage', text: '电压' },
{ label: 'internalTemperature', text: '温度' },
{ label: 'powerFactor', text: '功率' }
]
if (res.data.latestTelemetry) {
arr.forEach((item) => {
item.value = res.data.latestTelemetry[item.label] || '--'
})
realTimeData.value = arr
}
circuitBreaker.value = res.data
console.log('circuitBreaker', circuitBreaker.value)
}
}
// echarts图
const getDeviceDetailsListByPage = async () => {
if (!dateRange.value || dateRange.value.length !== 2) {
console.warn('[Trend] 日期范围无效')
return
}
// 格式化时间
// const startTime = formatDateTime(dateRange.value[0])
// const endTime = formatDateTime(dateRange.value[1])
const deviceId = deviceList.value[currentCheck.value].id
try {
const result = await serve.getDeviceDetailsListByPage({
page: 1,
@@ -119,12 +287,8 @@
order: 'desc',
desc: true
})
if (result.code === 0) {
// 提取数据
const list = result.data.list || []
// 调试:打印第一条数据的所有字段
const timeData = []
const chartData = {
voltage: [], // 电压V
@@ -137,7 +301,6 @@
list.forEach((item) => {
timeData.push(item.CreatedAt)
// 兼容多种命名风格PascalCase / camelCase
chartData.voltage.push(item.voltage)
chartData.leakageCurrent.push(item.leakageCurrent)
chartData.cumulativeElectricity.push(item.cumulativeElectricity)
@@ -145,42 +308,24 @@
chartData.internalTemperature.push(item.internalTemperature)
chartData.powerFactor.push(item.powerFactor)
})
console.log('[Trend] X轴时间数据:', timeData)
console.log('[Trend] 处理后的图表数据:', chartData)
// 更新图表
updateChartData(timeData, chartData)
}
} catch (e) {}
}
// 监听 device 变化,重新请求数据
watch(
() => props.device,
(newDevice, oldDevice) => {
if (newDevice && (!oldDevice || newDevice.ID !== oldDevice.ID)) {
getTableData()
}
nextTick(() => {
triggerResize()
})
},
{ deep: true, immediate: false }
)
onMounted(() => {
// 父级用 v-show 切换 Tab组件挂载时可能是 display:None尺寸为 0
// 需要等容器有实际尺寸后再渲染 v-chart否则 ECharts 初始化时会报
// "Can't get DOM width or height"
if (chartContainerRef.value) {
// 先检查一次(处理初始就是可见的情况)
nextTick(checkContainerReady)
// 监听容器尺寸变化:从 0 变为有尺寸时(例如切到当前 Tab触发 isReady
resizeObserver = new ResizeObserver(() => {
checkContainerReady()
})
resizeObserver.observe(chartContainerRef.value)
}
})
// watch(
// () => props.device,
// (newDevice, oldDevice) => {
// if (newDevice && (!oldDevice || newDevice.ID !== oldDevice.ID)) {
// getDeviceDetailsListByPage()
// }
// nextTick(() => {
// triggerResize()
// })
// },
// { deep: true, immediate: false }
// )
onBeforeUnmount(() => {
if (resizeObserver) {
@@ -204,29 +349,11 @@
// 触发图表 resize
nextTick(() => {
// triggerResize()
getTableData()
getDeviceDetailsListByPage()
})
}
}
// 重置日期
const resetDateRange = () => {
dateRange.value = []
nextTick(() => {
triggerResize()
})
}
// 生成模拟数据
const generateMockData = () => {
const hours = []
for (let i = 0; i < 24; i++) {
hours.push(`${i}:00`)
}
return hours
}
// 基础折线图配置
// data: 数据数组xAxis: X 轴数据(可选)
const createLineOption = (name, color, data = [], xAxis = []) => {
@@ -243,7 +370,7 @@
},
xAxis: {
type: 'category',
data: xAxis.length ? xAxis : generateMockData(),
data: xAxis,
boundaryGap: false,
axisLine: {
lineStyle: {
@@ -343,6 +470,30 @@
}
])
// 合分闸
const changeStatus = () => {
const row = circuitBreaker.value.device || {}
if (!row.ID) {
ElMessage.error('请选择设备')
return
}
ElMessageBox.confirm(row.deviceStatus == 1 ? '确认分闸吗?' : '确认合闸吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await serve.deviceOperation({
id: row.ID,
gatewayId: row.gatewayId,
para: row.deviceStatus == 1 ? '0xA2' : '0xA1'
})
if (res.code === 0) {
ElMessage.success('操作成功')
getDeviceGatewayListByPage()
}
})
}
// 更新图表数据
const updateChartData = (timeData, chartData) => {
chartList.value.forEach((item) => {
@@ -350,11 +501,125 @@
item.option = createLineOption(item.title, item.color, seriesData, timeData)
})
}
// 多选
const checkAll = ref(false)
const isIndeterminate = ref(false)
const checkedDevices = ref([])
const handleCheckAllChange = (val) => {
checkedDevices.value = val ? deviceList.value.map((item) => item.id) : []
isIndeterminate.value = false
}
const handleCheckedDevicesChange = (value) => {
const checkedCount = value.length
checkAll.value = checkedCount === deviceList.value.length
isIndeterminate.value = checkedCount > 0 && checkedCount < deviceList.value.length
}
</script>
<style lang="scss" scoped>
.trend-content {
padding: 0;
// @apply text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded my-2 rounded-lg
display: flex;
// flex-direction: column;
gap: 10px;
box-sizing: border-box;
.left {
@apply bg-white p-4 text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded rounded-lg
width: 270px;
.title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary, #303133);
> span {
margin-right: 50px;
}
// margin: 0 0 16px 0;
// padding-bottom: 12px;
// border-bottom: 1px solid var(--el-border-color-lighter, #ebeef5);
}
.line-item {
> span {
&:nth-child(1) {
display: inline-block;
width: 40px;
}
&:nth-child(2) {
display: inline-block;
width: 140px;
margin-right: 15px;
}
}
}
.ouliy {
padding: 5px;
border-radius: 5px;
&.active {
background-color: var(--el-color-primary-light-9);
}
&:hover {
background-color: var(--el-color-primary-light-8);
}
}
}
.right {
height: calc(100vh - 6rem - 120px);
flex: 1;
overflow: auto;
.top {
@apply bg-white p-4 text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded rounded-lg;
margin-bottom: 10px;
.title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary, #303133);
}
.info {
display: flex;
// flex-direction: column;
gap: 100px;
}
.item {
padding: 12px 12px 0 12px;
> div {
height: 30px;
white-space: nowrap;
> span {
&:nth-child(1) {
display: inline-block;
width: 120px;
font-size: 14px;
color: var(--el-text-color-regular, #ddd);
}
&:nth-child(2) {
font-size: 14px;
color: var(--el-text-color-primary, #303133);
}
}
}
}
.other {
:deep(.el-table .el-table__cell) {
padding: 2px;
}
div {
&:nth-child(1) {
display: flex;
justify-content: flex-end;
margin-bottom: 20px;
}
}
}
}
.bottom {
@apply bg-white p-4 text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded rounded-lg;
.title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary, #303133);
}
}
}
}
.trend-toolbar {
@@ -379,7 +644,6 @@
background-color: var(--el-bg-color-overlay, #ffffff);
border-radius: 8px;
padding: 16px;
border: 1px solid var(--el-border-color, #e4e7ed);
margin-bottom: 16px;
transition: all 0.3s ease;

View File

@@ -1,25 +1,36 @@
<template>
<div class="device-detail-page">
<div class="detail-header">
<!-- <div class="detail-header">
<el-button icon="ArrowLeft" circle @click="handleBack" />
<span class="detail-title">设备信息</span>
</div> -->
<div class="tabs-box">
<el-button icon="ArrowLeft" circle @click="handleBack" />
<span class="title">设备信息{{ device?.gatewayMac }}</span>
<el-radio-group v-model="activeMenu" size="large" fill="#409eff">
<el-radio-button label="设备详情" value="info" />
<el-radio-button label="线路列表" value="line" />
<el-radio-button label="运行趋势" value="trend" />
</el-radio-group>
</div>
<el-menu :default-active="activeMenu" mode="horizontal" class="detail-menu" @select="handleMenuSelect">
<!-- <el-menu :default-active="activeMenu" mode="horizontal" class="detail-menu" @select="handleMenuSelect">
<el-menu-item index="info">设备详情</el-menu-item>
<el-menu-item index="alarm">报警记录</el-menu-item>
<el-menu-item index="trend">历史曲线</el-menu-item>
</el-menu>
</el-menu> -->
<DeviceInfo v-show="activeMenu === 'info'" :device="device" />
<DeviceTrend v-show="activeMenu === 'trend'" :device="device" />
<DeviceAlarm v-show="activeMenu === 'alarm'" :device="device" />
<DeviceInfo v-if="activeMenu === 'info'" :device="device" />
<DeviceLine v-if="activeMenu === 'line'" :device="device" />
<DeviceTrend v-if="activeMenu === 'trend'" :device="device" />
<!-- <DeviceAlarm v-show="activeMenu === 'alarm'" :device="device" /> -->
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import DeviceInfo from './components/info/index.vue'
import DeviceLine from './components/line/index.vue'
import DeviceTrend from './components/trend/index.vue'
import DeviceAlarm from './components/alarm/index.vue'
@@ -30,23 +41,36 @@
}
})
const emit = defineEmits(['back'])
const activeMenu = ref('info')
const handleMenuSelect = (index) => {
activeMenu.value = index
}
const emit = defineEmits(['back'])
// const activeMenu = ref('info')
// const handleMenuSelect = (index) => {
// activeMenu.value = index
// }
const handleBack = () => {
activeMenu.value = 'info'
activeMenu.value = ''
emit('back')
}
</script>
<style lang="scss" scoped>
.device-detail-page {
padding: 20px;
// padding: 20px;
.tabs-box {
@apply p-4 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded my-2 rounded-lg;
display: flex;
align-items: center;
> .title {
margin: 0 15px;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary, #303133);
}
}
}
.detail-header {
@@ -56,12 +80,6 @@
margin-bottom: 16px;
}
.detail-title {
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary, #303133);
}
.detail-menu {
margin-bottom: 20px;
// border-radius: 8px;

View File

@@ -2,22 +2,10 @@
<div>
<div class="gva-search-box">
<el-form ref="searchForm" :inline="true" :model="searchInfo">
<el-form-item label="设备ID">
<el-input v-model="searchInfo.ID" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="设备状态">
<el-select v-model="searchInfo.deviceStatus" clearable placeholder="请选择">
<el-option label="合闸" :value="1" />
<el-option label="分闸" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="协议">
<el-select v-model="searchInfo.protocol" clearable placeholder="请选择">
<el-option label="HTTP" value="HTTP" />
<el-option label="UDP" value="UDP" />
<el-option label="HEX" value="HEX" />
</el-select>
<el-form-item label="网关MAC">
<el-input v-model="searchInfo.gatewayMac" placeholder="请输入" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="search" @click="onSubmit"> 查询 </el-button>
<el-button icon="refresh" @click="onReset"> 重置 </el-button>
@@ -32,9 +20,9 @@
<div v-for="item in tableData" :key="item.ID" class="device-card" @click="handleCardClick(item, $event)">
<div class="device-image">
<img src="@/assets/img/equipment/breaker.png" alt="设备" />
<div class="signal-icon" :class="{ online: item.netStatus == 1 }">
<!-- <div class="signal-icon" :class="{ online: item.netStatus == 1 }">
<i class="el-icon-connection"></i>
</div>
</div> -->
</div>
<div class="device-info">
<div class="card-menu">
@@ -44,9 +32,9 @@
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="item.deviceStatus == 1" command="changeStatus">分闸</el-dropdown-item>
<!-- <el-dropdown-item v-if="item.deviceStatus == 1" command="changeStatus">分闸</el-dropdown-item>
<el-dropdown-item v-else command="changeStatus">分闸</el-dropdown-item>
<el-dropdown-item v-if="[1].includes(item.lockStatus)" command="unlock">解锁</el-dropdown-item>
<el-dropdown-item v-if="[1].includes(item.lockStatus)" command="unlock">解锁</el-dropdown-item> -->
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -55,29 +43,39 @@
<el-tag :type="item.netStatus == 1 ? 'primary' : 'info'">
{{ item.netStatus == 1 ? '在线' : '离线' }}
</el-tag>
<el-tag
<!-- <el-tag
v-if="item.cbTypeName !== 'T30' && item.deviceStatus != 1"
:type="item.deviceStatus == 1 ? 'success' : 'danger'"
>
{{ item.deviceStatus == 1 ? '合闸' : '分闸' }}
</el-tag>
<el-tag v-if="item.lockStatus != 0" :type="['success', 'info', 'danger'][item.lockStatus]">
</el-tag> -->
<!-- <el-tag v-if="item.lockStatus != 0" :type="['success', 'info', 'danger'][item.lockStatus]">
{{ ['正常', '远程合闸禁止', '锁定'][item.lockStatus] }}
</el-tag>
</el-tag> -->
</div>
<div class="device-name">{{ item.cbTypeName }}</div>
<div class="device-name">{{ item.gatewayMac }}</div>
<div class="device-id-row">
<span class="device-id">ID:{{ item.ID }}</span>
<el-button size="small" type="primary" link icon="CopyDocument" @click.stop="copyDeviceId(item.ID)">
<span class="device-id">ID:{{ item.gatewayId }}</span>
<el-button
size="small"
type="primary"
link
icon="CopyDocument"
@click.stop="copyDeviceId(item.gatewayId)"
>
</el-button>
</div>
<div class="device-location">
<el-icon><LocationInformation /></el-icon>
<span>{{ item.gatewayMac }}</span>
<span style="white-space: break-spaces">{{ item.gatewayAddress }}</span>
</div>
<div class="device-channels">
<el-icon><Postcard /></el-icon>
<span>{{ item.brand }}</span>
<el-icon><Operation /></el-icon>
<span>{{ '3' }}</span>
</div>
<div v-if="item.alarmType" class="device-alarm">
<el-icon style="color: #f56c6c"><WarningFilled /></el-icon>
<span style="color: #f56c6c">{{ item.alarmCreatedAt }} {{ item.alarmType }} </span>
</div>
</div>
</div>
@@ -113,9 +111,7 @@
const emit = defineEmits(['select-device'])
const searchInfo = ref({
deviceStatus: '',
protocol: '',
ID: ''
gatewayMac: ''
})
const page = ref(1)
@@ -124,7 +120,7 @@
const tableData = ref([])
onMounted(() => {
getTableData()
getDeviceGatewayListByPage()
})
// 点击设备卡片
@@ -154,7 +150,7 @@
})
if (res.code === 0) {
ElMessage.success('操作成功')
getTableData()
getDeviceGatewayListByPage()
}
})
}
@@ -173,7 +169,7 @@
})
if (res.code === 0) {
ElMessage.success('操作成功')
getTableData()
getDeviceGatewayListByPage()
}
})
}
@@ -181,15 +177,15 @@
// 分页
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
getDeviceGatewayListByPage()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
getDeviceGatewayListByPage()
}
// 查询
const getTableData = async () => {
const table = await serve.getDeviceListByPage({
const getDeviceGatewayListByPage = async () => {
const table = await serve.getDeviceGatewayListByPage({
page: page.value,
pageSize: pageSize.value,
...searchInfo.value
@@ -204,16 +200,14 @@
const onSubmit = () => {
page.value = 1
getTableData()
getDeviceGatewayListByPage()
}
const onReset = () => {
searchInfo.value = {
deviceStatus: '',
protocol: '',
ID: ''
gatewayMac: ''
}
getTableData()
getDeviceGatewayListByPage()
}
// 复制设备ID
@@ -249,7 +243,7 @@
.device-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
gap: 12px;
width: 100%;
background-color: var(--el-bg-color, #f5f7fa);
@@ -309,7 +303,7 @@
flex-direction: column;
gap: 6px;
position: relative;
padding-right: 20px;
// padding-right: 20px;
}
.card-menu {
@@ -347,14 +341,15 @@
.device-id {
flex: 1;
font-size: 12px;
font-size: 14px;
color: var(--el-color-primary, #409eff);
word-break: break-all;
}
}
.device-location,
.device-channels {
.device-channels,
.device-alarm {
display: flex;
align-items: center;
gap: 4px;
@@ -372,4 +367,11 @@
text-overflow: ellipsis;
}
}
// .device-alarm {
// display: flex;
// // align-items: center;
// gap: 4px;
// font-size: 12px;
// color: var(--el-text-color-regular, #606266);
// }
</style>

View File

@@ -81,7 +81,8 @@
nextTick(() => {
document.getElementsByClassName(
'flex flex-col md:flex-row gap-2 items-center text-sm text-slate-700 dark:text-slate-500 justify-center py-2'
)[0].style.opacity = 0
)[0].style.display = 'none'
// opacity = 0
})
})