2025年04月12日18:35:52

This commit is contained in:
luojiayi 2025-04-12 18:35:54 +08:00
parent f82d3934ba
commit 6b04c453d9
33 changed files with 659 additions and 420 deletions

2
auto-imports.d.ts vendored
View File

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

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

@ -22,6 +22,10 @@ export interface TRoleList {
createTime?: string createTime?: string
roleMenu?: TRoleMenu[] roleMenu?: TRoleMenu[]
} }
export interface TbindWeb {
devices: number[]
accountId: number
}
export interface TStatisticsDevice { export interface TStatisticsDevice {
@ -131,11 +135,10 @@ export namespace TLogin {
export namespace TAccount { export namespace TAccount {
export interface IAdd { export interface IAdd {
username: string; username: string;
password: string;
phone: string; phone: string;
name: string; name: string;
orgId: number; orgId?: number | string;
roleId: number; roleId: number | string;
status: number; status: number;
} }
export interface IListReq extends Ipaging { export interface IListReq extends Ipaging {
@ -282,6 +285,11 @@ export namespace TOrg {
} }
} }
export interface TstatisticsUseCount {
defaultCount: number;
monitorCount: number;
outsideCount: number;
}
export namespace TWarnRecord { export namespace TWarnRecord {
export interface IListReq extends Ipaging { export interface IListReq extends Ipaging {
@ -314,6 +322,12 @@ export namespace TWarningDetail {
interface TParams { interface TParams {
id: number | string id: number | string
} }
interface THealthData {
bo: number
hr: number
temp: number
time: number
}
interface TRes { interface TRes {
address: string address: string
adminName: string adminName: string
@ -321,7 +335,7 @@ export namespace TWarningDetail {
creatUser: string creatUser: string
createTime: string createTime: string
deviceId: string deviceId: string
healthData: string healthData: THealthData[]
id: number id: number
lat: number lat: number
lng: number lng: number

View File

@ -1,5 +1,5 @@
import request from '../utils/request'; import request from '../utils/request';
import { TLogin, TAccount, IpagingRes, TDevice, TOrg, TRoleList, TStatisticsDevice, statisticsContentReq, statisticsContentRes, TStatisticsCount, TWarnRecord, TWarningDetail, TWarningConfirm, TDeviceConfigModify, TDeviceConfig, THealthLatestData, TLocateRecord, TSetUseStatus, TRoleMenuList, TRoleModify } from "./index.d"; import { TLogin, TAccount, IpagingRes, TDevice, TOrg, TRoleList, TStatisticsDevice, statisticsContentReq, statisticsContentRes, TStatisticsCount, TWarnRecord, TWarningDetail, TWarningConfirm, TDeviceConfigModify, TDeviceConfig, THealthLatestData, TLocateRecord, TSetUseStatus, TRoleMenuList, TRoleModify, TbindWeb, TstatisticsUseCount } from "./index.d";
export const fetchLogin = (p: TLogin.Ireq): Promise<TLogin.IRes> => { export const fetchLogin = (p: TLogin.Ireq): Promise<TLogin.IRes> => {
return request({ return request({
@ -76,6 +76,14 @@ export const accountList = (p: TAccount.IListReq): Promise<IpagingRes<TAccount.I
params: p params: p
}); });
}; };
// 获取组织下账号列表
export const accountListOrg = (orgId: number): Promise<TAccount.IListRes[]> => {
return request({
url: '/v1/api/account/list/org',
method: 'get',
params: { orgId }
});
};
// 重置密码 // 重置密码
export const passwordReset = (p: TAccount.IResetPwd): Promise<null> => { export const passwordReset = (p: TAccount.IResetPwd): Promise<null> => {
return request({ return request({
@ -173,6 +181,13 @@ export const orgList = (p?: TOrg.IListReq): Promise<IpagingRes<TOrg.IOrgRecordRe
params: p params: p
}); });
}; };
// 机构列表
export const orgAllList = (): Promise<TOrg.IOrgRecordRes[]> => {
return request({
url: '/v1/api/org/list',
method: 'get',
});
};
// 删除机构 // 删除机构
export const orgDelete = (p?: TOrg.Idel): Promise<null> => { export const orgDelete = (p?: TOrg.Idel): Promise<null> => {
@ -183,6 +198,15 @@ export const orgDelete = (p?: TOrg.Idel): Promise<null> => {
}); });
}; };
// web设备绑定管理员
export const bindWeb = (p: TbindWeb): Promise<null> => {
return request({
url: '/v1/api/device/web/bind/web',
method: 'post',
data: p
});
};
// 获取角色列表 // 获取角色列表
export const roleList = (): Promise<TRoleList[]> => { export const roleList = (): Promise<TRoleList[]> => {
return request({ return request({
@ -251,6 +275,14 @@ export const warnRecord = (p: TWarnRecord.IListReq): Promise<IpagingRes<TWarnRec
}); });
}; };
// 获取设备使用数量
export const statisticsUseCount = (): Promise<TstatisticsUseCount> => {
return request({
url: '/v1/api/statistics/useCount',
method: 'get',
});
};
// 预警记录 // 预警记录
export const warningDetail = (p: TWarningDetail.TParams): Promise<TWarningDetail.TRes> => { export const warningDetail = (p: TWarningDetail.TParams): Promise<TWarningDetail.TRes> => {
return request({ return request({

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -47,7 +47,6 @@ const emit = defineEmits(["close", "confirm"]);
const handleAudioEnd = () => { const handleAudioEnd = () => {
emit("close"); emit("close");
// if (audioPlayer.value) { // if (audioPlayer.value) {
// audioPlayer.value.play().catch((error) => { // audioPlayer.value.play().catch((error) => {
// console.error(":", error); // console.error(":", error);
@ -56,23 +55,11 @@ const handleAudioEnd = () => {
}; };
onMounted(() => { onMounted(() => {
// DOM if (audioPlayer.value) {
// if (audioPlayer.value) { audioPlayer.value.play().catch((error) => {
// setTimeout(() => { console.error("自动播放被阻止:", error);
// audioPlayer.value.play(); });
// }); }
// audioPlayer.value.play();
// //
// audioPlayer.value
// .play()
// .then(() => {
// //
// audioPlayer.value.muted = false;
// })
// .catch((error) => {
// console.log("", error);
// });
// }
}); });
</script> </script>

View File

@ -7,7 +7,7 @@
{{ item }}{{ index != tab.list.length - 1 ? " / " : "" }} {{ item }}{{ index != tab.list.length - 1 ? " / " : "" }}
</div> </div>
</div> </div>
<div class="web-time">{{ format(time) }} 更新</div> <div class="web-time">{{ format(comm.time) }} 更新</div>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="header-user-con"> <div class="header-user-con">
@ -33,7 +33,7 @@
<!-- 用户名下拉菜单 --> <!-- 用户名下拉菜单 -->
<el-dropdown class="user-name" trigger="click" @command="handleCommand"> <el-dropdown class="user-name" trigger="click" @command="handleCommand">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
{{ user.username }} {{ comm.user.username }}
<el-icon class="el-icon--right"> <el-icon class="el-icon--right">
<arrow-down /> <arrow-down />
</el-icon> </el-icon>
@ -49,23 +49,24 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from "vue"; import { inject, onMounted } from "vue";
import { useSidebarStore } from "../store/sidebar"; import { useSidebarStore } from "../store/sidebar";
import { useCommonStore } from "../store/common"; import { useCommonStore } from "../store/common";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import imgurl from "../assets/img/img.jpg"; import imgurl from "../assets/img/avatar.png";
import { useTabsStore } from "@/store/tabs"; import { useTabsStore } from "@/store/tabs";
import { routes } from "@/router/index"; import { routes } from "@/router/index";
import { RouteRecordRaw } from "vue-router";
import { format } from "@/utils"; import { format } from "@/utils";
const ws: any = inject("ws");
const tab = useTabsStore(); const tab = useTabsStore();
const username: string | null = localStorage.getItem("vuems_name"); const username: string | null = localStorage.getItem("vuems_name");
const message: number = 2; const message: number = 2;
const sidebar = useSidebarStore(); const sidebar = useSidebarStore();
const { user, time, clearStore } = useCommonStore(); const comm = useCommonStore();
// onMounted(() => { // onMounted(() => {
// if (document.body.clientWidth < 1500) { // if (document.body.clientWidth < 1500) {
// sidebar.handleCollapse(); // sidebar.handleCollapse();
@ -76,8 +77,9 @@ const { user, time, clearStore } = useCommonStore();
const router = useRouter(); const router = useRouter();
const handleCommand = (command: string) => { const handleCommand = (command: string) => {
if (command == "loginout") { if (command == "loginout") {
clearStore(); comm.clearStore();
router.push("/login"); router.push("/login");
ws.close();
} else if (command == "user") { } else if (command == "user") {
router.push("/ucenter"); router.push("/ucenter");
} }

View File

@ -45,7 +45,16 @@
table-layout="auto" table-layout="auto"
> >
<template v-for="item in columns" :key="item.prop"> <template v-for="item in columns" :key="item.prop">
<el-table-column v-if="item.visible" :prop="item.prop" :label="item.label" :width="item.width" :type="item.type" :align="item.align || 'center'"> <el-table-column
v-if="item.visible"
:prop="item.prop"
:label="item.label"
:width="item.width"
:type="item.type"
:align="item.align || 'center'"
:fixed="item.fixed"
:reserve-selection="item.reserveSelection"
>
<template #default="{ row, column, $index }" v-if="item.type === 'index'"> <template #default="{ row, column, $index }" v-if="item.type === 'index'">
{{ getIndex($index) }} {{ getIndex($index) }}
</template> </template>
@ -144,6 +153,10 @@ const props = defineProps({
type: Function, type: Function,
default: () => {}, default: () => {},
}, },
selectionChange: {
type: Function,
default: () => {},
},
editFunc: { editFunc: {
type: Function, type: Function,
default: () => {}, default: () => {},
@ -178,8 +191,10 @@ const tableRowClassName = ({ row, rowIndex }: { row: TableItem; rowIndex: number
// //
const multipleSelection = ref([]); const multipleSelection = ref([]);
const handleSelectionChange = (selection: any[]) => { const handleSelectionChange = (selection: any[]) => {
multipleSelection.value = selection; multipleSelection.value = selection;
props.selectionChange(selection);
}; };
// //

View File

@ -1,19 +0,0 @@
<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>

View File

@ -1,120 +0,0 @@
<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,8 +13,7 @@
</router-view> </router-view>
</div> </div>
</div> </div>
<Alarm ref="alarmRef" v-if="visible" @close="visible = false" />
<!-- <Alarm ref="alarmRef" v-if="visible" @close="close" /> -->
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -22,7 +21,9 @@ import { useSidebarStore } from "@/store/sidebar";
import vHeader from "@/components/header.vue"; import vHeader from "@/components/header.vue";
import vSidebar from "@/components/sidebar.vue"; import vSidebar from "@/components/sidebar.vue";
import Alarm from "@/components/alarm.vue"; import Alarm from "@/components/alarm.vue";
import { ref } from "vue"; import { inject, onBeforeUnmount, onMounted, ref } from "vue";
import { ElMessageBox } from "element-plus";
const ws: any = inject("ws");
// import useWebSocket from "@/utils/webSocket"; // import useWebSocket from "@/utils/webSocket";
// const { onMessage } = useWebSocket(); // const { onMessage } = useWebSocket();
@ -30,13 +31,14 @@ import { ref } from "vue";
// console.log(res, "WebSocket "); // console.log(res, "WebSocket ");
// if (res.cmd == "warning") { // if (res.cmd == "warning") {
// console.log(""); // console.log("");
// visible.value = true;
// } // }
// }); // });
const sidebar = useSidebarStore(); const sidebar = useSidebarStore();
const alarmRef = ref(null); const alarmRef = ref(null);
const audioPlayer = ref(null); const audioPlayer = ref(null);
const visible = ref(true); const visible = ref(false);
const getInclude = (routeList: any) => { const getInclude = (routeList: any) => {
if (!routeList) return []; if (!routeList) return [];
@ -51,27 +53,35 @@ const getInclude = (routeList: any) => {
}); });
return list; return list;
}; };
const close = async () => {
try { const handleBeforeUnload = (event: any) => {
audioPlayer.value.play(); localStorage.removeItem("isReload");
console.log(1111);
} catch (error) {
console.log(error, 2222);
}
}; };
setTimeout(() => { onMounted(() => {
audioPlayer.value.play().catch((error) => { const isReload = localStorage.getItem("isReload");
console.error("自动播放被阻止:", error); // if (isReload !== "true") {
// ElMessageBox.alert("", "", {
// confirmButtonText: "OK",
// callback: () => {
// localStorage.setItem("isReload", "true");
// },
// });
// }
// if (ws.socket == null) {
// ws.connect();
// }
}); });
}, 5000);
const playAudio = () => { onMounted(() => {
if (audioPlayer.value) { window.addEventListener("beforeunload", handleBeforeUnload);
audioPlayer.value.play().catch((error) => {
console.error("自动播放被阻止:", error);
}); });
}
}; onBeforeUnmount(() => {
window.removeEventListener("beforeunload", handleBeforeUnload);
});
// const include = getInclude(routes[0].children); // const include = getInclude(routes[0].children);
</script> </script>

View File

@ -7,12 +7,14 @@ import { usePermissStore } from './store/permiss';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import 'element-plus/dist/index.css'; import 'element-plus/dist/index.css';
import './assets/css/icon.css'; import './assets/css/icon.css';
import WebSocketService from "@/utils/webSocket.js";
const ws = new WebSocketService()
const app = createApp(App); const app = createApp(App);
const pinia = createPinia(); const pinia = createPinia();
pinia.use(piniaPluginPersistedstate); pinia.use(piniaPluginPersistedstate);
app.use(pinia); app.use(pinia);
app.use(router); app.use(router);
app.provide('ws', ws)
// 注册elementplus图标 // 注册elementplus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) { for (const [key, component] of Object.entries(ElementPlusIconsVue)) {

View File

@ -17,7 +17,7 @@ import m4_a from "@/assets/img/m4_a.png";
export const routes: RouteRecordRaw[] = [ export const routes: RouteRecordRaw[] = [
{ {
path: '/', path: '/',
component: Layout, component: () => import('@/layout/index.vue'),
redirect: '/statisticalCenter', redirect: '/statisticalCenter',
children: [ children: [
{ {
@ -123,14 +123,12 @@ const router = createRouter({
}); });
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
NProgress.start(); NProgress.start();
const permiss = usePermissStore(); const permiss = usePermissStore();
const comm = useCommonStore(); const comm = useCommonStore();
const tab = useTabsStore(); const tab = useTabsStore();
tab.setTabsItem(to.meta.tabs); tab.setTabsItem(to.meta.tabs);
comm.setTime() comm.setTime()
if (typeof to.meta.permiss == 'string' && !permiss.key.includes(to.meta.permiss)) { if (typeof to.meta.permiss == 'string' && !permiss.key.includes(to.meta.permiss)) {
// 如果没有权限则进入403 // 如果没有权限则进入403
next('/403'); next('/403');

View File

@ -18,5 +18,6 @@ export interface FormOptionList {
activeText?: string; activeText?: string;
inactiveText?: string; inactiveText?: string;
required?: boolean; required?: boolean;
defalut?: any;
validator?: Function; validator?: Function;
} }

View File

@ -10,6 +10,7 @@ export class MapCustom {
} }
// 多边形回显 // 多边形回显
polyEditor(list, fn) { polyEditor(list, fn) {
if (!list) return;
this.map.clearMap(); this.map.clearMap();
let pathList = []; let pathList = [];
list.forEach((item, index) => { list.forEach((item, index) => {

View File

@ -22,6 +22,8 @@ service.interceptors.request.use(
service.interceptors.response.use( service.interceptors.response.use(
(response: AxiosResponse) => { (response: AxiosResponse) => {
const { data, headers } = response; const { data, headers } = response;
if (headers['content-type'] === "application/vnd.ms-excel;charset=utf-8") return data
if (data.code !== 200) { if (data.code !== 200) {
ElMessage.error(data.msg) ElMessage.error(data.msg)

View File

@ -1,33 +1,32 @@
import { ref, onMounted, onUnmounted } from 'vue'; import { useCommonStore } from "@/store/common";
export default class WebSocketService {
class WebSocketService {
constructor() { constructor() {
this.url = import.meta.env.VITE_APP_URL_WEBSOCKET; this.url = import.meta.env.VITE_APP_URL_WEBSOCKET;
this.socket = null; this.socket = null;
this.isAlive = false; // 用于判断心跳是否正常 this.isAlive = false; // 用于判断心跳是否正常
this.reconnectAttempts = 0; // 重连尝试次数 this.reconnectAttempts = 0; // 重连尝试次数
this.MAX_RECONNECT_ATTEMPTS = 5; // 最大重连次数 this.MAX_RECONNECT_ATTEMPTS = 5; // 最大重连次数
this.HEARTBEAT_INTERVAL = 5000; // 心跳间隔时间 (30秒) // this.HEARTBEAT_INTERVAL = 5000; // 心跳间隔时间 (30秒)
// this.HEARTBEAT_INTERVAL = 30000; // 心跳间隔时间 (30秒) this.HEARTBEAT_INTERVAL = 30000; // 心跳间隔时间 (30秒)
this.heartbeatTimer = null; this.heartbeatTimer = null;
this.reconnectTimer = null; this.reconnectTimer = null;
this.connect(); // this.connect();
} }
connect() { connect() {
this.socket = new WebSocket(this.url); this.socket = new WebSocket(this.url);
this.socket.onopen = () => { this.socket.onopen = () => {
console.log('WebSocket已连接'); console.log('WebSocket已连接');
this.webScoketLogin(); this.webScoketLogin();
this.reconnectAttempts = 0; // 成功连接后重置重试次数 this.reconnectAttempts = 0; // 成功连接后重置重试次数
this.startHeartbeat(); // 开始心跳检测 this.startHeartbeat(); // 开始心跳检测
}; };
this.onMessage()
this.socket.onmessage = (event) => { this.socket.onmessage = (event) => {
// 处理接收到的消息 // 处理接收到的消息
console.log('Message received:', event.data); console.log('WebSocket收到消息:', event.data);
this.receivePong()
}; };
this.socket.onclose = () => { this.socket.onclose = () => {
@ -49,8 +48,9 @@ class WebSocketService {
cmd: "webLogin", cmd: "webLogin",
}) })
} }
// 开始发送心跳
startHeartbeat() { startHeartbeat() {
console.log('开始发送心跳');
if (!this.heartbeatTimer) { if (!this.heartbeatTimer) {
this.heartbeatTimer = setInterval(() => { this.heartbeatTimer = setInterval(() => {
if (this.socket.readyState === WebSocket.OPEN) { if (this.socket.readyState === WebSocket.OPEN) {
@ -60,7 +60,7 @@ class WebSocketService {
}, this.HEARTBEAT_INTERVAL); }, this.HEARTBEAT_INTERVAL);
} }
} }
// 关闭发送心态
stopHeartbeat() { stopHeartbeat() {
if (this.heartbeatTimer) { if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer); clearInterval(this.heartbeatTimer);
@ -69,8 +69,6 @@ class WebSocketService {
} }
sendPing() { sendPing() {
console.log('发送心跳', JSON.stringify({ cmd: 'heartbeat' }));
this.socket.send(JSON.stringify({ cmd: 'heartbeat' })); this.socket.send(JSON.stringify({ cmd: 'heartbeat' }));
setTimeout(() => { setTimeout(() => {
if (!this.isAlive) { if (!this.isAlive) {
@ -83,25 +81,39 @@ class WebSocketService {
receivePong() { receivePong() {
this.isAlive = true; this.isAlive = true;
} }
// 重新连接
attemptReconnect() { attemptReconnect() {
const comm = useCommonStore();
if (!comm.user.token) return
if (this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS) { if (this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS) {
this.reconnectAttempts++; this.reconnectAttempts++;
console.log(`Attempting to reconnect (${this.reconnectAttempts})...`); console.log(`正在尝试重新连接 (${this.reconnectAttempts})...`);
this.reconnectTimer = setTimeout(() => { this.reconnectTimer = setTimeout(() => {
this.connect(); this.connect();
}, 1000 * this.reconnectAttempts); // 指数退避算法 }, 1000 * this.reconnectAttempts); // 指数退避算法
} else { } else {
console.error('Max reconnect attempts reached'); console.error('达到的最大重连尝试数');
} }
} }
// 发送消息
sendMessage(message) { sendMessage(message) {
console.log('sendMessage');
if (this.socket && this.socket.readyState === WebSocket.OPEN) { if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(message)); this.socket.send(JSON.stringify(message));
} }
} }
onMessage = (callback) => {
this.socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.cmd == "webLogin" && !data.code == 200) return this.webScoketLogin()
callback && callback(data);
if (event.data.includes('heartbeat')) {
this.receivePong();
}
};
};
// 关闭连接
close() { close() {
this.stopHeartbeat(); this.stopHeartbeat();
if (this.reconnectTimer) { if (this.reconnectTimer) {
@ -115,44 +127,3 @@ class WebSocketService {
} }
} }
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

@ -5,6 +5,7 @@
<div class="map-content"> <div class="map-content">
<div class="map" id="mapcontainer"></div> <div class="map" id="mapcontainer"></div>
<div class="chart"> <div class="chart">
<div class="title">设备生理数据</div>
<div ref="chartRef" style="width: 100%; height: 100%"></div> <div ref="chartRef" style="width: 100%; height: 100%"></div>
</div> </div>
</div> </div>
@ -20,7 +21,12 @@
</div> </div>
<div class="info-text">绑定关联人名称{{ curData.username }}</div> <div class="info-text">绑定关联人名称{{ curData.username }}</div>
<div class="info-text">绑定关联人警号{{ curData.userNumber }}</div> <div class="info-text">绑定关联人警号{{ curData.userNumber }}</div>
<div class="info-text">状态{{ statusEnum[curData.status] }}</div> <div class="info-text">
状态
<el-tag :type="curData.status == 1 ? 'success' : 'danger'">
{{ statusEnum[curData.status] }}
</el-tag>
</div>
<div class="info-text">紧急电话{{ curData.adminPhone }}</div> <div class="info-text">紧急电话{{ curData.adminPhone }}</div>
<div class="info-text">隶属辖区{{ curData.orgName }}</div> <div class="info-text">隶属辖区{{ curData.orgName }}</div>
@ -77,13 +83,14 @@
<script setup lang="ts" name="incidentDispose"> <script setup lang="ts" name="incidentDispose">
import location from "@/assets/img/location.png"; import location from "@/assets/img/location.png";
import { MapCustom } from "@/utils/mapCustom"; import { MapCustom } from "@/utils/mapCustom";
import { onMounted, ref, reactive, watch } from "vue"; import { onMounted, ref, reactive, watch, onDeactivated, onUnmounted } from "vue";
import Upload from "@/components/upload-img.vue"; import Upload from "@/components/upload-img.vue";
import * as echarts from "echarts"; import * as echarts from "echarts";
import { warningDetail, warningConfirm } from "@/api/index"; import { warningDetail, warningConfirm } from "@/api/index";
import { TWarningConfirm, TWarningDetail } from "@/api/index.d"; import { TWarningConfirm, TWarningDetail } from "@/api/index.d";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { ElMessage, FormInstance, FormRules } from "element-plus"; import { ElMessage, FormInstance, FormRules } from "element-plus";
import { debounce, format } from "@/utils";
// //
enum unitEnum { enum unitEnum {
@ -109,29 +116,70 @@ const chartRef = ref(null);
const disabled = ref(false); const disabled = ref(false);
const ruleFormRef = ref<FormInstance>(); const ruleFormRef = ref<FormInstance>();
let map = null; let map = null;
let myChart = null;
const ringOptions = { const options = {
title: { tooltip: {
text: "Stacked Line", trigger: "axis",
left: "3%", formatter: function (params) {
let unit = { 0: "次/分", 1: "%", 2: "℃" };
var res = format(options.times[params[0].dataIndex]) + "<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: "3%",
containLabel: true,
}, },
xAxis: { xAxis: {
type: "category", type: "category",
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], boundaryGap: false,
data: [],
}, },
times: [],
yAxis: { yAxis: {
type: "value", type: "value",
}, },
grid: {
left: "10%",
right: "4%",
bottom: "10%",
},
series: [ series: [
{ {
data: [820, 932, 901, 934, 1290, 1330, 1320], itemStyle: {
color: "#FF4241",
},
name: "心率",
type: "line", type: "line",
smooth: true, showSymbol: false,
data: [],
time: [],
},
{
itemStyle: {
color: "#12CCA2",
},
name: "血氧",
type: "line",
showSymbol: false,
data: [],
time: [],
},
{
itemStyle: {
color: "#FF7D00",
},
showSymbol: false,
name: "体表温度",
type: "line",
data: [],
time: [],
}, },
], ],
}; };
@ -149,7 +197,7 @@ let curData = ref<TWarningDetail.TRes>({
creatUser: "", creatUser: "",
createTime: "", createTime: "",
deviceId: "", deviceId: "",
healthData: "", healthData: [],
id: 0, id: 0,
lat: 0, lat: 0,
lng: 0, lng: 0,
@ -195,6 +243,16 @@ const getData = async () => {
let icon = map.newIcon(location); let icon = map.newIcon(location);
let marker = map.marker({ icon, position: [116.406315, 39.908775] }); let marker = map.marker({ icon, position: [116.406315, 39.908775] });
marker.setMap(map.map); marker.setMap(map.map);
if (res.healthData && res.healthData.length) {
res.healthData.forEach((item) => {
options.times.push(item.time);
options.xAxis.data.push(format(item.time, "HH:mm:ss"));
options.series[0].data.push(item.hr);
options.series[1].data.push(item.bo);
options.series[2].data.push(item.temp);
});
myChart.setOption(options);
}
} catch (error) {} } catch (error) {}
}; };
const submitForm = async (formEl: FormInstance | undefined) => { const submitForm = async (formEl: FormInstance | undefined) => {
@ -208,14 +266,22 @@ const submitForm = async (formEl: FormInstance | undefined) => {
} }
}); });
}; };
const handleResize = debounce(() => {
myChart.resize();
}, 200);
onMounted(() => { onMounted(() => {
getData(); getData();
map = new MapCustom({ dom: "mapcontainer", center: [116.406315, 39.908775] }); map = new MapCustom({ dom: "mapcontainer", center: [116.406315, 39.908775] });
if (chartRef.value) { if (chartRef.value) {
const myChart = echarts.init(chartRef.value); myChart = echarts.init(chartRef.value);
myChart.setOption(ringOptions); myChart.setOption(options);
window.addEventListener("resize", handleResize);
} }
}); });
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
});
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
@ -245,6 +311,21 @@ onMounted(() => {
background-color: #fff; background-color: #fff;
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
position: relative;
.title {
position: absolute;
left: 0px;
top: 20px;
&::before {
display: inline-block;
margin-right: 10px;
content: "";
width: 2px;
height: 14px;
border-radius: 20px;
background: #061451;
}
}
} }
} }

View File

@ -51,9 +51,9 @@ const rules = reactive<FormRules<typeof ruleForm>>({
const submitForm = async (formEl: FormInstance | undefined) => { const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return; if (!formEl) return;
await formEl.validate((valid, fields) => { await formEl.validate((valid, fields) => {
if (valid) { if (valid) {
localStorage.setItem("isReload", "true");
fetchLogin(ruleForm).then((res) => { fetchLogin(ruleForm).then((res) => {
comm.setUser(res); comm.setUser(res);
comm.getRoleList(); comm.getRoleList();

View File

@ -2,17 +2,17 @@
<div class="device"> <div class="device">
<div class="device-head"> <div class="device-head">
<div class="title">设备列表</div> <div class="title">设备列表</div>
<el-select class="select" v-model="paging.mode" @change="handelMode"> <el-select class="select" v-model="props.paging.mode" @change="handelMode">
<el-option label="全部" :value="undefined" /> <el-option label="全部" value="" />
<el-option label="常规模式" value="0" /> <el-option label="常规模式" value="0" />
<el-option label="审讯模式" value="1" /> <el-option label="审讯模式" value="1" />
<el-option label="户外押送" value="2" /> <el-option label="户外押送" value="2" />
</el-select> </el-select>
</div> </div>
<div v-infinite-scroll="load" :infinite-scroll-immediate="false" class="device-list noScrollbar infinite-list" style="overflow: auto"> <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"> <el-popover :width="350" class="box-item" placement="right" v-for="item in props.list" :key="item.id">
<template #reference> <template #reference>
<div class="item" :class="{ active: deviceId === item.deviceId }" @click="emit('click', item)"> <div class="item" :class="{ active: props.deviceInfo?.deviceId === item.deviceId }" @click="emit('click', item)">
<div class="item-img"> <div class="item-img">
<img src="@/assets/img/handcuffs.png" alt="" srcset="" /> <img src="@/assets/img/handcuffs.png" alt="" srcset="" />
</div> </div>
@ -75,7 +75,9 @@ enum modeEnum {
"户外押送", "户外押送",
} }
const emit = defineEmits(["click"]); const emit = defineEmits(["click"]);
const { list, paging, api, deviceId } = defineProps({ // const { list, paging, api, deviceInfo }
const props = defineProps({
list: { list: {
type: Array<TDevice.IListRes>, type: Array<TDevice.IListRes>,
default: () => [], default: () => [],
@ -88,19 +90,19 @@ const { list, paging, api, deviceId } = defineProps({
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
deviceId: { deviceInfo: {
type: Number || String, type: Object,
default: () => "", default: undefined,
}, },
}); });
const handelMode = () => { const handelMode = () => {
paging.page += 1; props.paging.page += 1;
api(); props.api();
}; };
const load = () => { const load = () => {
paging.page += 1; props.paging.page += 1;
api(); props.api();
}; };
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -4,11 +4,11 @@
<div class="title">当前设备告警记录</div> <div class="title">当前设备告警记录</div>
<div class="search"> <div class="search">
<el-select class="select" placeholder="请选择事件类型" v-model="paging.warnType" style="width: 150px; margin-right: 20px" @change="handelChange"> <el-select class="select" placeholder="请选择事件类型" v-model="paging.warnType" style="width: 150px; margin-right: 20px" @change="handelChange">
<el-option label="全部" :value="undefined" /> <el-option label="全部" value="" />
<el-option v-for="(item, index) in warnTypeList" :key="index" :label="item" :value="index" /> <el-option v-for="(item, index) in warnTypeList" :key="index" :label="item" :value="index" />
</el-select> </el-select>
<el-select class="select" placeholder="请选择处理状态" v-model="paging.status" style="width: 150px" @change="handelChange"> <el-select class="select" placeholder="请选择处理状态" v-model="paging.status" style="width: 150px" @change="handelChange">
<el-option label="全部" :value="undefined" /> <el-option label="全部" value="" />
<el-option label="待处理" value="0" /> <el-option label="待处理" value="0" />
<el-option label="已处理" value="1" /> <el-option label="已处理" value="1" />
</el-select> </el-select>

View File

@ -2,7 +2,7 @@
<div class="container"> <div class="container">
<el-row class="el-row" :gutter="20"> <el-row class="el-row" :gutter="20">
<el-col :span="6" class="el-row-left" <el-col :span="6" class="el-row-left"
><DeviceInfo :deviceId="deviceInfo?.deviceId" :paging="devicePaging" :api="getdeviceList" :list="deviceData" @click="handelClickDevice" ><DeviceInfo :deviceInfo="deviceInfo" :paging="devicePaging" :api="getdeviceList" :list="deviceData" @click="handelClickDevice"
/></el-col> /></el-col>
<el-col :span="18" class="el-row-right scrollbar"> <el-col :span="18" class="el-row-right scrollbar">
<MonitoringTop :funcList="funcList" /> <MonitoringTop :funcList="funcList" />
@ -48,7 +48,7 @@ import { MapCustom } from "@/utils/mapCustom";
import * as echarts from "echarts"; import * as echarts from "echarts";
import { deviceList, healthLatestData, warningRecord, locateRecord } from "@/api/index"; import { deviceList, healthLatestData, warningRecord, locateRecord } from "@/api/index";
import { TDevice, THealthLatestData } from "@/api/index.d"; import { TDevice, THealthLatestData } from "@/api/index.d";
import { onMounted, onDeactivated, ref, reactive } from "vue"; import { onMounted, onDeactivated, ref, reactive, onUnmounted } from "vue";
import { debounce, format } from "@/utils"; import { debounce, format } from "@/utils";
import heart from "@/assets/img/heart.png"; import heart from "@/assets/img/heart.png";
import temperature from "@/assets/img/temperature.png"; import temperature from "@/assets/img/temperature.png";
@ -58,6 +58,7 @@ import location from "@/assets/img/location.png";
const chartRef = ref(null); const chartRef = ref(null);
let myChart = null; let myChart = null;
let newMap = null; let newMap = null;
let Interval = null;
let funcList = ref([ let funcList = ref([
{ title: "当前心率", en: "DANGQIANXINLV", icon: heart, unit: "次/分", num: 0, color: "#FF0303" }, { title: "当前心率", en: "DANGQIANXINLV", icon: heart, unit: "次/分", num: 0, color: "#FF0303" },
{ title: "当前血氧", en: "DANGQIANXUEYANG", icon: blood, unit: "%", num: 0, color: "#8B51FD" }, { title: "当前血氧", en: "DANGQIANXUEYANG", icon: blood, unit: "%", num: 0, color: "#8B51FD" },
@ -67,7 +68,18 @@ let funcList = ref([
const options = { const options = {
tooltip: { tooltip: {
trigger: "axis", 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: { xAxis: {
type: "category", type: "category",
data: [], data: [],
@ -83,6 +95,7 @@ const options = {
name: "", name: "",
data: [], data: [],
type: "line", type: "line",
showSymbol: false,
itemStyle: { itemStyle: {
color: "#ff4567", // 线 color: "#ff4567", // 线
}, },
@ -107,6 +120,7 @@ const getdeviceList = async () => {
deviceInfo.value = res.records[1]; deviceInfo.value = res.records[1];
getHealthLatestData(); getHealthLatestData();
getLocateRecord(); getLocateRecord();
IntervalFn();
} }
}; };
@ -134,6 +148,7 @@ const getOptionsData = (list: { time: string; value: number }[], name: string, c
options.series.data = []; options.series.data = [];
if (list && list.length) { if (list && list.length) {
list.forEach((item) => { list.forEach((item) => {
options.times.push(item.time);
options.xAxis.data.push(format(item.time, "HH:mm:ss")); options.xAxis.data.push(format(item.time, "HH:mm:ss"));
options.series.data.push(item.value); options.series.data.push(item.value);
}); });
@ -166,7 +181,12 @@ const getLocateRecord = () => {
const handleResize = debounce(() => { const handleResize = debounce(() => {
myChart.resize(); myChart.resize();
}, 200); }, 200);
const IntervalFn = () => {
Interval = setInterval(() => {
getHealthLatestData();
getLocateRecord();
}, 60000);
};
const handelClickDevice = (val: TDevice.IListRes) => { const handelClickDevice = (val: TDevice.IListRes) => {
deviceInfo.value = val; deviceInfo.value = val;
getHealthLatestData(); getHealthLatestData();
@ -180,7 +200,8 @@ onMounted(() => {
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
} }
}); });
onDeactivated(() => { onUnmounted(() => {
clearInterval(Interval);
window.removeEventListener("resize", handleResize); window.removeEventListener("resize", handleResize);
}); });
</script> </script>

View File

@ -30,7 +30,7 @@ interface TPaging {
page: number; page: number;
size: number; size: number;
total: number; total: number;
deviceId?: number; deviceId?: number | string;
} }
const router = useRouter(); const router = useRouter();

View File

@ -39,15 +39,17 @@ import StatisticsWarning from "./statisticsWarning.vue";
import EmergencyList from "./emergencyList.vue"; import EmergencyList from "./emergencyList.vue";
import * as echarts from "echarts"; import * as echarts from "echarts";
import { MapCustom } from "@/utils/mapCustom"; import { MapCustom } from "@/utils/mapCustom";
import { statisticsDevice, statisticsContent, statisticsCount, statisticsWarningapi, warnRecord } from "@/api/index"; import { statisticsDevice, statisticsContent, statisticsCount, statisticsWarningapi, warnRecord, statisticsUseCount } from "@/api/index";
import { onMounted, ref, reactive, onDeactivated } from "vue"; import { onMounted, ref, reactive, onDeactivated, onUnmounted } from "vue";
import { debounce } from "@/utils"; import { debounce, format } from "@/utils";
import { TWarnRecord } from "@/api/index.d"; import { TWarnRecord } from "@/api/index.d";
import handcuffs from "@/assets/img/handcuffs.png"; import handcuffs from "@/assets/img/handcuffs.png";
import onLine from "@/assets/img/onLine.png"; import onLine from "@/assets/img/onLine.png";
import report from "@/assets/img/report.png"; import report from "@/assets/img/report.png";
import newly from "@/assets/img/newly.png"; import newly from "@/assets/img/newly.png";
let d = new Date();
const chartRef = ref(null); const chartRef = ref(null);
const chartRef1 = ref(null); const chartRef1 = ref(null);
const chartRef2 = ref(null); const chartRef2 = ref(null);
@ -95,9 +97,9 @@ let option1 = ref({
type: "pie", type: "pie",
radius: ["20%", "50%"], radius: ["20%", "50%"],
data: [ data: [
{ value: 1048, name: "常规模式" }, { value: 0, name: "常规模式" },
{ value: 735, name: "户外押送" }, { value: 0, name: "户外押送" },
{ value: 580, name: "审讯模式" }, { value: 0, name: "审讯模式" },
], ],
}, },
}); });
@ -122,12 +124,13 @@ let option2 = ref({
], ],
}, },
}); });
const paging = reactive({ const paging = reactive({
page: 1, page: 1,
size: 10, size: 10,
total: 0, total: 0,
deviceId: "", deviceId: "",
startDate: `${format(d, "YYYY-MM-DD")} 00:00:00`,
endDate: `${format(d, "YYYY-MM-DD")} 23:59:59`,
}); });
const tableData = ref<TWarnRecord.IListRes[]>([]); const tableData = ref<TWarnRecord.IListRes[]>([]);
@ -156,6 +159,16 @@ const getStatisticsWarningApi = () => {
myChart2.setOption(option2.value); myChart2.setOption(option2.value);
}); });
}; };
const getStatisticsUseCount = () => {
statisticsUseCount().then((res) => {
option1.value.series.data = [
{ value: res.defaultCount, name: "常规模式" },
{ value: res.outsideCount, name: "户外押送" },
{ value: res.monitorCount, name: "审讯模式" },
];
myChart1.setOption(option1.value);
});
};
const getStatisticsContent = (res) => { const getStatisticsContent = (res) => {
statisticsContent(res).then((res) => { statisticsContent(res).then((res) => {
option.value.xAxis.data = res?.times; option.value.xAxis.data = res?.times;
@ -212,6 +225,7 @@ onMounted(() => {
getData(); getData();
getStatisticsCount(); getStatisticsCount();
getStatisticsWarningApi(); getStatisticsWarningApi();
getStatisticsUseCount();
new MapCustom({ dom: "mapcontainer" }); new MapCustom({ dom: "mapcontainer" });
@ -228,7 +242,7 @@ onMounted(() => {
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
} }
}); });
onDeactivated(() => { onUnmounted(() => {
window.removeEventListener("resize", handleResize); window.removeEventListener("resize", handleResize);
}); });
</script> </script>

View File

@ -29,7 +29,7 @@
<template #header> <template #header>
<div class="card-header">使用记录</div> <div class="card-header">使用记录</div>
</template> </template>
<TableCustom :hasToolbar="false" :columns="record" :tableData="tableData" :total="paging.total" :currentPage="paging.page" :changePage="changePage"> <TableCustom :hasToolbar="false" :columns="record" :tableData="tableData" :paging="paging" :changePage="changePage">
<template #status="{ rows }"> <template #status="{ rows }">
{{ recordStatusEnum[rows.status] }} {{ recordStatusEnum[rows.status] }}
</template> </template>

View File

@ -0,0 +1,103 @@
<template>
<el-form ref="ruleFormRef" label-position="top" :model="ruleForm" status-icon :rules="rules" label-width="auto" class="demo-ruleForm">
<el-form-item label="已选择手铐">
<div>{{ deviceNames }}</div>
</el-form-item>
<el-form-item label="辖区" prop="org">
<el-select v-model="ruleForm.org" placeholder="请选择辖区" @change="getAccountListOrg">
<el-option v-for="item in orgAllData" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="人员" prop="accountId">
<el-select v-model="ruleForm.accountId" placeholder="请选择人员">
<el-option v-for="item in accountLis" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item class="footr">
<el-button type="primary" @click="submitForm(ruleFormRef)"> 保存 </el-button>
<el-button @click="emit('close')">取消</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { computed, onMounted, PropType, reactive, ref, watch } from "vue";
import type { FormInstance, FormRules } from "element-plus";
import { TDevice, TOrg } from "@/api/index.d";
import { accountListOrg } from "@/api/index";
interface IForm {
devices: number[];
org: number | string;
accountId: number | string;
}
const ruleFormRef = ref<FormInstance>();
const accountLis = ref([]);
const props = defineProps({
orgAllData: {
type: Array as PropType<TOrg.IOrgRecordRes[]>,
dfault: () => {},
},
selectDeviceList: {
type: Array as PropType<TDevice.IListRes[]>,
dfault: () => {},
},
api: {
type: Function,
},
});
const emit = defineEmits(["close"]);
const getAccountListOrg = (orgId: number, accountId?: number) => {
accountLis.value = [];
ruleForm.accountId = "";
accountListOrg(orgId).then((res) => {
accountLis.value = res;
if (accountId) {
ruleForm.accountId = accountId;
}
});
};
const deviceNames = computed(() => props.selectDeviceList.map((item) => item.deviceId).join("、"));
const ruleForm = reactive<IForm>({
devices: props.selectDeviceList.map((item) => item.deviceId),
org: "",
accountId: "",
});
onMounted(() => {
if (props.selectDeviceList.length == 1) {
let item = props.selectDeviceList[0];
ruleForm.org = item.orgId;
getAccountListOrg(item.orgId, item.accountId);
}
});
const rules = reactive<FormRules<typeof ruleForm>>({
org: [{ required: true, message: "请选择辖区", trigger: "blur" }],
accountId: [{ required: true, message: "请选择人员", trigger: "blur" }],
});
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate((valid) => {
if (valid) {
props.api(ruleForm);
}
});
};
</script>
<style scoped lang="less">
.footr {
margin-top: 20px;
}
.footr :deep(.el-form-item__content) {
display: flex;
justify-content: flex-end;
align-items: flex-end;
margin-top: 20px;
}
</style>

View File

@ -2,10 +2,10 @@
<div class="container"> <div class="container">
<TableSearch :query="query" :options="searchOpt" :search="handleSearch" /> <TableSearch :query="query" :options="searchOpt" :search="handleSearch" />
<div class="table-container"> <div class="table-container">
<TableCustom :columns="columns" :tableData="tableData" :paging="paging" :changePage="changePage"> <TableCustom :selection-change="handleSelect" :columns="columns" :tableData="tableData" :paging="paging" :changePage="changePage">
<template #toolbarBtn> <template #toolbarBtn>
<!-- <el-button type="primary" @click="handleAdd">新增</el-button> --> <!-- <el-button type="primary" @click="handleAdd">新增</el-button> -->
<el-button @click="handleEdit">手铐关联</el-button> <el-button @click="handleEdit()">设备关联</el-button>
<!-- <el-button>地图位置</el-button> --> <!-- <el-button>地图位置</el-button> -->
<!-- <el-button>导出</el-button> --> <!-- <el-button>导出</el-button> -->
</template> </template>
@ -41,18 +41,22 @@
<!-- <TableEdit :form-data="rowData" :options="controlOptions" :update="updateData" /> --> <!-- <TableEdit :form-data="rowData" :options="controlOptions" :update="updateData" /> -->
<DeviceControl v-model="controlForm" @change="handelControl" /> <DeviceControl v-model="controlForm" @change="handelControl" />
</el-dialog> </el-dialog>
<el-dialog title="设备关联" v-model="visible2" width="700px" destroy-on-close :close-on-click-modal="false" @close="closeDialog">
<BindArea :orgAllData="orgAllData" :selectDeviceList="selectDeviceList" @close="visible2 = false" :api="bindWebFn" />
</el-dialog>
</div> </div>
</template> </template>
<script setup lang="ts" name="basetable"> <script setup lang="ts" name="basetable">
import { ref, reactive, onMounted } from "vue"; import { ref, reactive, onMounted } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { deviceList, setMode, deviceGetLocation, deviceControl, setUseStatus, setStatus } from "@/api/index"; import { deviceList, setMode, deviceGetLocation, deviceControl, setUseStatus, setStatus, orgAllList, bindWeb } from "@/api/index";
import { TDevice } from "@/api/index.d"; import { TbindWeb, TDevice, TOrg } from "@/api/index.d";
import TableCustom from "@/components/table-custom.vue"; import TableCustom from "@/components/table-custom.vue";
import TableSearch from "@/components/table-search.vue"; import TableSearch from "@/components/table-search.vue";
import TableEdit from "@/components/table-edit.vue"; import TableEdit from "@/components/table-edit.vue";
import DeviceControl from "./deviceControl.vue"; import DeviceControl from "./deviceControl.vue";
import BindArea from "./bindArea.vue";
import { TableItem } from "@/types/table"; import { TableItem } from "@/types/table";
import { FormOption, FormOptionList } from "@/types/form-option"; import { FormOption, FormOptionList } from "@/types/form-option";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
@ -82,9 +86,8 @@ let TableEditOptions = ref<FormOption>({
const addOp = [{ type: "input", label: "唯一编码", prop: "name", required: true }]; const addOp = [{ type: "input", label: "唯一编码", prop: "name", required: true }];
const editOp = [ const editOp = [
{ type: "select", label: "选择手铐", prop: "name", required: true }, { type: "select", label: "选择手铐", prop: "name", required: true },
{ type: "input", label: "唯一编码", prop: "name1" }, { type: "select", label: "辖区列表", prop: "name2" },
{ type: "select", label: "辖区绑定", prop: "name2" }, { type: "select", label: "账号列表", prop: "name3" },
{ type: "input", label: "紧急电话", prop: "name3" },
]; ];
// //
@ -125,24 +128,24 @@ const searchOpt = ref<FormOptionList[]>([
const handleSearch = () => { const handleSearch = () => {
changePage(1); changePage(1);
}; };
const selectDeviceList = ref<TDevice.IListRes[]>([]);
// //
let columns = ref([ let columns = ref([
{ type: "selection" }, { type: "selection", reserveSelection: true },
{ type: "index", label: "序号", width: 55, align: "center" }, { type: "index", label: "序号", width: 55, align: "center" },
{ prop: "deviceId", label: "手铐IMEI", width: 120 }, { prop: "deviceId", label: "IMEI", width: 120 },
{ prop: "adminName", label: "绑定警察名称", width: 180 }, { prop: "adminName", label: "绑定警察名称", width: 180 },
{ prop: "adminUsername", label: "绑定警察账户", width: 120 }, { prop: "adminUsername", label: "绑定警察账户", width: 120 },
{ prop: "orgName", label: "关联辖区", width: 120 },
{ prop: "battery", label: "电量", width: 100 }, { prop: "battery", label: "电量", width: 100 },
{ prop: "deviceVersion", label: "版本号", width: 100 }, { prop: "deviceVersion", label: "版本号", width: 100 },
{ prop: "status", label: "设备状态", width: 100 }, { prop: "status", label: "设备状态", width: 100 },
{ prop: "mode", label: "当前模式", width: 100 }, { prop: "mode", label: "当前模式", width: 100 },
{ prop: "useStatus", label: "使用状态", width: 100 }, { prop: "useStatus", label: "使用状态", width: 100 },
{ prop: "deviceSwitch", label: "启用开关", width: 100 }, { prop: "deviceSwitch", label: "启用开关", width: 100 },
{ prop: "orgName", label: "关联辖区编号", width: 120 },
{ prop: "createTime", label: "最新通信时间", width: 180 }, { prop: "createTime", label: "最新通信时间", width: 180 },
{ prop: "createTime", label: "创建时间", width: 180 }, { prop: "createTime", label: "创建时间", width: 180 },
{ prop: "operator", label: "操作", width: 400 }, { prop: "operator", label: "操作", width: 400, fixed: "right" },
]); ]);
const paging = reactive({ const paging = reactive({
page: 1, page: 1,
@ -155,18 +158,32 @@ const controlForm = reactive({
}); });
const tableData = ref<TDevice.IListRes[]>([]); const tableData = ref<TDevice.IListRes[]>([]);
const orgAllData = ref<TOrg.IOrgRecordRes[]>([]);
const getData = async () => { const getData = async () => {
const res = await deviceList({ ...paging, ...query }); const res = await deviceList({ ...paging, ...query });
tableData.value = res.records; tableData.value = res.records;
paging.total = res.total; paging.total = res.total;
}; };
const bindWebFn = (val: TbindWeb) => {
bindWeb(val).then(() => {
ElMessage.success("操作成功");
visible2.value = false;
getData();
});
};
const handleSelect = (val: TDevice.IListRes[]) => {
selectDeviceList.value = val;
};
const changePage = (val: number) => { const changePage = (val: number) => {
paging.page = val; paging.page = val;
getData(); getData();
}; };
const visible1 = ref(false); const visible1 = ref(false);
const visible2 = ref(false);
const visible = ref(false); const visible = ref(false);
const isEdit = ref(false); const isEdit = ref(false);
const rowData = ref<TDevice.IListRes>(); const rowData = ref<TDevice.IListRes>();
@ -178,10 +195,11 @@ const handleAdd = () => {
// //
const handleEdit = (row?: TDevice.IListRes) => { const handleEdit = (row?: TDevice.IListRes) => {
rowData.value = { ...row }; if (row) {
TableEditOptions.value.list = editOp; selectDeviceList.value = [row];
isEdit.value = true; }
visible.value = true; if (selectDeviceList.value.length == 0) return ElMessage.warning("请选择设备");
visible2.value = true;
}; };
const handelRow = (row?: TDevice.IListRes) => { const handelRow = (row?: TDevice.IListRes) => {
@ -199,6 +217,12 @@ const handelSwitch = (val: TDevice.IListRes, type: number) => {
ElMessage.success("操作成功"); ElMessage.success("操作成功");
}); });
}; };
const getOrgAllList = () => {
orgAllList().then((res) => {
orgAllData.value = res;
});
};
const handelControl = (type: number) => { const handelControl = (type: number) => {
switch (type) { switch (type) {
case 4: case 4:
@ -247,6 +271,7 @@ const closeDialog = () => {
onMounted(() => { onMounted(() => {
getData(); getData();
getOrgAllList();
}); });
// //

View File

@ -82,10 +82,8 @@ let InfoWin = null;
const params = reactive<TLocateRecord.TReq>({ const params = reactive<TLocateRecord.TReq>({
deviceId: query.deviceId as string, deviceId: query.deviceId as string,
// startDate: `${format(d, "YYYY-MM-DD")} 00:00:00`, startDate: `${format(d, "YYYY-MM-DD")} 00:00:00`,
// endDate: `${format(d, "YYYY-MM-DD")} 23:59:59`, endDate: `${format(d, "YYYY-MM-DD")} 23:59:59`,
startDate: "2021-04-14 00:00:00",
endDate: "2025-05-14 23:59:59",
}); });
const weekChange = (val: any) => { const weekChange = (val: any) => {

View File

@ -0,0 +1,104 @@
<template>
<el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" label-width="100px">
<el-form-item label="用户名称" prop="name">
<el-input v-model="ruleForm.name" placeholder="请输入用户名称" />
</el-form-item>
<el-form-item label="电话号码" prop="phone">
<el-input :maxlength="11" v-model="ruleForm.phone" placeholder="请输入电话号码" />
</el-form-item>
<el-form-item label="警员号" prop="username">
<el-input :maxlength="20" v-model="ruleForm.username" placeholder="请输入警员号" />
</el-form-item>
<el-form-item label="类型" prop="roleId">
<el-select v-model="ruleForm.roleId" placeholder="请选择人员">
<el-option v-for="item in comm.roleList" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="辖区" prop="orgId">
<el-select v-model="ruleForm.orgId" placeholder="请选择辖区">
<el-option v-for="item in orgAllData" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch v-model="ruleForm.status" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item class="footr">
<el-button type="primary" @click="submitForm(ruleFormRef)"> 保存 </el-button>
<el-button @click="emit('close')">取消</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { onMounted, PropType, reactive, ref } from "vue";
import type { FormInstance, FormRules } from "element-plus";
import { TDevice, TOrg } from "@/api/index.d";
import { useCommonStore } from "@/store/common";
import { TAccount } from "@/api/index.d";
const comm = useCommonStore();
const ruleFormRef = ref<FormInstance>();
const { orgAllData, formData, api } = defineProps({
orgAllData: {
type: Array as PropType<TOrg.IOrgRecordRes[]>,
dfault: () => {},
},
formData: {
type: Object,
required: true,
},
api: {
type: Function,
},
});
const emit = defineEmits(["close"]);
const ruleForm = reactive(
formData
? { ...formData }
: {
name: "",
phone: "",
username: "",
roleId: "",
orgId: "",
status: 1,
}
);
onMounted(() => {});
const rules = reactive<FormRules<typeof ruleForm>>({
name: [{ required: true, message: "请输入用户名称", trigger: "blur" }],
phone: [
{ required: true, message: "请输入电话号码", trigger: "blur" },
{ min: 11, message: "请输入11位手机号", trigger: "blur" },
],
username: [
{ required: true, message: "请输入警员号", trigger: "blur" },
{ min: 5, message: "警员号长度不能小于5位", trigger: "blur" },
],
roleId: [{ required: true, message: "请选择人员", trigger: "blur" }],
orgId: [{ required: true, message: "请选择辖区", trigger: "blur" }],
});
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate((valid) => {
if (valid) {
api(ruleForm);
}
});
};
</script>
<style scoped lang="less">
.footr {
margin-top: 20px;
}
.footr :deep(.el-form-item__content) {
display: flex;
justify-content: flex-end;
align-items: flex-end;
margin-top: 20px;
}
</style>

View File

@ -38,7 +38,10 @@
</TableCustom> </TableCustom>
</div> </div>
<el-dialog :title="dialogTitle" v-model="visible" width="700px" destroy-on-close :close-on-click-modal="false" @close="closeDialog"> <el-dialog :title="dialogTitle" v-model="visible" width="700px" destroy-on-close :close-on-click-modal="false" @close="closeDialog">
<TableEdit :form-data="rowData" :options="options" :edit="isEdit" :update="updateData" @close="closeDialog" /> <AddUser :orgAllData="orgAllData" :form-data="rowData" :api="updateData" @close="closeDialog" />
</el-dialog>
<el-dialog title="重置密码" v-model="visible1" width="700px" destroy-on-close :close-on-click-modal="false" @close="visible1 = false">
<ResetPwd :api="handleResetPwd" @close="visible1 = false" />
</el-dialog> </el-dialog>
<el-dialog title="编辑类型" v-model="typeVisible" width="800px" destroy-on-close :close-on-click-modal="false" @close="closeDialog"> <el-dialog title="编辑类型" v-model="typeVisible" width="800px" destroy-on-close :close-on-click-modal="false" @close="closeDialog">
<UserType :typeHeadList="typeHeadList" :roleListData="roleListData" @complete="saveType" @addType="addType" /> <UserType :typeHeadList="typeHeadList" :roleListData="roleListData" @complete="saveType" @addType="addType" />
@ -50,16 +53,18 @@
<script setup lang="ts" name="basetable"> <script setup lang="ts" name="basetable">
import { ref, reactive, onMounted } from "vue"; import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { accountAdd, accountModify, accountList, accountDeletet, passwordReset, roleList, roleMenuList, roleModify, roleAdd } from "@/api/index"; import { accountAdd, accountModify, accountList, accountDeletet, passwordReset, roleList, roleMenuList, roleModify, roleAdd, orgAllList } from "@/api/index";
import TableCustom from "@/components/table-custom.vue"; import TableCustom from "@/components/table-custom.vue";
import TableSearch from "@/components/table-search.vue"; import TableSearch from "@/components/table-search.vue";
import TableEdit from "@/components/table-edit.vue"; import TableEdit from "@/components/table-edit.vue";
import BatchImp from "@/components/batch-imp.vue"; import BatchImp from "@/components/batch-imp.vue";
import AddUser from "./addUser.vue";
import { useCommonStore } from "@/store/common"; import { useCommonStore } from "@/store/common";
import UserType from "./userType.vue"; import UserType from "./userType.vue";
import ResetPwd from "./resetPwd.vue";
import { TableItem } from "@/types/table"; import { TableItem } from "@/types/table";
import { FormOption, FormOptionList } from "@/types/form-option"; import { FormOption, FormOptionList } from "@/types/form-option";
import { TAccount, TRoleList, TRoleMenuList, TRoleModify } from "@/api/index.d"; import { TAccount, TOrg, TRoleList, TRoleMenuList, TRoleModify } from "@/api/index.d";
// import { Hide, View } from "@element-plus/icons-vue"; // import { Hide, View } from "@element-plus/icons-vue";
enum roleEnum { enum roleEnum {
@ -70,14 +75,15 @@ enum roleEnum {
} }
const comm = useCommonStore(); const comm = useCommonStore();
const visible = ref(false); const visible = ref(false);
const visible1 = ref(false);
const typeVisible = ref(false); const typeVisible = ref(false);
const isEdit = ref(false); const isEdit = ref(false);
const dialogTitle = ref(""); const dialogTitle = ref("");
const rowData = ref<TAccount.IListRes | undefined>(); const rowData = ref<TAccount.IListRes | undefined>();
// //
let typeHeadList = ref<TRoleMenuList.TRes[]>([]); let typeHeadList = ref<TRoleMenuList.TRes[]>([]);
const orgAllData = ref<TOrg.IOrgRecordRes[]>([]);
// //
const query = reactive({ const query = reactive({
name: "", name: "",
@ -125,51 +131,12 @@ let columns = ref([
{ prop: "phone", label: "手机号" }, { prop: "phone", label: "手机号" },
// { prop: "password", label: "", width: 120 }, // { prop: "password", label: "", width: 120 },
{ prop: "roleId", label: "类型" }, { prop: "roleId", label: "类型" },
{ prop: "orgName", label: "辖区" },
{ prop: "createTime", label: "创建时间" }, { prop: "createTime", label: "创建时间" },
{ prop: "status", label: "状态" }, { prop: "status", label: "状态" },
{ prop: "operator", label: "操作", width: 250 }, { prop: "operator", label: "操作", width: 250 },
]); ]);
let addOpt = {
labelWidth: "100px",
span: 24,
list: [
{ placeholder: "请输入用户名称", type: "input", label: "用户名称", prop: "name", required: true },
{ placeholder: "请输入电话号码", type: "input", label: "电话号码", prop: "phone", required: true },
{ placeholder: "请输入警员号", type: "input", label: "警员号", prop: "username", required: true },
{
placeholder: "请选择类型",
type: "select",
label: "类型",
opts: [
{ label: "管理员", value: -1 },
{ label: "警察", value: 1 },
{ label: "辅警", value: 2 },
{ label: "协警", value: 3 },
],
prop: "roleId",
required: true,
},
{ placeholder: "请输入密码", type: "password", label: "密码", prop: "password", required: true },
{ type: "switch", label: "状态", prop: "status", activeValue: 1, inactiveValue: 0 },
// { placeholder: "", type: "password", label: "", prop: "confirmpassword" },
// { placeholder: "", type: "textarea", label: "", prop: "mon8ey" },
],
};
// /
let options = ref<FormOption>();
//
let pwdOpt = {
labelWidth: "100px",
span: 24,
list: [
// { type: "password", label: "", prop: "name1", required: true },
{ type: "password", label: "新密码", prop: "password", required: true },
// { type: "password", label: "", prop: "money3", required: true },
],
};
const handleImp = () => { const handleImp = () => {
batchImpRef.value.dialogVisible = true; batchImpRef.value.dialogVisible = true;
}; };
@ -182,15 +149,7 @@ const changePage = (val: number) => {
paging.page = val; paging.page = val;
getData(); getData();
}; };
const roleListFn = () => {
roleList().then((res) => {
roleListData.value = res;
let opts = res?.map((item) => {
return { label: item.name, value: item.id };
});
addOpt.list[3].opts = opts;
});
};
const getData = async () => { const getData = async () => {
const res = await accountList({ ...paging, ...query }); const res = await accountList({ ...paging, ...query });
tableData.value = res.records?.map((item) => { tableData.value = res.records?.map((item) => {
@ -200,6 +159,11 @@ const getData = async () => {
paging.total = res.total; paging.total = res.total;
}; };
const getOrgAllList = () => {
orgAllList().then((res) => {
orgAllData.value = res;
});
};
// //
const handelDel = (row: TableItem) => { const handelDel = (row: TableItem) => {
ElMessageBox.confirm("确定要删除吗?", "提示", { ElMessageBox.confirm("确定要删除吗?", "提示", {
@ -219,7 +183,7 @@ const updateData = (res) => {
p = { ...res, id: rowData.value.id }; p = { ...res, id: rowData.value.id };
msg = "重置成功"; msg = "重置成功";
} else { } else {
p = { ...res, orgId: comm.user.orgId }; p = { ...res };
api = isEdit.value ? accountModify : accountAdd; api = isEdit.value ? accountModify : accountAdd;
msg = isEdit.value ? "修改成功" : "新增成功"; msg = isEdit.value ? "修改成功" : "新增成功";
} }
@ -230,41 +194,44 @@ const updateData = (res) => {
getData(); getData();
}); });
}; };
const handleResetPwd = (res) => {
passwordReset({ ...res, id: rowData.value.id }).then((res) => {
ElMessage.success("密码重置成功");
visible1.value = true;
});
};
const saveType = (row: TRoleModify.Ireq) => { const saveType = (row: TRoleModify.Ireq) => {
const api = row.id ? roleModify : roleAdd; const api = row.id ? roleModify : roleAdd;
api(row).then(() => { api(row).then(() => {
ElMessage.success("保存成功"); ElMessage.success("保存成功");
comm.getRoleList(); comm.getRoleList();
// roleListFn();
// typeVisible.value = false;
}); });
}; };
const addType = () => { const addType = () => {
roleListData.value.push({ name: "", roleMenu: [] }); roleListData.value.push({ name: "", roleMenu: [] });
}; };
const roleListFn = () => {
roleList().then((res) => {
roleListData.value = res;
});
};
const closeDialog = () => { const closeDialog = () => {
visible.value = false; visible.value = false;
isEdit.value = false;
}; };
const handelRow = (name: string, row?: TAccount.IListRes) => { const handelRow = (name: string, row?: TAccount.IListRes) => {
if (name == "add") { if (name == "add") {
dialogTitle.value = "新增用户"; dialogTitle.value = "新增用户";
options.value = { ...addOpt };
rowData.value = undefined; rowData.value = undefined;
isEdit.value = false; visible.value = true;
} else if (name == "pwd") { } else if (name == "pwd") {
dialogTitle.value = "重置密码";
options.value = { ...pwdOpt };
rowData.value = { ...row }; rowData.value = { ...row };
isEdit.value = true; visible1.value = true;
} else if (name == "edit") { } else if (name == "edit") {
dialogTitle.value = "编辑"; dialogTitle.value = "编辑";
isEdit.value = true;
options.value = { ...addOpt };
rowData.value = { ...row }; rowData.value = { ...row };
}
visible.value = true; visible.value = true;
}
}; };
const getRoleMenuList = () => { const getRoleMenuList = () => {
roleMenuList().then((res) => { roleMenuList().then((res) => {
@ -272,9 +239,10 @@ const getRoleMenuList = () => {
}); });
}; };
onMounted(() => { onMounted(() => {
roleListFn();
getData(); getData();
getRoleMenuList(); getRoleMenuList();
getOrgAllList();
roleListFn();
}); });
</script> </script>

View File

@ -0,0 +1,58 @@
<template>
<el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" label-width="100px">
<el-form-item label="新密码" prop="password">
<el-input v-model="ruleForm.password" type="password" placeholder="请输入新密码" />
</el-form-item>
<el-form-item class="footr">
<el-button type="primary" @click="submitForm(ruleFormRef)"> 保存 </el-button>
<el-button @click="emit('close')">取消</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref } from "vue";
import type { FormInstance } from "element-plus";
const ruleFormRef = ref<FormInstance>();
const { api } = defineProps({
api: {
type: Function,
},
});
const emit = defineEmits(["close"]);
const ruleForm = reactive({
password: "",
});
onMounted(() => {});
const rules = reactive({
password: [
{ required: true, message: "请输入新密码", trigger: "blur" },
{ min: 6, message: "密码长度不能小于6位", trigger: "blur" },
],
});
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate((valid) => {
if (valid) {
api(ruleForm);
}
});
};
</script>
<style scoped lang="less">
.footr {
margin-top: 20px;
}
.footr :deep(.el-form-item__content) {
display: flex;
justify-content: flex-end;
align-items: flex-end;
margin-top: 20px;
}
</style>

View File

@ -1,31 +0,0 @@
<!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>