王晓寒 преди 4 месеца
родител
ревизия
8094eb9f28
променени са 36 файла, в които са добавени 2568 реда и са изтрити 1721 реда
  1. +2
    -4
      SafeCampus.WEB/components.d.ts
  2. +1
    -1
      SafeCampus.WEB/index.html
  3. +86
    -0
      SafeCampus.WEB/public/static/rtsPlayer.html
  4. +5
    -1
      SafeCampus.WEB/src/api/interface/index.ts
  5. +1
    -0
      SafeCampus.WEB/src/api/interface/sys/index.ts
  6. +15
    -0
      SafeCampus.WEB/src/api/interface/sys/usermanage/index.ts
  7. +67
    -0
      SafeCampus.WEB/src/api/interface/sys/usermanage/personnel.ts
  8. +1
    -1
      SafeCampus.WEB/src/api/modules/index.ts
  9. +18
    -1
      SafeCampus.WEB/src/api/modules/monitor/live.ts
  10. +52
    -0
      SafeCampus.WEB/src/api/modules/usermanage/classManage.ts
  11. +16
    -0
      SafeCampus.WEB/src/api/modules/usermanage/index.ts
  12. +72
    -0
      SafeCampus.WEB/src/api/modules/usermanage/personnel.ts
  13. +4
    -0
      SafeCampus.WEB/src/api/modules/warn/zjrq.ts
  14. Двоични данни
      SafeCampus.WEB/src/assets/images/home/carame.png
  15. Двоични данни
      SafeCampus.WEB/src/assets/images/home/handle.png
  16. Двоични данни
      SafeCampus.WEB/src/assets/images/home/participation.png
  17. Двоични данни
      SafeCampus.WEB/src/assets/images/home/warn.png
  18. +246
    -0
      SafeCampus.WEB/src/components/Selectors/ClassUserselector/index.vue
  19. +48
    -0
      SafeCampus.WEB/src/components/Selectors/ClassUserselector/interface.ts
  20. +6
    -3
      SafeCampus.WEB/src/components/TreeFilter/index.vue
  21. +2
    -2
      SafeCampus.WEB/src/layouts/components/Footer/index.vue
  22. +13
    -1
      SafeCampus.WEB/src/styles/element.scss
  23. +54
    -6
      SafeCampus.WEB/src/views/home/index.scss
  24. +380
    -3
      SafeCampus.WEB/src/views/home/index.vue
  25. +0
    -1609
      SafeCampus.WEB/src/views/monitor/live/ali.css
  26. +118
    -0
      SafeCampus.WEB/src/views/monitor/live/components/form.vue
  27. +106
    -0
      SafeCampus.WEB/src/views/monitor/live/components/moveForm.vue
  28. +127
    -0
      SafeCampus.WEB/src/views/monitor/live/components/userForm.vue
  29. +78
    -0
      SafeCampus.WEB/src/views/monitor/live/index.scss
  30. +256
    -80
      SafeCampus.WEB/src/views/monitor/live/index.vue
  31. +192
    -0
      SafeCampus.WEB/src/views/userManage/personnel/components/form/form_basic.vue
  32. +120
    -0
      SafeCampus.WEB/src/views/userManage/personnel/components/form/index.vue
  33. +107
    -0
      SafeCampus.WEB/src/views/userManage/personnel/components/formClass/index.vue
  34. +291
    -0
      SafeCampus.WEB/src/views/userManage/personnel/index.vue
  35. +3
    -3
      SafeCampus.WEB/src/views/warn/statistion/index.vue
  36. +81
    -6
      SafeCampus.WEB/src/views/warn/zjrq/index.vue

+ 2
- 4
SafeCampus.WEB/components.d.ts Целия файл

@@ -24,7 +24,6 @@ declare module 'vue' {
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCol: typeof import('element-plus/es')['ElCol']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDialog: typeof import('element-plus/es')['ElDialog']
@@ -43,13 +42,10 @@ declare module 'vue' {
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSpace: typeof import('element-plus/es')['ElSpace']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
@@ -59,6 +55,8 @@ declare module 'vue' {
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree']
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
ESign: typeof import('./src/components/ESign/index.vue')['default']
FormContainer: typeof import('./src/components/Form/FormContainer/index.vue')['default']
Grid: typeof import('./src/components/Grid/index.vue')['default']


+ 1
- 1
SafeCampus.WEB/index.html Целия файл

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/qjkj.ico" />
<link rel="icon" href="/api/sys/ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%- title %></title>
<!-- <script src="https://unpkg.com/aliyun-rts-sdk@1.2.1/dist/aliyun-rts-sdk.js"></script> -->


+ 86
- 0
SafeCampus.WEB/public/static/rtsPlayer.html Целия файл

@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="IE=edge">
<meta name="viewport"
content="width=device-width, height=device-height, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
<title>Aliplayer Rts Demo</title>
<link rel="stylesheet"
href="https://g.alicdn.com/apsara-media-box/imp-web-player/2.16.3/skins/default/aliplayer-min.css" />
<script type="text/javascript" charset="utf-8"
src="https://g.alicdn.com/apsara-media-box/imp-web-player/2.16.3/aliplayer-min.js"></script>
</head>

<body style="margin: 0;">
<div class="prism-player" id="player-con"></div>
<script>
// 使用: /static/rtsPlayer.html?rtsUrl=
/**
* 播放器默认播放 source 提供的 rts 拉流地址,如果失败,则会自动降级至 rtsFallbackSource 提供的拉流地址(如 HLS 地址)。
* 可能的降级场景包括:
* 1. 浏览器不支持 RTS,直接降级
* 2. RTS 信令请求失败(拉流地址无效、https配置无效、RTS配置无效等),直接降级
* 3. RTS 起播超时或中途断流,按自定义策略重试失败后降级
**/

// 更多播放器配置请参考 https://player.alicdn.com/aliplayer/index.html
let rtsUrl = getUrlParams(location.href)['rtsUrl'] || ''
var options = {
"id": "player-con",
"source": rtsUrl,
"rtsFallbackSource": "降级地址,如HLS",
"width": "100%",
"height": "500px",
"autoplay": true,
"isLive": true,
"playsinline": true,
"skipRtsSupportCheck": false, // 对于不在 https://help.aliyun.com/document_detail/397569.html 中的浏览器,可以传 true 跳过检查,强制使用 RTS(有风险,需要自测保证)

/**
* RTS 拉流超时会默认重试
* 以下两个参数用来控制降级之前的重试策略,比如 3000 毫秒超时,重试一次,如果再拉不到流就降级,那么总共等待 6000 毫秒降级
**/
// RTS 多久拉不到流会重试,默认 3000 ms
// rtsLoadDataTimeout: 2000,

// RTS 拉不到流重试的次数,默认 5,此参数建议设为 1,即重试 1 次后降级,可以减少降级等待时间
liveRetry: 1,
};

var player = new Aliplayer(options, function () {/* player ready */ });

// 降级时会触发此事件
player.on('rtsFallback', function (event) {
console.log('[EVENT]rtsFallback', event.paramData);
// event.paramData.reason 降级的原因
// event.paramData.fallbackUrl 降级到的地址
})

player.on('error', function (event) {
console.log('[EVENT]error', event.paramData);
})

// 当RTS拉流成功时触发,通过订阅该事件,可以获取到RTS TraceId
player.on('rtsTraceId', function (data) {
console.log('[EVENT]rtsTraceId', data.paramData);
// event.paramData.traceId 拉流的TraceId
// event.paramData.source 当前RTS流的播放地址
})


function getUrlParams(url) {
const reg = /(\w+)=([^&]+)/g;
const params = {};
let match;
while ((match = reg.exec(url)) !== null) {
params[match[1]] = match[2];
}
return params;
}
</script>
</body>

+ 5
- 1
SafeCampus.WEB/src/api/interface/index.ts Целия файл

@@ -51,6 +51,11 @@ export interface ReqId {
/** id */
id: number | string;
}
/** id请求参数 */
export interface ReqPersonId {
/** id */
personId: number | string;
}

/** id请求参数 */
export interface ReqstartId {
@@ -68,7 +73,6 @@ export interface ReqstopId {
}

export interface setWarn {
configJson: string;
}



+ 1
- 0
SafeCampus.WEB/src/api/interface/sys/index.ts Целия файл

@@ -20,3 +20,4 @@ export * from "./audit";
export * from "./organization";
export * from "./auth";
export * from "./warn";
export * from "./usermanage";

+ 15
- 0
SafeCampus.WEB/src/api/interface/sys/usermanage/index.ts Целия файл

@@ -0,0 +1,15 @@
/**
* @description
* @license Apache License Version 2.0
* @Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛
* @remarks
* SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款:
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改SimpleAdmin源码头部的版权声明。
* 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin
* 4.基于本软件的作品,只能使用 SimpleAdmin 作为后台服务,除外情况不可商用且不允许二次分发或开源。
* 5.请不得将本软件应用于危害国家安全、荣誉和利益的行为,不能以任何形式用于非法为目的的行为不要删除和修改作者声明。
* 6.任何基于本软件而产生的一切法律纠纷和责任,均于我司无关
* @see https://gitee.com/dotnetmoyu/SimpleAdmin
*/
export * from "./personnel";

+ 67
- 0
SafeCampus.WEB/src/api/interface/sys/usermanage/personnel.ts Целия файл

@@ -0,0 +1,67 @@
/**
* @description 用户管理接口
* @license Apache License Version 2.0
* @Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛
* @remarks
* SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款:
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改SimpleAdmin源码头部的版权声明。
* 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin
* 4.基于本软件的作品,只能使用 SimpleAdmin 作为后台服务,除外情况不可商用且不允许二次分发或开源。
* 5.请不得将本软件应用于危害国家安全、荣誉和利益的行为,不能以任何形式用于非法为目的的行为不要删除和修改作者声明。
* 6.任何基于本软件而产生的一切法律纠纷和责任,均于我司无关
*/

import { ReqPage } from "@/api";
/**
* @Description: 人员管理接口
* @Author: syy
* @Date: 2023-12-15 15:34:54
*/

export namespace SysUserPersonnel {
export interface Page extends ReqPage {}

/** 底库信息 */
export interface ClassPage {
personSetId?: string | number | undefined;
personSetName?: string | undefined;
id?: string | undefined;
name?: string | undefined;
personId?: string | undefined | number;
}
/** 人脸信息 */
export interface SysUserAvatar {
/** 人脸 */
personId?: string | undefined | number;
faceId?: string | number;
faceUrl: string;
uid?: string | number;
}
// 人脸删除
export interface SysUserFace {
personId: string | undefined;
faceIds: Array<string | number>;
}

/** 用户信息 */
export interface SysUserPerInfo {
/** 人员id */
personId?: string;
/** 姓名 */
name?: string;
/** 年龄 */
age?: number | string;
/** 性别 */
gender?: string;
/** 手机 */
phone?: string;
/** 扩展字段 */
extData?: string;
/** 人脸 */
faces: Array<SysUserAvatar>;
/** 分组 */
personSets: Array<ClassPage>;
personSetId?: number | string;
}
}

+ 1
- 1
SafeCampus.WEB/src/api/modules/index.ts Целия файл

@@ -19,4 +19,4 @@ export * from "./warn";
export * from "./monitor";
export * from "./sysconfig";
export * from "./statistion";
export * from "./usermanage";

+ 18
- 1
SafeCampus.WEB/src/api/modules/monitor/live.ts Целия файл

@@ -36,5 +36,22 @@ const monitorLIVEApi = {
},
};
/**
* @Description: 监控管理按钮权限码
* @Author: huguodong
* @Date: 2024-02-20 09:51:15
*/
const monitorLiveButtonCode = {
/** 新增监控 */
add: "monitorLiveAdd",
/** 编辑监控 */
edit: "monitorLiveEdit",
/** 删除监控 */
delete: "monitorLiveDelete",
/** 批量删除监控 */
batchDelete: "monitorLiveBatchDelete",
/** 复制监控 */
copy: "monitorLiveCopy"
};

export { monitorLIVEApi };
export { monitorLIVEApi, monitorLiveButtonCode };

+ 52
- 0
SafeCampus.WEB/src/api/modules/usermanage/classManage.ts Целия файл

@@ -0,0 +1,52 @@
/**
* @description 单页管理接口
* @license Apache License Version 2.0
* @Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛
* @remarks
* SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款:
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改SimpleAdmin源码头部的版权声明。
* 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin
* 4.基于本软件的作品,只能使用 SimpleAdmin 作为后台服务,除外情况不可商用且不允许二次分发或开源。
* 5.请不得将本软件应用于危害国家安全、荣誉和利益的行为,不能以任何形式用于非法为目的的行为不要删除和修改作者声明。
* 6.任何基于本软件而产生的一切法律纠纷和责任,均于我司无关
* @see https://gitee.com/dotnetmoyu/SimpleAdmin
*/
import { moduleRequest } from "@/api/request";
import { ReqId, SysUserPersonnel } from "@/api/interface";
const http = moduleRequest("/business/dfieldApi/");

/**
* @Description: 单页管理
* @Author: SYY
* @Date: 2023-12-15 15:34:54
*/
const userManageClassManageApi = {
/** 查询底库列表 */
page(params: SysUserPersonnel.ClassPage) {
return http.get("queryAll", params);
},
/** 删除底库 */
delete(params: ReqId) {
return http.delete("deleteDfieldD", params);
},
/** 创建底库 */
add(params: SysUserPersonnel.ClassPage) {
return http.post("createDfieldA", params);
},
/** 更新底库 */
update(params: SysUserPersonnel.ClassPage) {
return http.put("updateDfieldU", params);
}
};

const userClassButtonCode = {
/** 新增人员 */
add: "userManageClassManageAdd",
/** 删除人员 */
edit: "userManageClassManageEdit",
/** 删除人员 */
delete: "userManageClassManageDelete"
};

export { userManageClassManageApi, userClassButtonCode };

+ 16
- 0
SafeCampus.WEB/src/api/modules/usermanage/index.ts Целия файл

@@ -0,0 +1,16 @@
/**
* @description
* @license Apache License Version 2.0
* @Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛
* @remarks
* SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款:
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改SimpleAdmin源码头部的版权声明。
* 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin
* 4.基于本软件的作品,只能使用 SimpleAdmin 作为后台服务,除外情况不可商用且不允许二次分发或开源。
* 5.请不得将本软件应用于危害国家安全、荣誉和利益的行为,不能以任何形式用于非法为目的的行为不要删除和修改作者声明。
* 6.任何基于本软件而产生的一切法律纠纷和责任,均于我司无关
* @see https://gitee.com/dotnetmoyu/SimpleAdmin
*/
export * from "./personnel";
export * from "./classManage";

+ 72
- 0
SafeCampus.WEB/src/api/modules/usermanage/personnel.ts Целия файл

@@ -0,0 +1,72 @@
/**
* @description 单页管理接口
* @license Apache License Version 2.0
* @Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛
* @remarks
* SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款:
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改SimpleAdmin源码头部的版权声明。
* 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin
* 4.基于本软件的作品,只能使用 SimpleAdmin 作为后台服务,除外情况不可商用且不允许二次分发或开源。
* 5.请不得将本软件应用于危害国家安全、荣誉和利益的行为,不能以任何形式用于非法为目的的行为不要删除和修改作者声明。
* 6.任何基于本软件而产生的一切法律纠纷和责任,均于我司无关
* @see https://gitee.com/dotnetmoyu/SimpleAdmin
*/
import { moduleRequest } from "@/api/request";
import { ReqId, ResPage, ReqPersonId, SysUserPersonnel } from "@/api/interface";
const http = moduleRequest("/business/personApi/");

/**
* @Description: 单页管理
* @Author: SYY
* @Date: 2023-12-15 15:34:54
*/
const userManagePersonnelApi = {
/** 获取单页分页 */
page(params: SysUserPersonnel.Page) {
return http.post("pageQuery", params);
},
/** 获取单页详情 */
detail(params: ReqId) {
return http.get("getPersionById", params);
},
/** 删除人员 */
delete(params: ReqId) {
return http.delete("deletePersonD", params);
},
/** 新增人员 */
add(params: SysUserPersonnel.SysUserPerInfo) {
return http.post("createPersonA", params);
},
/** 修改人员 */
update(params: SysUserPersonnel.SysUserPerInfo) {
return http.put("updatePersionU", params);
},
/** 添加人脸 */
addFace(params: SysUserPersonnel.SysUserAvatar) {
return http.post("addFaceA", params);
},
/** 删除人脸 */
deleteFace(params: SysUserPersonnel.SysUserFace) {
return http.post("deleteFaceD", params);
},
/**底库绑定 */
personBindDfie(params: SysUserPersonnel.ClassPage) {
return http.post("personBindDfie", params);
},
/**底库解绑*/
personUnBindDfie(params: SysUserPersonnel.ClassPage) {
return http.post("personUnBindDfie", params);
}
};

const userPerButtonCode = {
/** 新增人员 */
add: "userManagePersonnelAdd",
/** 删除人员 */
edit: "userManagePersonnelEdit",
/** 删除人员 */
delete: "userManagePersonnelDelete"
};

export { userManagePersonnelApi, userPerButtonCode };

+ 4
- 0
SafeCampus.WEB/src/api/modules/warn/zjrq.ts Целия файл

@@ -38,6 +38,10 @@ const warnZJRQApi = {
warnGroup(params: ReqId) {
return http.get("getInfo", params);
},
/** 获取告警分组 */
warnType(params: ReqId) {
return http.get("getAlarmType", params);
},
};

export { warnZJRQApi };

Двоични данни
SafeCampus.WEB/src/assets/images/home/carame.png Целия файл

Преди След
Ширина: 112  |  Височина: 112  |  Големина: 4.0 KiB

Двоични данни
SafeCampus.WEB/src/assets/images/home/handle.png Целия файл

Преди След
Ширина: 112  |  Височина: 112  |  Големина: 3.3 KiB

Двоични данни
SafeCampus.WEB/src/assets/images/home/participation.png Целия файл

Преди След
Ширина: 112  |  Височина: 112  |  Големина: 4.8 KiB

Двоични данни
SafeCampus.WEB/src/assets/images/home/warn.png Целия файл

Преди След
Ширина: 112  |  Височина: 112  |  Големина: 3.9 KiB

+ 246
- 0
SafeCampus.WEB/src/components/Selectors/ClassUserselector/index.vue Целия файл

@@ -0,0 +1,246 @@
<!--
* @Description: 用户选择器
* @Author: huguodong
* @Date: 2023-12-15 15:40:45
!-->
<template>
<form-container v-model="visible" :title="`${userName}选择`" form-size="90%" v-bind="$attrs">
<div class="-mt-15px min-h-350px">
<el-row :gutter="12" justify="space-between">
<el-col :span="4">
<el-tabs v-model="activeName" type="card" stretch class="min-h-350px">
<el-tab-pane :label="`${orgName}`" class="ml-5px mr-5px" name="org">
<el-scrollbar max-height="650px">
<TreeFilter
label="name"
class="filterWidth"
:title="`${orgName}列表`"
:show-all="!biz"
:default-expand-all="false"
:request-api="orgTreeApi"
@change="changeOrgTreeFilter"
/>
</el-scrollbar>
</el-tab-pane>
<!-- <el-tab-pane v-if="positionTreeApi" :label="`${positionName}`" class="ml-5px mr-5px" name="pos">
<el-scrollbar max-height="650px">
<TreeFilter
label="name"
class="filterWidth"
:title="`${positionName}列表`"
:show-all="!biz"
:default-expand-all="false"
:request-api="positionTreeApi"
@change="changePositionTreeFilter"
/>
</el-scrollbar>
</el-tab-pane>
<el-tab-pane v-if="roleTreeApi" label="角色" class="ml-5px mr-5px" name="role">
<el-scrollbar max-height="650px">
<TreeFilter
label="name"
class="filterWidth"
title="角色列表"
:show-all="!biz"
:default-expand-all="false"
:request-api="roleTreeApi"
@change="changeRoleTreeFilter"
/>
</el-scrollbar>
</el-tab-pane> -->
</el-tabs>
</el-col>
<el-col :span="10">
<ProTable ref="userTable" :columns="columns" :tool-button="false" :init-param="initParam" :request-api="userSelectorApi">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<el-button type="primary" @click="addRecords(userTable!.tableData)">添加当前</el-button>
<el-button type="primary" plain :disabled="!scope.isSelected" @click="addRecords(scope.selectedList)">添加选中</el-button>
</template>
<!-- 操作 -->
<template #operation="scope">
<el-button type="primary" link :icon="Plus" plain @click="addRecords([scope.row])">添加</el-button>
</template>
</ProTable>
</el-col>
<el-col :span="10">
<ProTable ref="chooseTable" :columns="columns" :tool-button="true" :data="chooseDataTmp" @search="searchRecords" @reset="resetRecords">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<el-button type="danger" @click="delRecords(chooseTable!.tableData)">删除当前</el-button>
<el-button type="danger" plain :disabled="!scope.isSelected" @click="delRecords(scope.selectedList)">删除选中</el-button>
</template>
<template #toolButton>
<span>已选择:{{ chooseData.length }}人</span>
<span v-if="maxCount">,最多选择:{{ maxCount }}人</span>
</template>
<!-- 操作 -->
<template #operation="scope">
<el-button type="danger" link :icon="Delete" plain @click="delRecords([scope.row])">删除</el-button>
</template>
</ProTable>
</el-col>
</el-row>
</div>
<template #footer>
<div class="mt-20px">
<el-button @click="onClose"> 取消 </el-button>
<el-button type="primary" @click="handleOk"> 确定 </el-button>
</div>
</template>
</form-container>
</template>

<script setup lang="ts" name="UserSelector">
import { SysUser, SysPosition, SysRole } from "@/api";
import { UserSelectProps, UserSelectTableInitParams } from "./interface";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { ElMessage } from "element-plus";
import { Plus, Delete } from "@element-plus/icons-vue";
const activeName = ref("org");
const emit = defineEmits({ successful: null }); // 自定义事件
// const handleClick = (tab: TabsPaneContext, event: Event) => {
// console.log(tab, event);
// };
const visible = ref(false); //是否显示

// 定义组件props
const props = withDefaults(defineProps<UserSelectProps>(), {
multiple: false,
biz: false
});

// 根据是否业务显示不同名称
const userName = props.biz ? "人员" : "用户";
const positionName = props.biz ? "职位" : "岗位";
const orgName = props.biz ? "机构" : "班级";

// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
const initParam = reactive<UserSelectTableInitParams>({});
// 获取 userTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const userTable = ref<ProTableInstance>();
// 获取 chooseTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const chooseTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<SysUser.SysUserInfo>[] = [
{ type: "selection", fixed: "left", width: 50 },
{ prop: "operation", label: "操作", width: 80, fixed: "left" },
{ prop: "account", label: "账号", search: { el: "input", span: 2 } },
{ prop: "name", label: "姓名" }
];

/** 显示选择器 */
function showSelector(data: SysUser.SysUserInfo[] = []) {
visible.value = true;
chooseDataTmp.value = data;
chooseData.value = data;
}

/** 关闭选择器 */
function onClose() {
visible.value = false;
chooseDataTmp.value = [];
chooseData.value = [];
}

/** 提交数据 */
function handleOk() {
visible.value = false;
emit("successful", chooseData.value);
}

/** 部门切换 */
function changeOrgTreeFilter(val: number | string) {
userTable.value!.pageable.pageNum = 1;
if (val != "") {
// 如果传入的val不为空
initParam.orgId = val;
} else {
initParam.orgId = null;
}
}

/** 职位切换 */
function changePositionTreeFilter(val: number | string, data: SysPosition.SysPositionTree) {
userTable.value!.pageable.pageNum = 1;
if (data.isPosition) {
// 如果是职位
initParam.positionId = val;
} else {
initParam.positionId = null;
}
}
/** 角色切换 */
function changeRoleTreeFilter(val: number | string, data: SysRole.SysRoleTree) {
userTable.value!.pageable.pageNum = 1;
if (data.isRole) {
// 如果是角色
initParam.roleId = val;
} else {
// 置空
initParam.roleId = null;
}
}

const chooseData = ref<SysUser.SysUserInfo[]>([]); //选择的数据
const chooseDataTmp = ref<SysUser.SysUserInfo[]>([]); //临时选择的数据

/** 添加记录 */
function addRecords(records: any[]) {
//如果不是多选,判断是否已经添加了
if (!props.multiple) {
if (chooseData.value.length > 0 || records.length > 1) {
ElMessage.warning("只可选择一条");
return;
}
chooseData.value = records;
chooseDataTmp.value = chooseData.value;
} else {
//如果是多选,先判断已添加列表是否有重复,有则过滤掉,没有则直接添加
records = records.filter(item => !chooseData.value.find(it => it.id == item.id));
if (props.maxCount && props.maxCount < records.length + chooseData.value.length) {
ElMessage.warning("最多选择" + props.maxCount + "条");
return;
}
chooseData.value = chooseData.value.concat(records); //添加到已选中列表
chooseDataTmp.value = chooseData.value;
}
chooseTable.value?.refresh(); //刷新表格
}

/** 删除记录 */
function delRecords(records: any[]) {
chooseData.value = chooseData.value.filter(item => !records.includes(item)); //过滤掉已选中的
chooseDataTmp.value = chooseData.value;
chooseTable.value?.refresh(); //刷新表格
}

/** 搜索记录 */
function searchRecords() {
if (chooseTable.value?.searchParam?.account) {
//搜索account符合的记录
chooseDataTmp.value = chooseDataTmp.value.filter(item => item.account.includes(chooseTable.value?.searchParam.account)); //过滤掉已选中的
chooseTable.value?.refresh(); //刷新表格
}
}

/** 重置记录 */
function resetRecords() {
chooseDataTmp.value = chooseData.value;
chooseTable.value?.refresh(); //刷新表格
}
// 暴露方法
defineExpose({ showSelector });
</script>

<style lang="scss" scoped>
.filterWidth {
width: 100%;
}
:deep(.el-tabs--border-card > .el-tabs__content) {
padding: 5px;
}
:deep(.table-main) {
height: 90%;
}
</style>

+ 48
- 0
SafeCampus.WEB/src/components/Selectors/ClassUserselector/interface.ts Целия файл

@@ -0,0 +1,48 @@
/**
* @description 用户选择器接口
* @license Apache License Version 2.0
* @Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛
* @remarks
* SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款:
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改SimpleAdmin源码头部的版权声明。
* 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin
* 4.基于本软件的作品,只能使用 SimpleAdmin 作为后台服务,除外情况不可商用且不允许二次分发或开源。
* 5.请不得将本软件应用于危害国家安全、荣誉和利益的行为,不能以任何形式用于非法为目的的行为不要删除和修改作者声明。
* 6.任何基于本软件而产生的一切法律纠纷和责任,均于我司无关
* @see https://gitee.com/dotnetmoyu/SimpleAdmin
*/
import UserSelector from "./index.vue";

/** 用户选择器属性 */
export interface UserSelectProps {
/** 组织树api */
orgTreeApi: (data?: any) => Promise<any>;
/** 职位选择api */
positionTreeApi?: (data?: any) => Promise<any>;
/** 角色选择api */
roleTreeApi?: (data?: any) => Promise<any>;
/** 用户选择api */
userSelectorApi: (data?: any) => Promise<any>;
/** 是否多选 */
multiple?: boolean;
/** 最大用户数 */
maxCount?: number;
/** 是否是业务 */
biz?: boolean;
}

/** 用户选择器表格初始化参数 */
export interface UserSelectTableInitParams {
/** 组织ID */
orgId?: number | string | null;
/** 职位ID */
positionId?: number | string | null;
/** 角色ID */
roleId?: number | string | null;
}

/**
* @description 用户选择器实例类型
*/
export type UserSelectorInstance = Omit<InstanceType<typeof UserSelector>, keyof ComponentPublicInstance | keyof UserSelectProps>;

+ 6
- 3
SafeCampus.WEB/src/components/TreeFilter/index.vue Целия файл

@@ -1,9 +1,11 @@
<template>
<div class="card filter">
<div class="card filter" :style="{ width: width }">
<h4 v-if="title" class="title sle">
{{ title }}
</h4>
<slot name="header"></slot>
<el-input v-model="filterText" placeholder="输入关键字进行过滤" clearable />

<el-scrollbar :style="{ height: title ? `calc(100% - 95px)` : `calc(100% - 56px)` }">
<el-tree
ref="treeRef"
@@ -24,8 +26,8 @@
@check="handleCheckChange"
>
<template #default="scope">
<span class="el-tree-node__label">
<slot :row="scope">
<span class="el-tree-node__label" style="width: 100%">
<slot :row="scope" name="label">
{{ scope.node.label }}
</slot>
</span>
@@ -53,6 +55,7 @@ interface TreeFilterProps {
checkStrictly?: boolean; // 是否开启子节点和父节点不关联 ==> 非必传,默认为 false
topName?: string; // 顶级分类名称 ==> 非必传,默认为 “全部”
showAll?: boolean; // 是否显示全部选项 ==> 非必传,默认为 true
width: string;
}
const props = withDefaults(defineProps<TreeFilterProps>(), {
id: "id",


+ 2
- 2
SafeCampus.WEB/src/layouts/components/Footer/index.vue Целия файл

@@ -1,7 +1,7 @@
<template>
<div class="footer flx-center">
<!-- <a :href="props.sysCopyrightUrl" target="_blank"> {{ props.sysCopyright }} </a>
<a v-for="link in props.footerLinks" :key="link.name" :href="link.url" target="_blank" class="mx-1">&nbsp;|&nbsp;{{ link.name }}</a> -->
<a :href="props.sysCopyrightUrl" target="_blank"> {{ props.sysCopyright }} </a>
<a v-for="link in props.footerLinks" :key="link.name" :href="link.url" target="_blank" class="mx-1">&nbsp;|&nbsp;{{ link.name }}</a>
</div>
</template>
<script setup lang="ts">


+ 13
- 1
SafeCampus.WEB/src/styles/element.scss Целия файл

@@ -249,13 +249,25 @@

/* el-dialog */
.el-dialog {
padding: 0 !important;
.el-dialog__header {
padding: 15px 20px;
padding: 16px;
margin: 0;
border-bottom: 1px solid var(--el-border-color-lighter);
.el-dialog__title {
font-size: 17px;
}
.el-dialog__headerbtn {
top: 6px;
}
}
.el-dialog__body {
padding: 20px;
}
.el-dialog__footer {
box-sizing: border-box;
padding: 15px 16px;
border-top: 1px solid var(--el-border-color-lighter);
}
}



+ 54
- 6
SafeCampus.WEB/src/views/home/index.scss Целия файл

@@ -1,12 +1,60 @@
.home {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
.home-bg {
width: 70%;
max-width: 1200px;
margin-bottom: 20px;
margin-bottom: 15px;
height: 390px;
// height: 424px;
.home-bg-title {
display: flex;
height: 50px;
align-items: center;
div:first-child {
height: 15px;
width: 4px;
background: #3a84ff;
margin-right: 10px;
}
div:nth-child(2) {
font-size: 20px;
font-weight: 600;
margin-left: 10px;
}
}
.home-bg-content {
height: calc(100% - 50px);
.home-bg-content-item {
margin-left: 15px;
margin-top: 60px;
display: flex;
align-items: center;
.home-bg-content-item-icon {
margin-right: 15px;
width: 70px;
height: 70px;
img {
width: 100%;
height: 100%;
}
}
.home-bg-content-item-content {
.home-bg-content-item-title {

}
.home-bg-content-item-value {
margin-top: 10px;
font-size: 20px;
font-weight: 600;
// color: #3a84ff;
}
}
}
}
.handleBox ::v-deep(.card) {
padding:0;
}
}
.topCard {
height: 355px;
}
}

+ 380
- 3
SafeCampus.WEB/src/views/home/index.vue Целия файл

@@ -1,10 +1,387 @@
<template>
<div class="home card">
<img class="home-bg" src="@/assets/images/welcome.png" alt="welcome" />
<div class="home">
<el-row :gutter="20">
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
<div class="home-bg card">
<div class="home-bg-title">
<div></div>
<div>基础数据</div>
</div>
<div class="home-bg-content">
<el-row :gutter="20">
<el-col :span="12"
><div class="home-bg-content-item">
<div class="home-bg-content-item-icon">
<img src="@/assets/images/home/carame.png" alt="" />
</div>
<div class="home-bg-content-item-content">
<div class="home-bg-content-item-title">摄像头数量</div>
<div class="home-bg-content-item-value">3</div>
</div>
</div></el-col
>
<el-col :span="12"
><div class="home-bg-content-item">
<div class="home-bg-content-item-icon">
<img src="@/assets/images/home/warn.png" alt="" />
</div>
<div class="home-bg-content-item-content">
<div class="home-bg-content-item-title">告警信息总量</div>
<div class="home-bg-content-item-value">197218</div>
</div>
</div></el-col
>
<el-col :span="12"
><div class="home-bg-content-item">
<div class="home-bg-content-item-icon">
<img src="@/assets/images/home/handle.png" alt="" />
</div>
<div class="home-bg-content-item-content">
<div class="home-bg-content-item-title">处理意见提交</div>
<div class="home-bg-content-item-value">40</div>
</div>
</div></el-col
>
<el-col :span="12"
><div class="home-bg-content-item">
<div class="home-bg-content-item-icon">
<img src="@/assets/images/home/participation.png" alt="" />
</div>
<div class="home-bg-content-item-content">
<div class="home-bg-content-item-title">参与安防教师</div>
<div class="home-bg-content-item-value">22</div>
</div>
</div></el-col
>
</el-row>
</div>
</div>
</el-col>
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
<div class="home-bg card">
<div class="home-bg-title">
<div></div>
<div>今日告警情况</div>
</div>
<div class="home-bg-content">
<div ref="chart1" style="width: 100%; height: 100%"></div>
</div>
</div>
</el-col>
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
<div class="home-bg card">
<div class="home-bg-title">
<div></div>
<div>今日处理情况</div>
</div>
<div class="home-bg-content">
<div ref="chart2" style="width: 100%; height: 100%"></div>
</div>
</div>
</el-col>
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
<div class="home-bg card">
<div class="home-bg-title">
<div></div>
<div>高危预警信息统计</div>
</div>
<div class="home-bg-content handleBox">
<ProTable ref="proTable" title="视频列表" :toolButton="false" :pagination="false" :columns="columns" :data="tableData"> </ProTable>
</div>
</div>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="16">
<div class="home-bg card">
<div class="home-bg-title">
<div></div>
<div>统计分析</div>
</div>
<div class="home-bg-content handleBox">
<div ref="chart3" style="width: 100%; height: 100%"></div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>

<script setup lang="ts" name="home"></script>
<script setup lang="ts" name="home">
import { ref, watch, provide, onMounted, unref, computed, reactive } from "vue";
import { ElMessage } from "element-plus";
import { monitorLIVEApi } from "@/api";
import { ZJRQ } from "@/api/interface";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { useDictStore } from "@/stores/modules";
import { statistionApi } from "@/api";
import * as echarts from "echarts";

const value = ref(true);
const chart1 = ref(null);
const chart2 = ref(null);
const chart3 = ref(null);

onMounted(() => {
getDataChart();
getWeekData();
});
function getDataChart() {
setTimeout(async () => {
await statistionApi.warnstatistion({}).then(res => {
let { code, data } = res;

if (code == 200) {
let chartData1 = data.alarm.map(item => {
return {
value: item.count,
name: item.name
};
});
let chartData2 = data.hand.map(item => {
return {
value: item.count,
name: item.name
};
});
// let chartData = [
// {
// name: "1",
// value: 100
// },
// {
// name: "2",
// value: 200
// }
// ];
getCharts1(chartData1);
getCharts2(chartData2);
}
});
});
}
function getCharts1(data) {
const chart = echarts.init(chart1.value);
const option = {
tooltip: {
trigger: "item"
},
// legend: {
// top: "5%",
// left: "center"
// },
legend: {
orient: "vertical",
left: "right",
top: "middle"
},
series: [
{
name: "今日告警情况",
type: "pie",
radius: ["60%", "90%"],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: "#fff",
borderWidth: 2
},
label: {
show: false,
position: "center"
},
emphasis: {
label: {
show: true,
fontSize: 25,
fontWeight: "bold"
}
},
labelLine: {
show: false
},
data
}
]
};

chart.setOption(option);
window.addEventListener("resize", function () {
chart.resize();
});
}
function getCharts2(data) {
const chartstation = echarts.init(chart2.value);
const option = {
tooltip: {
trigger: "item"
},
// legend: {
// top: "5%",
// left: "center"
// },
legend: {
orient: "vertical",
left: "right",
top: "middle"
},
series: [
{
name: "今日处理情况",
type: "pie",
radius: ["60%", "90%"],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: "#fff",
borderWidth: 2
},
label: {
show: false,
position: "center"
},
emphasis: {
label: {
show: true,
fontSize: 25,
fontWeight: "bold"
}
},
labelLine: {
show: false
},
data
}
]
};

chartstation.setOption(option);
window.addEventListener("resize", function () {
chartstation.resize();
});
}

function getWeekData() {
setTimeout(async () => {
await statistionApi.weekstatistion({}).then(res => {
let { code, data } = res;

if (code == 200) {
// let chartData = data;
let time = data.dataX;
let chartData = data.dataY.map(item => {
return {
data: item.data,
name: item.name,
type: "bar",
barWidth: "12px", // 设置柱子粗细
itemStyle: {
normal: {
barBorderRadius: [30, 30, 0, 0]
}
}
};
});
getCharts3(time, chartData);
}
});
});
}
function getCharts3(time, data) {
const chartstation3 = echarts.init(chart3.value);
const option = {
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow"
}
},
legend: {},
grid: {
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true
},
yAxis: {
type: "value",
boundaryGap: [0, 0.01]
},
xAxis: {
type: "category",
data: time
},
series: data
};

chartstation3.setOption(option);
window.addEventListener("resize", function () {
chartstation3.resize();
});
}

// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
const dictStore = useDictStore();

// 表格配置项
const columns: ColumnProps<ZJRQ.WarnInfo>[] = [
{
prop: "type",
label: "数据类型"
// render: () => {
// return "楼道";
// }
},
{
prop: "warntotal",
label: "告警总量"
// render: () => {
// return "楼道";
// }
},
{
prop: "lowrisk",
label: "低危信息 "
},
{
prop: "highrisk",
label: "高危信息"
}
];
const tableData = ref([
{
type: "今日",
warntotal: "828",
lowrisk: "675",
highrisk: "153"
},
{
type: "本周",
warntotal: "828",
lowrisk: "675",
highrisk: "153"
},
{
type: "本月",
warntotal: "19813",
lowrisk: "17671",
highrisk: "2129"
},
{
type: "上月",
warntotal: "21789",
lowrisk: "0",
highrisk: "0"
},

{
type: "环比",
warntotal: "-9.07%",
lowrisk: "0%",
highrisk: "0%"
}
]);
const tableLoading = ref(false);
</script>

<style scoped lang="scss">
@import "./index.scss";


+ 0
- 1609
SafeCampus.WEB/src/views/monitor/live/ali.css
Файловите разлики са ограничени, защото са твърде много
Целия файл


+ 118
- 0
SafeCampus.WEB/src/views/monitor/live/components/form.vue Целия файл

@@ -0,0 +1,118 @@
<!--
* @Description: 表单
* @Author: huguodong
* @Date: 2023-12-15 15:45:28
!-->
<template>
<div>
<form-container v-model="visible" :title="`${orgProps.opt}监控`" form-size="600px">
<el-form
ref="liveFormRef"
:rules="rules"
:disabled="orgProps.disabled"
:model="orgProps.record"
:hide-required-asterisk="orgProps.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="摄像头名称" prop="name">
<s-input v-model="orgProps.record.name"></s-input>
</s-form-item>
<s-form-item label="所属学校" prop="parentId">
<org-selector v-model:org-value="orgProps.record.parentId" :org-tree-api="bizOrgApi.tree" :show-all="false" />
</s-form-item>
<s-form-item label="设备IP" prop="codeip">
<s-input v-model="orgProps.record.codeip" clearable></s-input>
</s-form-item>
<s-form-item label="分辨率" prop="status">
<s-input v-model="orgProps.record.status" clearable></s-input>
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!orgProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</div>
</template>

<script setup lang="ts">
import { SysOrg, SysUser, bizOrgApi, bizPositionApi, sysRoleApi, bizUserApi } from "@/api";
import { FormOptEnum, SysDictEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";

const visible = ref(false); //是否显示表单
const dictStore = useDictStore(); //字典仓库

// 通用状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);

// 表单参数
const orgProps = reactive<FormProps.Base<SysOrg.SysOrgInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});

// 表单验证规则
const rules = reactive({
name: [required("请输入摄像头名称")],
parentId: [required("请选择所属学校")],
codeip: [required("请选择设备IP")],
status: [required("请输入分辨率")]
});

/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<SysOrg.SysOrgInfo>) {
Object.assign(orgProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
orgProps.record.sortCode = 99;
// orgProps.record.status = statusOptions[0].value;
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
bizOrgApi.detail({ id: props.record.id }).then(res => {
orgProps.record = res.data;
});
}
}

// 提交数据(新增/编辑)
const liveFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
liveFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
console.log(orgProps);
return;
//提交表单
await bizOrgApi
.submitForm(orgProps.record, orgProps.record.id != undefined)
.then(() => {
orgProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}

/** 关闭表单*/
function onClose() {
visible.value = false;
}

// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>

<style lang="scss" scoped></style>

+ 106
- 0
SafeCampus.WEB/src/views/monitor/live/components/moveForm.vue Целия файл

@@ -0,0 +1,106 @@
<!--
* @Description: 表单
* @Author: huguodong
* @Date: 2023-12-15 15:45:28
!-->
<template>
<div>
<form-container v-model="visible" title="选择分组" form-size="600px">
<el-form
ref="liveFormRef"
:rules="rules"
:disabled="orgProps.disabled"
:model="orgProps.record"
:hide-required-asterisk="orgProps.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="所属学校" prop="parentId">
<org-selector v-model:org-value="orgProps.record.parentId" :org-tree-api="bizOrgApi.tree" :show-all="false" />
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!orgProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</div>
</template>

<script setup lang="ts">
import { SysOrg, SysUser, bizOrgApi, bizPositionApi, sysRoleApi, bizUserApi } from "@/api";
import { FormOptEnum, SysDictEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";

const visible = ref(false); //是否显示表单
const dictStore = useDictStore(); //字典仓库

// 通用状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);

// 表单参数
const orgProps = reactive<FormProps.Base<SysOrg.SysOrgInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});

// 表单验证规则
const rules = reactive({
parentId: [required("请选择所属学校")]
});

/**
* 打开表单
* @param props 表单参数
*/
function omMove(props: FormProps.Base<SysOrg.SysOrgInfo>) {
Object.assign(orgProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
orgProps.record.sortCode = 99;
// orgProps.record.status = statusOptions[0].value;
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
bizOrgApi.detail({ id: props.record.id }).then(res => {
orgProps.record = res.data;
});
}
}

// 提交数据(新增/编辑)
const liveFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
liveFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
console.log(orgProps);
return;
//提交表单
await bizOrgApi
.submitForm(orgProps.record, orgProps.record.id != undefined)
.then(() => {
orgProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}

/** 关闭表单*/
function onClose() {
visible.value = false;
}

// 暴露给父组件的方法
defineExpose({
omMove
});
</script>

<style lang="scss" scoped></style>

+ 127
- 0
SafeCampus.WEB/src/views/monitor/live/components/userForm.vue Целия файл

@@ -0,0 +1,127 @@
<!--
* @Description: 表单
* @Author: huguodong
* @Date: 2023-12-15 15:45:28
!-->
<template>
<div>
<form-container v-model="visible" title="人员选择" form-size="600px">
<el-form
ref="userFormRef"
:rules="rules"
:disabled="liveUserProps.disabled"
:model="liveUserProps.record"
:hide-required-asterisk="liveUserProps.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="指定分组推送人" prop="directorId">
<el-button link type="primary" @click="showSelector">选择</el-button>
<el-tag v-if="liveUserProps.record.directorId" class="ml-3px" type="warning" closable @close="removeDirector">{{
liveUserProps.record.directorInfo?.name
}}</el-tag>
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!liveUserProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
<user-selector ref="userSelectorRef" :org-tree-api="sysOrgApi.tree" :user-selector-api="sysUserApi.selector" @successful="handleChooseUser" />
</div>
</template>

<script setup lang="ts">
import { SysOrg, SysUser, sysOrgApi, sysPositionApi, sysRoleApi, sysUserApi } from "@/api";
import { FormOptEnum, SysDictEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
import { UserSelectorInstance } from "@/components/Selectors/UserSelector/interface";

const visible = ref(false); //是否显示表单
const dictStore = useDictStore(); //字典仓库

// 表单参数
const liveUserProps = reactive<FormProps.Base<SysOrg.SysOrgInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});

// 表单验证规则
const rules = reactive({
directorId: [required("请选择指定分组推送人")]
});

/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<SysOrg.SysOrgInfo>) {
Object.assign(liveUserProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
}
visible.value = true; //显示表单
// if (props.record.id) {
// //如果传了id,就去请求api获取record
// sysOrgApi.detail({ id: props.record.id }).then(res => {
// liveUserProps.record = res.data;
// });
// }
}

// 提交数据(新增/编辑)
const userFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
userFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
return;
//提交表单
await sysOrgApi
.submitForm(liveUserProps.record, liveUserProps.record.id != undefined)
.then(() => {
liveUserProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}

/** 关闭表单*/
function onClose() {
visible.value = false;
}

const userSelectorRef = ref<UserSelectorInstance>(); //用户选择器引用
/** 显示用户选择器 */
function showSelector() {
//将liveUserProps.record.directorInfo转为 SysUser.SysUserInfo[]类型
const directorInfo = liveUserProps.record.directorInfo ? [liveUserProps.record.directorInfo] : [];
userSelectorRef.value?.showSelector(directorInfo);
}

/** 选择用户 */
function handleChooseUser(data: SysUser.SysUserInfo[]) {
// 选择用户后,将用户id赋值给liveUserProps.record.directorId
if (data.length > 0) {
liveUserProps.record.directorId = data[0].id;
liveUserProps.record.directorInfo = data[0];
}
}

/** 移除主管 */
function removeDirector() {
liveUserProps.record.directorId = null;
liveUserProps.record.directorInfo = null;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>

<style lang="scss" scoped></style>

+ 78
- 0
SafeCampus.WEB/src/views/monitor/live/index.scss Целия файл

@@ -0,0 +1,78 @@
ul,li {
list-style: none;
padding: 0;
margin: 0;
}

.treeBox {
box-sizing: border-box;
width: 280px;
height: 100%;
padding: 14px;
margin-right: 10px;
flex-shrink: 1;
.title {
margin: 0 0 15px;
font-size: 18px;
font-weight: bold;
color: var(--el-color-info-dark-2);
letter-spacing: 0.5px;
}
.btn {

}
.treeContent {
padding: 10px 0;
// height: calc(100% - 100px);
// overflow: auto;
.el-tree-node__content {
height: 33px;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
.node-label {
width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 16px;
}
}
:deep(.el-tree-node__content) {
height: 50px;
}
}
.el-input {
margin: 0 0 15px;
}
.el-scrollbar {
:deep(.el-tree) {
height: 80%;
overflow: auto;
.el-tree-node__content {
height: 33px;
}
}
:deep(.el-tree--highlight-current) {
.el-tree-node.is-current > .el-tree-node__content {
background-color: var(--el-color-primary);
.el-tree-node__label,
.el-tree-node__expand-icon {
color: white;
}
.is-leaf {
color: transparent;
}
}
}
}
}
.table-box {
width: calc(100% - 280px);
}

+ 256
- 80
SafeCampus.WEB/src/views/monitor/live/index.vue Целия файл

@@ -4,66 +4,236 @@
* @Date: 2023-12-15 15:44:05
!-->
<template>
<div class="table-box">
<ProTable ref="proTable" title="视频列表" :columns="columns" :request-api="monitorLIVEApi.page">
<!-- 表格 header 按钮 -->
<div class="main-box">
<div class="card treeBox">
<p class="title">摄像头分组管理</p>
<div class="btn">
<el-button @click="append('add', {})" type="primary">添加分组</el-button>
</div>
<!-- <div class="treeContent">
<ul class="treeList" v-for="(item, i) in treeData" :key="i">
<li class="treeItem">
<div class="treeLabel">{{ item.label }}</div>
<div class="treeBtn">
<el-icon><Edit /></el-icon>
<el-icon><Delete /></el-icon>
</div>
</li>
</ul>
</div> -->
<div class="treeContent">
<el-tree
style="max-width: 600px"
:data="treeData"
node-key="id"
default-expand-all
:expand-on-click-node="false"
:check-on-click-node="true"
:highlight-current="true"
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<span class="custom-tree-node">
<span class="node-label" :title="node.label">{{ node.label }}</span>
<span v-if="data.id && data.id != '-1'">
<el-icon size="16" @click.stop="append('edit', data)"><Edit /></el-icon>
<el-icon size="16" @click.stop="pushPerson('push', data)" style="margin-left: 8px"><UserFilled /></el-icon>
<el-icon size="16" @click.stop="remove(node, data)" style="margin-left: 8px"><Delete /></el-icon>
</span>
</span>
</template>
</el-tree>
</div>
</div>
<div class="table-box">
<ProTable ref="proTable" title="视频列表" :columns="columns" :request-api="monitorLIVEApi.page">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button v-auth="monitorLiveButtonCode.add" suffix="摄像头" @click="onOpen(FormOptEnum.ADD)" />
<s-button
type="danger"
plain
suffix="摄像头"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选数据')"
/>
<el-button plain @click="omMove(FormOptEnum.ADD)" type="success">移动至分组</el-button>
</template>
<!-- 表格 菜单类型 按钮 -->
<!-- <template #menuType="scope">
<el-space wrap>
<el-tag v-if="scope.row.menuType === MenuTypeDictEnum.MENU" type="success">{{
dictStore.dictTranslation(SysDictEnum.MENU_TYPE, MenuTypeDictEnum.MENU)
}}</el-tag>
<el-tag v-else-if="scope.row.menuType === MenuTypeDictEnum.LINK" type="warning">{{
dictStore.dictTranslation(SysDictEnum.MENU_TYPE, MenuTypeDictEnum.LINK)
}}</el-tag>
<el-tag v-else type="info">{{ dictStore.dictTranslation(SysDictEnum.MENU_TYPE, scope.row.menuType) }}</el-tag>
<el-tag v-if="scope.row.isHome === true" type="danger">首页</el-tag>
</el-space>
</template> -->
<!-- 操作 -->
<template #operation="scope">
<s-button link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)">编辑</s-button>
<s-button link :opt="FormOptEnum.VIEW" @click="onDetail(scope.row)"> 查看 </s-button>
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `确定删除该摄像头吗?`)" />
<s-button link :opt="FormOptEnum.VIEW" @click="pushPerson(scope.row)"> 推送人 </s-button>
</template>
</ProTable>
<!-- 添加分组弹框 -->
<el-dialog v-model="groupVisible" :title="groupTitle" width="600px" :before-close="closeGroup">
<el-form :model="groupForm" :rules="groupRules" ref="groupFormRef" label-width="80px">
<el-form-item label="分组名称" prop="name">
<el-input v-model="groupForm.name" placeholder="请输入分组名称"></el-input>
</el-form-item>
</el-form>

<!-- 表格 菜单类型 按钮 -->
<template #menuType="scope">
<el-space wrap>
<el-tag v-if="scope.row.menuType === MenuTypeDictEnum.MENU" type="success">{{
dictStore.dictTranslation(SysDictEnum.MENU_TYPE, MenuTypeDictEnum.MENU)
}}</el-tag>
<el-tag v-else-if="scope.row.menuType === MenuTypeDictEnum.LINK" type="warning">{{
dictStore.dictTranslation(SysDictEnum.MENU_TYPE, MenuTypeDictEnum.LINK)
}}</el-tag>
<el-tag v-else type="info">{{ dictStore.dictTranslation(SysDictEnum.MENU_TYPE, scope.row.menuType) }}</el-tag>
<el-tag v-if="scope.row.isHome === true" type="danger">首页</el-tag>
</el-space>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button link :opt="FormOptEnum.VIEW" @click="onDetail(scope.row)"> 查看 </s-button>
<!-- <s-button link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)">处理</s-button>
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `确定删除该预警吗?`)" /> -->
</template>
</ProTable>
<el-dialog v-model="visible" :title="detailData.title" width="830px" :before-close="handleClose">
<div>
<div class="dialogHeader">
<div></div>
<div class="dialogBtn" @click="refreshUrl">
<el-icon color="#409efc" :size="20">
<Refresh />
</el-icon>
<div>刷新视频</div>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="onSubmit">提交</el-button>
<el-button @click="closeGroup">取消</el-button>
</div>
</template>
</el-dialog>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
<!-- 人员选择 -->
<userForm ref="userFormRef" />
<!-- 移动至分组 -->
<moveForm ref="moveFormRef" />
<!-- 视频详情 -->
<el-dialog v-model="visible" :title="detailData.title" width="830px" :before-close="closeGroup">
<div>
<div class="dialogHeader">
<div></div>
<div class="dialogBtn" @click="refreshUrl">
<el-icon color="#409efc" :size="20">
<Refresh />
</el-icon>
<div>刷新视频</div>
</div>
</div>
<div v-if="visible || showVideo" class="prism-player" id="player-con"></div>
<!-- <VideoPlay :videoUrl="detailData.videoUrl" :videoType="detailData.videoType" /> -->
<!-- <video style="width: 100%; height: 500px" id="video" controls muted="false"></video> -->
<!-- <iframe src="/static/artcvideo.html" width="100%" height="600px" ref="iframeDom"></iframe> -->
</div>
<div v-if="visible || showVideo" class="prism-player" id="player-con"></div>
<!-- <VideoPlay :videoUrl="detailData.videoUrl" :videoType="detailData.videoType" /> -->
<!-- <video style="width: 100%; height: 500px" id="video" controls muted="false"></video> -->
<!-- <iframe src="/static/artcvideo.html" width="100%" height="600px" ref="iframeDom"></iframe> -->
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
</div>
</template>
</el-dialog>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup lang="tsx" name="sysSpa">
import VideoPlay from "@/components/VideoPlay/videoplay.vue";
import { ElMessage } from "element-plus";
import { monitorLIVEApi } from "@/api";
import { ElMessage,ElMessageBox } from "element-plus";
import type Node from 'element-plus/es/components/tree/src/model/node'
import { monitorLIVEApi, monitorLiveButtonCode } from "@/api";
import { ZJRQ } from "@/api/interface";
import { useHandleData } from "@/hooks/useHandleData";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { useDictStore } from "@/stores/modules";
import Form from "./components/form.vue";
import userForm from "./components/userForm.vue";
import moveForm from "./components/moveForm.vue";
import { FormOptEnum, SysDictEnum, MenuTypeDictEnum } from "@/enums";
import './aliyun-rts-sdk.js'
// import './aliyun-rts-sdk.js'
import './ali.js'
const groupVisible = ref(false); //是否显示表单
const groupTitle = ref('新增分组');
const groupForm = reactive({
name: ''
});
const groupRules = ref({
name: [{ required: true, message: '请输入分组名称', trigger: 'blur' }]
});
const groupFormRef = ref(null);
const closeGroup = () => {
groupVisible.value = false;
groupFormRef.value.resetFields();
groupForm.name = ''
}
const onSubmit = () => {
groupFormRef.value.validate((valid:any) => {
if (valid) {
closeGroup()
ElMessage.success('提交成功');
} else {
return false;
}
});
};
// 摄像头分组
interface Tree {
id: any
label: string
children?: Tree[]
}
let id = 1000
const handleNodeClick = (data: Tree) => {
console.log(data)
}

const treeData = ref<Tree[]>([
{
id: '',
label: '全部',
},
{
id: '-1',
label: '无分组',
},
{
id: 1,
label: '走廊',
},
{
id: 2,
label: '大厅',
},
{
id: 3,
label: '厨房',
},
])
// 新增分组
const append = (type:string,data: Tree) => {
console.log(type,data)
groupVisible.value = true;
if(type == 'edit') {
groupForm.name = treeData.value.find(item => item.id ==data.id).label;
}

// const newChild = { id: id++, label: 'testtest', children: [] }
// if (!data.children) {
// data.children = []
// }
// data.children.push(newChild)
// treeData.value = [...treeData.value]
}
// 删除分组
const remove = (node: Node, data: Tree) => {
const parent = node.parent
const children: Tree[] = parent.data.children || parent.data
const index = children.findIndex((d) => d.id === data.id)
children.splice(index, 1)
treeData.value = [...treeData.value]
}
// 设置分组推送人
// async function pushPerson(type:string,data: Tree) {
// }
const visible = ref(false); //是否显示表单
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
@@ -72,7 +242,7 @@
// 表格配置项
const columns: ColumnProps<ZJRQ.WarnInfo>[] = [
// { type: "selection", fixed: "left", width: 80 },
{ type: "selection", fixed: "left", width: 80 },
// { prop: "searchKey", label: "关键字", search: { el: "input" }, isShow: false },
// {
// prop: "poiId",
@@ -107,29 +277,57 @@
prop: "resWidth",
label: "分辨率",
render: (row) => {
console.log(row,88)
return row.row.resWidth + '*' + row.row.resHeight;
}
},
{ prop: "operation", label: "操作", width: 250, fixed: "right" }
];
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
return
// 二次确认 => 请求api => 刷新表格
await useHandleData(monitorLIVEApi.delete, { ids }, msg);
RefreshTable();
}



const moveFormRef = ref<InstanceType<typeof Form> | null>(null);
function omMove(opt: FormOptEnum, record: {} | SysOrg.SysOrgInfo = {}) {
moveFormRef.value?.omMove({ opt: opt, record: record, successful: RefreshTable });
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh();
}
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | SysOrg.SysOrgInfo = {}) {
formRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
}

// 人员选择
// 表单引用
const userFormRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function pushPerson(opt: FormOptEnum, record: {} | SysOrg.SysOrgInfo = {}) {
userFormRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
}
// 详情数据
// let detailData: globalThis.Ref<{}>
let detailData = reactive({
@@ -174,16 +372,6 @@
// detailData.videoUrl = data.pullStreamUrls[2].url;
detailData.streamId = data.streamId;
detailData.videoToken = data.videoToken;
// let timer = setInterval(() => {
// num+=1
// if(num > 2) {

// } else {
// getvideo1()
// }
// },200)
// getvideo1()
getvideo2()
}
@@ -195,7 +383,6 @@
// let url = detailData.videoUrl.replace('http://rts-pull-live.deepeleph.com', '/Files')
// console.log(url,888)
let pullStreamUrl = detailData.videoUrl;
console.log(pullStreamUrl,888)
const mediaEle = document.querySelector('video');
aliRts.on("onError", (err) => {
console.log(`errorCode: ${err.errorCode}`);
@@ -275,11 +462,16 @@
detailData.videoUrl = ''
detailData.videoType = ''
stopUrl();
player.value.dispose();
if(player.value) {
player.value.dispose();
}
};
function stopUrl() {
detailData.videoUrl = ''
player.value.dispose();
if(player.value) {
player.value.dispose();
}
let params = {
sensorId: detailData.sensorId,
streamId: detailData.streamId,
@@ -291,31 +483,15 @@
if (code == 200) {
// ElMessage.success(msg);
}
});
})
}



</script>
<style lang="scss" scoped>
@import url("https://g.alicdn.com/apsara-media-box/imp-web-player/2.16.3/skins/default/aliplayer-min.css");
.detailpic {
width: 800px;
object-fit: cover;
}
.linebox {
margin-top: 20px;
}
/* 自定义样式 */
.vjs-custom-skin .vjs-play-control {
/* 播放按钮的样式 */
}
@import "./index.scss";
.dialogHeader {
display: flex;
justify-content: space-between;


+ 192
- 0
SafeCampus.WEB/src/views/userManage/personnel/components/form/form_basic.vue Целия файл

@@ -0,0 +1,192 @@
<!--
* @Description: 人员表单
* @Author: syy
* @Date: 2023-12-15 15:45:50
-->
<template>
<div class="userManageForm">
<div>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="人员姓名" prop="name">
<s-input v-model="userInfo.name"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="所属班级" prop="personSetId">
<s-select v-model="userInfo.personSetId" :options="treeData" label="personSetName" value="personSetId"></s-select>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24">
<s-form-item label="上传人脸" prop="faces">
<el-upload
v-model:file-list="fileList"
action="/api/business/personApi/uploadFile"
list-type="picture-card"
:on-success="handleAvatarSuccess"
:on-error="handleAvatarError"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
accept=".jpg, .jpeg, .png"
:headers="{
Authorization: `${TokenEnum.TOKEN_PREFIX} ${accessToken}`
}"
>
<el-icon><Plus /></el-icon>
</el-upload>
<el-dialog v-model="dialogVisible" title="查看图片">
<img w-full :src="dialogImageUrl" alt="Preview Image" style="width: 100%" />
</el-dialog>
</s-form-item>
</el-col>
</el-row>

<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="性别" prop="gender">
<s-radio-group v-model="userInfo.gender" :options="genderOptions" />
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="年龄" prop="age">
<s-input v-model="userInfo.age"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="手机号" prop="phone">
<s-input v-model="userInfo.phone"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="扩展字段" prop="extData">
<s-input v-model="userInfo.extData"></s-input>
</s-form-item>
</el-col>
</el-row>
</div>
</div>
</template>

<script setup lang="ts">
import { SysUserPersonnel, userManagePersonnelApi, userManageClassManageApi } from "@/api";
import { Plus } from "@element-plus/icons-vue";
import { useUserStore } from "@/stores/modules";
import type { UploadProps, UploadUserFile } from "element-plus";
import { ElMessage } from "element-plus";
import { TokenEnum } from "@/enums";
// props
interface FormProps {
modelValue: Partial<SysUserPersonnel.SysUserPerInfo>;
}
const emit = defineEmits(["update:modelValue"]); //定义emit
const props = defineProps<FormProps>(); //定义props
// 人员信息
const userInfo = computed({
get: () => props.modelValue,
set: val => emit("update:modelValue", val)
});
/* */
const userStore = useUserStore();
const { accessToken } = userStore;
const fileList = ref([]);
const faces = ref<SysUserPersonnel.SysUserAvatar[]>([]);
const dialogImageUrl = ref("");
const dialogVisible = ref(false);
const treeData = ref<{ [key: string]: any }[]>([]);
const handleRemove: UploadProps["onRemove"] = uploadFile => {
const index = faces.value.findIndex(item => item.uid === uploadFile.uid);
if (index > -1) {
faces.value.splice(index, 1);
}
userInfo.value.faces = faces.value;
if (uploadFile.personId) {
userManagePersonnelApi.deleteFace({ personId: uploadFile.personId, faceIds: [uploadFile.uid] }).then(res => {});
}
};

const handlePictureCardPreview: UploadProps["onPreview"] = uploadFile => {
dialogImageUrl.value = uploadFile.url!;
dialogVisible.value = true;
};
const handleAvatarSuccess: UploadProps["onSuccess"] = (response, uploadFile) => {
if (response.code === 200) {
faces.value.push({ faceUrl: response.data, uid: uploadFile.uid });
userInfo.value.faces = faces.value;
ElMessage.success(response.msg);
} else {
ElMessage.error(response.msg);
fileList.value = fileList.value.splice(0, fileList.value.length - 1);
}
};
const handleAvatarError: UploadProps["onError"] = (error, uploadFile, uploadFiles) => {
console.log(error, uploadFile, uploadFiles, "err");
};
// 通用状态选项
const genderOptions = ref([
{
label: "未知",
value: "GENDER_UNKNOWN"
},
{
label: "男",
value: "GENDER_MALE"
},
{
label: "女",
value: "GENDER_FEMALE"
}
]);
const getRequestData = async () => {
const { data } = await userManageClassManageApi.page();
treeData.value = data;
console.log(treeData.value, "treeData");
};
onMounted(() => {
// 初始化
userInfo.value.gender = userInfo.value.gender ? userInfo.value.gender : genderOptions.value[0].value;
getRequestData();
if (userInfo.value.personId) {
if (userInfo.value.faces?.length > 0) {
fileList.value = [
...JSON.parse(JSON.stringify(userInfo.value.faces)).map(item => {
return {
url: item.faceUrl,
uid: item.faceId,
personId: userInfo.value.personId
};
})
];
faces.value = [
...JSON.parse(JSON.stringify(userInfo.value.faces)).map(item => {
return {
faceUrl: item.faceUrl,
uid: item.faceId,
personId: userInfo.value.personId
};
})
];
}
}
});
</script>

<style lang="scss" scoped>
:deep(.el-input__wrapper) {
width: 100% !important;
}
:deep(.el-date-editor.el-input) {
width: 92% !important;
}
:deep(.el-upload-list--picture-card .el-upload-list__item) {
width: 100px !important;
height: 100px !important;
}
:deep(.el-upload--picture-card) {
width: 100px !important;
height: 100px !important;
}
</style>

+ 120
- 0
SafeCampus.WEB/src/views/userManage/personnel/components/form/index.vue Целия файл

@@ -0,0 +1,120 @@
<!--
* @Description: 表单
* @Author: syy
* @Date: 2023-12-15 15:45:59
-->
<template>
<div>
<form-container v-model="visible" :title="`${sysUserProps.opt}人员`" form-size="800px" @close="onClose">
<el-form
ref="sysUserFormRef"
:rules="rules"
:disabled="sysUserProps.disabled"
:model="sysUserProps.record"
:hide-required-asterisk="sysUserProps.disabled"
label-width="auto"
label-suffix=" :"
>
<el-tabs v-model="activeName">
<Basic v-model="sysUserProps.record"></Basic>
</el-tabs>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!sysUserProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</div>
</template>

<script setup lang="ts" name="SysUserPersonnelForm">
import { SysUserPersonnel, userManagePersonnelApi } from "@/api";
import { FormOptEnum, SysDictEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
import Basic from "./form_basic.vue";

const visible = ref(false); //是否显示表单
const activeName = ref("basic");
// 表单参数
const sysUserProps = reactive<FormProps.Base<SysUserPersonnel.SysUserPerInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});

// 表单验证规则
const rules = reactive({
// personId: [required("请输入人员ID")],
name: [required("请输入姓名")],
gender: [required("请选择性别")],
faces: [required("请上传人脸图片")],
phone: [required("请输入手机号")]
// extData: [required("请输入扩展数据")]
});

/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<SysUserPersonnel.SysUserPerInfo>) {
Object.assign(sysUserProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
// sysUserProps.record.sortCode = 99;
}
visible.value = true; //显示表单
if (props.record.personId) {
//如果传了id,就去请求api获取record
userManagePersonnelApi.detail({ id: props.record.personId }).then(res => {
sysUserProps.record = res.data;
});
}
}

// 提交数据(新增/编辑)
const sysUserFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
sysUserFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
if (sysUserProps.record.faces.length === 0) {
return ElMessage.error("请上传人脸图片");
}
if (sysUserProps.record.personId) {
await userManagePersonnelApi
.update(sysUserProps.record)
.then(() => {
sysUserProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
} else {
await userManagePersonnelApi
.add(sysUserProps.record)
.then(() => {
sysUserProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
}
});
}

/** 关闭表单*/
function onClose() {
visible.value = false;
activeName.value = "basic";
}

// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>

<style lang="scss" scoped></style>

+ 107
- 0
SafeCampus.WEB/src/views/userManage/personnel/components/formClass/index.vue Целия файл

@@ -0,0 +1,107 @@
<!--
* @Description: 表单
* @Author: syy
* @Date: 2023-12-15 15:45:59
-->
<template>
<div>
<form-container v-model="visibleClass" :title="`${sysUserProps.opt}班级`" form-size="400px" @close="onClose">
<el-form
ref="sysUserFormRef"
:rules="rules"
:disabled="sysUserProps.disabled"
:model="sysUserProps.record"
:hide-required-asterisk="sysUserProps.disabled"
label-width="auto"
label-suffix=" :"
>
<div>
<el-row :gutter="16">
<el-col :span="22">
<s-form-item label="班级名称" prop="personSetName">
<s-input v-model="sysUserProps.record.personSetName"></s-input>
</s-form-item>
</el-col>
</el-row>
</div>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!sysUserProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</div>
</template>

<script setup lang="ts" name="SysUserPerformClass">
import { ref } from "vue";
import { SysUserPersonnel, userManageClassManageApi } from "@/api";
import { FormOptEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";

const visibleClass = ref(false); //是否显示表单
// 表单参数
const sysUserProps = reactive<FormProps.Base<SysUserPersonnel.ClassPage>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
personSetName: [required("请输入班级名称")]
});

/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<SysUserPersonnel.ClassPage>) {
Object.assign(sysUserProps, props); //合并参数
visibleClass.value = true; //显示表单
sysUserProps.record = props.record;
}

// 提交数据(新增/编辑)
const sysUserFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
sysUserFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
sysUserProps.record.name = sysUserProps.record.personSetName;
//提交表单
if (sysUserProps.record.personSetId) {
sysUserProps.record.id = sysUserProps.record.personSetId;
await userManageClassManageApi
.update(sysUserProps.record)
.then(() => {
sysUserProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
} else {
await userManageClassManageApi
.add(sysUserProps.record)
.then(() => {
sysUserProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
}
});
}

/** 关闭表单*/
function onClose() {
visibleClass.value = false;
}

// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>

<style lang="scss" scoped></style>

+ 291
- 0
SafeCampus.WEB/src/views/userManage/personnel/index.vue Целия файл

@@ -0,0 +1,291 @@
<!--
* @Description: 人员管理
* @Author: syy
* @Date: 2024-7-15
-->
<template>
<div class="main-box">
<TreeFilter
ref="treeFilter"
label="personSetName"
id="personSetId"
width="300px"
:show-all="false"
:request-api="userManageClassManageApi.page"
@change="changeTreeFilter"
>
<template v-slot:header>
<s-button suffix="班级" @click="addClass(FormOptEnum.ADD)" style="margin-bottom: 15px" />
</template>
<template v-slot:label="{ row }">
<span class="custom-tree-node">
<span>{{ row.node.label }}</span>
<span>
<a @click="addClass(FormOptEnum.EDIT, row.node.data)">
<el-icon><Edit /></el-icon>
</a>
<a style="margin-left: 8px" @click="addDelete(row.node.data.personSetId, '删除班级')">
<el-icon><Delete /></el-icon>
</a>
</span>
</span>
</template>
</TreeFilter>
<div class="table-box">
<ProTable ref="proTable" title="人员管理" :columns="columns" rowKey="personId" :request-api="userManagePersonnelApi.page">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button v-auth="userPerButtonCode.add" suffix="人员" @click="onOpen(FormOptEnum.ADD, { personSetId: personSetId })" />
<s-button
v-auth="userPerButtonCode.delete"
type="danger"
:opt="FormOptEnum.DELETE"
plain
suffix="人员"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选人员')"
/>
</template>
<!-- 表格操作栏 -->
<template #operation="scope">
<el-space>
<s-button v-auth="userPerButtonCode.edit" link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button v-auth="userPerButtonCode.delete" link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.personId], `删除人员`)" />
<el-dropdown @command="handleCommand">
<el-link type="primary" :underline="false" :icon="ArrowDown"> 更多 </el-link>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="command(scope.row, cmdEnum.AddFace)"
><el-upload
ref="upload"
class="upload-demo"
action="/api/business/personApi/uploadFile"
:show-file-list="false"
:on-success="handleAvatarSuccess"
accept=".jpg, .jpeg, .png"
:headers="{
Authorization: `${TokenEnum.TOKEN_PREFIX} ${accessToken}`
}"
>
<template #trigger>
{{ cmdEnum.AddFace }}
</template>
</el-upload>
</el-dropdown-item>
<el-dropdown-item :command="command(scope.row, cmdEnum.UnderpantsUnBinding)">
{{ cmdEnum.UnderpantsUnBinding }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-space>
</template>
</ProTable>
</div>
<!-- 人员新增/编辑表单 -->
<Form ref="formRef"></Form>
<!-- 班级新增/编辑表单 -->
<FormClass ref="formRefC" />
<!-- 预览头像 -->
<el-dialog v-model="visible" title="查看头像" width="830px" :before-close="handleClose">
<div style="display: flex; align-items: center; justify-content: center">
<img class="detailpic" :src="faceUrl" alt="" />
</div>
</el-dialog>
</div>
</template>
<script setup lang="tsx" name="SysUserPersonnel">
import { userManagePersonnelApi,userPerButtonCode,SysUserPersonnel,userManageClassManageApi } from "@/api";
import { useHandleData } from "@/hooks/useHandleData";
import { FormOptEnum } from "@/enums";
import Form from "./components/form/index.vue";
import FormClass from "./components/formClass/index.vue";
import { ArrowDown } from "@element-plus/icons-vue";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import TreeFilter from "@/components/TreeFilter/index.vue";
import { useUserStore } from "@/stores/modules";
import { TokenEnum } from "@/enums";
import type { UploadProps } from "element-plus";
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const faceUrl = ref('');
const visible = ref(false); //是否显示人员表单
const proTable = ref<ProTableInstance>();
const treeFilter = ref<InstanceType<typeof TreeFilter> | null>(null);
const userStore = useUserStore();
const { accessToken } = userStore;
// 表格配置项
const columns: ColumnProps<SysUserPersonnel.SysUserPerInfo>[] = [
{ type: "selection", fixed: "left", width: 50 },
{
prop: "faceUrl",
label: "人脸",
render: scope => {
return (
<img src={scope.row.faces.length > 0 ? scope.row.faces[0].faceUrl : ''} onClick={() => viewHeadImage(scope)} style='width:50px;height:50px;' alt=''/>
);
}
},
{
prop: "name",
label: "姓名"
},
{
prop: "personId",
label: "人员ID"
},
{
prop: "age",
label: "年龄"
},
{
prop: "personSets",
label: "所属班级",
render: scope => {
return scope.row.personSets.length > 0 ? scope.row.personSets[0].personSetName : ''
}
},
{ prop: "operation", label: "操作", width: 250, fixed: "right" }
];
const viewHeadImage = (scope: any) => {
faceUrl.value = scope.row.faces[0].faceUrl;
visible.value = true
console.log(faceUrl);
};
const handleClose = () => {
visible.value = false;
};


// 人员表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
// 班级表单引用
const formRefC = ref<InstanceType<typeof FormClass> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | SysUserPersonnel.SysUserPerInfo = {}) {
formRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
}

/**
* 打开班级表单
* @param opt 操作类型
* @param record 记录
*/

function addClass(opt: FormOptEnum, record: {} | SysUserPersonnel.ClassPage = {}) {
formRefC.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
}
/**
* 班级删除
* @param ids id数组
*/
async function addDelete(id: string[],msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(userManageClassManageApi.delete, { id }, msg);
RefreshTable(); //刷新表格
}

/**
* 人员删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
if(ids.length === 0){
ElMessage({
message: '请选择要删除的人员',
type: 'warning'
});
return
}
// 二次确认 => 请求api => 刷新表格
await useHandleData(userManagePersonnelApi.delete, {id: ids.join(",") }, msg);
RefreshTable(); //刷新表格
}

// 刷新表格
const RefreshTable = () => {
proTable.value?.refresh();
treeFilter.value?.refresh(); //刷新树形筛选器
}


/** 更多下拉菜单命令枚举 */
enum cmdEnum {
AddFace = "添加人脸",
UnderpantsUnBinding = "底库解绑"
}
/** 下拉菜单参数接口 */
interface Command {
row: SysUserPersonnel.SysUserPerInfo;
command: cmdEnum;
}

/**配置command的参数 */
function command(row: SysUserPersonnel.SysUserPerInfo, command: cmdEnum): Command {
return {
row: row,
command: command
};
}
/**
* 更多下拉菜单点击事件
* @param command
*/
const personId = ref<number | string>(); //人员id
function handleCommand(command: Command) {
switch (command.command) {
case cmdEnum.AddFace:
personId.value = command.row.personId; //获取人员id
break
case cmdEnum.UnderpantsUnBinding:
userManagePersonnelApi.personUnBindDfie({
personId:command.row.personId,
personSetId: command.row.personSets[0].personSetId
}).then(res=>{
if(res.code == 200){
ElMessage.success('底库解绑成功');
RefreshTable()
}
})
break;
}
}
const handleAvatarSuccess: UploadProps["onSuccess"] = (response, uploadFile) => {
if (response.code === 200) {
userManagePersonnelApi.addFace({
personId: personId.value,
faceUrl: response.data
}).then(res=>{
RefreshTable()
})
} else {
ElMessage.error(response.msg);
}
};

/** 部门切换 */
const personSetId = ref<number | string>()
function changeTreeFilter(val: number | string) {
personSetId.value = val
proTable.value!.pageable.pageNum = 1;
proTable.value!.searchParam.personSetId = val;
proTable.value!.search();
}
</script>
<style scoped lang="scss">
.table-box {
width: 100%;
height: 100%;
}
.custom-tree-node {
display: flex;
flex: 1;
align-items: center;
justify-content: space-between;
padding-right: 8px;
font-size: 14px;
}
</style>

+ 3
- 3
SafeCampus.WEB/src/views/warn/statistion/index.vue Целия файл

@@ -7,13 +7,13 @@
<div class="card content-main">
<el-row :gutter="20">
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<div ref="chart1" style="width: 600px; height: 400px"></div>
<div ref="chart1" style="width: 600px; height: 378px"></div>
</el-col>
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<div ref="chart2" style="width: 600px; height: 400px"></div>
<div ref="chart2" style="width: 600px; height: 378px"></div>
</el-col>
<el-col :span="24">
<div ref="chart3" style="width: 100%; height: 400px"></div>
<div ref="chart3" style="width: 100%; height: 378px"></div>
</el-col>
</el-row>
</div>


+ 81
- 6
SafeCampus.WEB/src/views/warn/zjrq/index.vue Целия файл

@@ -110,7 +110,28 @@ import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { useDictStore } from "@/stores/modules";
import { FormOptEnum, SysDictEnum, MenuTypeDictEnum } from "@/enums";
const visible = ref(false); //是否显示表单

onMounted(() => {
getWarnTypeList();
});
let warnOptions = ref([]);
function getWarnTypeList() {
setTimeout(async ()=> {
await warnZJRQApi.warnType({}).then(res => {
let { code, data } = res;
if (code == 200) {
warnOptions.value = data.map(item => {
return {
label: item.name,
value: item.code
};
})
}
});
})
}
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
const dictStore = useDictStore();
@@ -118,7 +139,7 @@ const dictStore = useDictStore();
// 表格配置项
const columns: ColumnProps<ZJRQ.WarnInfo>[] = [
{ type: "selection", fixed: "left", width: 80 },
{ prop: "searchKey", label: "关键字", search: { el: "input" }, isShow: false },
// { prop: "searchKey", label: "关键字", search: { el: "input" }, isShow: false },
// { prop: "cameraId", label: "所属摄像头", search: { el: "input" }, isShow: false },
// { prop: "alarmTypeDesc", label: "告警类型", search: { el: "input" }, isShow: false },
{
@@ -151,21 +172,75 @@ const columns: ColumnProps<ZJRQ.WarnInfo>[] = [
}
},
{
prop: "alarmTypeDesc",
label: "告警类型"
prop: "alarmType",
label: "告警类型",
enum: warnOptions,
search: {
el: "tree-select",
// span: 1
}
},
{
prop: "warnHand",
label: "处理状态",
enum: [
{
label: "已处理",
value: 1
},
{
label: "未处理",
value: 0
}
],
render: scope => {
if (scope.row.warnHand === 1) {
return "已处理";
} else {
return "未处理";
}
}
},
search: {
el: "tree-select",
// span: 1
}
},
{ prop: "tick", label: "预警时间" },
{
prop: "tick",
label: "预警时间",
search: {
// 自定义 search 组件
span: 1,
render: ({ searchParam }) => {
return (
<div class="flex-center">
<el-date-picker
style="150px; flex-shink: 1;"
v-model={searchParam.StartTick}
type="datetime"
placeholder="开始时间"
format="YYYY-MM-DD HH:mm:ss"
date-format="YYYY-MM-DD"
time-format="HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
<span class="mr10 ml10">-</span>
<el-date-picker
style="150px;"
v-model={searchParam.EndTick}
type="datetime"
placeholder="结束时间"
format="YYYY-MM-DD HH:mm:ss"
date-format="YYYY-MM-DD"
time-format="HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</div>
);
}
}

},
{ prop: "operation", label: "操作", width: 250, fixed: "right" }
];



Зареждане…
Отказ
Запис