2025年04月10日13:33:36

This commit is contained in:
luojiayi 2025-04-10 13:33:38 +08:00
parent 5f0cda3d45
commit 054d5fb5ca
15 changed files with 361 additions and 116 deletions

2
components.d.ts vendored
View File

@ -14,6 +14,7 @@ declare module '@vue/runtime-core' {
CustomInput: typeof import('./src/components/CustomInput.vue')['default']
DeviceInfo: typeof import('./src/components/deviceInfo.vue')['default']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButradioton: typeof import('element-plus/es')['ElButradioton']
ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCard: typeof import('element-plus/es')['ElCard']
@ -36,6 +37,7 @@ declare module '@vue/runtime-core' {
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']

25
src/api/index.d.ts vendored
View File

@ -164,6 +164,10 @@ export namespace TDevice {
mode: number
monitorMode: number
battery: number
adminUsername: string
adminPhone: string
userNumber: string
adminType: string
name: string
username: string
password: string
@ -183,7 +187,7 @@ export namespace TDevice {
}
export interface IRecordReq extends Ipaging {
deviceId?: string | string[];
deviceId?: any;
}
export interface IRecordRes {
id: number
@ -323,3 +327,22 @@ export namespace TWarningDetail {
warnType: number
}
}
export namespace THealthLatestData {
interface TReq {
deviceId: number | string
}
interface TRes {
hr: number
hrTime: string
hrArr: any[],
bo: number
boArr: any[],
boTime: string
temp: number
tempTime: string
tempArr: any[]
}
}

View File

@ -1,5 +1,5 @@
import request from '../utils/request';
import { TLogin, TAccount, IpagingRes, TDevice, TOrg, TRoleList, TStatisticsDevice, statisticsContentReq, statisticsContentRes, TStatisticsCount, TWarnRecord, TWarningDetail, TWarningConfirm, TDeviceConfigModify, TDeviceConfig } from "./index.d";
import { TLogin, TAccount, IpagingRes, TDevice, TOrg, TRoleList, TStatisticsDevice, statisticsContentReq, statisticsContentRes, TStatisticsCount, TWarnRecord, TWarningDetail, TWarningConfirm, TDeviceConfigModify, TDeviceConfig, THealthLatestData } from "./index.d";
export const fetchLogin = (p: TLogin.Ireq): Promise<TLogin.IRes> => {
return request({
@ -269,4 +269,13 @@ export const deviceConfigModify = (p: TDeviceConfigModify): Promise<null> => {
});
};
// 获取最新健康数据
export const healthLatestData = (p: THealthLatestData.TReq): Promise<THealthLatestData.TRes> => {
return request({
url: '/v1/web/health/latestData',
method: 'get',
params: p
});
};

View File

@ -68,12 +68,12 @@
</el-table>
<el-pagination
v-if="hasPagination"
:current-page="currentPage"
:page-size="pageSize"
:current-page="paging.page"
:page-size="paging.size"
:pager-count="5"
:background="true"
:layout="layout"
:total="total"
:total="paging.total"
@current-change="handleCurrentChange"
/>
</div>
@ -84,8 +84,23 @@ import { toRefs, PropType, ref } from "vue";
import { Delete, Edit, View, Refresh } from "@element-plus/icons-vue";
import { ElMessageBox } from "element-plus";
import { TableItem } from "@/types/table";
interface TPaging {
page: number;
size: number;
total: number;
}
const props = defineProps({
paging: {
type: Object as PropType<TPaging>,
default: () => {
return {
page: 1,
size: 10,
total: 0,
};
},
},
//
tableData: {
type: Array,

View File

@ -2,7 +2,7 @@
<div class="container">
<TableSearch :query="query" :options="searchOpt" :search="handleSearch" />
<div class="table-container">
<TableCustom :columns="columns" :tableData="tableData" :total="paging.total" :refresh="getData" :currentPage="paging.page" :changePage="changePage">
<TableCustom :columns="columns" :tableData="tableData" :paging="paging" :changePage="changePage">
<template #status="{ rows }">
<el-tag :type="rows.status == 1 ? 'success' : 'danger'">
{{ statusEnum[rows.status] }}

View File

@ -2,16 +2,25 @@
<div class="deviceStatistics card">
<div class="card-head">
<div class="title">当前设备历史数据</div>
<el-button-group>
<el-button type="primary">心率</el-button>
<el-button>血氧</el-button>
<el-button>体表温度</el-button>
</el-button-group>
<el-radio-group v-model="radio" @change="change">
<el-radio-button label="心率" value="1" />
<el-radio-button label="血氧" value="2" />
<el-radio-button label="体表温度" value="3" />
</el-radio-group>
</div>
<slot name="chart"></slot>
</div>
</template>
<script setup></script>
<script setup lang="ts">
import { ref } from "vue";
const radio = ref("1");
const emit = defineEmits(["change"]);
const change = (e) => {
emit("change", e);
};
</script>
<style scoped lang="less">
.card {
height: 100%;

View File

@ -2,60 +2,56 @@
<div class="device">
<div class="device-head">
<div class="title">设备列表</div>
<el-select class="select">
<el-option label="全部" value="0" />
<el-option label="常规模式" value="1" />
<el-select class="select" v-model="paging.mode" @change="handelMode">
<el-option label="全部" :value="undefined" />
<el-option label="常规模式" value="0" />
<el-option label="审讯模式" value="1" />
<el-option label="户外押送" value="2" />
<el-option label="审讯模式" value="3" />
</el-select>
</div>
<div class="device-list noScrollbar">
<el-popover :width="400" class="box-item" placement="bottom" v-for="item in 4" :key="item">
<div v-infinite-scroll="load" :infinite-scroll-immediate="false" class="device-list noScrollbar infinite-list" style="overflow: auto">
<el-popover :width="350" class="box-item" placement="bottom" v-for="item in list" :key="item.id">
<template #reference>
<div class="item">
<div class="item-img">
<img src="@/assets/img/handcuffs.png" alt="" srcset="" />
</div>
<div class="item-content">
<div class="item-content-name">手铐-002</div>
<div class="item-content-num">860116079430636</div>
<div class="item-content-name">手铐-{{ item.id }}</div>
<div class="item-content-num">{{ item.deviceId }}</div>
</div>
<div class="item-right">
<div class="battery">
<div class="battery-icon">
<img src="@/assets/img/battery.png" alt="" srcset="" />
</div>
<div class="battery-num">98%</div>
<div class="battery-num">{{ item.battery }}%</div>
</div>
<div class="user">admin</div>
<div class="user">{{ item.userNumber || "--" }}</div>
</div>
</div>
</template>
<template #default>
<div class="demo-rich-conent">
<div>
<span class="lable">手铐关联人</span>
张三
</div>
<div>
<span class="lable">类型</span>
警察
{{ item.adminType }}
</div>
<div>
<span class="lable">警号</span>
123456
{{ item.adminUsername }}
</div>
<div>
<span class="lable">联系电话</span>
13007777555
{{ item.adminPhone }}
</div>
<div>
<span class="lable">佩戴者编号</span>
BJ-0112
<span class="lable">状态</span>
<el-tag :type="statusColor[item.status]">{{ statusEnum[item.status] }}</el-tag>
</div>
<div>
<span class="lable">阈值</span>
12
<span class="lable">模式</span>
<el-tag :type="modeColor[item.mode]">{{ modeEnum[item.mode] }}</el-tag>
</div>
</div>
</template>
@ -63,21 +59,62 @@
</div>
</div>
</template>
<script setup></script>
<script setup lang="ts">
import { TDevice } from "@/api/index.d";
const statusColor = ["danger", "success", "warning"];
const modeColor = ["primary", "danger", "warning"];
enum statusEnum {
"离线" = 0,
"在线",
"充电中",
}
enum modeEnum {
"常规",
"审讯模式",
"户外押送",
}
const { list, paging, api } = defineProps({
list: {
type: Array<TDevice.IListRes>,
default: () => [],
},
api: {
type: Function,
default: () => {},
},
paging: {
type: Object,
default: () => {},
},
});
const handelMode = () => {
paging.page += 1;
api();
};
const load = () => {
paging.page += 1;
api();
};
</script>
<style scoped lang="less">
.device {
width: 100%;
flex-shrink: 0;
overflow: hidden;
background: #ffffff;
display: flex;
flex-direction: column;
height: 100%;
.device-head {
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
margin-top: 14px;
margin: 14px 0;
.title {
color: #061451;
font-size: 18px;
@ -97,7 +134,9 @@
}
}
.device-list {
flex: 1;
overflow: auto;
list-style: none;
.item {
height: 89px;
background: #ffffff;
@ -162,10 +201,12 @@
.demo-rich-conent {
display: flex;
flex-direction: column;
.lable {
display: inline-block;
width: 90px;
text-align: right;
margin-top: 10px;
}
}
</style>

View File

@ -3,53 +3,98 @@
<div class="card-head">
<div class="title">当前设备告警记录</div>
<div class="search">
<el-select class="select" style="width: 120px; margin-right: 20px">
<el-option label="户外押送" value="1" />
<el-option label="审讯模式" value="2" />
<el-select class="select" placeholder="请选择事件类型" v-model="paging.warnType" style="width: 150px; margin-right: 20px" @change="handelChange">
<el-option label="全部" :value="undefined" />
<el-option v-for="(item, index) in warnTypeList" :key="index" :label="item" :value="index" />
</el-select>
<el-select class="select" style="width: 120px">
<el-option label="户外押送" value="1" />
<el-option label="审讯模式" value="2" />
<el-select class="select" placeholder="请选择处理状态" v-model="paging.status" style="width: 150px" @change="handelChange">
<el-option label="全部" :value="undefined" />
<el-option label="待处理" value="0" />
<el-option label="已处理" value="1" />
</el-select>
</div>
</div>
<TableCustom :columns="columns" :tableData="tableData" :total="page.total" :refresh="getData" :currentPage="page.index" :changePage="changePage">
<template #state="{ rows }">
<el-tag :type="rows.state ? 'success' : 'danger'">
{{ rows.state ? "正常" : "异常" }}
</el-tag>
<TableCustom :columns="columns" :tableData="tableData" :paging="paging" :changePage="changePage">
<template #status="{ rows }">
<el-tag :type="statusColor[rows.status]">{{ warningStatusEnum[rows.status] }}</el-tag>
</template>
<template #warnType="{ rows }">
{{ warnTypeEnum[rows.warnType] }}
</template>
</TableCustom>
</div>
</template>
<script setup>
defineProps({
tableData: {
type: Array,
default: () => [],
},
page: {
<script setup lang="ts">
import TableCustom from "@/components/table-custom.vue";
import { warningRecord } from "@/api/index";
import { ref, reactive, watch } from "vue";
import { TDevice } from "@/api/index.d";
const statusColor = ["danger", "success"];
enum warningStatusEnum {
"待处理",
"已处理",
}
enum warnTypeEnum {
"SOS告警",
"围栏告警",
"破坏告警",
"低电告警",
"心率告警",
"血氧告警",
"体温告警",
}
const warnTypeList = ["SOS告警", "围栏告警", "破坏告警", "低电告警", "心率告警", "血氧告警", "体温告警"];
const paging = reactive({
page: 1,
size: 4,
total: 0,
deviceId: undefined,
status: undefined,
warnType: undefined,
});
const tableData = ref<TDevice.IWarningRecordRes[]>([]);
const props = defineProps({
deviceInfo: {
type: Object,
default: () => ({}),
},
getData: {
type: Function,
default: () => {},
},
changePage: {
type: Function,
default: () => {},
default: undefined,
},
});
watch(
() => props.deviceInfo,
(newVal) => {
if (newVal) {
paging.deviceId = newVal.deviceId;
getData && getData();
}
},
{ immediate: true } //
);
//
let columns = [
{ type: "index", label: "序号", width: 55, align: "center" },
{ prop: "name", label: "事件类型" },
{ prop: "name", label: "触发时间" },
{ prop: "state", label: "处理状态" },
{ prop: "warnType", label: "事件类型" },
{ prop: "status", label: "处理状态" },
{ prop: "createTime", label: "触发时间" },
];
const getData = async () => {
const res = await warningRecord(paging);
tableData.value = res.records;
paging.total = res.total;
};
const handelChange = () => {
paging.page = 1;
getData();
};
const changePage = (val: number) => {
paging.page = val;
getData();
};
</script>
<style scoped lang="less">
.card {

View File

@ -1,35 +1,35 @@
<template>
<div class="container scrollbar">
<el-row :gutter="20">
<el-col :span="7"><DeviceInfo /></el-col>
<el-col :span="17">
<MonitoringTop />
<div v-if="false">
<div class="container">
<el-row class="el-row" :gutter="20">
<el-col :span="6" class="el-row-left"><DeviceInfo :paging="devicePaging" :api="getdeviceList" :list="deviceData" /></el-col>
<el-col :span="18" class="el-row-right scrollbar">
<MonitoringTop :funcList="funcList" />
<div v-if="deviceInfo?.status != 2">
<div class="monitoringMap" id="mapcontainer"></div>
<el-row :gutter="20" style="margin-top: 20px">
<el-col :span="12">
<DeviceHistory>
<DeviceHistory @change="handelRadio">
<template #chart>
<div ref="chartRef" style="width: 100%; height: 100%"></div>
</template>
</DeviceHistory>
</el-col>
<el-col :span="12">
<DeviceRecord :tableData="tableData" :page="page" :getData="getData" :changePage="changePage" />
<DeviceRecord :deviceInfo="deviceInfo" />
</el-col>
</el-row>
</div>
<div v-else>
<el-row :gutter="20" style="margin-top: 20px">
<el-col :span="24" style="height: 400px">
<DeviceHistory>
<DeviceHistory @change="handelRadio">
<template #chart>
<div ref="chartRef" style="width: 100%; height: 100%"></div>
</template>
</DeviceHistory>
</el-col>
<el-col :span="24" style="margin-top: 20px">
<DeviceRecord :tableData="tableData" :page="page" :getData="getData" :changePage="changePage" />
<DeviceRecord :deviceInfo="deviceInfo" />
</el-col>
</el-row>
</div>
@ -44,58 +44,125 @@ import DeviceHistory from "./deviceHistory.vue";
import DeviceRecord from "./deviceRecord.vue";
import { MapCustom } from "@/utils/mapCustom";
import * as echarts from "echarts";
import { fetchData } from "@/api/index";
import { deviceList, healthLatestData, warningRecord } from "@/api/index";
import { TDevice, THealthLatestData } from "@/api/index.d";
import { onMounted, onDeactivated, ref, reactive } from "vue";
import { TableItem } from "@/types/table";
import { debounce } from "@/utils";
import { debounce, format } from "@/utils";
import heart from "@/assets/img/heart.png";
import temperature from "@/assets/img/temperature.png";
import blood from "@/assets/img/blood.png";
const chartRef = ref(null);
let myChart = null;
let funcList = ref([
{ title: "当前心率", en: "DANGQIANXINLV", icon: heart, unit: "次/分", num: 0, color: "#FF0303" },
{ title: "当前血氧", en: "DANGQIANXUEYANG", icon: blood, unit: "%", num: 0, color: "#8B51FD" },
{ title: "当前体表温度", en: "DANGQIANTIBIAOWENDU", icon: temperature, unit: "次/分", num: 0, color: "#FF6905" },
]);
const ringOptions = {
const options = {
tooltip: {
trigger: "axis",
},
xAxis: {
type: "category",
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
data: [],
},
grid: {
left: "5%",
right: "4%",
},
yAxis: {
type: "value",
},
series: [
{
data: [820, 932, 901, 934, 1290, 1330, 1320],
series: {
name: "",
data: [],
type: "line",
itemStyle: {
color: "#ff4567", // 线
},
smooth: true,
},
],
};
const page = reactive({
index: 1,
const paging = reactive({
page: 1,
size: 10,
total: 200,
total: 0,
status: undefined,
warnType: undefined,
});
const devicePaging = reactive({
page: 1,
size: 10,
mode: undefined,
});
const HealthData = ref<THealthLatestData.TRes>();
const tableData = ref<TableItem[]>([]);
const deviceInfo = ref<TDevice.IListRes>();
const tableData = ref<TDevice.IWarningRecordRes[]>([]);
const deviceData = ref<TDevice.IListRes[]>([]);
const getData = async () => {
const res = await fetchData();
tableData.value = res.data.list;
const getdeviceList = async () => {
const res = await deviceList(devicePaging);
deviceData.value = res.records;
if (res.records.length > 0) {
deviceInfo.value = res.records[1];
getHealthLatestData();
// getData();
}
};
const getData = async () => {
const res = await warningRecord({ ...paging, deviceId: deviceInfo.value.deviceId });
tableData.value = res.records;
paging.total = res.total;
};
const getHealthLatestData = () => {
healthLatestData({ deviceId: deviceInfo.value.deviceId }).then((res) => {
HealthData.value = res;
funcList.value[0].num = res.hr;
funcList.value[1].num = res.bo;
funcList.value[2].num = res.temp;
getOptionsData(HealthData.value.hrArr, "心率", "#FF0303");
});
};
getData();
const changePage = (val: number) => {
page.index = val;
paging.page = val;
getData();
};
const handelRadio = (val: number) => {
if (val == 1) {
getOptionsData(HealthData.value.hrArr, "心率", "#FF0303");
} else if (val == 2) {
getOptionsData(HealthData.value.boArr, "血氧", "#983AFC");
} else if (val == 3) {
getOptionsData(HealthData.value.tempArr, "体表温度", "#FFA91F");
}
};
const getOptionsData = (list: { time: string; value: number }[], name: string, color: string) => {
options.xAxis.data = [];
options.series.data = [];
list.forEach((item) => {
options.xAxis.data.push(format(item.time, "HH:mm:ss"));
options.series.data.push(item.value);
});
options.series.name = name;
options.series.itemStyle.color = color;
myChart.setOption(options);
};
const handleResize = debounce(() => {
myChart.resize();
}, 200);
onMounted(() => {
getdeviceList();
new MapCustom({ dom: "mapcontainer" });
if (chartRef.value) {
myChart = echarts.init(chartRef.value);
myChart.setOption(ringOptions);
window.addEventListener("resize", handleResize);
}
});
@ -104,6 +171,24 @@ onDeactivated(() => {
});
</script>
<style scoped lang="less">
.container {
overflow: hidden;
display: flex;
.el-row {
flex: 1;
overflow: hidden;
display: flex;
.el-row-left {
height: 100%;
display: flex;
overflow: hidden;
}
.el-row-right {
height: 100%;
overflow: auto;
}
}
}
.monitoringMap {
width: 100%;
height: 400px;

View File

@ -8,7 +8,7 @@
<div class="en">{{ item.en }}</div>
</div>
<div class="item-left-bottom">
<div class="num" :style="{ color: item.color }">{{ item.num }}</div>
<div class="num" :style="{ color: item.color }">{{ item.num || "--" }}</div>
<div class="unit" :style="{ color: item.color }">{{ item.unit }}</div>
</div>
</div>
@ -19,15 +19,21 @@
</el-col>
</el-row>
</template>
<script setup>
import heart from "@/assets/img/heart.png";
import temperature from "@/assets/img/temperature.png";
import blood from "@/assets/img/blood.png";
let funcList = [
{ title: "当前心率", en: "DANGQIANXINLV", icon: heart, unit: "次/分", num: 186, color: "#FF0303" },
{ title: "当前血氧", en: "DANGQIANXUEYANG", icon: blood, unit: "%", num: 99, color: "#8B51FD" },
{ title: "当前体表温度", en: "DANGQIANTIBIAOWENDU", icon: temperature, unit: "次/分", num: 86, color: "#FF6905" },
];
<script setup lang="ts">
interface TFuncList {
title: string;
en: string;
icon: string;
unit: string;
num: number;
color: string;
}
defineProps({
funcList: {
type: Array<TFuncList>,
default: () => [],
},
});
</script>
<style scoped lang="less">
.monitoring-top {

View File

@ -6,7 +6,7 @@
<el-input style="width: 240px" v-model="search" placeholder="搜索设备IMEI号" :suffix-icon="Search" @input="handleInput" />
</div>
</div>
<TableCustom :columns="columns" :tableData="tableData" :total="page.total" :currentPage="page.index" :changePage="changePage">
<TableCustom :columns="columns" :tableData="tableData" :paging="page" :changePage="changePage">
<template #location="{ rows }">
<el-button type="success" link :icon="View" v-if="rows.warnType == 0 || rows.warnType == 1" @click="toIncidentDispose(rows.deviceId)"> 查看 </el-button>
</template>
@ -19,12 +19,20 @@
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { PropType, ref } from "vue";
import { Search, View } from "@element-plus/icons-vue";
import TableCustom from "@/components/table-custom.vue";
import { debounce } from "@/utils";
import { useRouter } from "vue-router";
import { TWarnRecord } from "@/api/index.d";
interface TPaging {
page: number;
size: number;
total: number;
deviceId?: number;
}
const router = useRouter();
enum statusEnum {
"待处理",
@ -38,8 +46,12 @@ const { tableData, page, getData, changePage } = defineProps({
default: () => [],
},
page: {
type: Object,
default: () => ({}),
type: Object as PropType<TPaging>,
default: () => ({
page: 1,
size: 10,
total: 0,
}),
},
getData: {
type: Function,

View File

@ -2,7 +2,7 @@
<div class="container">
<TableSearch :query="query" :options="searchOpt" :search="handleSearch" />
<div class="table-container">
<TableCustom :columns="columns" :tableData="tableData" :total="paging.total" :refresh="getData" :currentPage="paging.page" :changePage="changePage">
<TableCustom :columns="columns" :tableData="tableData" :paging="paging" :refresh="getData" :changePage="changePage">
<template #toolbarBtn>
<el-button type="primary" @click="handleAdd">新增</el-button>
<!-- <el-button>导出</el-button> -->

View File

@ -55,7 +55,7 @@
<el-tag :type="statusColor[rows.status]">{{ warningStatusEnum[rows.status] }}</el-tag>
</template>
<template #warnType="{ rows }">
{{ warnTypeEnum[rows.status] }}
{{ warnTypeEnum[rows.warnType] }}
</template>
</TableCustom>
</el-card>

View File

@ -2,7 +2,7 @@
<div class="container">
<TableSearch :query="query" :options="searchOpt" :search="handleSearch" />
<div class="table-container">
<TableCustom :columns="columns" :tableData="tableData" :total="paging.total" :refresh="getData" :currentPage="paging.page" :changePage="changePage">
<TableCustom :columns="columns" :tableData="tableData" :paging="paging" :refresh="getData" :changePage="changePage">
<template #toolbarBtn>
<!-- <el-button type="primary" @click="handleAdd">新增</el-button> -->
<el-button @click="handleEdit">手铐关联</el-button>
@ -205,10 +205,8 @@ const closeDialog = () => {
visible.value = false;
isEdit.value = false;
};
console.log(222222);
onMounted(() => {
console.log(111111);
getData();
});

View File

@ -2,7 +2,7 @@
<div class="container">
<TableSearch :query="query" :options="searchOpt" :search="handleSearch" />
<div class="table-container">
<TableCustom :columns="columns" :tableData="tableData" :total="paging.total" :refresh="getData" :currentPage="paging.page" :changePage="changePage">
<TableCustom :columns="columns" :tableData="tableData" :paging="paging" :refresh="getData" :changePage="changePage">
<template #toolbarBtn>
<el-button type="primary" @click="typeVisible = true">类型编辑</el-button>
<el-button type="primary" @click="handelRow('add')">新增用户</el-button>