2025年04月30日18:18:19

This commit is contained in:
luojiayi 2025-04-30 18:18:21 +08:00
parent dd4d7b2412
commit 65bcb4b974
17 changed files with 439 additions and 31 deletions

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

@ -185,6 +185,7 @@ export namespace TDevice {
battery: number
otaFlag: number
adminUsername: string
adminName: string
adminPhone: string
userNumber: string
adminType: string
@ -247,7 +248,7 @@ export namespace TDevice {
createTime: string
}
export interface IWarningRecordRes {
id: number
id: number | string
userNumber: string
deviceId: string
warnType: number
@ -414,4 +415,37 @@ export namespace TRoleMenuList {
menus: TMenus[]
}
}
export namespace TDataRecord {
interface TMenus {
id: number
type: number
name: string
createTime: string
}
interface ThealthData {
temp: number
hr: number
time: number
bo: number
}
interface TReq {
useRecordId: number | string
}
interface TRes {
id: number
userNumber: number
adminName: string
username: number
adminPhone: number
deviceId: number
warnType: number
type: number
orgName: string
value: number
maxValue: number
minValue: number
healthData: ThealthData[]
}
}

View File

@ -1,5 +1,5 @@
import request from '../utils/request';
import { TLogin, TAccount, IpagingRes, TDevice, TOrg, TRoleList, statisticsContentReq, statisticsContentRes, TStatisticsCount, TWarnRecord, TWarningDetail, TWarningConfirm, TDeviceConfigModify, TDeviceConfig, THealthLatestData, TLocateRecord, TSetUseStatus, TRoleMenuList, TRoleModify, TbindWeb, TstatisticsUseCount, TAccountSetStatus } from "./index.d";
import { TLogin, TAccount, IpagingRes, TDevice, TOrg, TRoleList, statisticsContentReq, statisticsContentRes, TStatisticsCount, TWarnRecord, TWarningDetail, TWarningConfirm, TDeviceConfigModify, TDeviceConfig, THealthLatestData, TLocateRecord, TSetUseStatus, TRoleMenuList, TRoleModify, TbindWeb, TstatisticsUseCount, TAccountSetStatus, TDataRecord } from "./index.d";
export const fetchLogin = (p: TLogin.Ireq): Promise<TLogin.IRes> => {
return request({
@ -378,4 +378,13 @@ export const roleMenuList = (): Promise<TRoleMenuList.TRes[]> => {
});
};
// 获取最新健康数据使用记录
export const dataRecord = (params: TDataRecord.TReq): Promise<TDataRecord.TRes> => {
return request({
url: '/v1/api/health/data/record',
method: 'get',
params
});
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

BIN
src/assets/img/narrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

View File

@ -60,11 +60,11 @@
</template>
<template #default="{ row, column, $index }" v-if="!item.type">
<slot :name="item.prop" :rows="row" :index="$index">
<template v-if="item.prop == 'operator'">
<!-- <template v-if="item.prop == 'operator'">
<el-button type="primary" size="small" :icon="Edit" @click="editFunc(row)"> 编辑 </el-button>
<el-button type="danger" size="small" :icon="Delete" @click="handleDelete(row)"> 删除 </el-button>
</template>
<span v-else-if="item.formatter">
</template> -->
<span v-if="item.formatter">
{{ item.formatter(row[item.prop]) }}
</span>
<span v-else>

View File

@ -1,4 +1,4 @@
import { computed } from "vue";
import { computed, onBeforeMount, onMounted, onUnmounted, ref } from "vue";
export function useVModel(props, propsName, emit) {
return computed({
@ -19,3 +19,83 @@ export function useVModel(props, propsName, emit) {
})
}
// 全屏
export function useFullScreen() {
const isFullScreen = ref(false)
const toggleFullScreen = (domId: string) => {
if (!isFullScreen.value) {
requestFullScreen(domId);
} else {
exitFull();
}
isFullScreen.value = !isFullScreen.value;
}
//全屏
const requestFullScreen = (el) => {
const element: any = document.querySelector(`#${el}`);
let win: any = window
var requestMethod =
element.requestFullScreen || //W3C
element.webkitRequestFullScreen || //Chrome等
element.mozRequestFullScreen || //FireFox
element.msRequestFullScreen; //IE11
if (requestMethod) {
requestMethod.call(element);
} else if ("ActiveXObject" in window) {
//for Internet Explorer
var wscript = win.ActiveXObject("WScript.Shell");
if (wscript !== null) {
wscript.SendKeys("{F11}");
}
}
}
//退出全屏
const exitFull = () => {
// 判断各种浏览
let doc: any = document
let win: any = window
var exitMethod =
doc.exitFullscreen || //W3C
doc.mozCancelFullScreen || //Chrome等
doc.webkitExitFullscreen; //FireFox
if (exitMethod) {
exitMethod.call(doc);
} else if (typeof win.ActiveXObject !== "undefined") {
var wscript = new win.ActiveXObject("WScript.Shell");
if (wscript !== null) {
wscript.SendKeys("{F11}");
}
}
}
const checkFull = () => {
let doc: any = document
//判断浏览器是否处于全屏状态 (需要考虑兼容问题)
var flag = doc.mozFullScreen ||
doc.fullScreen ||
//谷歌浏览器及Webkit内核浏览器
doc.webkitIsFullScreen ||
doc.webkitRequestFullScreen ||
doc.mozRequestFullScreen ||
doc.msFullscreenEnabled
if (flag === undefined) {
isFullScreen.value = false
}
return flag;
}
const fullChange = () => {
if (!checkFull() && isFullScreen.value) {
isFullScreen.value = false;
}
}
onMounted(() => {
window.addEventListener('resize', fullChange)
});
onUnmounted(() => {
window.removeEventListener('resize', fullChange)
});
return { isFullScreen, toggleFullScreen };
}

View File

@ -10,7 +10,7 @@ const showErrorNotification = throttle((message: string) => {
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_APP_URL,
timeout: 5000
timeout: 20000
});
service.interceptors.request.use(

View File

@ -80,7 +80,9 @@ const handleSearch = () => {
let columns = ref([
{ type: "index", label: "序号", width: 55, align: "center" },
{ prop: "deviceId", label: "手铐IMEI号" },
{ prop: "userNumber", label: "关联人员" },
{ prop: "adminName", label: "绑定用户" },
{ prop: "username", label: "警员号" },
{ prop: "userNumber", label: "佩戴者" },
{ prop: "warnType", label: "事件类型" },
{ prop: "createTime", label: "时间" },
{ prop: "status", label: "处理状态" },

View File

@ -18,8 +18,9 @@
<div class="info-text">
告警类型<span style="color: red">{{ warnTypeEnum[curData.warnType] }}</span>
</div>
<div class="info-text">绑定关联人名称{{ curData.username }}</div>
<div class="info-text">绑定关联人警号{{ curData.userNumber }}</div>
<div class="info-text">绑定用户{{ curData.adminName }}</div>
<div class="info-text">绑定警员号{{ curData.username }}</div>
<div class="info-text">佩戴者{{ curData.userNumber || "--" }}</div>
<div class="info-text">
状态
<el-tag :type="curData.status == 1 ? 'success' : 'danger'">
@ -385,7 +386,7 @@ onUnmounted(() => {
.info-text {
color: #061451;
font-size: 18px;
font-size: 16px;
margin-bottom: 20px;
}
.info-box {

View File

@ -39,7 +39,11 @@
{{ item.adminType }}
</div>
<div>
<span class="lable">警号</span>
<span class="lable">用户名称</span>
{{ item.adminName }}
</div>
<div>
<span class="lable">警员号</span>
{{ item.adminUsername }}
</div>
<div>

View File

@ -1,5 +1,10 @@
<template>
<div class="monitoringMap" id="mapcontainer"></div>
<div class="monitoringMap" id="mapcontainer">
<div class="toolbox" @click="toggleFullScreen('mapcontainer')">
<img :src="fullScreen" alt="" v-if="!isFullScreen" />
<img :src="narrow" alt="" v-else />
</div>
</div>
<div :style="{ display: 'none' }">
<InfoWindow class="infoBox" :value="locationInfo" @close="InfoWin.close()" />
</div>
@ -13,11 +18,14 @@ import InfoWindow from "@/components/InfoWindow.vue";
import ViaMarker from "@/assets/img/via-marker.png";
import endMarker from "@/assets/img/end-marker.png";
import startMarker from "@/assets/img/start-marker.png";
import fullScreen from "@/assets/img/fullScreen.png";
import narrow from "@/assets/img/narrow.png";
import { useFullScreen } from "@/utils/hooks";
let InfoWin = null;
let newMap = null;
let locationInfo = ref({});
const { isFullScreen, toggleFullScreen } = useFullScreen();
const props = defineProps({
deviceId: {
type: String || Number,
@ -100,10 +108,6 @@ const getLocateRecord = () => {
});
};
const fn = () => {
// this.cluster.on("click", this.clusterClickEvent);
};
onMounted(() => {
newMap = new MapCustom({ dom: "mapcontainer" });
getLocateRecord();
@ -114,5 +118,19 @@ onMounted(() => {
width: 100%;
height: 400px;
flex-shrink: 0;
position: relative;
.toolbox {
width: 32px;
height: 32px;
padding: 5px;
position: absolute;
right: 20px;
top: 20px;
// padding: 5px;
background: #fff;
border-radius: 4px;
z-index: 2;
cursor: pointer;
}
}
</style>

View File

@ -22,6 +22,9 @@
<template #warnType="{ rows }">
{{ warnTypeEnum[rows.warnType] }}
</template>
<template #operator="{ rows }">
<el-button type="primary" size="small" link @click="toIncidentDispose(rows.id)"> 处理事件 </el-button>
</template>
</TableCustom>
</div>
</template>
@ -30,6 +33,7 @@ import TableCustom from "@/components/table-custom.vue";
import { warningRecord } from "@/api/index";
import { ref, reactive, watch } from "vue";
import { TDevice } from "@/api/index.d";
import { useRouter } from "vue-router";
const statusColor = ["danger", "success"];
enum warningStatusEnum {
@ -45,6 +49,7 @@ enum warnTypeEnum {
"血氧告警",
"体温告警",
}
const router = useRouter();
const warnTypeList = ["SOS告警", "围栏告警", "破坏告警", "低电告警", "心率告警", "血氧告警", "体温告警"];
const paging = reactive({
page: 1,
@ -69,6 +74,7 @@ let columns = [
{ prop: "warnType", label: "事件类型" },
{ prop: "status", label: "处理状态" },
{ prop: "createTime", label: "触发时间" },
{ prop: "operator", label: "操作" },
];
const getData = async () => {
const res = await warningRecord(paging);
@ -95,6 +101,12 @@ watch(
},
{ immediate: true } //
);
const toIncidentDispose = (id: string) => {
router.push({
path: "/incidentDispose",
query: { id },
});
};
</script>
<style scoped lang="less">
.card {

View File

@ -38,7 +38,6 @@ import DeviceLocationMap from "./deviceLocationMap.vue";
import { deviceList, healthLatestData } from "@/api/index";
import { TDevice, THealthLatestData } from "@/api/index.d";
import { onMounted, ref, reactive, onUnmounted } from "vue";
import { format } from "@/utils";
import heart from "@/assets/img/heart.png";
import temperature from "@/assets/img/temperature.png";
import blood from "@/assets/img/blood.png";

View File

@ -8,7 +8,12 @@
</div>
<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.id)"> 查看 </el-button>
<el-button type="success" link v-if="rows.warnType == 0 || rows.warnType == 1" @click="toIncidentDispose(rows.id)"> 查看 </el-button>
<div v-else>--</div>
</template>
<template #operator="{ rows }">
<el-button type="primary" v-if="!rows.status" size="small" link @click="toIncidentDispose(rows.id)"> 处理事件 </el-button>
<div v-else>--</div>
</template>
<template #warnType="{ rows }">
{{ warnTypeEnum[rows.warnType] }}
@ -82,6 +87,7 @@ let columns = [
{ prop: "createTime", label: "时间" },
{ prop: "location", label: "地理位置" },
{ prop: "status", label: "处理状态" },
{ prop: "operator", label: "操作" },
];
const handleInput = debounce((e) => {
page.deviceId = e;

View File

@ -0,0 +1,234 @@
<template>
<el-dialog title="历史记录" v-model="visible" width="70%" destroy-on-close :close-on-click-modal="false" @close="visible = false">
<div class="monitoringMap" id="mapcontainer"></div>
<div ref="chartRef" class="chart"></div>
<div :style="{ display: 'none' }">
<InfoWindow class="infoBox" :value="locationInfo" @close="InfoWin.close()" />
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { debounce, format } from "@/utils";
import { MapCustom } from "@/utils/mapCustom";
import { nextTick, onMounted, onUnmounted, PropType, ref, watch } from "vue";
import * as echarts from "echarts";
import { dataRecord, locateRecord } from "@/api";
import { TDevice } from "@/api/index.d";
import ViaMarker from "@/assets/img/via-marker.png";
import endMarker from "@/assets/img/end-marker.png";
import startMarker from "@/assets/img/start-marker.png";
import InfoWindow from "@/components/InfoWindow.vue";
const chartRef = ref(null);
let myChart = null;
let InfoWin = null;
let newMap = null;
const visible = ref(false);
let locationInfo = ref({});
defineExpose({
visible,
});
const props = defineProps({
recordInofo: {
type: Object as PropType<TDevice.IUseRecordRes>,
default: () => {},
},
});
const options = {
tooltip: {
trigger: "axis",
formatter: function (params) {
let unit = { 0: "次/分", 1: "%", 2: "℃" };
var res = format(Number(params[0].name)) + "<br/>";
res += params
.map(function (param, index) {
return param.marker + param.seriesName + "" + param.value + unit[index] + "<br/>";
})
.join("");
return res;
},
},
legend: {
data: ["心率", "体表温度", "血氧"],
},
grid: {
left: "0%",
right: "4%",
bottom: "6%",
containLabel: true,
},
xAxis: {
type: "category",
boundaryGap: false,
data: [],
axisLabel: {
formatter: function (item) {
return format(Number(item), "HH:mm:ss");
},
},
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "slider", //sliderinside
start: 90, // 10%
end: 100, // 60%
show: true,
xAxisIndex: [0],
handleSize: 0, // 2
height: 12, //
bottom: -2, //
borderColor: "#eee",
fillerColor: "#E7E7E7",
backgroundColor: "#eee", //
showDataShadow: false, // auto
showDetail: false, // true
realtime: true, //
filterMode: "filter",
handleStyle: {
borderRadius: "20",
},
},
],
series: [
{
itemStyle: {
color: "#FF4241",
},
name: "心率",
type: "line",
showSymbol: false,
data: [],
time: [],
},
{
itemStyle: {
color: "#12CCA2",
},
name: "血氧",
type: "line",
showSymbol: false,
data: [],
time: [],
},
{
itemStyle: {
color: "#FF7D00",
},
showSymbol: false,
name: "体表温度",
type: "line",
data: [],
time: [],
},
],
};
watch(visible, (val) => {
if (val) {
nextTick(() => {
newMap = new MapCustom({ dom: "mapcontainer" });
myChart = echarts.init(chartRef.value);
getLocateRecord();
dataRecord({ useRecordId: props.recordInofo.id }).then((res) => {
if (res.healthData && res.healthData.length) {
res.healthData.forEach((item) => {
options.xAxis.data.push(item.time);
options.series[0].data.push(item.hr);
options.series[1].data.push(item.bo);
options.series[2].data.push(item.temp);
});
}
myChart.setOption(options);
});
});
} else {
myChart = null;
newMap = null;
window.removeEventListener("resize", handleResize);
}
});
const getLocateRecord = () => {
locateRecord({
deviceId: props.recordInofo.deviceId,
startDate: props.recordInofo.createTime,
endDate: props.recordInofo.updateTime,
}).then((res) => {
if (res && res.length) {
let list = res;
newMap.clearMap();
newMap.setCenter([list[0].lng, list[0].lat]);
newMap.polyline(list);
let startIcon = newMap.newIcon({
image: startMarker,
size: [20, 29],
});
let endIcon = newMap.newIcon({
image: endMarker,
size: [20, 29],
});
let ViaIcon = newMap.newIcon({
image: ViaMarker,
size: [20, 29],
});
let markers = [];
list.forEach((item, index) => {
if (list.length < 50) {
let marker: any = "";
if (index == 0) {
marker = newMap.marker({ icon: endIcon, position: [item.lng, item.lat], zIndex: 13 });
} else if (index == list.length - 1) {
marker = newMap.marker({ icon: startIcon, position: [item.lng, item.lat], zIndex: 13 });
} else {
marker = newMap.marker({ icon: ViaIcon, position: [item.lng, item.lat], zIndex: 12 });
}
marker.on("click", () => {
locationInfo.value = item;
InfoWin = newMap.infoWindow();
InfoWin.open(newMap.map, marker.getPosition());
});
markers.push(marker);
} else {
if (index % 5 == 0) {
let marker: any = "";
if (index == 0) {
marker = newMap.marker({ icon: endIcon, position: [item.lng, item.lat], zIndex: 13 });
} else if (index == list.length - 1) {
marker = newMap.marker({ icon: startIcon, position: [item.lng, item.lat], zIndex: 13 });
} else {
marker = newMap.marker({ icon: ViaIcon, position: [item.lng, item.lat], zIndex: 12 });
}
marker.on("click", () => {
locationInfo.value = item;
InfoWin = newMap.infoWindow();
InfoWin.open(newMap.map, marker.getPosition());
});
markers.push(marker);
}
}
});
newMap.map.add(markers);
}
});
};
const handleResize = debounce(() => {
myChart && myChart.resize();
}, 200);
onMounted(() => {
window.addEventListener("resize", handleResize);
});
</script>
<style scoped lang="less">
.monitoringMap {
height: 300px;
}
.chart {
height: 300px;
margin-top: 20px;
}
</style>

View File

@ -7,12 +7,12 @@
<el-row :gutter="20">
<el-col :span="6">
<div class="item"><span>手铐序号</span>{{ query.id }}</div>
<div class="item"><span>绑定管理员</span>{{ query.adminName }}</div>
<div class="item"><span>绑定用户</span>{{ query.adminName }}</div>
<div class="item"><span>设备状态</span>{{ statusEnum[query.status as string] }}</div>
</el-col>
<el-col :span="6">
<div class="item"><span>IMEI号</span>{{ query.deviceId }}</div>
<div class="item"><span>绑定管理者账</span>{{ query.adminUsername }}</div>
<div class="item"><span>绑定警员</span>{{ query.adminUsername }}</div>
<div class="item"><span>当前电量</span>{{ query.battery }}%</div>
</el-col>
<el-col :span="6">
@ -33,9 +33,9 @@
<template #status="{ rows }">
{{ recordStatusEnum[rows.status] }}
</template>
<template #operator="{ rows }">
<el-button link type="primary" size="small"> 历史记录 </el-button>
</template>
<!-- <template #operator="{ rows }">
<el-button link type="primary" size="small" @click="viewHistory(rows)" v-if="rows.updateTime"> 历史记录 </el-button>
</template> -->
</TableCustom>
</el-card>
<el-card class="baseInfo">
@ -45,7 +45,6 @@
<TableCustom :hasToolbar="false" :columns="columns" :tableData="warningTableData" :paging="paging1" :changePage="changeWarningPage">
<template #operator="{ rows }">
<el-button link type="primary" size="small" @click="toIncidentDispose(rows.id)" v-if="rows.status == 0"> 处理事件 </el-button>
<div v-else></div>
</template>
<template #status="{ rows }">
<el-tag :type="statusColor[rows.status]">{{ warningStatusEnum[rows.status] }}</el-tag>
@ -55,6 +54,7 @@
</template>
</TableCustom>
</el-card>
<DeviceHistory ref="deviceHistoryRef" :recordInofo="recordInofo" />
</div>
</template>
@ -65,6 +65,8 @@ import TableCustom from "@/components/table-custom.vue";
import { TableItem } from "@/types/table";
import { useRouter, useRoute } from "vue-router";
import { TDevice } from "@/api/index.d";
import DeviceHistory from "./deviceHistory.vue";
const { query } = useRoute();
const router = useRouter();
@ -90,15 +92,18 @@ enum warnTypeEnum {
"血氧告警",
"体温告警",
}
const deviceHistoryRef = ref(null);
const recordInofo = ref<TDevice.IUseRecordRes>();
const statusColor = ["danger", "success"];
//
let record = ref([
{ type: "index", label: "序号", width: 55, align: "center" },
{ prop: "adminName", label: "管理员" },
{ prop: "adminName", label: "用户名称" },
{ prop: "username", label: "警员号" },
{ prop: "userNumber", label: "佩戴者" },
{ prop: "createTime", label: "开始使用时间" },
{ prop: "updateTime", label: "结束使用时间" },
{ prop: "operator", label: "操作" },
// { prop: "operator", label: "" },
]);
//
@ -153,6 +158,10 @@ const toIncidentDispose = (id: string) => {
query: { id },
});
};
const viewHistory = (row: TDevice.IUseRecordRes) => {
recordInofo.value = row;
deviceHistoryRef.value.visible = true;
};
</script>
<style scoped lang="less">

View File

@ -134,15 +134,15 @@ let columns = ref([
{ type: "selection", reserveSelection: true },
{ type: "index", label: "序号", width: 55, align: "center" },
{ prop: "deviceId", label: "IMEI", width: 120 },
{ prop: "adminName", label: "绑定警察名称", width: 180 },
{ prop: "adminUsername", label: "绑定警察账户", width: 120 },
{ prop: "adminName", label: "绑定用户名称", width: 180 },
{ prop: "adminUsername", label: "绑定警员号", width: 120 },
{ prop: "orgName", label: "关联辖区", width: 120 },
{ prop: "battery", label: "电量", width: 100 },
{ prop: "deviceVersion", label: "版本号", width: 100 },
{ prop: "status", label: "设备状态", width: 100 },
{ prop: "mode", label: "当前模式", width: 100 },
{ prop: "useStatus", label: "使用状态", width: 100 },
{ prop: "deviceSwitch", label: "启用开关", width: 100 },
// { prop: "deviceSwitch", label: "", width: 100 },
{ prop: "createTime", label: "最新通信时间", width: 180 },
{ prop: "createTime", label: "创建时间", width: 180 },
{ prop: "operator", label: "操作", width: 400, fixed: "right" },