Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14906a83d2 | ||
|
|
f182a7d736 | ||
|
|
b8d29b5006 | ||
|
|
d3e9fcce4c | ||
|
|
1890cca75f |
@@ -1,8 +1,8 @@
|
||||
import service from '@/utils/request'
|
||||
|
||||
export const getDeviceWarnList = (data) => {
|
||||
export const getAlarmRecordListByPage = (data) => {
|
||||
return service({
|
||||
url: '/device/getDeviceWarnList',
|
||||
url: '/device/getAlarmRecordListByPage',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
|
||||
@@ -16,9 +16,9 @@ export const deviceOperation = (data) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const getDeviceDetailsInfoByRemote = (data) => {
|
||||
export const getDeviceDetailsListByPage = (data) => {
|
||||
return service({
|
||||
url: '/device/getDeviceDetailsInfoByRemote',
|
||||
url: '/device/getDeviceDetailsListByPage',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
|
||||
BIN
src/assets/img/equipment/breaker.png
Normal file
BIN
src/assets/img/equipment/breaker.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -13,6 +13,11 @@
|
||||
"/src/view/dashboard/index.vue": "Dashboard",
|
||||
"/src/view/equipment/alarmRecord/index.vue": "Index",
|
||||
"/src/view/equipment/gateway/index.vue": "Index",
|
||||
"/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/trend/index.vue": "Index",
|
||||
"/src/view/equipment/list/components/detail/index.vue": "Index",
|
||||
"/src/view/equipment/list/components/list/index.vue": "Index",
|
||||
"/src/view/equipment/list/index.vue": "Index",
|
||||
"/src/view/equipment/particulars/index.vue": "Index",
|
||||
"/src/view/error/index.vue": "Error",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@use '@/style/iconfont.css';
|
||||
@use "./transition.scss";
|
||||
@use './transition.scss';
|
||||
|
||||
.html-grey {
|
||||
filter: grayscale(100%);
|
||||
@@ -18,11 +18,11 @@
|
||||
|
||||
.gva-btn-list {
|
||||
@apply mb-3 flex items-center flex-wrap gap-2;
|
||||
.el-button+.el-button{
|
||||
.el-button + .el-button {
|
||||
@apply ml-0 !important;
|
||||
}
|
||||
.el-upload{
|
||||
.el-button{
|
||||
.el-upload {
|
||||
.el-button {
|
||||
@apply ml-0 !important;
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
|
||||
.gva-search-box {
|
||||
@apply p-4 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded my-2;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.gva-form-box {
|
||||
|
||||
@@ -3,13 +3,9 @@
|
||||
<!-- <warning-bar title="注:右上角头像下拉可切换角色" /> -->
|
||||
<div class="gva-search-box">
|
||||
<el-form ref="searchForm" :inline="true" :model="searchInfo">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="searchInfo.username" placeholder="设备ID" />
|
||||
<el-form-item label="设备ID">
|
||||
<el-input v-model="searchInfo.deviceId" placeholder="设备ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="searchInfo.nickname" placeholder="设备状态" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit"> 查询 </el-button>
|
||||
<el-button icon="refresh" @click="onReset"> 重置 </el-button>
|
||||
@@ -18,7 +14,7 @@
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="addUser">新增用户</el-button>
|
||||
<!-- <el-button type="primary" icon="plus" @click="addUser">新增用户</el-button> -->
|
||||
</div>
|
||||
<el-table :data="tableData">
|
||||
<el-table-column align="left" label="网关ID" prop="gatewayId" width="200" />
|
||||
@@ -85,7 +81,7 @@
|
||||
}
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await serve.getDeviceWarnList({
|
||||
const table = await serve.getAlarmRecordListByPage({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchInfo.value
|
||||
@@ -105,10 +101,7 @@
|
||||
|
||||
const onReset = () => {
|
||||
searchInfo.value = {
|
||||
username: '',
|
||||
nickname: '',
|
||||
phone: '',
|
||||
email: ''
|
||||
deviceId: ''
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
<!-- <warning-bar title="注:右上角头像下拉可切换角色" /> -->
|
||||
<div class="gva-search-box">
|
||||
<el-form ref="searchForm" :inline="true" :model="searchInfo">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="searchInfo.username" placeholder="设备ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="searchInfo.nickname" placeholder="设备状态" />
|
||||
<el-form-item label="网关ID">
|
||||
<el-input v-model="searchInfo.gatewayId" placeholder="网关ID" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
@@ -18,7 +15,7 @@
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="addUser">新增用户</el-button>
|
||||
<!-- <el-button type="primary" icon="plus" @click="addUser">新增用户</el-button> -->
|
||||
</div>
|
||||
<el-table :data="tableData">
|
||||
<el-table-column align="left" label="网关ID" prop="gatewayId" width="200" />
|
||||
@@ -105,10 +102,7 @@
|
||||
|
||||
const onReset = () => {
|
||||
searchInfo.value = {
|
||||
username: '',
|
||||
nickname: '',
|
||||
phone: '',
|
||||
email: ''
|
||||
gatewayId: ''
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="detail-content">
|
||||
<div class="detail-card">
|
||||
<h3>报警记录</h3>
|
||||
<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-column prop="status" label="处理状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '已处理' ? 'success' : 'danger'">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
</el-table>
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import * as serve from '@/api/equipment/alarmRecord'
|
||||
|
||||
const props = defineProps({
|
||||
device: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
})
|
||||
|
||||
// 报警数据
|
||||
const tableData = ref([])
|
||||
|
||||
// 分页相关
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
|
||||
// 当前页变化
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 每页大小变化
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询报警记录
|
||||
const getTableData = async () => {
|
||||
const deviceId = props.device?.ID
|
||||
if (deviceId) {
|
||||
try {
|
||||
const table = await serve.getAlarmRecordListByPage({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
deviceId,
|
||||
sortBy: 'CreatedAt',
|
||||
desc: true
|
||||
})
|
||||
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 字段')
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 device 变化,device 更新时重新请求
|
||||
watch(
|
||||
() => props.device,
|
||||
(newDevice) => {
|
||||
if (newDevice) {
|
||||
getTableData()
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: false }
|
||||
)
|
||||
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: '警告'
|
||||
}
|
||||
]
|
||||
</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);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-alarm {
|
||||
padding: 40px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<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>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">网关ID:</span>
|
||||
<span class="detail-value">{{ device?.gatewayId || '未设置' }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">设备类型名称:</span>
|
||||
<span class="detail-value">{{ device?.cbTypeName || '未设置' }}</span>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
device: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
})
|
||||
</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);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,413 @@
|
||||
<template>
|
||||
<div ref="chartContainerRef" class="trend-content">
|
||||
<div class="trend-toolbar">
|
||||
<span class="toolbar-label">时间范围:</span>
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
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>
|
||||
<v-chart class="chart" :option="item.option" autoresize />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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'
|
||||
|
||||
use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
|
||||
|
||||
const props = defineProps({
|
||||
device: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
})
|
||||
|
||||
// 容器 ref
|
||||
const chartContainerRef = ref(null)
|
||||
// 容器就绪状态:父级用 v-show 切换时,初始为 display:None,宽度为 0,需要等容器有尺寸后再渲染图表
|
||||
const isReady = ref(false)
|
||||
let resizeObserver = null
|
||||
|
||||
// 检测容器是否已具备尺寸
|
||||
const checkContainerReady = () => {
|
||||
if (isReady.value || !chartContainerRef.value) return
|
||||
const { clientWidth, clientHeight } = chartContainerRef.value
|
||||
if (clientWidth > 0 && clientHeight > 0) {
|
||||
isReady.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 触发图表 resize
|
||||
const triggerResize = () => {
|
||||
// 触发 window resize 事件让 vue-echarts autoresize 生效
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
}
|
||||
|
||||
// 格式化时间 - 转为 yyyy-MM-dd HH:mm:ss
|
||||
const formatDateTime = (date) => {
|
||||
if (!date) return ''
|
||||
// 如果已经是字符串(YYYY-MM-DD HH:mm:ss 格式),直接返回
|
||||
if (typeof date === 'string') {
|
||||
// 验证格式
|
||||
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(date)) {
|
||||
return date
|
||||
}
|
||||
// 如果是 ISO 格式,转换一下
|
||||
const d = new Date(date)
|
||||
if (isNaN(d.getTime())) return date
|
||||
return formatDateTime(d)
|
||||
}
|
||||
// 如果是 Date 对象
|
||||
if (date instanceof Date) {
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if (!dateRange.value || dateRange.value.length !== 2) {
|
||||
console.warn('[Trend] 日期范围无效')
|
||||
return
|
||||
}
|
||||
// 格式化时间
|
||||
// const startTime = formatDateTime(dateRange.value[0])
|
||||
// const endTime = formatDateTime(dateRange.value[1])
|
||||
try {
|
||||
const result = await serve.getDeviceDetailsListByPage({
|
||||
page: 1,
|
||||
pageSize: 999,
|
||||
deviceId,
|
||||
startCreatedAt: dateRange.value[0],
|
||||
endCreatedAt: dateRange.value[1],
|
||||
orderKey: 'voltage',
|
||||
sortBy: 'CreatedAt',
|
||||
order: 'desc',
|
||||
desc: true
|
||||
})
|
||||
|
||||
if (result.code === 0) {
|
||||
// 提取数据
|
||||
const list = result.data.list || []
|
||||
// 调试:打印第一条数据的所有字段
|
||||
|
||||
const timeData = []
|
||||
const chartData = {
|
||||
voltage: [], // 电压(V)
|
||||
leakageCurrent: [], // 漏电流值(mA)
|
||||
cumulativeElectricity: [], // 累计用电量(kWh)
|
||||
current: [], // 电流值(A)
|
||||
internalTemperature: [], // 内部温度(℃)
|
||||
powerFactor: [] // 功率因数
|
||||
}
|
||||
|
||||
list.forEach((item) => {
|
||||
timeData.push(item.CreatedAt)
|
||||
// 兼容多种命名风格:PascalCase / camelCase
|
||||
chartData.voltage.push(item.voltage)
|
||||
chartData.leakageCurrent.push(item.leakageCurrent)
|
||||
chartData.cumulativeElectricity.push(item.cumulativeElectricity)
|
||||
chartData.current.push(item.current)
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect()
|
||||
resizeObserver = null
|
||||
}
|
||||
})
|
||||
|
||||
// 日期范围 - 默认当前时间前一小时
|
||||
const getTodayRange = () => {
|
||||
const now = new Date()
|
||||
const start = new Date(now.getTime() - 3600 * 1000)
|
||||
return [formatDateTime(start), formatDateTime(now)]
|
||||
}
|
||||
const dateRange = ref(getTodayRange())
|
||||
|
||||
// 日期变化处理
|
||||
const handleDateChange = (val) => {
|
||||
if (val && val.length === 2) {
|
||||
// console.log('[Trend] 日期范围:', val)
|
||||
// 触发图表 resize
|
||||
nextTick(() => {
|
||||
// triggerResize()
|
||||
getTableData()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 重置日期
|
||||
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 = []) => {
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '8%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxis.length ? xAxis : generateMockData(),
|
||||
boundaryGap: false,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#909399'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#909399'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#ebeef5',
|
||||
type: 'dashed'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name,
|
||||
type: 'line',
|
||||
data,
|
||||
smooth: true,
|
||||
// 去掉数据点标点,全一条线
|
||||
symbol: 'none',
|
||||
itemStyle: {
|
||||
color
|
||||
},
|
||||
lineStyle: {
|
||||
color,
|
||||
width: 2
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: color + '40' },
|
||||
{ offset: 1, color: color + '00' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const chartList = ref([
|
||||
{
|
||||
key: 'voltage',
|
||||
title: '电压',
|
||||
unit: 'V',
|
||||
color: '#409EFF',
|
||||
option: createLineOption('电压', '#409EFF', [], [])
|
||||
},
|
||||
{
|
||||
key: 'leakageCurrent',
|
||||
title: '漏电流值',
|
||||
unit: 'mA',
|
||||
color: '#67C23A',
|
||||
option: createLineOption('漏电流', '#67C23A', [], [])
|
||||
},
|
||||
{
|
||||
key: 'cumulativeElectricity',
|
||||
title: '累计用电量',
|
||||
unit: 'kWh',
|
||||
color: '#E6A23C',
|
||||
option: createLineOption('用电量', '#E6A23C', [], [])
|
||||
},
|
||||
{
|
||||
key: 'current',
|
||||
title: '电流值',
|
||||
unit: 'A',
|
||||
color: '#F56C6C',
|
||||
option: createLineOption('电流', '#F56C6C', [], [])
|
||||
},
|
||||
{
|
||||
key: 'internalTemperature',
|
||||
title: '内部温度',
|
||||
unit: '℃',
|
||||
color: '#909399',
|
||||
option: createLineOption('内部温度', '#909399', [], [])
|
||||
},
|
||||
{
|
||||
key: 'powerFactor',
|
||||
title: '功率因数',
|
||||
unit: '',
|
||||
color: '#1ABC9C',
|
||||
option: createLineOption('功率因数', '#1ABC9C', [], [])
|
||||
}
|
||||
])
|
||||
|
||||
// 更新图表数据
|
||||
const updateChartData = (timeData, chartData) => {
|
||||
chartList.value.forEach((item) => {
|
||||
const seriesData = chartData[item.key] || []
|
||||
item.option = createLineOption(item.title, item.color, seriesData, timeData)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.trend-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.trend-toolbar {
|
||||
width: 40%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: var(--el-bg-color-overlay, #ffffff);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--el-border-color, #e4e7ed);
|
||||
}
|
||||
|
||||
.toolbar-label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular, #606266);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
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;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary, #303133);
|
||||
}
|
||||
|
||||
.chart-unit {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-regular, #606266);
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 220px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
71
src/view/equipment/list/components/detail/index.vue
Normal file
71
src/view/equipment/list/components/detail/index.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="device-detail-page">
|
||||
<div class="detail-header">
|
||||
<el-button icon="ArrowLeft" circle @click="handleBack" />
|
||||
<span class="detail-title">设备信息</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<DeviceInfo v-show="activeMenu === 'info'" :device="device" />
|
||||
<DeviceTrend v-show="activeMenu === 'trend'" :device="device" />
|
||||
<DeviceAlarm v-show="activeMenu === 'alarm'" :device="device" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import DeviceInfo from './components/info/index.vue'
|
||||
import DeviceTrend from './components/trend/index.vue'
|
||||
import DeviceAlarm from './components/alarm/index.vue'
|
||||
|
||||
defineProps({
|
||||
device: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['back'])
|
||||
|
||||
const activeMenu = ref('info')
|
||||
|
||||
const handleMenuSelect = (index) => {
|
||||
activeMenu.value = index
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
activeMenu.value = 'info'
|
||||
emit('back')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.device-detail-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
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;
|
||||
// background-color: var(--el-bg-color-overlay, #ffffff);
|
||||
// border: 1px solid var(--el-border-color, #e4e7ed);
|
||||
}
|
||||
</style>
|
||||
375
src/view/equipment/list/components/list/index.vue
Normal file
375
src/view/equipment/list/components/list/index.vue
Normal file
@@ -0,0 +1,375 @@
|
||||
<template>
|
||||
<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>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit"> 查询 </el-button>
|
||||
<el-button icon="refresh" @click="onReset"> 重置 </el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<!-- <el-button type="primary" icon="plus" @click="addUser">新增用户</el-button> -->
|
||||
</div>
|
||||
<div class="device-grid">
|
||||
<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 }">
|
||||
<i class="el-icon-connection"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-info">
|
||||
<div class="card-menu">
|
||||
<el-dropdown trigger="click" @command="(cmd) => handleDeviceAction(cmd, item)">
|
||||
<span class="menu-icon">
|
||||
<el-icon><MoreFilled /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<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-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<div class="status-tags">
|
||||
<el-tag :type="item.netStatus == 1 ? 'primary' : 'info'">
|
||||
{{ item.netStatus == 1 ? '在线' : '离线' }}
|
||||
</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]">
|
||||
{{ ['正常', '远程合闸禁止', '锁定'][item.lockStatus] }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="device-name">{{ item.cbTypeName }}</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)">
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="device-location">
|
||||
<el-icon><LocationInformation /></el-icon>
|
||||
<span>{{ item.gatewayMac }}</span>
|
||||
</div>
|
||||
<div class="device-channels">
|
||||
<el-icon><Postcard /></el-icon>
|
||||
<span>{{ item.brand }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import * as serve from '@/api/equipment/list'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { MoreFilled } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select-device'])
|
||||
|
||||
const searchInfo = ref({
|
||||
deviceStatus: '',
|
||||
protocol: '',
|
||||
ID: ''
|
||||
})
|
||||
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
|
||||
// 点击设备卡片
|
||||
const handleCardClick = (device, event) => {
|
||||
// 如果点击的是 el-dropdown 相关元素,不跳转
|
||||
if (event) {
|
||||
const target = event.target
|
||||
// 检查点击目标是否是 el-dropdown 内部元素
|
||||
if (target.closest('.el-dropdown') || target.closest('.el-popper') || target.closest('.card-menu')) {
|
||||
return
|
||||
}
|
||||
}
|
||||
emit('select-device', device)
|
||||
}
|
||||
|
||||
// 解锁
|
||||
const unlock = async (row) => {
|
||||
ElMessageBox.confirm('确认解锁吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await serve.deviceOperation({
|
||||
id: row.ID,
|
||||
gatewayId: row.gatewayId,
|
||||
para: '0xAD'
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('操作成功')
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 合分闸
|
||||
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('操作成功')
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await serve.getDeviceListByPage({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchInfo.value
|
||||
})
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
searchInfo.value = {
|
||||
deviceStatus: '',
|
||||
protocol: '',
|
||||
ID: ''
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 复制设备ID
|
||||
const copyDeviceId = async (id) => {
|
||||
if (!id) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(id)
|
||||
ElMessage.success('复制成功')
|
||||
} catch {
|
||||
ElMessage.error('复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理设备操作
|
||||
const handleDeviceAction = (command, item) => {
|
||||
switch (command) {
|
||||
case 'changeStatus':
|
||||
changeStatus(item)
|
||||
break
|
||||
case 'unlock':
|
||||
unlock(item)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 设备卡片容器
|
||||
.container-device {
|
||||
padding: 24px;
|
||||
background-color: var(--el-bg-color, #f5f7fa);
|
||||
}
|
||||
|
||||
.device-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
background-color: var(--el-bg-color, #f5f7fa);
|
||||
}
|
||||
|
||||
.device-card {
|
||||
background-color: var(--el-bg-color-overlay, #ffffff);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
color: var(--el-text-color-regular, #303133);
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid var(--el-border-color, #e4e7ed);
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border-color: var(--el-color-primary, #409eff);
|
||||
}
|
||||
}
|
||||
|
||||
.device-image {
|
||||
position: relative;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.signal-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-placeholder, #9ca3af);
|
||||
|
||||
&.online {
|
||||
color: #10b981;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.device-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.card-menu {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-placeholder, #94a3b8);
|
||||
font-size: 18px;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-primary, #303133);
|
||||
}
|
||||
}
|
||||
|
||||
.status-tags {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary, #303133);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.device-id-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.device-id {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: var(--el-color-primary, #409eff);
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.device-location,
|
||||
.device-channels {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-regular, #606266);
|
||||
|
||||
.el-icon {
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,244 +1,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- <warning-bar title="注:右上角头像下拉可切换角色" /> -->
|
||||
<div class="gva-search-box">
|
||||
<el-form ref="searchForm" :inline="true" :model="searchInfo">
|
||||
<el-form-item label="设备状态">
|
||||
<el-select v-model="searchInfo.deviceStatus" placeholder="请选择">
|
||||
<el-option label="合闸" :value="1" />
|
||||
<el-option label="分闸" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit"> 查询 </el-button>
|
||||
<el-button icon="refresh" @click="onReset"> 重置 </el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="addUser">新增用户</el-button>
|
||||
</div>
|
||||
<el-table :data="tableData" row-key="ID">
|
||||
<!-- <el-table-column align="left" label="设备图片" min-width="75">
|
||||
<template #default="scope">
|
||||
<CustomPic style="margin-top: 8px" :pic-src="scope.row.headerImg" />
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column align="left" label="设备ID" prop="cbId">
|
||||
<template #default="{ row }">
|
||||
{{ row.cbId || '未设置' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column align="left" label="项目ID" prop="projectId">
|
||||
<template #default="{ row }">
|
||||
{{ row.projectId || '未设置' }}
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column align="left" label="网关ID" prop="gatewayId">
|
||||
<template #default="{ row }">
|
||||
{{ row.gatewayId || '未设置' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="设备状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.cbTypeName !== 'T30'" :type="row.deviceStatus == 1 ? 'success' : 'danger'">{{
|
||||
row.deviceStatus == 1 ? '合闸' : '分闸'
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="锁定状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="['success', 'info', 'danger'][row.lockStatus]">{{
|
||||
['正常', '远程合闸禁止', '锁定'][row.lockStatus]
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="网络">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.netStatus == 1 ? 'primary' : 'info'">{{ row.netStatus == 1 ? '在线' : '离线' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="设备类型名称" prop="cbTypeName" />
|
||||
<el-table-column align="left" label="网关mac" prop="gatewayMac" />
|
||||
|
||||
<el-table-column align="left" label="协议" prop="protocol" />
|
||||
<el-table-column align="left" label="品牌" prop="brand" />
|
||||
|
||||
<el-table-column align="left" label="安装时间" prop="createTime">
|
||||
<template #default="{ row }">
|
||||
{{ row.createTime || '未设置' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" fixed="right" width="240">
|
||||
<template #default="{ row }">
|
||||
<span class="action-buttons">
|
||||
<el-button v-if="row.deviceStatus == 1" type="primary" link icon="SortUp" @click="changeStatus(row)">
|
||||
分闸
|
||||
</el-button>
|
||||
<el-button v-else type="primary" link icon="SortDown" @click="changeStatus(row)">合闸</el-button>
|
||||
<el-button v-if="[1].includes(row.lockStatus)" type="primary" link icon="Unlock" @click="unlock(row)">
|
||||
解锁
|
||||
</el-button>
|
||||
<el-dropdown>
|
||||
<span class="el-dropdown-link">
|
||||
更多
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>
|
||||
<el-button type="primary" link icon="Odometer" @click="updateParticulars(row)">
|
||||
实时数据
|
||||
</el-button>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DeviceList v-show="!showDetail" @select-device="handleSelectDevice" />
|
||||
<DeviceDetail v-show="showDetail" :device="currentDevice" @back="handleBack" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import * as serve from '@/api/equipment/list'
|
||||
import { ref } from 'vue'
|
||||
import DeviceList from './components/list/index.vue'
|
||||
import DeviceDetail from './components/detail/index.vue'
|
||||
|
||||
import { nextTick, ref, watch, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
// import { id } from 'element-plus/es/locale'
|
||||
import { useAppStore } from '@/pinia'
|
||||
const showDetail = ref(false)
|
||||
const currentDevice = ref(null)
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const searchInfo = ref({
|
||||
deviceStatus: ''
|
||||
})
|
||||
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
// 解锁
|
||||
const unlock = async (row) => {
|
||||
ElMessageBox.confirm('确认解锁吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await serve.deviceOperation({
|
||||
id: row.ID,
|
||||
gatewayId: row.gatewayId,
|
||||
para: '0xAD'
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('操作成功')
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
// 更新
|
||||
const updateParticulars = async (row) => {
|
||||
const res = await serve.getDeviceDetailsInfoByRemote({
|
||||
deviceId: row.ID
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
// ElMessage.error(res.msg || '更新失败')
|
||||
}
|
||||
}
|
||||
// 合分闸
|
||||
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('操作成功')
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
const handleSelectDevice = (device) => {
|
||||
currentDevice.value = device
|
||||
showDetail.value = true
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await serve.getDeviceListByPage({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchInfo.value
|
||||
})
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
searchInfo.value = {
|
||||
deviceStatus: ''
|
||||
}
|
||||
getTableData()
|
||||
const handleBack = () => {
|
||||
showDetail.value = false
|
||||
currentDevice.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header-img-box {
|
||||
@apply w-52 h-52 border border-solid border-gray-300 rounded-xl flex justify-center items-center cursor-pointer;
|
||||
}
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.el-dropdown-link {
|
||||
cursor: pointer;
|
||||
color: var(--el-color-primary);
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user