2025年04月28日17:18:08

This commit is contained in:
luojiayi 2025-04-28 17:18:11 +08:00
parent 9b61be74c7
commit 5d600041cf
17 changed files with 310 additions and 204 deletions

View File

@ -1,5 +1,11 @@
# VITE_APP_URL = 'http://192.168.3.116:8001/'
# VITE_APP_URL_WEBSOCKET = 'http://192.168.3.116:8000/api/websocket'
VITE_APP_URL = 'http://api.handcuff.youaikang.cn'
VITE_APP_URL_WEBSOCKET = 'ws://device.handcuff.youaikang.cn:8000/api/websocket'
VITE_APP_URL = 'http://47.112.185.26:8001/'
VITE_APP_URL_WEBSOCKET = 'ws://47.112.185.26:8000/api/websocket'
# VITE_APP_URL = 'http://api.handcuff.zhuhaiguangdun.cn'
# VITE_APP_URL_WEBSOCKET = 'ws://device.handcuff.zhuhaiguangdun.cn:8000/api/websocket'
# VITE_APP_URL = 'http://api.handcuff.youaikang.cn'
# VITE_APP_URL_WEBSOCKET = 'http://device.handcuff.youaikang.cn:8000/api/websocket'

View File

@ -1,5 +1,5 @@
VITE_APP_URL = 'http://api.handcuff.youaikang.cn'
VITE_APP_URL_WEBSOCKET = 'http://device.handcuff.youaikang.cn:8000/api/websocket'
VITE_APP_URL = 'http://47.112.185.26:8001/'
VITE_APP_URL_WEBSOCKET = 'ws://47.112.185.26:8000/api/websocket'
# VITE_APP_URL = 'http://api.handcuff.zhuhaiguangdun.cn'
# VITE_APP_URL_WEBSOCKET = 'ws://device.handcuff.zhuhaiguangdun.cn:8000/api/websocket'

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -2,8 +2,7 @@
<div class="infoBox">
<div class="infoBox-head">
<div class="item">
<div class="span">手铐</div>
<div class="span">{{ value.id }}</div>
{{ value.deviceId }}
</div>
<div class="item" style="cursor: pointer" @click="emit('close')">
<svg t="1744273365398" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8493" width="24" height="24">
@ -17,10 +16,6 @@
</div>
</div>
<div class="infoBox-content">
<div class="item">
<div class="item-left">IMEI号</div>
<div class="item-right">{{ value.deviceId }}</div>
</div>
<div class="item">
<div class="item-left">定位时间</div>
<div class="item-right">{{ value.locationTime }}</div>
@ -86,12 +81,8 @@ enum locateEnum {
font-size: 16px;
color: #333;
font-weight: 400;
.span {
flex-shrink: 0;
font-weight: 700;
}
flex-shrink: 0;
font-weight: 700;
.battery {
width: 34px;
height: 16px;

View File

@ -38,7 +38,6 @@ const handleBeforeUnload = (event: any) => {
};
const onMessage = (res) => {
console.log(res, "WebSocket接收服务器消息");
if (res.cmd == "warning") {
alarmRef.value.visible = true;
alarmRef.value.warningList.unshift({ ...res, createTime: format(new Date(), "HH:mm:ss") });
@ -51,20 +50,21 @@ const onMessage = (res) => {
};
onMounted(() => {
// const isReload = localStorage.getItem("isReload");
// if (isReload !== "true") {
// ElMessageBox.alert("", "", {
// confirmButtonText: "OK",
// callback: () => {
// localStorage.setItem("isReload", "true");
// },
// });
// }
const isReload = localStorage.getItem("isReload");
if (isReload !== "true") {
ElMessageBox.alert("由于浏览器安全策略,用户必须点击屏幕才能播放告警声音", "提示", {
confirmButtonText: "OK",
callback: () => {
localStorage.setItem("isReload", "true");
},
});
}
if (ws.socket == null) {
ws.connect();
}
ws.onMessage(onMessage);
window.addEventListener("beforeunload", handleBeforeUnload);
});

View File

@ -103,20 +103,30 @@ export class MapCustom {
})
}
newIcon(url) {
return new AMap.Icon({
newIcon(option) {
let data = {
size: new AMap.Size(36, 42),
image: url, // Icon的图像
image: '', // Icon的图像
imageSize: new AMap.Size(36, 42)
})
}
if (typeof option == 'string') {
data.image = option
} else {
data = {
image: option.image, // Icon的图像
size: new AMap.Size(option.size[0], option.size[1]),
imageSize: new AMap.Size(option.size[0], option.size[1]),
}
}
return new AMap.Icon({ ...data })
}
// 创建marker
marker({ icon, position }) {
marker({ icon, position, offset }) {
return new AMap.Marker({
icon,
position,
map: this.map,
offset: new AMap.Pixel(-20, -40),
offset: new AMap.Pixel(-13, -26),
})
}
lngLat(lng, lat) {

View File

@ -41,9 +41,10 @@ export default class WebSocketService {
}
webScoketLogin() {
const comm = useCommonStore();
this.sendMessage({
username: "admin",
password: "111111",
username: comm.user.username,
token: comm.user.token,
type: 'web',
cmd: "webLogin",
})
@ -106,6 +107,8 @@ export default class WebSocketService {
onMessage = (callback) => {
this.socket.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(data, 'onMessage');
if (data.cmd == "webLogin" && !data.code == 200) return this.webScoketLogin()
callback && callback(data);
if (event.data.includes('heartbeat')) {

View File

@ -13,7 +13,6 @@
<el-col :span="7">
<div class="right-content">
<div class="info scrollbar">
<div class="info-text">设备序号{{ curData.id }}</div>
<div class="info-text">IMEI号{{ curData.deviceId }}</div>
<div class="info-text">告警时间{{ curData.createTime }}</div>
<div class="info-text">
@ -77,6 +76,9 @@
</div>
</el-col>
</el-row>
<div :style="{ display: 'none' }">
<InfoWindow class="infoBox" :value="locationInfo" @close="InfoWin.close()" />
</div>
</div>
</template>
@ -117,6 +119,8 @@ const disabled = ref(false);
const ruleFormRef = ref<FormInstance>();
let map = null;
let myChart = null;
let InfoWin = null;
let locationInfo = ref({});
const options = {
tooltip: {
@ -138,7 +142,7 @@ const options = {
grid: {
left: "0%",
right: "4%",
bottom: "3%",
bottom: "6%",
containLabel: true,
},
xAxis: {
@ -150,6 +154,28 @@ const options = {
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: {
@ -235,14 +261,13 @@ watch(
const getData = async () => {
try {
const res = await warningDetail({ id: query.id as string });
locationInfo.value = res;
curData.value = res;
ruleForm.name = res.rname;
ruleForm.username = res.rname;
ruleForm.content = res.rcontent;
ruleForm.images = JSON.parse(res.rimg);
let icon = map.newIcon(location);
let marker = map.marker({ icon, position: [116.406315, 39.908775] });
marker.setMap(map.map);
if (res.healthData && res.healthData.length) {
res.healthData.forEach((item) => {
options.times.push(item.time);
@ -253,6 +278,16 @@ const getData = async () => {
});
myChart.setOption(options);
}
map.clearMap();
map.setCenter([res.lng, res.lat]);
let icon = map.newIcon(location);
let marker = map.marker({ icon, position: [res.lng, res.lat] });
marker.setMap(map.map);
marker.on("click", () => {
InfoWin = map.infoWindow();
InfoWin.open(map.map, marker.getPosition());
});
} catch (error) {}
};
const submitForm = async (formEl: FormInstance | undefined) => {

View File

@ -14,10 +14,7 @@
<el-form-item>
<el-button type="primary" :disabled="disabled" @click="submitForm(ruleFormRef)" class="btn" size="large"> </el-button>
</el-form-item>
<div class="hint">
如遇到账号问题请联系管理员<br />
管理员联系方式13812345678 邮箱123456@qq.com
</div>
<div class="hint">如遇到账号问题请联系管理员</div>
</el-form>
</div>
</div>

View File

@ -8,30 +8,118 @@
<el-radio-button label="体表温度" value="3" />
</el-radio-group>
</div>
<div class="card-chart">
<slot name="chart"></slot>
</div>
<div ref="chartRef" class="card-chart"></div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { debounce, format } from "@/utils";
import { onMounted, onUnmounted, ref } from "vue";
import * as echarts from "echarts";
const radio = ref("1");
const chartRef = ref(null);
let myChart: any = null;
const emit = defineEmits(["change"]);
defineExpose({ radio });
const options = {
tooltip: {
trigger: "axis",
formatter: function (params) {
let unit = { 心率: "次/分", 血氧: "%", 体表温度: "℃" };
var res = format(options.times[params[0].dataIndex]) + "<br/>";
res += params
.map(function (param, index) {
return param.marker + param.seriesName + "" + param.value + unit[param.seriesName] + "<br/>";
})
.join("");
return res;
},
},
times: [],
xAxis: {
type: "category",
data: [],
},
yAxis: {
type: "value",
},
grid: {
left: "5%",
right: "4%",
bottom: "20%",
},
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: {
name: "",
data: [],
type: "line",
showSymbol: false,
itemStyle: {
color: "#ff4567", // 线
},
smooth: true,
},
};
const getOptionsData = (list: { time: string; value: number }[], name: string, color: string) => {
options.xAxis.data = [];
options.series.data = [];
if (list && list.length) {
list.forEach((item) => {
options.times.push(item.time);
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);
defineExpose({ radio, getOptionsData });
onMounted(() => {
myChart = echarts.init(chartRef.value);
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
});
const change = (e) => {
emit("change", e);
};
</script>
<style scoped lang="less">
.card {
// height: 100%;
min-height: 385px;
flex-shrink: 0;
background: #ffffff;
padding: 16px 10px;
box-sizing: border-box;
flex: 1;
margin-right: 20px;
display: flex;
flex-direction: column;

View File

@ -10,7 +10,9 @@ import { format } from "@/utils";
import { MapCustom } from "@/utils/mapCustom";
import { onMounted, ref, watch } from "vue";
import InfoWindow from "@/components/InfoWindow.vue";
import location from "@/assets/img/location.png";
import ViaMarker from "@/assets/img/via-marker.png";
import endMarker from "@/assets/img/end-marker.png";
import startMarker from "@/assets/img/start-marker.png";
let InfoWin = null;
let newMap = null;
@ -44,9 +46,28 @@ const getLocateRecord = () => {
newMap.clearMap();
newMap.setCenter([list[0].lng, list[0].lat]);
newMap.polyline(list);
let icon = newMap.newIcon(location);
list.forEach((item) => {
let marker = newMap.marker({ icon, position: [item.lng, item.lat] });
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],
});
list.forEach((item, index) => {
let marker: any = "";
if (index == 0) {
marker = newMap.marker({ icon: startIcon, position: [item.lng, item.lat] });
} else if (index == list.length - 1) {
marker = newMap.marker({ icon: endIcon, position: [item.lng, item.lat] });
} else {
marker = newMap.marker({ icon: ViaIcon, position: [item.lng, item.lat] });
}
marker.setMap(newMap.map);
marker.on("click", () => {
locationInfo.value = item;

View File

@ -48,7 +48,7 @@ enum warnTypeEnum {
const warnTypeList = ["SOS告警", "围栏告警", "破坏告警", "低电告警", "心率告警", "血氧告警", "体温告警"];
const paging = reactive({
page: 1,
size: 4,
size: 5,
total: 0,
deviceId: undefined,
status: undefined,
@ -98,10 +98,12 @@ watch(
</script>
<style scoped lang="less">
.card {
background: #ffffff;
min-height: 345px;
height: 100%;
padding: 16px 10px;
box-sizing: border-box;
flex-shrink: 0;
background: #ffffff;
.card-head {
color: #061451;
font-size: 18px;

View File

@ -1,38 +1,32 @@
<template>
<div class="container">
<el-row class="el-row" :gutter="20">
<el-col :span="6" class="el-row-left"
><DeviceInfo :deviceInfo="deviceInfo" :paging="devicePaging" :list="deviceData" :handelMode="handelMode" :load="handelLoad" @click="handelClickDevice"
/></el-col>
<el-col :span="18" class="el-row-right scrollbar">
<MonitoringTop :funcList="funcList" />
<div class="el-row-right-content" v-if="deviceInfo?.mode == 0 || deviceInfo?.mode == 2">
<DeviceLocationMap :device-id="deviceInfo.deviceId" />
<div class="box">
<DeviceHistory @change="handelRadio" ref="devHisRef">
<template #chart>
<div ref="chartRef" style="width: 100%; height: 100%"></div>
</template>
</DeviceHistory>
<div class="el-row-left">
<DeviceInfo :deviceInfo="deviceInfo" :paging="devicePaging" :list="deviceData" :handelMode="handelMode" :load="handelLoad" @click="handelClickDevice" />
</div>
<div class="el-row-right scrollbar">
<MonitoringTop :funcList="funcList" />
<div class="el-row-right-content" v-if="deviceInfo?.mode == 0 || deviceInfo?.mode == 2">
<DeviceLocationMap :device-id="deviceInfo.deviceId" />
<el-row class="box" :gutter="20">
<el-col :span="12" :xs="24" :sm="24" :md="24" :lg="24" :xl="12" style="margin-bottom: 20px">
<DeviceHistory @change="handelRadio" ref="devHisRef" />
</el-col>
<el-col :span="12" :xs="24" :sm="24" :md="24" :lg="24" :xl="12">
<DeviceRecord :deviceInfo="deviceInfo" />
</div>
</div>
<div class="el-row-right-content" v-else>
<el-row :gutter="20" style="margin-top: 20px">
<el-col :span="24">
<DeviceHistory @change="handelRadio" ref="devHisRef">
<template #chart>
<div ref="chartRef1" style="width: 100%; height: 350px"></div>
</template>
</DeviceHistory>
</el-col>
<el-col :span="24" style="margin-top: 20px">
<DeviceRecord :deviceInfo="deviceInfo" />
</el-col>
</el-row>
</div>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
<div class="el-row-right-content" v-else>
<el-row :gutter="20">
<el-col :span="24" style="margin-bottom: 20px">
<DeviceHistory @change="handelRadio" ref="devHisRef" />
</el-col>
<el-col :span="24">
<DeviceRecord :deviceInfo="deviceInfo" />
</el-col>
</el-row>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
@ -40,21 +34,17 @@ import DeviceInfo from "./deviceInfo.vue";
import MonitoringTop from "./monitoringTop.vue";
import DeviceHistory from "./deviceHistory.vue";
import DeviceRecord from "./deviceRecord.vue";
import * as echarts from "echarts";
import DeviceLocationMap from "./deviceLocationMap.vue";
import { deviceList, healthLatestData } from "@/api/index";
import { TDevice, THealthLatestData } from "@/api/index.d";
import { onMounted, ref, reactive, onUnmounted, watch, nextTick } from "vue";
import { debounce, format } from "@/utils";
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";
import DeviceLocationMap from "./deviceLocationMap.vue";
const chartRef = ref(null);
const chartRef1 = ref(null);
const devHisRef = ref(null);
const loadFlag = ref(false);
let myChart = null;
let Interval = null;
let funcList = ref([
{ title: "当前心率", en: "DANGQIANXINLV", icon: heart, unit: "次/分", num: 0, color: "#FF0303" },
@ -126,28 +116,6 @@ const HealthData = ref<THealthLatestData.TRes>();
const deviceInfo = ref<TDevice.IListRes>();
const deviceData = ref<TDevice.IListRes[]>([]);
watch(
() => deviceInfo,
(newVal) => {
if (newVal.value) {
if (newVal.value?.mode == 0 || newVal.value?.mode == 2) {
nextTick(() => {
myChart = echarts.init(chartRef.value);
});
options.grid.left = "10%";
options.grid.bottom = "30%";
} else {
options.grid.left = "5%";
options.grid.bottom = "20%";
nextTick(() => {
myChart = echarts.init(chartRef1.value);
});
}
}
},
{ deep: true }
);
const getdeviceList = async () => {
const res = await deviceList(devicePaging);
deviceData.value = [...deviceData.value, ...res.records];
@ -167,35 +135,19 @@ const getHealthLatestData = () => {
funcList.value[0].num = res.hr;
funcList.value[1].num = res.bo;
funcList.value[2].num = res.temp;
handelRadio(devHisRef.value.radio);
});
};
const handelRadio = (val: number) => {
console.log("刷新");
if (val == 1) {
getOptionsData(HealthData.value.hrArr, "心率", "#FF0303");
devHisRef.value?.getOptionsData(HealthData.value.hrArr, "心率", "#FF0303");
} else if (val == 2) {
getOptionsData(HealthData.value.boArr, "血氧", "#983AFC");
devHisRef.value?.getOptionsData(HealthData.value.boArr, "血氧", "#983AFC");
} else if (val == 3) {
getOptionsData(HealthData.value.tempArr, "体表温度", "#FFA91F");
devHisRef.value?.getOptionsData(HealthData.value.tempArr, "体表温度", "#FFA91F");
}
};
const getOptionsData = (list: { time: string; value: number }[], name: string, color: string) => {
options.xAxis.data = [];
options.series.data = [];
if (list && list.length) {
list.forEach((item) => {
options.times.push(item.time);
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 IntervalFn = () => {
Interval = setInterval(() => {
@ -218,47 +170,42 @@ const handelClickDevice = (val: TDevice.IListRes) => {
deviceInfo.value = val;
getHealthLatestData();
};
const handleResize = debounce(() => {
myChart.resize();
}, 200);
onMounted(() => {
getdeviceList();
myChart = echarts.init(chartRef.value ? chartRef.value : chartRef1.value);
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
clearInterval(Interval);
window.removeEventListener("resize", handleResize);
});
</script>
<style scoped lang="less">
.container {
overflow: hidden;
display: flex;
.el-row {
flex: 1;
overflow: hidden;
flex-direction: row;
.el-row-left {
width: 335px;
// height: 100%;
display: flex;
.el-row-left {
height: 100%;
display: flex;
overflow: hidden;
flex-shrink: 0;
}
.el-row-right {
width: 100%;
overflow: auto;
margin-left: 20px;
.el-row-right-content {
overflow: hidden;
}
.el-row-right {
height: 100%;
overflow: auto;
.el-row-right-content {
flex: 1;
// height: 100%;
.box {
height: 100%;
flex-shrink: 0;
margin-top: 20px;
display: flex;
flex-direction: column;
overflow: hidden;
.box {
height: 388px;
flex-shrink: 0;
overflow: hidden;
margin-top: 20px;
display: flex;
justify-content: space-between;
justify-content: space-between;
.el-col {
height: 100%;
}
}
}

View File

@ -10,6 +10,9 @@
<template #location="{ rows }">
<el-button type="success" link :icon="View" v-if="rows.warnType == 0 || rows.warnType == 1" @click="toIncidentDispose(rows.id)"> 查看 </el-button>
</template>
<template #warnType="{ rows }">
{{ warnTypeEnum[rows.warnType] }}
</template>
<template #status="{ rows }">
<el-tag :type="rows.status == 1 ? 'success' : 'danger'">
{{ statusEnum[rows.status] }}
@ -38,6 +41,15 @@ enum statusEnum {
"待处理",
"已处理",
}
enum warnTypeEnum {
"SOS告警",
"围栏告警",
"破坏告警",
"低电告警",
"心率告警",
"血氧告警",
"体温告警",
}
const search = ref("");
const { tableData, page, getData, changePage } = defineProps({
@ -66,6 +78,7 @@ const { tableData, page, getData, changePage } = defineProps({
let columns = [
{ type: "index", label: "序号", width: 55, align: "center" },
{ prop: "deviceId", label: "IMEI号" },
{ prop: "warnType", label: "事件类型" },
{ prop: "createTime", label: "时间" },
{ prop: "location", label: "地理位置" },
{ prop: "status", label: "处理状态" },

View File

@ -228,43 +228,36 @@ const getOrgAllList = () => {
});
};
const handelControl = (type: number) => {
switch (type) {
case 4:
deviceGetLocation({
deviceId: rowData.value.deviceId,
}).then(() => {
ElMessage.success("操作成功");
});
break;
case 5:
setMode({
deviceId: rowData.value.deviceId,
mode: controlForm.mode,
}).then(() => {
getData();
ElMessage.success("操作成功");
});
break;
case 6:
deviceOta(rowData.value.deviceId).then(() => {
getData();
ElMessage.success("操作成功");
});
break;
default:
let cmdEnum = {
1: "poweroff",
2: "restart",
3: "factory",
};
deviceControl({
deviceId: rowData.value.deviceId,
cmd: cmdEnum[type],
}).then(() => {
ElMessage.success("操作成功");
});
break;
let api: Function;
let params: any = {};
if (type == 4) {
api = deviceGetLocation;
params = { deviceId: rowData.value.deviceId };
} else if (type == 5) {
api = setMode;
params = {
deviceId: rowData.value.deviceId,
mode: controlForm.mode,
};
} else if (type == 6) {
api = deviceOta;
params = rowData.value.deviceId;
} else {
let cmdEnum = {
1: "poweroff",
2: "restart",
3: "factory",
};
api = deviceControl;
params = {
deviceId: rowData.value.deviceId,
cmd: cmdEnum[type],
};
}
api(params).then(() => {
getData();
ElMessage.success("操作成功");
});
visible1.value = false;
};