2025年04月11日18:46:50

This commit is contained in:
luojiayi 2025-04-11 18:46:52 +08:00
parent 3346134b9a
commit f82d3934ba
27 changed files with 632 additions and 179 deletions

View File

@ -1 +1,2 @@
VITE_APP_URL = 'http://192.168.3.116:8001'
VITE_APP_URL_WEBSOCKET = 'http://192.168.3.116:8000/api/websocket'

View File

@ -1,3 +1,4 @@
VITE_APP_URL = 'http://192.168.3.116:8001/'
VITE_APP_URL_WEBSOCKET = 'http://192.168.3.116:8000/api/websocket'

2
auto-imports.d.ts vendored
View File

@ -1,5 +1,5 @@
// Generated by 'unplugin-auto-import'
export {}
declare global {
const ElMessage: typeof import('element-plus/es')['ElMessage']
}

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

@ -20,7 +20,7 @@ export interface TRoleList {
id?: number
name: string
createTime?: string
roleMenu: TRoleMenu[]
roleMenu?: TRoleMenu[]
}
@ -81,9 +81,10 @@ export interface TDeviceConfigModify {
export namespace TRoleModify {
export interface Ireq {
id: number
id?: number
name: string
roleMenu: TRoleMenu[]
menus?: TRoleMenu[]
}
}
@ -257,9 +258,6 @@ export namespace TDevice {
}
export namespace TOrg {
export interface IAdd {
name: string;
@ -267,7 +265,7 @@ export namespace TOrg {
parentId: string;
}
export interface IListReq extends Ipaging {
name?: number;
name?: string;
}
export interface Idel {
id: number;

View File

@ -3,7 +3,7 @@ import { TLogin, TAccount, IpagingRes, TDevice, TOrg, TRoleList, TStatisticsDevi
export const fetchLogin = (p: TLogin.Ireq): Promise<TLogin.IRes> => {
return request({
url: '/v1/web/login',
url: '/v1/api/login',
method: 'post',
data: p
});
@ -12,7 +12,7 @@ export const fetchLogin = (p: TLogin.Ireq): Promise<TLogin.IRes> => {
// 批量导入账号样例
export const exportDemoAccount = (): Promise<any> => {
return request({
url: '/v1/web/upload/exportDemo/account',
url: '/v1/api/upload/exportDemo/account',
method: 'get',
responseType: 'blob'
});
@ -47,7 +47,7 @@ export const fetchRoleData = () => {
// 添加用户
export const accountAdd = (p: TAccount.IAdd): Promise<null> => {
return request({
url: '/v1/web/account/add',
url: '/v1/api/account/add',
method: 'post',
data: p
});
@ -55,7 +55,7 @@ export const accountAdd = (p: TAccount.IAdd): Promise<null> => {
// 删除用户
export const accountDeletet = (p: TAccount.Idel): Promise<null> => {
return request({
url: '/v1/web/account/delete',
url: '/v1/api/account/delete',
method: 'post',
data: p
});
@ -63,7 +63,7 @@ export const accountDeletet = (p: TAccount.Idel): Promise<null> => {
// 修改用户
export const accountModify = (p: TAccount.IAdd): Promise<null> => {
return request({
url: '/v1/web/account/modify',
url: '/v1/api/account/modify',
method: 'post',
data: p
});
@ -71,7 +71,7 @@ export const accountModify = (p: TAccount.IAdd): Promise<null> => {
// 获取用户列表
export const accountList = (p: TAccount.IListReq): Promise<IpagingRes<TAccount.IListRes>> => {
return request({
url: '/v1/web/account/list',
url: '/v1/api/account/list',
method: 'get',
params: p
});
@ -79,7 +79,7 @@ export const accountList = (p: TAccount.IListReq): Promise<IpagingRes<TAccount.I
// 重置密码
export const passwordReset = (p: TAccount.IResetPwd): Promise<null> => {
return request({
url: '/v1/web/account/password/reset',
url: '/v1/api/account/password/reset',
method: 'post',
data: p
});
@ -89,7 +89,7 @@ export const passwordReset = (p: TAccount.IResetPwd): Promise<null> => {
// 获取设备列表
export const deviceList = (p: TDevice.IListReq): Promise<IpagingRes<TDevice.IListRes>> => {
return request({
url: '/v1/web/device/list',
url: '/v1/api/device/list',
method: 'get',
params: p
});
@ -98,7 +98,7 @@ export const deviceList = (p: TDevice.IListReq): Promise<IpagingRes<TDevice.ILis
// 设置监测模式
export const setMonitor = (p: TDevice.ISetMonitor): Promise<null> => {
return request({
url: '/v1/web/device/set/monitor',
url: '/v1/api/device/set/monitor',
method: 'post',
data: p
});
@ -107,7 +107,7 @@ export const setMonitor = (p: TDevice.ISetMonitor): Promise<null> => {
// 设置模式
export const setMode = (p: TDevice.ISetMonitor): Promise<null> => {
return request({
url: '/v1/web/device/set/mode',
url: '/v1/api/device/set/mode',
method: 'post',
data: p
});
@ -116,7 +116,7 @@ export const setMode = (p: TDevice.ISetMonitor): Promise<null> => {
// 获取定位
export const deviceGetLocation = (p: TDevice.ISetMonitor): Promise<null> => {
return request({
url: '/v1/web/device/getLocate',
url: '/v1/api/device/getLocate',
method: 'post',
data: p
});
@ -125,7 +125,7 @@ export const deviceGetLocation = (p: TDevice.ISetMonitor): Promise<null> => {
// 设备控制
export const deviceControl = (p: TDevice.ISetMonitor): Promise<null> => {
return request({
url: '/v1/web/device/control',
url: '/v1/api/device/control',
method: 'post',
data: p
});
@ -134,7 +134,7 @@ export const deviceControl = (p: TDevice.ISetMonitor): Promise<null> => {
// 设备使用记录
export const deviceUseRecord = (p: TDevice.IRecordReq): Promise<IpagingRes<TDevice.IUseRecordRes>> => {
return request({
url: '/v1/web/device/use/record',
url: '/v1/api/device/use/record',
method: 'get',
params: p
});
@ -143,7 +143,7 @@ export const deviceUseRecord = (p: TDevice.IRecordReq): Promise<IpagingRes<TDevi
// 预警记录
export const warningRecord = (p: TDevice.IRecordReq): Promise<IpagingRes<TDevice.IWarningRecordRes>> => {
return request({
url: '/v1/web/warning/record',
url: '/v1/api/warning/record',
method: 'get',
params: p
});
@ -152,7 +152,7 @@ export const warningRecord = (p: TDevice.IRecordReq): Promise<IpagingRes<TDevice
// 新增机构
export const orgAdd = (p: TOrg.IAdd): Promise<null> => {
return request({
url: '/v1/web/org/add',
url: '/v1/api/org/add',
method: 'post',
data: p
});
@ -160,7 +160,7 @@ export const orgAdd = (p: TOrg.IAdd): Promise<null> => {
// 修改机构
export const orgModify = (p: TOrg.IAdd): Promise<null> => {
return request({
url: '/v1/web/org/modify',
url: '/v1/api/org/modify',
method: 'post',
data: p
});
@ -168,7 +168,7 @@ export const orgModify = (p: TOrg.IAdd): Promise<null> => {
// 机构列表
export const orgList = (p?: TOrg.IListReq): Promise<IpagingRes<TOrg.IOrgRecordRes>> => {
return request({
url: '/v1/web/org/list',
url: '/v1/api/org/page',
method: 'get',
params: p
});
@ -177,7 +177,7 @@ export const orgList = (p?: TOrg.IListReq): Promise<IpagingRes<TOrg.IOrgRecordRe
// 删除机构
export const orgDelete = (p?: TOrg.Idel): Promise<null> => {
return request({
url: '/v1/web/org/delete',
url: '/v1/api/org/delete',
method: 'post',
data: p
});
@ -186,7 +186,7 @@ export const orgDelete = (p?: TOrg.Idel): Promise<null> => {
// 获取角色列表
export const roleList = (): Promise<TRoleList[]> => {
return request({
url: '/v1/web/role/list',
url: '/v1/api/role/list',
method: 'get',
});
};
@ -194,7 +194,16 @@ export const roleList = (): Promise<TRoleList[]> => {
// 修改角色信息
export const roleModify = (p: TRoleModify.Ireq): Promise<null> => {
return request({
url: '/v1/web/role/modify',
url: '/v1/api/role/modify',
method: 'post',
data: p
});
};
// 添加角色信息
export const roleAdd = (p: TRoleModify.Ireq): Promise<null> => {
return request({
url: '/v1/api/role/add',
method: 'post',
data: p
});
@ -203,7 +212,7 @@ export const roleModify = (p: TRoleModify.Ireq): Promise<null> => {
// 设备在线统计
export const statisticsDevice = (): Promise<TStatisticsDevice> => {
return request({
url: '/v1/web/statistics/device',
url: '/v1/api/statistics/device',
method: 'get',
});
};
@ -211,7 +220,7 @@ export const statisticsDevice = (): Promise<TStatisticsDevice> => {
// 内容数据
export const statisticsContent = (p: statisticsContentReq): Promise<statisticsContentRes> => {
return request({
url: '/v1/web/statistics/content',
url: '/v1/api/statistics/content',
method: 'post',
data: p
});
@ -220,7 +229,7 @@ export const statisticsContent = (p: statisticsContentReq): Promise<statisticsCo
// 获取统计数据
export const statisticsCount = (): Promise<TStatisticsCount> => {
return request({
url: '/v1/web/statistics/count',
url: '/v1/api/statistics/count',
method: 'get',
});
};
@ -228,7 +237,7 @@ export const statisticsCount = (): Promise<TStatisticsCount> => {
// 告警统计
export const statisticsWarningapi = (): Promise<statisticsContentRes> => {
return request({
url: '/v1/web/statistics/warning',
url: '/v1/api/statistics/warning',
method: 'get',
});
};
@ -236,7 +245,7 @@ export const statisticsWarningapi = (): Promise<statisticsContentRes> => {
// 预警记录
export const warnRecord = (p: TWarnRecord.IListReq): Promise<IpagingRes<TWarnRecord.IListRes>> => {
return request({
url: '/v1/web/warning/record',
url: '/v1/api/warning/record',
method: 'get',
params: p
});
@ -245,7 +254,7 @@ export const warnRecord = (p: TWarnRecord.IListReq): Promise<IpagingRes<TWarnRec
// 预警记录
export const warningDetail = (p: TWarningDetail.TParams): Promise<TWarningDetail.TRes> => {
return request({
url: '/v1/web/warning/detail',
url: '/v1/api/warning/detail',
method: 'get',
params: p
});
@ -254,7 +263,7 @@ export const warningDetail = (p: TWarningDetail.TParams): Promise<TWarningDetail
// 预警记录
export const warningConfirm = (p: TWarningConfirm): Promise<null> => {
return request({
url: '/v1/web/warning/confirm',
url: '/v1/api/warning/confirm',
method: 'post',
data: p
});
@ -263,7 +272,7 @@ export const warningConfirm = (p: TWarningConfirm): Promise<null> => {
// 获取专项配置
export const deviceConfig = (p: TDeviceConfig.Treq): Promise<TDeviceConfig.Tres> => {
return request({
url: '/v1/web/device/deviceConfig',
url: '/v1/api/device/deviceConfig',
method: 'get',
params: p
});
@ -272,7 +281,7 @@ export const deviceConfig = (p: TDeviceConfig.Treq): Promise<TDeviceConfig.Tres>
// 修改专项配置
export const deviceConfigModify = (p: TDeviceConfigModify): Promise<null> => {
return request({
url: '/v1/web/device/deviceConfig/modify',
url: '/v1/api/device/deviceConfig/modify',
method: 'post',
data: p
});
@ -281,7 +290,7 @@ export const deviceConfigModify = (p: TDeviceConfigModify): Promise<null> => {
// 获取最新健康数据
export const healthLatestData = (p: THealthLatestData.TReq): Promise<THealthLatestData.TRes> => {
return request({
url: '/v1/web/health/latestData',
url: '/v1/api/health/latestData',
method: 'get',
params: p
});
@ -290,7 +299,7 @@ export const healthLatestData = (p: THealthLatestData.TReq): Promise<THealthLate
// 设备定位记录
export const locateRecord = (p: TLocateRecord.TReq): Promise<TLocateRecord.TRes[]> => {
return request({
url: '/v1/web/device/locate/record',
url: '/v1/api/device/locate/record',
method: 'get',
params: p
});
@ -299,7 +308,7 @@ export const locateRecord = (p: TLocateRecord.TReq): Promise<TLocateRecord.TRes[
// 设置使用状态
export const setUseStatus = (p: TSetUseStatus.TReq): Promise<null> => {
return request({
url: '/v1/web/device/set/useStatus',
url: '/v1/api/device/set/useStatus',
method: 'post',
data: p
});
@ -308,7 +317,7 @@ export const setUseStatus = (p: TSetUseStatus.TReq): Promise<null> => {
// 设置禁用状态
export const setStatus = (p: TSetUseStatus.TReq): Promise<null> => {
return request({
url: '/v1/web/device/set/status',
url: '/v1/api/device/set/status',
method: 'post',
data: p
});
@ -317,7 +326,7 @@ export const setStatus = (p: TSetUseStatus.TReq): Promise<null> => {
// 获取菜单列表
export const roleMenuList = (): Promise<TRoleMenuList.TRes[]> => {
return request({
url: '/v1/web/role/menu/list',
url: '/v1/api/role/menu/list',
method: 'get',
});
};

Binary file not shown.

View File

@ -18,9 +18,9 @@
<div class="button-glow"></div>
</button>
<!-- <audio ref="audioPlayer" autoplay loop>
<source src="../assets/audio/alarm.mp3" type="video/mp3" />
</audio> -->
<audio ref="audioPlayer" hidden>
<source src="../assets/audio/alarm.mp3" type="audio/mpeg" />
</audio>
</div>
</div>
</transition>
@ -43,14 +43,16 @@ defineProps({
},
});
defineEmits(["close", "confirm"]);
const emit = defineEmits(["close", "confirm"]);
const handleAudioEnd = () => {
if (audioPlayer.value) {
audioPlayer.value.play().catch((error) => {
console.error("自动播放被阻止:", error);
});
}
emit("close");
// if (audioPlayer.value) {
// audioPlayer.value.play().catch((error) => {
// console.error(":", error);
// });
// }
};
onMounted(() => {

View File

@ -34,7 +34,7 @@ interface Tdata {
}
const comm = useCommonStore();
const ACTION = import.meta.env.VITE_APP_URL + "/v1/web/upload/account";
const ACTION = import.meta.env.VITE_APP_URL + "/v1/api/upload/account";
const HEADERS = {
"Access-Token": comm.user.token,
};

View File

@ -24,6 +24,7 @@
style="width: 220px"
v-else-if="item.type === 'daterange'"
v-model="query[item.prop]"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="-"
start-placeholder="开始日期"

View File

@ -1,17 +1,20 @@
<template>
<el-upload
:style="`--width: ${size}px; --height: ${size}px`"
:style="`--width: ${props.size}px; --height: ${props.size}px`"
class="upload"
:class="{ hide: disabled }"
accept="image/*"
list-type="picture-card"
v-model:file-list="fileList"
:action="ACTION"
:headers="HEADERS"
name="files"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
:on-success="handleSuccess"
:limit="9"
:before-upload="beforeUpload"
:disabled="disabled"
>
<el-icon><Plus /></el-icon>
</el-upload>
@ -22,9 +25,9 @@
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { ref, watch } from "vue";
import { Plus } from "@element-plus/icons-vue";
import { ElMessage, UploadProps } from "element-plus";
import { ElMessage, UploadFiles, UploadProps } from "element-plus";
import { useCommonStore } from "@/store/common";
interface Tdata {
@ -35,12 +38,12 @@ interface Tdata {
msg: string;
}
const comm = useCommonStore();
const ACTION = import.meta.env.VITE_APP_URL + "/v1/web/upload/warning/img";
const ACTION = import.meta.env.VITE_APP_URL + "/v1/api/upload/warning/img";
const HEADERS = {
"Access-Token": comm.user.token,
};
const { size, modelValue } = defineProps({
const props = defineProps({
size: {
type: Number,
default: 100,
@ -49,6 +52,14 @@ const { size, modelValue } = defineProps({
type: Array,
default: () => [],
},
disabled: {
type: Boolean,
default: () => false,
},
limit: {
type: Number,
default: () => 9,
},
});
const emit = defineEmits(["update:modelValue"]);
@ -58,6 +69,22 @@ const fileList = ref<any[]>([]);
const dialogImageUrl = ref("");
const dialogVisible = ref(false);
watch(
() => props.modelValue,
(newVal) => {
if (newVal && newVal.length) {
fileList.value = newVal.map((item) => {
return {
name: item,
response: { code: 200, msg: "成功", data: [item] },
status: "success",
url: item,
};
});
}
}
);
const beforeUpload = (rawFile: any) => {
const isLtSize = rawFile.size / 1024 / 1024 < 5;
if (!isLtSize) {
@ -67,13 +94,13 @@ const beforeUpload = (rawFile: any) => {
return true;
};
const handleSuccess = ({ code, data, msg }: Tdata) => {
if (code != 200) return ElMessage.error(msg);
let list = fileList.value.map((item) => item?.response?.data?.imeUrl);
const handleSuccess: UploadProps["onSuccess"] = (_: any, _1: any, uploadFiles: any) => {
let list = uploadFiles.map((item) => item?.response?.data[0]);
emit("update:modelValue", list);
};
const handleRemove: UploadProps["onRemove"] = () => {
let list = fileList.value.map((item) => item?.response?.data?.imeUrl);
const handleRemove = (_: any, uploadFiles: any) => {
let list = uploadFiles.map((item) => item.response.data[0]);
emit("update:modelValue", list);
};
@ -84,17 +111,18 @@ const handlePictureCardPreview: UploadProps["onPreview"] = (uploadFile) => {
</script>
<style scoped lang="less">
.upload {
/deep/ .el-upload {
.upload :deep(.el-upload) {
width: var(--width);
height: var(--height);
}
/deep/ .el-upload-list__item {
.upload :deep(.el-upload-list__item) {
width: var(--width);
height: var(--height);
}
/deep/ .el-icon--close-tip {
.upload :deep(.el-icon--close-tip) {
display: none;
}
.hide :deep(.el-upload--picture-card) {
display: none;
}
</style>

View File

@ -0,0 +1,19 @@
<template>
<div class="app-wrapper">
<audio ref="audioRef" autoplay loop hidden>
<source src="../assets/audio/alarm.mp3" type="video/mp3" />
</audio>
<el-button @click="playAudio">Default</el-button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const audioSrc = ref(new URL("@/assets/audio/alarm.mp3", import.meta.url).href);
const audioRef = ref(null);
const playAudio = (context) => {
audioRef.value.play().catch((err) => {
// console.log(err, "");
});
};
</script>

120
src/layout/index copy.vue Normal file
View File

@ -0,0 +1,120 @@
<template>
<div class="app-wrapper">
<v-sidebar />
<v-header />
<div class="main-container">
<div class="app-main" :class="{ 'content-collapse': sidebar.collapse }">
<router-view v-slot="{ Component }">
<transition name="move" mode="out-in">
<keep-alive :include="[]">
<component :is="Component"></component>
</keep-alive>
</transition>
</router-view>
</div>
</div>
<!-- <audio ref="audioPlayer" src="../assets/audio/alarm.mp3" hidden /> -->
<audio ref="audioPlayer" autoplay loop hidden>
<source src="../assets/audio/alarm.mp3" type="audio/mpeg" />
</audio>
<!-- <Alarm ref="alarmRef" v-if="visible" @close="close" /> -->
</div>
</template>
<script setup lang="ts">
import { useSidebarStore } from "@/store/sidebar";
import vHeader from "@/components/header.vue";
import vSidebar from "@/components/sidebar.vue";
import Alarm from "@/components/alarm.vue";
import { ref } from "vue";
// import useWebSocket from "@/utils/webSocket";
// const { onMessage } = useWebSocket();
// onMessage((res) => {
// console.log(res, "WebSocket ");
// if (res.cmd == "warning") {
// console.log("");
// }
// });
const sidebar = useSidebarStore();
const alarmRef = ref(null);
const audioPlayer = ref(null);
const visible = ref(true);
const getInclude = (routeList: any) => {
if (!routeList) return [];
let list = [];
routeList.forEach((item) => {
if (item.meta && item.meta.keepAlive) {
list.push(item.name);
}
if (item.children && item.children.length) {
list = [...list, ...getInclude(item.children)];
}
});
return list;
};
const close = async () => {
try {
audioPlayer.value.play();
console.log(1111);
} catch (error) {
console.log(error, 2222);
}
};
// setTimeout(() => {
// audioRef.value.play().catch((error) => {
// console.error(":", error);
// });
// }, 5000);
const playAudio = () => {
if (audioPlayer.value) {
audioPlayer.value.play().catch((error) => {
console.error("自动播放被阻止:", error);
});
}
};
// const include = getInclude(routes[0].children);
</script>
<style scoped lang="less">
// @media screen and (max-width: 800px) {
// .main-container {
// margin-left: 0 !important;
// }
// .sidebar-container {
// width: 0 !important;
// }
// .app-wrapper-header {
// width: 100% !important;
// }
// }
.app-wrapper {
position: relative;
width: 100%;
height: 100vh;
background: #f0f2f5;
overflow: hidden;
.main-container {
height: 100vh;
margin-left: 210px;
min-height: 100%;
position: relative;
transition: 0.3s;
background-color: #f2f4fa;
}
.app-main {
// height: 100vh;
height: calc(100vh - 90px);
position: relative;
width: 100%;
display: flex;
flex-direction: column;
// padding-top: 70px;
margin-top: 70px;
box-sizing: border-box;
overflow: hidden;
}
}
</style>

View File

@ -13,15 +13,30 @@
</router-view>
</div>
</div>
<!-- <Alarm ref="alarmRef" v-if="visible" @close="close" /> -->
</div>
</template>
<script setup lang="ts">
import { useSidebarStore } from "@/store/sidebar";
import vHeader from "@/components/header.vue";
import vSidebar from "@/components/sidebar.vue";
import { routes } from "@/router/index";
import Alarm from "@/components/alarm.vue";
import { ref } from "vue";
// import useWebSocket from "@/utils/webSocket";
// const { onMessage } = useWebSocket();
// onMessage((res) => {
// console.log(res, "WebSocket ");
// if (res.cmd == "warning") {
// console.log("");
// }
// });
const sidebar = useSidebarStore();
const alarmRef = ref(null);
const audioPlayer = ref(null);
const visible = ref(true);
const getInclude = (routeList: any) => {
if (!routeList) return [];
@ -36,6 +51,27 @@ const getInclude = (routeList: any) => {
});
return list;
};
const close = async () => {
try {
audioPlayer.value.play();
console.log(1111);
} catch (error) {
console.log(error, 2222);
}
};
setTimeout(() => {
audioPlayer.value.play().catch((error) => {
console.error("自动播放被阻止:", error);
});
}, 5000);
const playAudio = () => {
if (audioPlayer.value) {
audioPlayer.value.play().catch((error) => {
console.error("自动播放被阻止:", error);
});
}
};
// const include = getInclude(routes[0].children);
</script>

View File

@ -1,11 +1,13 @@
import { defineStore } from 'pinia';
import { TLogin } from "@/api/index.d";
import { TLogin, TRoleList } from "@/api/index.d";
import { roleList } from '@/api';
export const useCommonStore = defineStore('common', {
state: () => {
return {
time: new Date(),
user: <TLogin.IRes>{}
user: <TLogin.IRes>{},
roleList: <TRoleList[]>[]
};
},
getters: {},
@ -13,6 +15,11 @@ export const useCommonStore = defineStore('common', {
setTime() {
this.time = new Date();
},
getRoleList() {
roleList().then(res => {
this.roleList = res
})
},
setUser(data: TLogin.IRes) {
this.user = data
},

158
src/utils/webSocket.js Normal file
View File

@ -0,0 +1,158 @@
import { ref, onMounted, onUnmounted } from 'vue';
class WebSocketService {
constructor() {
this.url = import.meta.env.VITE_APP_URL_WEBSOCKET;
this.socket = null;
this.isAlive = false; // 用于判断心跳是否正常
this.reconnectAttempts = 0; // 重连尝试次数
this.MAX_RECONNECT_ATTEMPTS = 5; // 最大重连次数
this.HEARTBEAT_INTERVAL = 5000; // 心跳间隔时间 (30秒)
// this.HEARTBEAT_INTERVAL = 30000; // 心跳间隔时间 (30秒)
this.heartbeatTimer = null;
this.reconnectTimer = null;
this.connect();
}
connect() {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
console.log('WebSocket已连接');
this.webScoketLogin();
this.reconnectAttempts = 0; // 成功连接后重置重试次数
this.startHeartbeat(); // 开始心跳检测
};
this.socket.onmessage = (event) => {
// 处理接收到的消息
console.log('Message received:', event.data);
};
this.socket.onclose = () => {
console.log('WebSocket连接已关闭');
this.stopHeartbeat();
this.attemptReconnect();
};
this.socket.onerror = (error) => {
console.log('WebSocket连接错误', error);
};
}
webScoketLogin() {
this.sendMessage({
username: "admin",
password: "111111",
type: 0,
cmd: "webLogin",
})
}
startHeartbeat() {
if (!this.heartbeatTimer) {
this.heartbeatTimer = setInterval(() => {
if (this.socket.readyState === WebSocket.OPEN) {
this.isAlive = false;
this.sendPing();
}
}, this.HEARTBEAT_INTERVAL);
}
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
sendPing() {
console.log('发送心跳', JSON.stringify({ cmd: 'heartbeat' }));
this.socket.send(JSON.stringify({ cmd: 'heartbeat' }));
setTimeout(() => {
if (!this.isAlive) {
console.warn('WebSocket heartbeat failed, attempting to reconnect...');
this.socket.close();
}
}, this.HEARTBEAT_INTERVAL / 2); // 如果没有pong响应则关闭并重连
}
receivePong() {
this.isAlive = true;
}
attemptReconnect() {
if (this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS) {
this.reconnectAttempts++;
console.log(`Attempting to reconnect (${this.reconnectAttempts})...`);
this.reconnectTimer = setTimeout(() => {
this.connect();
}, 1000 * this.reconnectAttempts); // 指数退避算法
} else {
console.error('Max reconnect attempts reached');
}
}
sendMessage(message) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(message));
}
}
close() {
this.stopHeartbeat();
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.socket) {
this.socket.close();
this.socket = null;
}
}
}
export default function useWebSocket() {
const ws = new WebSocketService();
const message = ref(null);
const isConnected = ref(false);
const onMessage = (callback) => {
ws.socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.cmd == "webLogin" && !data.code == 200) return ws.webScoketLogin()
callback(data);
if (event.data.includes('heartbeat')) {
ws.receivePong();
}
};
};
const onClose = () => {
ws.socket.onclose()
};
const onOpen = (callback) => {
ws.socket.onopen = () => {
isConnected.value = true;
callback();
};
};
// onMounted(() => {
// onOpen(() => console.log('Connected'));
// onClose(() => console.log('Disconnected'));
// onMessage((data) => console.log('Received:', data));
// });
// onUnmounted(() => {
// ws.close();
// });
return { isConnected, message, sendMessage: ws.sendMessage.bind(ws), onMessage, onClose };
}

View File

@ -44,23 +44,33 @@ enum warnTypeEnum {
}
const router = useRouter();
//
const query = reactive({
name: "",
});
const query = reactive<any>({});
const searchOpt = ref<FormOptionList[]>([
{ type: "select", label: "来源手铐:", prop: "name1", opts: [{ label: "866503071660886", value: "866503071660886" }] },
{ type: "select", label: "事件类型:", prop: "name2", opts: [{ label: "866503071660886", value: "866503071660886" }] },
{ type: "select", label: "事件级别:", prop: "name3", opts: [{ label: "866503071660886", value: "866503071660886" }] },
{ type: "input", label: "手铐IMEI号", prop: "deviceId" },
{
type: "select",
label: "告警类型:",
prop: "warnType",
opts: [
{ label: "SOS告警", value: 0 },
{ label: "围栏告警", value: 1 },
{ label: "破坏告警", value: 2 },
{ label: "低电告警", value: 3 },
{ label: "心率告警", value: 4 },
{ label: "血氧告警", value: 5 },
{ label: "体温告警", value: 6 },
],
},
{
type: "select",
label: "处理状态:",
prop: "status",
opts: [
{ label: "未处理", value: "0" },
{ label: "已处理", value: "1" },
{ label: "待处理", value: 0 },
{ label: "已处理", value: 1 },
],
},
{ type: "daterange", label: "创建时间:", prop: "name5" },
{ type: "daterange", label: "创建时间:", prop: "time" },
]);
const handleSearch = () => {
changePage(1);
@ -86,7 +96,10 @@ const tableData = ref<TWarnRecord.IListRes[]>([]);
const getData = async () => {
try {
const res = await warnRecord(paging);
console.log(query, "queryqueryquery");
let p = { ...paging, ...query, startDate: query.time?.[0], endDate: query.time?.[1] };
delete p.time;
const res = await warnRecord(p);
tableData.value = res.records;
paging.total = res.total;
} catch (error) {}
@ -97,10 +110,10 @@ const changePage = (val: number) => {
paging.page = val;
getData();
};
const toIncidentDispose = (deviceId: string) => {
const toIncidentDispose = (id: string) => {
router.push({
path: "/incidentDispose",
query: { deviceId },
query: { id },
});
};
</script>

View File

@ -47,26 +47,26 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="处理人:" prop="name">
<el-input v-model="ruleForm.name" clearable :disabled="curData.status == 1" />
<el-input v-model="ruleForm.name" clearable :disabled="disabled" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="警号:" prop="username">
<el-input v-model="ruleForm.username" clearable :disabled="curData.status == 1" />
<el-input v-model="ruleForm.username" clearable :disabled="disabled" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="处理记录:" prop="content">
<el-input v-model="ruleForm.content" clearable :autosize="{ minRows: 2, maxRows: 4 }" type="textarea" :disabled="curData.status == 1" />
<el-input v-model="ruleForm.content" clearable :autosize="{ minRows: 2, maxRows: 4 }" type="textarea" :disabled="disabled" />
</el-form-item>
<el-form-item label="处理图片:" prop="images">
<Upload v-model="ruleForm.images" :disabled="curData.status == 1" />
<Upload v-model="ruleForm.images" :disabled="disabled" />
</el-form-item>
</el-form>
</div>
</div>
<div class="right-foot">
<el-button type="primary" @click="submitForm(ruleFormRef)">保存</el-button>
<div class="right-foot" v-if="!disabled">
<el-button type="primary" @click="submitForm(ruleFormRef)" :disabled="disabled">保存</el-button>
</div>
</div>
</el-col>
@ -77,7 +77,7 @@
<script setup lang="ts" name="incidentDispose">
import location from "@/assets/img/location.png";
import { MapCustom } from "@/utils/mapCustom";
import { onMounted, ref, reactive } from "vue";
import { onMounted, ref, reactive, watch } from "vue";
import Upload from "@/components/upload-img.vue";
import * as echarts from "echarts";
import { warningDetail, warningConfirm } from "@/api/index";
@ -106,6 +106,7 @@ enum warnTypeEnum {
}
const { query } = useRoute();
const chartRef = ref(null);
const disabled = ref(false);
const ruleFormRef = ref<FormInstance>();
let map = null;
@ -135,7 +136,7 @@ const ringOptions = {
],
};
const ruleForm = reactive<TWarningConfirm>({
id: query.deviceId as string,
id: query.id as string,
name: "",
username: "",
content: "",
@ -174,17 +175,23 @@ const rules = reactive<FormRules<TWarningConfirm>>({
content: [{ required: true, message: "请输入处理记录", trigger: "blur" }],
images: [{ required: true, message: "请上传图片", trigger: "blur" }],
});
watch(
() => curData.value,
(newVal) => {
if (newVal && newVal.status == 1) {
disabled.value = true;
}
}
);
const getData = async () => {
try {
const res = await warningDetail({ id: query.deviceId as string });
const res = await warningDetail({ id: query.id as string });
curData.value = res;
ruleForm.name = res.rname;
ruleForm.username = res.rname;
ruleForm.content = res.rcontent;
ruleForm.images = JSON.parse(res.rimg);
console.log(ruleForm, "ruleFormruleFormruleFormruleForm");
let icon = map.newIcon(location);
let marker = map.marker({ icon, position: [116.406315, 39.908775] });
marker.setMap(map.map);
@ -245,6 +252,7 @@ onMounted(() => {
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
.info {
background: #ffffff;
height: 100%;

View File

@ -56,6 +56,7 @@ const submitForm = async (formEl: FormInstance | undefined) => {
if (valid) {
fetchLogin(ruleForm).then((res) => {
comm.setUser(res);
comm.getRoleList();
ElMessage({
message: "登录成功",
type: "success",

View File

@ -8,7 +8,7 @@
</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.deviceId)"> 查看 </el-button>
<el-button type="success" link :icon="View" v-if="rows.warnType == 0 || rows.warnType == 1" @click="toIncidentDispose(rows.id)"> 查看 </el-button>
</template>
<template #status="{ rows }">
<el-tag :type="rows.status == 1 ? 'success' : 'danger'">
@ -75,10 +75,10 @@ const handleInput = debounce((e) => {
getData();
}, 500);
const toIncidentDispose = (deviceId: string) => {
const toIncidentDispose = (id: string) => {
router.push({
path: "/incidentDispose",
query: { deviceId },
query: { id },
});
};
</script>

View File

@ -27,7 +27,6 @@ const { list } = defineProps({
default: () => [],
},
});
console.log(list, "list1111");
</script>
<style scoped lang="less">
.monitoring-top {

View File

@ -24,7 +24,7 @@
</template>
<script setup lang="ts" name="basetable">
import { ref, reactive } from "vue";
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { CirclePlusFilled } from "@element-plus/icons-vue";
import { fetchData, orgAdd, orgModify, orgList, orgDelete } from "@/api/index";
@ -45,17 +45,8 @@ const query = reactive({
name: "",
});
const searchOpt = ref<FormOptionList[]>([
{ type: "input", label: "辖区名称:", prop: "name1" },
{ type: "daterange", label: "创建时间:", prop: "name2" },
{
type: "select",
label: "状态:",
prop: "name3",
opts: [
{ label: "启用", value: "1" },
{ label: "禁用", value: "0" },
],
},
{ type: "input", label: "辖区名称:", prop: "name" },
// { type: "daterange", label: "", prop: "name2" },
]);
//
@ -77,11 +68,11 @@ const paging = reactive({
const tableData = ref<TOrg.IOrgRecordRes[]>([]);
const getData = async () => {
const res = await orgList(paging);
const res = await orgList({ ...paging, ...query });
tableData.value = res.records;
paging.total = res.total;
};
getData();
const handleSearch = () => {
changePage(1);
};
@ -128,6 +119,9 @@ const updateData = (res) => {
getData();
});
};
onMounted(() => {
getData();
});
</script>
<style scoped></style>

View File

@ -143,10 +143,10 @@ const changeWarningPage = (val: number) => {
paging1.page = val;
getWarningData();
};
const toIncidentDispose = (deviceId: string) => {
const toIncidentDispose = (id: string) => {
router.push({
path: "/incidentDispose",
query: { deviceId },
query: { id },
});
};
</script>

View File

@ -88,12 +88,39 @@ const editOp = [
];
//
const query = reactive({
name: "",
});
const query = reactive({});
const searchOpt = ref<FormOptionList[]>([
{ type: "input", label: "手铐SN", prop: "name" },
{ type: "input", label: "警察账号:", prop: "name1" },
{ type: "input", label: "手铐SN", prop: "deviceId" },
{ type: "input", label: "警察名称:", prop: "name" },
{
type: "select",
label: "当前模式:",
prop: "mode",
opts: [
{ label: "常规", value: "0" },
{ label: "审讯", value: "1" },
{ label: "户外押送", value: "2" },
],
},
{
type: "select",
label: "设备状态:",
prop: "status",
opts: [
{ label: "离线", value: "0" },
{ label: "在线", value: "1" },
{ label: "充电中", value: "2" },
],
},
{
type: "select",
label: "使用状态:",
prop: "useStatus",
opts: [
{ label: "未使用", value: "0" },
{ label: "使用中", value: "1" },
],
},
]);
const handleSearch = () => {
changePage(1);
@ -103,19 +130,18 @@ const handleSearch = () => {
let columns = ref([
{ type: "selection" },
{ type: "index", label: "序号", width: 55, align: "center" },
{ prop: "deviceId", label: "手铐IMEI" },
{ prop: "name", label: "绑定警察名称" },
{ prop: "username", label: "绑定警察账户" },
{ prop: "battery", label: "电量" },
{ prop: "deviceVersion", label: "版本号" },
{ prop: "status", label: "设备状态" },
{ prop: "mode", label: "当前模式" },
{ prop: "useStatus", label: "使用状态" },
{ prop: "deviceSwitch", label: "启用开关" },
{ prop: "orgName", label: "关联辖区编号" },
{ prop: "createTime", label: "最新通信时间" },
{ prop: "createTime", label: "创建时间" },
{ prop: "deviceId", label: "手铐IMEI", width: 120 },
{ prop: "adminName", label: "绑定警察名称", width: 180 },
{ prop: "adminUsername", 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: "orgName", label: "关联辖区编号", width: 120 },
{ prop: "createTime", label: "最新通信时间", width: 180 },
{ prop: "createTime", label: "创建时间", width: 180 },
{ prop: "operator", label: "操作", width: 400 },
]);
const paging = reactive({
@ -130,7 +156,7 @@ const controlForm = reactive({
const tableData = ref<TDevice.IListRes[]>([]);
const getData = async () => {
const res = await deviceList(paging);
const res = await deviceList({ ...paging, ...query });
tableData.value = res.records;
paging.total = res.total;
};

View File

@ -11,14 +11,14 @@
<!-- <el-button type="danger">删除</el-button> -->
</template>
<template #state="{ rows }">
<el-switch v-model="rows.state" :active-value="1" :inactive-value="0" />
<template #status="{ rows }">
<el-switch v-model="rows.status" :active-value="1" :inactive-value="0" />
</template>
<template #roleId="{ rows }">
{{ roleEnum[rows.roleId] }}
</template>
<!--
<template #password="{ rows }">
<div v-if="rows.flag" @click="rows.flag = false">
{{ rows.password }}
@ -28,7 +28,7 @@
******
<el-icon style="cursor: pointer"><Hide /></el-icon>
</div>
</template>
</template> -->
<template #operator="{ rows }">
<el-button link type="primary" size="small" @click="handelRow('pwd', rows)"> 重置密码 </el-button>
@ -50,7 +50,7 @@
<script setup lang="ts" name="basetable">
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { accountAdd, accountModify, accountList, accountDeletet, passwordReset, roleList, roleMenuList, roleModify } from "@/api/index";
import { accountAdd, accountModify, accountList, accountDeletet, passwordReset, roleList, roleMenuList, roleModify, roleAdd } from "@/api/index";
import TableCustom from "@/components/table-custom.vue";
import TableSearch from "@/components/table-search.vue";
import TableEdit from "@/components/table-edit.vue";
@ -59,17 +59,18 @@ import { useCommonStore } from "@/store/common";
import UserType from "./userType.vue";
import { TableItem } from "@/types/table";
import { FormOption, FormOptionList } from "@/types/form-option";
import { TAccount, TRoleList, TRoleMenuList } from "@/api/index.d";
import { TAccount, TRoleList, TRoleMenuList, TRoleModify } from "@/api/index.d";
// import { Hide, View } from "@element-plus/icons-vue";
enum roleEnum {
管理员 = -1,
警察 = 1,
辅警 = 2,
协警 = 3,
"管理员" = -1,
"警察" = 1,
"辅警" = 2,
"协警" = 3,
}
const comm = useCommonStore();
const visible = ref(false);
const typeVisible = ref(false);
const isEdit = ref(false);
@ -92,25 +93,25 @@ const roleListData = ref<TRoleList[]>([]);
const searchOpt = ref<FormOptionList[]>([
{ type: "input", label: "用户名:", prop: "name" },
{ type: "input", label: "警员号:", prop: "username" },
{
type: "select",
label: "用户类型:",
prop: "name1",
opts: [
{ label: "管理员", value: "1" },
{ label: "警察", value: "2" },
{ label: "辅警", value: "3" },
{ label: "协警", value: "4" },
],
prop: "roleId",
opts: comm.roleList.map((item) => {
return {
label: item.name,
value: item.id,
};
}),
},
{ type: "daterange", label: "创建时间:", prop: "name2" },
{
type: "select",
label: "状态:",
prop: "name3",
prop: "status",
opts: [
{ label: "启用", value: "1" },
{ label: "禁用", value: "0" },
{ label: "启用", value: "1" },
],
},
]);
@ -122,10 +123,10 @@ let columns = ref([
{ prop: "name", label: "用户名称" },
{ prop: "username", label: "警员号" },
{ prop: "phone", label: "手机号" },
{ prop: "password", label: "密码", width: 120 },
// { prop: "password", label: "", width: 120 },
{ prop: "roleId", label: "类型" },
{ prop: "createTime", label: "创建时间" },
{ prop: "state", label: "状态" },
{ prop: "status", label: "状态" },
{ prop: "operator", label: "操作", width: 250 },
]);
@ -191,7 +192,7 @@ const roleListFn = () => {
});
};
const getData = async () => {
const res = await accountList(paging);
const res = await accountList({ ...paging, ...query });
tableData.value = res.records?.map((item) => {
item.flag = false;
return item;
@ -229,11 +230,13 @@ const updateData = (res) => {
getData();
});
};
const saveType = (row: TRoleList) => {
roleModify(row).then(() => {
const saveType = (row: TRoleModify.Ireq) => {
const api = row.id ? roleModify : roleAdd;
api(row).then(() => {
ElMessage.success("保存成功");
roleListFn();
typeVisible.value = false;
comm.getRoleList();
// roleListFn();
// typeVisible.value = false;
});
};
const addType = () => {

View File

@ -26,7 +26,7 @@
<el-table-column prop="date" label="操作" width="200" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="emit('complete', row)">保存</el-button>
<el-button link type="primary" size="small" @click="handleComplete(row)">保存</el-button>
</template>
</el-table-column>
</el-table>
@ -49,6 +49,11 @@ const { roleListData, typeHeadList } = defineProps({
});
const emit = defineEmits(["complete", "addType"]);
const handleComplete = (row: TRoleList) => {
let p = { ...row, menus: row.roleMenu };
delete p.roleMenu;
emit("complete", p);
};
const isCheck = (ite, row) => {
let flag = false;

31
sta.html Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>背景音乐示例</title>
</head>
<body>
<h1>欢迎来到我的网站</h1>
<!-- 插入背景音乐 -->
<audio id="bgm" loop controls>
<source src="./src/assets/audio/alarm.mp3" type="audio/mpeg">
您的浏览器不支持 audio 标签。
</audio>
<!-- 点击页面播放音乐js -->
<script>
document.addEventListener('click', function () {
var audioElement = document.getElementById('bgm');
if (audioElement.paused) { // 检查音频是否已播放
audioElement.volume = 0.5; // 设置音量
audioElement.play(); // 播放音频
}
}, { once: true }); // 确保事件处理器只执行一次
</script>
</body>
</html>

View File

@ -1067,6 +1067,11 @@ hasown@^2.0.0:
dependencies:
function-bind "^1.1.2"
howler@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/howler/-/howler-2.2.4.tgz#bd3df4a4f68a0118a51e4bd84a2bfc2e93e6e5a1"
integrity sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==
html-void-elements@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-2.0.1.tgz#29459b8b05c200b6c5ee98743c41b979d577549f"
@ -1511,13 +1516,6 @@ pify@^4.0.1:
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
pinia-plugin-persist@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/pinia-plugin-persist/-/pinia-plugin-persist-1.0.0.tgz#fc696f225527f30bd5955109fafadd43c725e888"
integrity sha512-M4hBBd8fz/GgNmUPaaUsC29y1M09lqbXrMAHcusVoU8xlQi1TqgkWnnhvMikZwr7Le/hVyMx8KUcumGGrR6GVw==
dependencies:
vue-demi "^0.12.1"
pinia-plugin-persistedstate@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.2.0.tgz#31973291b768bd84ce24e55083e7171b2f451ad1"
@ -1966,11 +1964,6 @@ vue-demi@*, vue-demi@>=0.14.5:
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.7.tgz#8317536b3ef74c5b09f268f7782e70194567d8f2"
integrity sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==
vue-demi@^0.12.1:
version "0.12.5"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.12.5.tgz#8eeed566a7d86eb090209a11723f887d28aeb2d1"
integrity sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==
vue-demi@^0.13.11:
version "0.13.11"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.13.11.tgz#7d90369bdae8974d87b1973564ad390182410d99"