This commit is contained in:
xiaozhiyong
2026-06-08 13:59:43 +08:00
parent 14906a83d2
commit 93ccdace0f
7 changed files with 415 additions and 274 deletions

View File

@@ -3,9 +3,9 @@ VITE_CLI_PORT = 8080
VITE_SERVER_PORT = 8888
VITE_BASE_API = /api
VITE_FILE_API = /api
# VITE_BASE_PATH = http://192.168.1.9:8888
VITE_BASE_PATH = http://192.168.1.9:8888
# VITE_BASE_PATH = http://192.168.110.98:8888
VITE_BASE_PATH = https://www.xingoil.com/api
# VITE_BASE_PATH = https://www.xingoil.com/api
VITE_POSITION = open
VITE_EDITOR = code
// VITE_EDITOR = webstorm 如果使用webstorm开发且要使用dom定位到代码行功能 请先自定添加 webstorm到环境变量 再将VITE_EDITOR值修改为webstorm

View File

@@ -11,15 +11,14 @@
"/src/view/dashboard/components/table.vue": "Table",
"/src/view/dashboard/components/wiki.vue": "Wiki",
"/src/view/dashboard/index.vue": "Dashboard",
"/src/view/equipment/alarmRecord/index.vue": "Index",
"/src/view/equipment/gateway/index.vue": "Index",
"/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/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",
"/src/view/error/reload.vue": "Reload",
"/src/view/example/breakpoint/breakpoint.vue": "BreakPoint",
@@ -56,6 +55,8 @@
"/src/view/login/index.vue": "Login",
"/src/view/person/person.vue": "Person",
"/src/view/routerHolder.vue": "RouterHolder",
"/src/view/securityControl/alarmList/index.vue": "Index",
"/src/view/securityControl/index.vue": "SecurityControl",
"/src/view/superAdmin/api/api.vue": "Api",
"/src/view/superAdmin/authority/authority.vue": "Authority",
"/src/view/superAdmin/authority/components/apis.vue": "Apis",

View File

@@ -1,114 +0,0 @@
<template>
<div>
<!-- <warning-bar title="注:右上角头像下拉可切换角色" /> -->
<div class="gva-search-box">
<el-form ref="searchForm" :inline="true" :model="searchInfo">
<el-form-item label="设备ID">
<el-input v-model="searchInfo.deviceId" placeholder="设备ID" />
</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">
<el-table-column align="left" label="网关ID" prop="gatewayId" width="200" />
<el-table-column align="left" label="设备号" prop="imei" width="200" />
<el-table-column align="left" label="网关mac" prop="gatewayMac" width="200" />
<el-table-column align="left" label="网关地址" prop="gatewayAddress" />
<el-table-column align="left" label="经纬度" width="250">
<template #default="scope">
<span>{{ scope.row.gatewayLong }}</span>
<el-divider direction="vertical" />
<span>{{ scope.row.gatewayLat }}</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>
</div>
</template>
<script setup>
import * as serve from '@/api/equipment/alarmRecord'
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 appStore = useAppStore()
const searchInfo = ref({
username: '',
nickname: '',
phone: '',
email: ''
})
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
onMounted(() => {
getTableData()
})
// 分页
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
// 查询
const getTableData = async () => {
const table = await serve.getAlarmRecordListByPage({
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 = {
deviceId: ''
}
getTableData()
}
</script>
<style lang="scss">
.header-img-box {
@apply w-52 h-52 border border-solid border-gray-300 rounded-xl flex justify-center items-center cursor-pointer;
}
</style>

View File

@@ -0,0 +1,20 @@
<template>
<div>
<router-view v-slot="{ Component }">
<transition mode="out-in" name="el-fade-in-linear">
<keep-alive :include="routerStore.keepAliveRouters">
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
<script setup>
import { useRouterStore } from '@/pinia/modules/router'
const routerStore = useRouterStore()
defineOptions({
name: 'Equipment'
})
</script>

View File

@@ -1,156 +0,0 @@
<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="设备ID" prop="deviceId"> </el-table-column>
<el-table-column align="left" label="运行时长" prop="runtime">
<template #default="{ row }">
<span>{{ row.runtime || '' }}</span>
</template>
</el-table-column>
<el-table-column align="left" label="信号质量" prop="signalQuality">
<template #default="{ row }">
{{ row.signalQuality || '' }}
</template>
</el-table-column>
<el-table-column align="left" label="电压V" prop="leakageCurrent">
<template #default="{ row }">
{{ row.leakageCurrent || '' }}
</template>
</el-table-column>
<el-table-column align="left" label="漏电流值mA" prop="leakageCurrent">
<template #default="{ row }">
{{ row.leakageCurrent || '' }}
</template>
</el-table-column>
<el-table-column align="left" label="累计用电量" prop="cumulativeElectricity">
<template #default="{ row }">
{{ row.cumulativeElectricity || '' }}
</template>
</el-table-column>
<el-table-column align="left" label="电流值A" prop="current">
<template #default="{ row }">
{{ row.current || '' }}
</template>
</el-table-column>
<el-table-column align="left" label="内部温度(℃)" prop="internalTemperature">
<template #default="{ row }">
{{ row.internalTemperature || '' }}
</template>
</el-table-column>
<el-table-column align="left" label="N相下端温度" prop="nLowerTemperature">
<template #default="{ row }">
{{ row.nLowerTemperature || '' }}
</template>
</el-table-column>
<el-table-column align="left" label="功率因数(%" prop="powerFactor">
<template #default="{ row }">
{{ row.powerFactor || '' }}
</template>
</el-table-column>
<el-table-column label="操作" fixed="right">
<template #default="{ row }">
<!-- <el-button type="primary" link icon="delete" @click="deleteUserFunc(scope.row)">删除</el-button> -->
<!-- <el-button type="primary" link icon="Tickets" @click="openDetails(row)">查看</el-button> -->
</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 * as serve from '@/api/equipment/particulars'
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 appStore = useAppStore()
const searchInfo = ref({
deviceStatus: ''
})
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
onMounted(() => {
getTableData()
})
// 分页
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
// 查询
const getTableData = async () => {
const table = await serve.getDeviceDetailsListByPage({
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()
}
</script>
<style lang="scss">
.header-img-box {
@apply w-52 h-52 border border-solid border-gray-300 rounded-xl flex justify-center items-center cursor-pointer;
}
</style>

View File

@@ -0,0 +1,370 @@
<template>
<div>
<!-- <warning-bar title="注:右上角头像下拉可切换角色" /> -->
<div class="p-4 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded my-2">
<!-- <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane label="报警事件" name="first"></el-tab-pane>
<el-tab-pane label="预警事件" name="second"></el-tab-pane>
<el-tab-pane label="二级报警事件" name="third"></el-tab-pane>
</el-tabs> -->
<el-radio-group v-model="radio" size="large" fill="#409eff">
<el-radio-button label="报警事件" value="New York" />
<el-radio-button label="预警事件" value="Washington" />
<el-radio-button label="二级报警事件" value="Los Angeles" />
</el-radio-group>
</div>
<div class="gva-search-box">
<el-form ref="searchForm" :inline="true" :model="searchInfo">
<el-form-item label="设备ID">
<el-input v-model="searchInfo.deviceId" placeholder="设备ID" />
</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="echarts-box">
<div class="item pie">
<p class="title">报警类型</p>
<v-chart :option="alarmTypeOption" autoresize class="alarm-type-chart" />
</div>
<div class="item broken-line">
<p class="title">报警数量</p>
<v-chart :option="alarmCountOption" autoresize class="alarm-count-chart" />
</div>
</div>
<el-table :data="tableData">
<el-table-column align="left" label="网关ID" prop="gatewayId" width="200" />
</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 { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart, LineChart } from 'echarts/charts'
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
import VChart from 'vue-echarts'
import * as serve from '@/api/equipment/alarmRecord'
import { ref, watch, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// import { id } from 'element-plus/es/locale'
import { useAppStore } from '@/pinia'
// 按需注册 ECharts 模块
use([CanvasRenderer, PieChart, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
const appStore = useAppStore()
const searchInfo = ref({
username: '',
nickname: '',
phone: '',
email: ''
})
const radio = ref('New York')
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
// 报警类型映射(与 alarm/index.vue 中的 typeMap 保持一致)
const alarmTypeMap = {
45: { label: '设备事件', color: '#409EFF' },
'4f': { label: '操作事件', color: '#67C23A' }
}
// 报警类型环形图配置(响应式 option 交给 v-chart 渲染)
const alarmTypeOption = ref(buildAlarmTypeOption([]))
// 报警数量折线图配置
const alarmCountOption = ref(buildAlarmCountOption([]))
// 构建报警类型环形图 option
function buildAlarmTypeOption(list) {
// 按 warnType 分组统计
const countMap = {}
list.forEach((item) => {
const key = String(item.warnType ?? 'unknown')
countMap[key] = (countMap[key] || 0) + 1
})
// 转成 ECharts 数据
const chartData = Object.keys(countMap).map((key) => {
// key 已经是 StringalarmTypeMap 的键也是字符串
const meta = alarmTypeMap[key] || { label: key || '未知', color: '#909399' }
return {
name: meta.label,
value: countMap[key],
itemStyle: { color: meta.color }
}
})
const totalCount = chartData.reduce((sum, d) => sum + d.value, 0)
return {
tooltip: {
trigger: 'item',
formatter: (params) => `${params.name}<br/>数量:${params.value}${params.percent}%`
},
// 图例放右侧
legend: {
orient: 'vertical',
right: 8,
top: 'middle',
textStyle: { color: '#606266', fontSize: 12 },
itemWidth: 10,
itemHeight: 10
},
series: [
{
name: '报警类型',
type: 'pie',
radius: ['52%', '78%'],
center: ['38%', '52%'],
avoidLabelOverlap: false,
label: { show: false },
labelLine: { show: false },
itemStyle: {
borderRadius: 6,
borderColor: '#fff',
borderWidth: 2
},
// 中心文字
graphic: [
{
type: 'text',
left: '38%',
top: '46%',
style: {
text: totalCount,
textAlign: 'center',
fill: '#303133',
fontSize: 22,
fontWeight: 'bold'
}
},
{
type: 'text',
left: '38%',
top: '58%',
style: {
text: '总数',
textAlign: 'center',
fill: '#909399',
fontSize: 12
}
}
],
data: chartData
}
]
}
}
// 拉取全部报警数据用于统计(不分页拿全量),同时刷新两个图表
const loadChartData = async () => {
const res = await serve.getAlarmRecordListByPage({
page: 1,
pageSize: 9999
})
if (res.code === 0) {
const list = res.data.list || []
alarmTypeOption.value = buildAlarmTypeOption(list)
alarmCountOption.value = buildAlarmCountOption(list)
}
}
// 构建报警数量折线图 option按日期统计最近 30 天)
function buildAlarmCountOption(list) {
// 按日期分组YYYY-MM-DD
const countMap = {}
list.forEach((item) => {
const date = formatDate(item.CreatedAt || item.createdAt || item.time)
if (!date) return
countMap[date] = (countMap[date] || 0) + 1
})
// 生成最近 30 天的 X 轴(缺失日期补 0
const xAxisData = []
const seriesData = []
const today = new Date()
for (let i = 29; i >= 0; i--) {
const d = new Date(today)
d.setDate(today.getDate() - i)
const key = formatDateKey(d)
xAxisData.push(formatDateLabel(d))
seriesData.push(countMap[key] || 0)
}
return {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(15, 23, 42, 0.9)',
borderColor: 'transparent',
textStyle: { color: '#fff' }
},
grid: {
left: 36,
right: 16,
top: 16,
bottom: 28
},
xAxis: {
type: 'category',
data: xAxisData,
boundaryGap: false,
axisLine: { lineStyle: { color: '#e4e7ed' } },
axisLabel: { color: '#909399', fontSize: 11 }
},
yAxis: {
type: 'value',
minInterval: 1,
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: '#f0f2f5' } },
axisLabel: { color: '#909399', fontSize: 11 }
},
series: [
{
name: '报警数量',
type: 'line',
data: seriesData,
symbol: 'none', // 不画数据点,全一条线
smooth: true,
lineStyle: {
color: '#409EFF',
width: 2
},
// 折线下面积渐变
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(64, 158, 255, 0.35)' },
{ offset: 1, color: 'rgba(64, 158, 255, 0.02)' }
]
}
}
}
]
}
}
// 把后端时间字段格式化成 YYYY-MM-DD
function formatDate(value) {
if (!value) return ''
const d = new Date(value)
if (isNaN(d.getTime())) return ''
return formatDateKey(d)
}
function formatDateKey(d) {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
// X 轴显示用 MM-DD
function formatDateLabel(d) {
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${m}-${day}`
}
onMounted(() => {
getTableData()
loadChartData()
})
// 分页
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
// 查询
const getTableData = async () => {
const table = await serve.getAlarmRecordListByPage({
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 = {
deviceId: ''
}
getTableData()
}
</script>
<style lang="scss">
.header-img-box {
@apply w-52 h-52 border border-solid border-gray-300 rounded-xl flex justify-center items-center cursor-pointer;
}
.gva-table-box {
padding-top: 0 !important;
}
.echarts-box {
display: flex;
> .item {
height: 251px;
&.pie {
width: 370px;
}
&.broken-line {
flex: 1;
}
.title {
font-size: 14px;
font-weight: bold;
}
}
}
.alarm-type-chart {
width: 100%;
height: calc(100% - 30px);
}
.alarm-count-chart {
width: 100%;
height: calc(100% - 30px);
}
</style>

View File

@@ -0,0 +1,20 @@
<template>
<div>
<router-view v-slot="{ Component }">
<transition mode="out-in" name="el-fade-in-linear">
<keep-alive :include="routerStore.keepAliveRouters">
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
<script setup>
import { useRouterStore } from '@/pinia/modules/router'
const routerStore = useRouterStore()
defineOptions({
name: 'SecurityControl'
})
</script>