Compare commits
137 Commits
bacbb84d54
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40503b4f56 | ||
|
|
000472231c | ||
|
|
aba75fdd7c | ||
|
|
e19a9df909 | ||
|
|
7f195f3fc1 | ||
|
|
04a52895d7 | ||
|
|
bf3c8d8228 | ||
|
|
5ff0a2e048 | ||
|
|
1b55bf6935 | ||
|
|
8768ec3729 | ||
|
|
747e7000cd | ||
|
|
ef01ef8394 | ||
|
|
b638e7eaa0 | ||
|
|
93f3a48caf | ||
|
|
bbd23051e7 | ||
|
|
aaa223f41d | ||
|
|
7f376ce10c | ||
|
|
d78dd8d42e | ||
|
|
d193836c70 | ||
|
|
671c5b4200 | ||
|
|
13ebb3e51a | ||
|
|
43bfa65108 | ||
|
|
98a30948ec | ||
|
|
3976b854d1 | ||
|
|
3ec33d520d | ||
|
|
93a5feabdc | ||
|
|
88186b8dfc | ||
|
|
55ff3d7777 | ||
|
|
603c82939b | ||
|
|
da108742cc | ||
|
|
72716e0fa9 | ||
|
|
374e24035c | ||
|
|
04ca12b873 | ||
|
|
c76102bf96 | ||
|
|
5ef955f1fc | ||
|
|
0859fc76cd | ||
|
|
67fe72411c | ||
|
|
e05d3a49da | ||
|
|
a860697ba2 | ||
|
|
6e15e28f38 | ||
|
|
92c5cf293b | ||
|
|
ec47c164b0 | ||
|
|
8bd41d6758 | ||
|
|
f92baab6f7 | ||
|
|
a27b0ac7f2 | ||
|
|
4083da42d8 | ||
|
|
2d2f46b6f1 | ||
|
|
e4d014cf8c | ||
|
|
80a360fa0e | ||
|
|
7d2c70a19f | ||
|
|
a248e3b234 | ||
|
|
70ab453d94 | ||
|
|
36f0cedc62 | ||
|
|
df52a3e343 | ||
|
|
8d27df05d6 | ||
|
|
e47b8d354e | ||
|
|
ced12d5805 | ||
|
|
dd54ce489e | ||
|
|
00efe08873 | ||
|
|
c3420640a7 | ||
|
|
4c882ac309 | ||
|
|
334787f2cf | ||
|
|
de48277991 | ||
|
|
753f84ec26 | ||
|
|
3d58c73a88 | ||
|
|
457d0c3888 | ||
|
|
a8ea3746f4 | ||
|
|
55f0cd2192 | ||
|
|
2492b8ff5a | ||
|
|
13b00ac8de | ||
|
|
89d0dd2a1d | ||
|
|
47b272441c | ||
|
|
159d53080a | ||
|
|
7f9d9ed8bd | ||
|
|
ac997998e1 | ||
|
|
2c45ef4e05 | ||
|
|
5fe9342bbf | ||
|
|
93b97a1571 | ||
|
|
7b8509cc31 | ||
|
|
a5cdcedb38 | ||
|
|
17f7c0ad78 | ||
|
|
19252dd7cc | ||
|
|
3aad0a27cf | ||
|
|
1659e8f9bc | ||
|
|
b10f4c4172 | ||
|
|
b3d8da2365 | ||
|
|
3fadbf881b | ||
|
|
0eaa8a44f4 | ||
|
|
b404d4ff29 | ||
|
|
dc4415a1ff | ||
|
|
a4c8af1082 | ||
|
|
d5ed2b80b2 | ||
|
|
6f29b40e99 | ||
|
|
b68741b4a9 | ||
|
|
29cc052611 | ||
|
|
98c53dce04 | ||
|
|
624cfb4d0a | ||
|
|
f10c5bd4ea | ||
|
|
e05b524c39 | ||
|
|
f603317761 | ||
|
|
4b7ed6f727 | ||
|
|
d1ed96b8fc | ||
|
|
f149fca426 | ||
|
|
2dae286fa9 | ||
|
|
0e76ac4fcc | ||
|
|
7ef49859c1 | ||
|
|
c876b1e527 | ||
|
|
0e8dc5fd93 | ||
|
|
844515acc4 | ||
|
|
a5ccc6d57f | ||
|
|
ccf152cb6a | ||
|
|
8296fec882 | ||
|
|
9cec62260a | ||
|
|
8c15bca5c7 | ||
|
|
d94f28f1d3 | ||
|
|
dd5510e5c0 | ||
|
|
f30ed0b68c | ||
|
|
05ee38b119 | ||
|
|
2903640d8d | ||
|
|
d6a59b5ebb | ||
|
|
d840737c5e | ||
|
|
c761be6e31 | ||
|
|
ad6c6aa9c1 | ||
|
|
9fd64e62da | ||
|
|
a0d1c3739d | ||
|
|
72f30359c6 | ||
|
|
5fde4ea0f7 | ||
|
|
0422e43070 | ||
|
|
3f5b81424a | ||
|
|
949dd1f7f0 | ||
|
|
64d60b27dc | ||
|
|
d6e8afab6c | ||
|
|
42ee161da3 | ||
|
|
732eb453b4 | ||
|
|
0b2606bdf4 | ||
|
|
988cf99a03 | ||
|
|
3b001f79e0 |
@@ -1,5 +1,5 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = RuoYi-Vue-Plus多租户管理系统
|
||||
VITE_APP_TITLE = 可视化电子围栏管理系统
|
||||
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
|
||||
|
||||
# 开发环境配置
|
||||
@@ -35,4 +35,6 @@ VITE_APP_WEBSOCKET = false
|
||||
# sse 开关
|
||||
VITE_APP_SSE = true
|
||||
|
||||
VITE_BASE_API = "http://192.168.1.100:8080"
|
||||
# http://192.168.0.214:8089
|
||||
# VITE_BASE_API = "http://47.104.208.94:8089"
|
||||
VITE_BASE_API = "http://47.104.215.165:8080"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = RuoYi-Vue-Plus多租户管理系统
|
||||
VITE_APP_TITLE = 可视化电子围栏管理系统
|
||||
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
|
||||
|
||||
# 生产环境配置
|
||||
@@ -37,3 +37,5 @@ VITE_APP_WEBSOCKET = false
|
||||
|
||||
# sse 开关
|
||||
VITE_APP_SSE = true
|
||||
|
||||
VITE_BASE_API = "http://47.104.215.165:8089"
|
||||
|
||||
25
.github/copilot-instructions.md
vendored
Normal file
25
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# RTK — Token-Optimized CLI
|
||||
|
||||
**rtk** is a CLI proxy that filters and compresses command outputs, saving 60-90% tokens.
|
||||
|
||||
## Rule
|
||||
|
||||
Always prefix shell commands with `rtk`:
|
||||
|
||||
```bash
|
||||
# Instead of: Use:
|
||||
git status rtk git status
|
||||
git log -10 rtk git log -10
|
||||
cargo test rtk cargo test
|
||||
docker ps rtk docker ps
|
||||
kubectl get pods rtk kubectl pods
|
||||
```
|
||||
|
||||
## Meta commands (use directly)
|
||||
|
||||
```bash
|
||||
rtk gain # Token savings dashboard
|
||||
rtk gain --history # Per-command savings history
|
||||
rtk discover # Find missed rtk opportunities
|
||||
rtk proxy <cmd> # Run raw (no filtering) but track usage
|
||||
```
|
||||
12
.github/hooks/rtk-rewrite.json
vendored
Normal file
12
.github/hooks/rtk-rewrite.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "rtk hook copilot",
|
||||
"cwd": ".",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,6 +13,7 @@ selenium-debug.log
|
||||
|
||||
# Editor directories and files
|
||||
.claude
|
||||
.mimocode
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
|
||||
19
README.md
19
README.md
@@ -6,28 +6,21 @@
|
||||
|
||||
## 配套后端代码仓库地址
|
||||
|
||||
| 介绍 | 项目名 | 项目地址 |
|
||||
|------------|:-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 介绍 | 项目名 | 项目地址 |
|
||||
| ----------------- | :--------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 🔥 分布式集群框架 | RuoYi-Vue-Plus | - [Gitee](https://gitee.com/dromara/RuoYi-Vue-Plus)<br> - [GitHub](https://github.com/dromara/RuoYi-Vue-Plus)<br> - [GitCode](https://gitcode.com/dromara/RuoYi-Vue-Plus) |
|
||||
| 🔥 微服务框架 | RuoYi-Cloud-Plus | - [Gitee](https://gitee.com/dromara/RuoYi-Cloud-Plus)<br>- [GitHub](https://github.com/dromara/RuoYi-Cloud-Plus)<br> - [GitCode](https://gitcode.com/dromara/RuoYi-Cloud-Plus) |
|
||||
| 🔥 微服务框架 | RuoYi-Cloud-Plus | - [Gitee](https://gitee.com/dromara/RuoYi-Cloud-Plus)<br>- [GitHub](https://github.com/dromara/RuoYi-Cloud-Plus)<br> - [GitCode](https://gitcode.com/dromara/RuoYi-Cloud-Plus) |
|
||||
|
||||
## 分支说明
|
||||
|
||||
- ts分支(稳定发布主分支 生产可用)
|
||||
- main分支(稳定发布主分支 生产可用)
|
||||
- dev分支(开发分支 开发过程中使用)
|
||||
|
||||
## 前端运行
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install --registry=https://registry.npmmirror.com
|
||||
|
||||
# 启动服务
|
||||
npm run dev
|
||||
|
||||
# 构建生产环境
|
||||
npm run build:prod
|
||||
|
||||
# 启动命令
|
||||
./start.sh
|
||||
# 前端访问地址 http://localhost:80
|
||||
```
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
"$schema": "https://json.schemastore.org/package",
|
||||
"name": "ruoyi-vue-plus",
|
||||
"version": "5.6.1-2.6.1",
|
||||
"description": "RuoYi-Vue-Plus多租户管理系统",
|
||||
"description": "可视化电子围栏管理系统",
|
||||
"author": "LionLi",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.34.1",
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"dev": "vite serve --mode development",
|
||||
"build:prod": "vite build --mode production",
|
||||
"build:dev": "vite build --mode development",
|
||||
|
||||
@@ -24,3 +24,9 @@ export interface DemoQuery extends PageQuery {
|
||||
beginTime?: string;
|
||||
endTime?: string;
|
||||
}
|
||||
|
||||
/** 告警列表响应结构 */
|
||||
export interface WarningListResult {
|
||||
rows: DemoVO[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
29
src/api/video/index.ts
Normal file
29
src/api/video/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import request from '@/utils/request';
|
||||
import { AxiosPromise } from 'axios';
|
||||
|
||||
/**
|
||||
* 开始一直播预览
|
||||
* @param query
|
||||
* post
|
||||
* /haikang/isup/stream/live/start?deviceId=2057338485989367810
|
||||
*/
|
||||
export const listDemo = (deviceId?: string | number) => {
|
||||
return request({
|
||||
url: `/haikang/isup/stream/live/start?deviceId=${deviceId}`,
|
||||
method: 'post'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止-直播预览
|
||||
* @param query
|
||||
* post
|
||||
* /haikang/isup/stream/live/stop?id=2057338485989367810&channel=1
|
||||
*/
|
||||
export const addDemo = (data: any) => {
|
||||
return request({
|
||||
url: `/haikang/isup/stream/live/stop?id=${data}`,
|
||||
method: 'get'
|
||||
// params: data
|
||||
});
|
||||
};
|
||||
0
src/api/video/types.ts
Normal file
0
src/api/video/types.ts
Normal file
@@ -12,68 +12,37 @@
|
||||
<top-bar id="topbar-container" class="topbar-container" />
|
||||
</template>
|
||||
<div class="right-menu flex align-center">
|
||||
<template v-if="appStore.device !== 'mobile'">
|
||||
<!-- <el-select
|
||||
v-if="userId === 1 && tenantEnabled"
|
||||
v-model="companyName"
|
||||
class="min-w-244px mr-2"
|
||||
clearable
|
||||
filterable
|
||||
reserve-keyword
|
||||
:placeholder="proxy.$t('navbar.selectTenant')"
|
||||
@change="dynamicTenantEvent"
|
||||
@clear="dynamicClearEvent"
|
||||
>
|
||||
<el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"> </el-option>
|
||||
<template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
|
||||
</el-select> -->
|
||||
|
||||
<div v-if="appStore.device !== 'mobile'">
|
||||
<search-menu ref="searchMenuRef" />
|
||||
<!-- <el-tooltip content="搜索" effect="dark" placement="bottom">
|
||||
<div class="right-menu-item hover-effect" @click="openSearchMenu">
|
||||
<svg-icon class-name="search-icon" icon-class="search" />
|
||||
</div>
|
||||
</el-tooltip> -->
|
||||
<!-- 消息 -->
|
||||
<el-tooltip content="警报" effect="dark" placement="bottom">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
|
||||
<template #reference>
|
||||
<el-badge :value="newNotice > 0 ? newNotice : ''" :max="99">
|
||||
<div class="right-menu-item hover-effect" @click="goToWarningList">
|
||||
<el-icon><BellFilled /></el-icon>
|
||||
</div>
|
||||
</el-badge>
|
||||
</template>
|
||||
<template #default>
|
||||
<notice></notice>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<!-- <el-tooltip content="Github" effect="dark" placement="bottom">
|
||||
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip :content="proxy.$t('navbar.document')" effect="dark" placement="bottom">
|
||||
<ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip :content="proxy.$t('navbar.full')" effect="dark" placement="bottom">
|
||||
<screenfull id="screenfull" class="right-menu-item hover-effect" />
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip :content="proxy.$t('navbar.language')" effect="dark" placement="bottom">
|
||||
<lang-select id="lang-select" class="right-menu-item hover-effect" />
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip :content="proxy.$t('navbar.layoutSize')" effect="dark" placement="bottom">
|
||||
<size-select id="size-select" class="right-menu-item hover-effect" />
|
||||
</el-tooltip> -->
|
||||
</template>
|
||||
</div>
|
||||
<!-- 消息 - 移到 v-if 外面,手机端也能显示 -->
|
||||
<el-tooltip content="警报" effect="dark" placement="bottom">
|
||||
<div style="display: flex; align-items: center" class="mr-[30px]">
|
||||
<el-popover
|
||||
placement="bottom"
|
||||
transition="el-zoom-in-top"
|
||||
:width="400"
|
||||
:persistent="false"
|
||||
trigger="click"
|
||||
v-model:visible="popoverVisible"
|
||||
>
|
||||
<template #reference>
|
||||
<el-badge :value="infoData" :max="9999">
|
||||
<div class="right-menu-item hover-effect" @click="goToWarningList">
|
||||
<el-icon><BellFilled /></el-icon>
|
||||
</div>
|
||||
</el-badge>
|
||||
</template>
|
||||
<template #default>
|
||||
<notice :warningList="warningList"></notice>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<div class="avatar-container">
|
||||
<el-dropdown class="right-menu-item hover-effect" trigger="click" @command="handleCommand">
|
||||
<div class="avatar-wrapper">
|
||||
<!-- <img :src="logoUrl" class="user-avatar" /> -->
|
||||
<img :src="userStore.avatar" class="user-avatar" />
|
||||
<el-icon><caret-bottom /></el-icon>
|
||||
</div>
|
||||
@@ -82,9 +51,9 @@
|
||||
<router-link v-if="!dynamic" to="/user/profile">
|
||||
<el-dropdown-item>{{ proxy.$t('navbar.personalCenter') }}</el-dropdown-item>
|
||||
</router-link>
|
||||
<el-dropdown-item v-if="settingsStore.showSettings" command="setLayout">
|
||||
<!-- <el-dropdown-item v-if="settingsStore.showSettings" command="setLayout">
|
||||
<span>{{ proxy.$t('navbar.layoutSetting') }}</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-item> -->
|
||||
<el-dropdown-item divided command="logout">
|
||||
<span>{{ proxy.$t('navbar.logout') }}</span>
|
||||
</el-dropdown-item>
|
||||
@@ -111,12 +80,19 @@ import { ElMessageBoxOptions } from 'element-plus/es/components/message-box/src/
|
||||
import { NavTypeEnum } from '@/enums/NavTypeEnum';
|
||||
import Logo from '@/layout/components/Sidebar/Logo.vue';
|
||||
import TopBar from './TopBar';
|
||||
import { getDemo } from '@/api/systemList/index';
|
||||
import { useWarningListStore } from '@/store/modules/warnningList';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const warningListStore = useWarningListStore();
|
||||
const userStore = useUserStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const noticeStore = storeToRefs(useNoticeStore());
|
||||
const newNotice = ref(<number>0);
|
||||
const { warningList, warningListTotal } = storeToRefs(warningListStore);
|
||||
|
||||
const infoData = ref(warningListStore.warningListTotal ?? 0);
|
||||
const popoverVisible = ref(false);
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
@@ -208,8 +184,14 @@ const handleCommand = (command: string) => {
|
||||
//用深度监听 消息
|
||||
watch(
|
||||
() => noticeStore.state.value.notices,
|
||||
(newVal) => {
|
||||
async (newVal) => {
|
||||
newNotice.value = newVal.filter((item: any) => !item.read).length;
|
||||
//监听到消息变化,刷新告警列表
|
||||
await warningListStore.getWarningList({
|
||||
status: 0,
|
||||
pageNum: 1,
|
||||
pageSize: 300
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
@@ -219,8 +201,53 @@ const refresh = () => {
|
||||
};
|
||||
|
||||
const goToWarningList = () => {
|
||||
router.push('/warningList/list');
|
||||
router.push('/WarningList/WarningList/list');
|
||||
};
|
||||
|
||||
// 获取Logo图片
|
||||
const logoUrl = ref();
|
||||
const logoName = ref();
|
||||
onMounted(() => {
|
||||
getDemo().then((res) => {
|
||||
console.log('系统11111111111', res);
|
||||
|
||||
logoName.value = res.data.name;
|
||||
logoUrl.value = res.data.url;
|
||||
});
|
||||
});
|
||||
onMounted(() => {
|
||||
warningListStore.getWarningList({
|
||||
status: 0,
|
||||
pageNum: 1,
|
||||
pageSize: 300
|
||||
});
|
||||
});
|
||||
|
||||
//监听数据变化
|
||||
watch(
|
||||
() => warningListStore.warningListTotal,
|
||||
(newVal) => {
|
||||
infoData.value = newVal;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
// 告警数据变化时自动弹出弹窗
|
||||
// 当 isNotice 变为 true 时弹出弹窗,并重置为 false
|
||||
// let noticeFirstLoad = true; //解决初始化时弹出弹窗问题
|
||||
watch(
|
||||
() => noticeStore.state.value.isNotice,
|
||||
(newVal) => {
|
||||
// if (noticeFirstLoad) {
|
||||
// noticeFirstLoad = false;
|
||||
// return;
|
||||
// }
|
||||
if (newVal) {
|
||||
noticeStore.state.value.isNotice = false;
|
||||
popoverVisible.value = true;
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -352,7 +379,7 @@ const goToWarningList = () => {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 25px;
|
||||
top: 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<div class="sidebar-logo-container" :class="{ collapse: collapse }">
|
||||
<transition :enter-active-class="proxy?.animate.logoAnimate.enter" mode="out-in">
|
||||
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
||||
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
||||
<h1 v-else class="sidebar-title">可视化电子围栏管理系统</h1>
|
||||
<img v-if="showLogoImage" :src="logoUrl" class="sidebar-logo" />
|
||||
<h1 v-else class="sidebar-title">{{ logoName }}</h1>
|
||||
</router-link>
|
||||
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
||||
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
||||
<h1 class="sidebar-title">可视化电子围栏管理系统</h1>
|
||||
<img v-if="showLogoImage" :src="logoUrl" class="sidebar-logo" />
|
||||
<h1 class="sidebar-title">{{ logoName }}</h1>
|
||||
</router-link>
|
||||
</transition>
|
||||
</div>
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import variables from '@/assets/styles/variables.module.scss';
|
||||
import logo from '@/assets/logo/logo.png';
|
||||
import { useSettingsStore } from '@/store/modules/settings';
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
import { NavTypeEnum } from '@/enums/NavTypeEnum';
|
||||
@@ -27,13 +26,15 @@ defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const title = import.meta.env.VITE_APP_LOGO_TITLE;
|
||||
const settingsStore = useSettingsStore();
|
||||
const sideTheme = computed(() => settingsStore.sideTheme);
|
||||
const logoUrl = computed(() => settingsStore.appLogoUrl || '');
|
||||
const logoName = computed(() => settingsStore.appTitle || import.meta.env.VITE_APP_LOGO_TITLE || 'RuoYi-Vue-Plus');
|
||||
const showLogoImage = computed(() => Boolean(logoUrl.value));
|
||||
|
||||
// 获取Logo背景色
|
||||
const getLogoBackground = computed(() => {
|
||||
if (settingsStore.isDark) {
|
||||
if (settingsStore.dark) {
|
||||
return 'var(--sidebar-bg)';
|
||||
}
|
||||
if (settingsStore.navType == NavTypeEnum.TOP) {
|
||||
@@ -44,7 +45,7 @@ const getLogoBackground = computed(() => {
|
||||
|
||||
// 获取Logo文字颜色
|
||||
const getLogoTextColor = computed(() => {
|
||||
if (settingsStore.isDark) {
|
||||
if (settingsStore.dark) {
|
||||
return 'var(--sidebar-text)';
|
||||
}
|
||||
if (settingsStore.navType == NavTypeEnum.TOP) {
|
||||
|
||||
@@ -95,21 +95,25 @@ const isLastView = () => {
|
||||
};
|
||||
const filterAffixTags = (routes: RouteRecordRaw[], basePath = '') => {
|
||||
let tags: RouteLocationNormalized[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
routes.forEach((route) => {
|
||||
if (route.meta && route.meta.affix) {
|
||||
const tagPath = getNormalPath(basePath + '/' + route.path);
|
||||
tags.push({
|
||||
hash: '',
|
||||
matched: [],
|
||||
params: undefined,
|
||||
query: undefined,
|
||||
redirectedFrom: undefined,
|
||||
fullPath: tagPath,
|
||||
path: tagPath,
|
||||
name: route.name as string,
|
||||
meta: { ...route.meta }
|
||||
});
|
||||
if (!seen.has(tagPath)) {
|
||||
seen.add(tagPath);
|
||||
tags.push({
|
||||
hash: '',
|
||||
matched: [],
|
||||
params: undefined,
|
||||
query: undefined,
|
||||
redirectedFrom: undefined,
|
||||
fullPath: tagPath,
|
||||
path: tagPath,
|
||||
name: route.name as string,
|
||||
meta: { ...route.meta }
|
||||
});
|
||||
}
|
||||
}
|
||||
if (route.children) {
|
||||
const tempTags = filterAffixTags(route.children, route.path);
|
||||
@@ -124,7 +128,6 @@ const initTags = () => {
|
||||
const res = filterAffixTags(routes.value);
|
||||
affixTags.value = res;
|
||||
for (const tag of res) {
|
||||
// Must have tag name
|
||||
if (tag.name) {
|
||||
useTagsViewStore().addVisitedView(tag);
|
||||
}
|
||||
@@ -136,6 +139,10 @@ const addTags = () => {
|
||||
route.meta.title = route.query.title as string;
|
||||
}
|
||||
if (name) {
|
||||
// 如果 visitedViews 中已有同 title 的固定标签(affix),不再添加普通标签
|
||||
if (visitedViews.value.some((v) => v.meta?.affix && v.title === route.meta?.title)) return;
|
||||
// 常规去重:已有同 path 或 fullPath 的标签则跳过
|
||||
if (visitedViews.value.some((v) => v.path === route.path || v.fullPath === route.fullPath)) return;
|
||||
useTagsViewStore().addView(route as any);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,36 +1,45 @@
|
||||
<template>
|
||||
<div v-loading="state.loading" class="layout-navbars-breadcrumb-user-news">
|
||||
<div class="head-box">
|
||||
<el-icon><Sunrise /></el-icon>
|
||||
<div class="head-box-title"><p style="color: red; font-size: 16px; font-weight: 800">入侵警告</p></div>
|
||||
<!-- <div class="head-box-btn" @click="readAll">全部已读</div> -->
|
||||
</div>
|
||||
<div v-loading="state.loading" class="content-box">
|
||||
<template v-if="newsList.length > 0">
|
||||
<template v-if="warningListRows.length > 0">
|
||||
<!-- <div v-for="(v, k) in newsList" :key="k" class="content-box-item" @click="onNewsClick(k)"> -->
|
||||
<div v-for="(v, k) in newsList" :key="k" class="content-box-item">
|
||||
<div v-for="(v, k) in warningListRows" :key="k" class="content-box-item">
|
||||
<div class="item-conten">
|
||||
<div>{{ v.message }}</div>
|
||||
<div>{{ `设备[${v.deviceName}] 检测到异常移动, ${v.alarmTime}` }}</div>
|
||||
<div class="content-box-msg"></div>
|
||||
<div class="content-box-time">{{ v.time }}</div>
|
||||
<!-- <div class="content-box-time">{{ v.time }}</div> -->
|
||||
</div>
|
||||
<!-- 已读/未读 -->
|
||||
<!-- <span v-if="v.read" class="el-tag el-tag--success el-tag--mini read">已读</span> -->
|
||||
<button v-if="!v.read" class="el-tag el-tag--danger el-tag--mini read" @click="handleView(v)">查看画面</button>
|
||||
<el-button type="danger" @click="handleSee(v)">查看画面</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-else :description="'消息为空'"></el-empty>
|
||||
</div>
|
||||
<!-- <div v-if="newsList.length > 0" class="foot-box" @click="onGoToGiteeClick">前往gitee</div> -->
|
||||
<!-- 添加或修改测试单对话框 -->
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body fullscreen destroy-on-close>
|
||||
<el-dialog
|
||||
v-model="dialog.visible"
|
||||
:title="dialog.title"
|
||||
width="500px"
|
||||
class="monitor-dialog"
|
||||
:before-close="handleMonitorDialogClose"
|
||||
fullscreen
|
||||
destroy-on-close
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex w-full items-center justify-between gap-2 min-w-0 pr-8">
|
||||
<span class="truncate">实时监控-北门出入门监控</span>
|
||||
<span class="truncate">实时监控-{{ monitoringArea }}监控</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="monitor-video-wrap">
|
||||
<div class="monitor-video-stack">
|
||||
<video ref="monitorVideoRef" class="monitor-video" controls preload="metadata" :src="monitorVideoUrl">您的浏览器不支持视频播放</video>
|
||||
<video ref="monitorVideoRef" class="monitor-video" preload="metadata">您的浏览器不支持视频播放</video>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -40,10 +49,42 @@
|
||||
<script setup lang="ts" name="layoutBreadcrumbUserNews">
|
||||
import { useNoticeStore } from '@/store/modules/notice';
|
||||
import { listNotice } from '@/api/system/notice';
|
||||
import { initHls, destroyHls } from '@/utils/video';
|
||||
import { onBeforeRouteLeave } from 'vue-router';
|
||||
import { useWarningListStore } from '@/store/modules/warnningList';
|
||||
import type { WarningListResult } from '@/api/WarningList/types';
|
||||
import { addDemo } from '@/api/video/index';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
import { nextTick, onUnmounted } from 'vue';
|
||||
import { useVideosStore } from '@/store/modules/video';
|
||||
const videosStore = useVideosStore();
|
||||
const monitoringArea = ref<string>('');
|
||||
|
||||
const warningListStore = useWarningListStore();
|
||||
const noticeStore = useNoticeStore();
|
||||
const { getWarningList, handleAction } = warningListStore;
|
||||
const { warningListTotal, warningList } = storeToRefs(warningListStore);
|
||||
|
||||
// const props = defineProps<{
|
||||
// warningList?: WarningListResult;
|
||||
// }>();
|
||||
const warningListRows = computed(() => warningList.value?.rows || []);
|
||||
const { readAll } = useNoticeStore();
|
||||
const monitorVideoRef = ref<HTMLVideoElement>();
|
||||
|
||||
const getList = async () => {
|
||||
try {
|
||||
await getWarningList({
|
||||
status: 0,
|
||||
pageNum: 1,
|
||||
pageSize: 300
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取消息列表失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 定义变量内容
|
||||
const state = reactive({
|
||||
@@ -57,16 +98,62 @@ const dialog = reactive<DialogOption>({
|
||||
title: ''
|
||||
});
|
||||
|
||||
const parseNoticeItem = (notice: any) => {
|
||||
const rawMessage = String(notice.message ?? '');
|
||||
const [messagePart, videoIdPart] = rawMessage.split('|', 2);
|
||||
return {
|
||||
...notice,
|
||||
rawMessage,
|
||||
message: messagePart?.trim() ?? rawMessage,
|
||||
videoId: videoIdPart?.trim() ?? ''
|
||||
};
|
||||
};
|
||||
|
||||
//查看画面
|
||||
const handleView = (row: any) => {
|
||||
console.log('查看画面', row);
|
||||
// 在这里可以添加打开画面的逻辑
|
||||
if (!row || !row.videoUrl) {
|
||||
proxy?.$modal.msgError('视频地址不存在');
|
||||
const currentDeviceId = ref<string | number>();
|
||||
|
||||
/** 刷新视频(卡死时自动调用) */
|
||||
const handleRefresh = async () => {
|
||||
if (!currentDeviceId.value) return;
|
||||
destroyHls(monitorVideoRef);
|
||||
monitorVideoUrl.value = '';
|
||||
await videosStore.getVideos(currentDeviceId.value);
|
||||
monitorVideoUrl.value = videosStore.hisUrl || '';
|
||||
if (monitorVideoUrl.value) {
|
||||
await nextTick();
|
||||
initHls(monitorVideoUrl, monitorVideoRef, () => dialog.visible, handleRefresh);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSee = async (row: any) => {
|
||||
currentDeviceId.value = row?.deviceId;
|
||||
const videoId = row?.deviceId;
|
||||
console.log('查看画面', row, 'deviceId', videoId);
|
||||
if (!videoId) {
|
||||
proxy?.$modal.msgError('视频ID不存在');
|
||||
return;
|
||||
}
|
||||
monitorVideoUrl.value = row.videoUrl;
|
||||
await videosStore.getVideos(videoId);
|
||||
monitorVideoUrl.value = videosStore.hisUrl;
|
||||
monitoringArea.value = row.monitoringArea;
|
||||
dialog.visible = true;
|
||||
await nextTick();
|
||||
initHls(monitorVideoUrl, monitorVideoRef, () => dialog.visible, handleRefresh);
|
||||
await handleAction(row.id);
|
||||
await getList();
|
||||
};
|
||||
|
||||
const handleMonitorDialogClose = async (done: () => void) => {
|
||||
try {
|
||||
addDemo(currentDeviceId.value as string).then(() => {
|
||||
console.log('关闭视频111111');
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('关闭视频22222222', error);
|
||||
}
|
||||
// 卸载 Hls 与清理 video
|
||||
destroyHls(monitorVideoRef);
|
||||
done();
|
||||
};
|
||||
/**
|
||||
* 过滤消息(只保留最新的5条)
|
||||
@@ -76,7 +163,7 @@ const handleView = (row: any) => {
|
||||
const filterMessages = (messages: any[]) => {
|
||||
// 如果消息超过5条,则只保留最新的5条
|
||||
if (messages.length > 5) {
|
||||
return messages.slice(0, 5); // 只取前5条最新消息
|
||||
return messages.slice(-5); // 只取最后5条最新消息
|
||||
}
|
||||
|
||||
return messages;
|
||||
@@ -90,10 +177,9 @@ const getTableData = async () => {
|
||||
state.loading = true;
|
||||
const allNotices = [...noticeStore.state.notices]; // 复制数组避免直接修改原数据
|
||||
|
||||
// 过滤消息
|
||||
const filteredNotices = filterMessages(allNotices);
|
||||
// 过滤消息并解析 message/videoId
|
||||
const filteredNotices = allNotices.map(parseNoticeItem);
|
||||
newsList.value = filteredNotices;
|
||||
|
||||
state.loading = false;
|
||||
};
|
||||
|
||||
@@ -101,8 +187,9 @@ const getTableData = async () => {
|
||||
watch(
|
||||
() => noticeStore.state.notices,
|
||||
(newNotices) => {
|
||||
const filteredNotices = filterMessages([...newNotices]);
|
||||
const filteredNotices = [...newNotices].map(parseNoticeItem);
|
||||
newsList.value = filteredNotices;
|
||||
// console.log('监听消息队列', newsList.value);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
@@ -123,11 +210,13 @@ const onNewsClick = (item: any) => {
|
||||
|
||||
// 辅助函数:找到原始索引
|
||||
const findOriginalIndex = (filteredIndex: number) => {
|
||||
const filteredNotices = filterMessages([...noticeStore.state.notices]);
|
||||
const filteredNotices = [...noticeStore.state.notices].map(parseNoticeItem);
|
||||
const currentMessage = filteredNotices[filteredIndex];
|
||||
|
||||
// 在原始数组中查找对应消息的索引
|
||||
return noticeStore.state.notices.findIndex((notice: any) => notice.message === currentMessage.message && notice.time === currentMessage.time);
|
||||
return noticeStore.state.notices.findIndex(
|
||||
(notice: any) => String(notice.message) === currentMessage.rawMessage && notice.time === currentMessage.time
|
||||
);
|
||||
};
|
||||
|
||||
// 前往通知中心点击
|
||||
@@ -141,14 +230,82 @@ onMounted(() => {
|
||||
console.log('消息列表:', newsList.value);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
dialog.visible = false;
|
||||
destroyHls(monitorVideoRef);
|
||||
monitorVideoUrl.value = '';
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
destroyHls(monitorVideoRef);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
console.log('111111111111111111111_warningList.value', warningList.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 全屏监控弹窗:占满视口、禁止外层滚动,视频在可视区内尽可能放大 */
|
||||
.monitor-video-stack {
|
||||
:deep(.monitor-dialog.el-dialog) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.monitor-dialog .el-dialog__header) {
|
||||
flex-shrink: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
:deep(.monitor-dialog .el-dialog__body) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.monitor-video-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.monitor-video-stack {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monitor-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain; //cover裁剪
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.monitor-draw-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
cursor: crosshair;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.layout-navbars-breadcrumb-user-news {
|
||||
@@ -157,9 +314,14 @@ onMounted(() => {
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
box-sizing: border-box;
|
||||
color: var(--el-text-color-primary);
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
height: 35px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
.el-icon {
|
||||
font-size: 25px;
|
||||
color: red;
|
||||
}
|
||||
.head-box-btn {
|
||||
color: var(--el-color-primary);
|
||||
font-size: 13px;
|
||||
@@ -175,15 +337,19 @@ onMounted(() => {
|
||||
overflow: auto;
|
||||
font-size: 13px;
|
||||
.content-box-item {
|
||||
padding-top: 12px;
|
||||
background-color: rgba(255, 182, 193, 0.5);
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
// gap: 10px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 8px;
|
||||
&:last-of-type {
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.content-box-msg {
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
// margin-top: 5px;
|
||||
// margin-bottom: 5px;
|
||||
}
|
||||
.content-box-time {
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
22
src/main.ts
22
src/main.ts
@@ -30,6 +30,8 @@ import './permission';
|
||||
|
||||
// 国际化
|
||||
import i18n from '@/lang/index';
|
||||
import { useSettingsStore } from '@/store/modules/settings';
|
||||
import { getDemo } from '@/api/systemList/index';
|
||||
|
||||
// vxeTable
|
||||
import VXETable from 'vxe-table';
|
||||
@@ -54,4 +56,24 @@ app.use(plugins);
|
||||
// 自定义指令
|
||||
directive(app);
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
getDemo()
|
||||
.then((res) => {
|
||||
const name = res?.data?.name;
|
||||
const url = res?.data?.url;
|
||||
if (name || url) {
|
||||
settingsStore.setSystemInfo(name || '', url || '');
|
||||
|
||||
if (url) {
|
||||
const link = document.querySelector("link[rel='icon']") as HTMLLinkElement | null;
|
||||
if (link) {
|
||||
link.href = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// ignore
|
||||
});
|
||||
|
||||
app.mount('#app');
|
||||
|
||||
@@ -27,92 +27,93 @@ import Layout from '@/layout/index.vue';
|
||||
// 公共路由
|
||||
export const constantRoutes: RouteRecordRaw[] = [
|
||||
/* ----------------------------------------自定义路由(查看)-------------------------------------------------------- */
|
||||
//设备管理
|
||||
{
|
||||
path: '/equipmentList',
|
||||
component: Layout,
|
||||
name: 'Equipment',
|
||||
redirect: '/equipmentList/list',
|
||||
alwaysShow: true,
|
||||
meta: { title: '设备管理', icon: 'equipment' },
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'EquipmentList',
|
||||
component: () => import('@/views/equipmentList/list.vue'),
|
||||
meta: { title: '设备列表', icon: 'equipment' }
|
||||
}
|
||||
]
|
||||
},
|
||||
//报警管理
|
||||
{
|
||||
path: '/warningList',
|
||||
component: Layout,
|
||||
name: 'Warning',
|
||||
redirect: '/warningList/list',
|
||||
alwaysShow: true,
|
||||
meta: { title: '报警管理', icon: 'warningList' },
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'WarningList',
|
||||
component: () => import('@/views/WarningList/list.vue'),
|
||||
meta: { title: '报警列表', icon: 'warningList' }
|
||||
}
|
||||
]
|
||||
},
|
||||
//录像文件管理
|
||||
{
|
||||
path: '/fileList',
|
||||
component: Layout,
|
||||
name: 'File',
|
||||
redirect: '/fileList/list',
|
||||
meta: { title: '录像文件管理', icon: 'fileList' },
|
||||
alwaysShow: true,
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'FileList',
|
||||
component: () => import('@/views/fileList/list.vue'),
|
||||
meta: { title: '录像列表', icon: 'fileList' }
|
||||
}
|
||||
]
|
||||
},
|
||||
//系统设置
|
||||
{
|
||||
path: '/systemList',
|
||||
component: Layout,
|
||||
name: 'system',
|
||||
redirect: '/systemList/base',
|
||||
alwaysShow: true,
|
||||
meta: { title: '系统设置', icon: 'systemList' },
|
||||
children: [
|
||||
{
|
||||
path: 'base',
|
||||
name: 'Base',
|
||||
component: () => import('@/views/systemList/base/base.vue'),
|
||||
meta: { title: '基础设置', icon: 'base' }
|
||||
},
|
||||
{
|
||||
path: 'password',
|
||||
name: 'Password',
|
||||
component: () => import('@/views/systemList/password/password.vue'),
|
||||
meta: { title: '修改密码', icon: 'password' }
|
||||
},
|
||||
{
|
||||
path: 'role',
|
||||
name: 'Role',
|
||||
component: () => import('@/views/systemList/role/role.vue'),
|
||||
meta: { title: '角色配置', icon: 'role' }
|
||||
},
|
||||
{
|
||||
path: 'user',
|
||||
name: 'User',
|
||||
component: () => import('@/views/systemList/user/user.vue'),
|
||||
meta: { title: '用户配置', icon: 'user' }
|
||||
}
|
||||
]
|
||||
},
|
||||
// 以下路由已迁移至后端动态路由管理
|
||||
// 设备管理
|
||||
// {
|
||||
// path: '/equipmentList',
|
||||
// component: Layout,
|
||||
// name: 'Equipment',
|
||||
// redirect: '/equipmentList/list',
|
||||
// alwaysShow: true,
|
||||
// meta: { title: '设备管理', icon: 'equipment' },
|
||||
// children: [
|
||||
// {
|
||||
// path: 'list',
|
||||
// name: 'EquipmentList',
|
||||
// component: () => import('@/views/equipmentList/list.vue'),
|
||||
// meta: { title: '设备列表', icon: 'equipment' }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// // 报警管理
|
||||
// {
|
||||
// path: '/warningList',
|
||||
// component: Layout,
|
||||
// name: 'Warning',
|
||||
// redirect: '/warningList/list',
|
||||
// alwaysShow: true,
|
||||
// meta: { title: '报警管理', icon: 'warningList' },
|
||||
// children: [
|
||||
// {
|
||||
// path: 'list',
|
||||
// name: 'WarningList',
|
||||
// component: () => import('@/views/WarningList/list.vue'),
|
||||
// meta: { title: '报警列表', icon: 'warningList' }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// // 录像文件管理
|
||||
// {
|
||||
// path: '/fileList',
|
||||
// component: Layout,
|
||||
// name: 'File',
|
||||
// redirect: '/fileList/list',
|
||||
// meta: { title: '录像文件管理', icon: 'fileList' },
|
||||
// alwaysShow: true,
|
||||
// children: [
|
||||
// {
|
||||
// path: 'list',
|
||||
// name: 'FileList',
|
||||
// component: () => import('@/views/fileList/list.vue'),
|
||||
// meta: { title: '录像列表', icon: 'fileList' }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// // 系统设置
|
||||
// {
|
||||
// path: '/systemList',
|
||||
// component: Layout,
|
||||
// name: 'system',
|
||||
// redirect: '/systemList/base',
|
||||
// alwaysShow: true,
|
||||
// meta: { title: '系统设置', icon: 'systemList' },
|
||||
// children: [
|
||||
// {
|
||||
// path: 'base',
|
||||
// name: 'Base',
|
||||
// component: () => import('@/views/systemList/base/base.vue'),
|
||||
// meta: { title: '基础设置', icon: 'base' }
|
||||
// },
|
||||
// {
|
||||
// path: 'password',
|
||||
// name: 'Password',
|
||||
// component: () => import('@/views/systemList/password/password.vue'),
|
||||
// meta: { title: '修改密码', icon: 'password' }
|
||||
// },
|
||||
// {
|
||||
// path: 'role',
|
||||
// name: 'Role',
|
||||
// component: () => import('@/views/systemList/role/role.vue'),
|
||||
// meta: { title: '角色配置', icon: 'role' }
|
||||
// },
|
||||
// {
|
||||
// path: 'user',
|
||||
// name: 'User',
|
||||
// component: () => import('@/views/systemList/user/user.vue'),
|
||||
// meta: { title: '用户配置', icon: 'user' }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
/* ----------------------------------------自带路由配置-------------------------------------------------------- */
|
||||
{
|
||||
path: '/redirect',
|
||||
@@ -154,12 +155,13 @@ export const constantRoutes: RouteRecordRaw[] = [
|
||||
path: '',
|
||||
component: Layout,
|
||||
redirect: '/index',
|
||||
hidden: true,
|
||||
children: [
|
||||
{
|
||||
path: '/index',
|
||||
component: () => import('@/views/index.vue'),
|
||||
name: 'Index',
|
||||
meta: { title: '首页', icon: 'dashboard', affix: true }
|
||||
path: 'index',
|
||||
component: () => import('@/views/equipmentList/list.vue'),
|
||||
name: 'EquipmentList',
|
||||
meta: { title: '设备列表', icon: 'equipment', affix: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
30
src/store/modules/equipmentList.ts
Normal file
30
src/store/modules/equipmentList.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import { getDevicetList } from '@/api/equipmentList/index';
|
||||
|
||||
export const useDeviceStore = defineStore('devices', () => {
|
||||
// --- State (状态) ---
|
||||
// 相当于 Vuex 中的 state,用 ref 或 reactive 定义
|
||||
const deviceList = ref();
|
||||
|
||||
// --- Getters (计算属性) ---
|
||||
// 相当于 Vuex 中的 getters,直接用 computed 定义
|
||||
|
||||
// --- Actions (动作) ---
|
||||
// 相当于 Vuex 中的 actions/mutations,直接写同步或异步函数
|
||||
async function getdeviceList(query?: any) {
|
||||
try {
|
||||
const response = await getDevicetList(query);
|
||||
console.log('获取设备列表:', response);
|
||||
deviceList.value = response.rows;
|
||||
} catch (error) {
|
||||
console.error('获取设备列表失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
// ⚠️ 核心点:必须把需要暴露出去的变量和方法 return 出去!
|
||||
return {
|
||||
deviceList,
|
||||
getdeviceList
|
||||
};
|
||||
});
|
||||
@@ -10,7 +10,8 @@ interface NoticeItem {
|
||||
|
||||
export const useNoticeStore = defineStore('notice', () => {
|
||||
const state = reactive({
|
||||
notices: [] as NoticeItem[]
|
||||
notices: [] as NoticeItem[],
|
||||
isNotice: false as boolean
|
||||
});
|
||||
|
||||
const addNotice = (notice: NoticeItem) => {
|
||||
|
||||
@@ -19,6 +19,8 @@ export const useSettingsStore = defineStore('setting', () => {
|
||||
radiusBase: defaultSettings.radiusBase
|
||||
});
|
||||
const title = ref<string>(defaultSettings.title);
|
||||
const appTitle = ref<string>(defaultSettings.title);
|
||||
const appLogoUrl = ref<string>('');
|
||||
const theme = ref<string>(storageSetting.value.theme);
|
||||
const sideTheme = ref<string>(storageSetting.value.sideTheme);
|
||||
const showSettings = ref<boolean>(defaultSettings.showSettings);
|
||||
@@ -36,8 +38,17 @@ export const useSettingsStore = defineStore('setting', () => {
|
||||
title.value = value;
|
||||
useDynamicTitle();
|
||||
};
|
||||
|
||||
const setSystemInfo = (name: string, url: string) => {
|
||||
appTitle.value = name || defaultSettings.title;
|
||||
appLogoUrl.value = url || '';
|
||||
useDynamicTitle();
|
||||
};
|
||||
|
||||
return {
|
||||
title,
|
||||
appTitle,
|
||||
appLogoUrl,
|
||||
theme,
|
||||
sideTheme,
|
||||
showSettings,
|
||||
@@ -50,6 +61,7 @@ export const useSettingsStore = defineStore('setting', () => {
|
||||
dark,
|
||||
navType,
|
||||
radiusBase,
|
||||
setTitle
|
||||
setTitle,
|
||||
setSystemInfo
|
||||
};
|
||||
});
|
||||
|
||||
@@ -37,7 +37,12 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
||||
});
|
||||
};
|
||||
const addVisitedView = (view: RouteLocationNormalized): void => {
|
||||
if (visitedViews.value.some((v: RouteLocationNormalized) => v.path === view.path)) return;
|
||||
// 同时通过 path、fullPath、name 去重
|
||||
if (visitedViews.value.some((v: RouteLocationNormalized) =>
|
||||
v.path === view.path ||
|
||||
v.fullPath === view.fullPath ||
|
||||
(v.name && view.name && v.name === view.name)
|
||||
)) return;
|
||||
visitedViews.value.push(
|
||||
Object.assign({}, view, {
|
||||
title: view.meta?.title || 'no-name'
|
||||
|
||||
43
src/store/modules/video.ts
Normal file
43
src/store/modules/video.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import { addDemo, listDemo } from '@/api/video';
|
||||
import Hls from 'hls.js';
|
||||
|
||||
export const useVideosStore = defineStore('videos', () => {
|
||||
// --- State (状态) ---
|
||||
// 相当于 Vuex 中的 state,用 ref 或 reactive 定义
|
||||
const hisUrl = ref('');
|
||||
|
||||
// --- Getters (计算属性) ---
|
||||
// 相当于 Vuex 中的 getters,直接用 computed 定义
|
||||
|
||||
// --- Actions (动作) ---
|
||||
// 相当于 Vuex 中的 actions/mutations,直接写同步或异步函数
|
||||
async function getVideos(deviceId?: string | number) {
|
||||
hisUrl.value = '';
|
||||
try {
|
||||
const response = await listDemo(deviceId);
|
||||
console.log('获取视频:', response);
|
||||
hisUrl.value = response?.data || '';
|
||||
} catch (error) {
|
||||
console.error('获取视频失败', error);
|
||||
hisUrl.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function stopVideos(query: any) {
|
||||
try {
|
||||
const response = await addDemo(query);
|
||||
console.log('停止视频:', response);
|
||||
} catch (error) {
|
||||
console.error('停止视频失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
// ⚠️ 核心点:必须把需要暴露出去的变量和方法 return 出去!
|
||||
return {
|
||||
hisUrl,
|
||||
getVideos,
|
||||
stopVideos
|
||||
};
|
||||
});
|
||||
45
src/store/modules/warnningList.ts
Normal file
45
src/store/modules/warnningList.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/** 此数据源跟表列不一致,(后端接口重复使用因逻辑复杂, 经商定不动后端接口, 设置此数据源) 适用于消息 */
|
||||
/** 抽取公用代码更加复杂模糊-暂存数据源 */
|
||||
import { listDemo, updateDemo } from '@/api/WarningList/index';
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import { any } from 'vue-types';
|
||||
|
||||
export const useWarningListStore = defineStore('warningListStore', () => {
|
||||
// --- State (状态) ---
|
||||
const warningList = ref();
|
||||
const warningListTotal = ref(0);
|
||||
// --- Getters (计算属性) ---
|
||||
|
||||
// --- Actions (动作) ---
|
||||
async function getWarningList(query?: any) {
|
||||
try {
|
||||
const response = await listDemo({
|
||||
...query
|
||||
// deviceId,
|
||||
// status: 0,
|
||||
// pageNum: 1,
|
||||
// pageSize: 10
|
||||
});
|
||||
warningList.value = response.data;
|
||||
warningListTotal.value = response.data.total || 0;
|
||||
} catch (error) {
|
||||
console.error('获取消息列表失败', error);
|
||||
}
|
||||
}
|
||||
async function handleAction(ids: string | number | Array<string | number>) {
|
||||
try {
|
||||
await updateDemo(ids);
|
||||
} catch (error) {
|
||||
console.error('处理告警失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 核心点:必须把需要暴露出去的变量和方法 return 出去!
|
||||
return {
|
||||
warningList,
|
||||
warningListTotal,
|
||||
getWarningList,
|
||||
handleAction
|
||||
};
|
||||
});
|
||||
@@ -6,9 +6,12 @@ import { useSettingsStore } from '@/store/modules/settings';
|
||||
*/
|
||||
export const useDynamicTitle = () => {
|
||||
const settingsStore = useSettingsStore();
|
||||
const pageTitle = settingsStore.title || (defaultSettings.title as string);
|
||||
const appTitle = settingsStore.appTitle || (defaultSettings.title as string);
|
||||
|
||||
if (settingsStore.dynamicTitle) {
|
||||
document.title = settingsStore.title + ' - ' + import.meta.env.VITE_APP_TITLE;
|
||||
document.title = `${pageTitle} - ${appTitle}`;
|
||||
} else {
|
||||
document.title = defaultSettings.title as string;
|
||||
document.title = appTitle;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import { useNoticeStore } from '@/store/modules/notice';
|
||||
import { toRefs } from 'vue';
|
||||
|
||||
// 初始化
|
||||
export const initSSE = (url: any) => {
|
||||
const noticeStore = useNoticeStore();
|
||||
const { isNotice, notices } = toRefs(noticeStore.state);
|
||||
if (import.meta.env.VITE_APP_SSE === 'false') {
|
||||
return;
|
||||
}
|
||||
@@ -26,17 +29,27 @@ export const initSSE = (url: any) => {
|
||||
|
||||
watch(data, () => {
|
||||
if (!data.value) return;
|
||||
|
||||
// 打印接收到的 SSE 消息
|
||||
// console.log('=== SSE 接收到消息 ===');
|
||||
// console.log('消息内容:', data.value);
|
||||
// console.log('消息类型:', typeof data.value);
|
||||
// console.log('接收时间:', new Date().toLocaleString());
|
||||
// console.log('=====================');
|
||||
|
||||
useNoticeStore().addNotice({
|
||||
message: data.value,
|
||||
read: false,
|
||||
time: new Date().toLocaleString()
|
||||
});
|
||||
ElNotification({
|
||||
title: '消息',
|
||||
message: data.value,
|
||||
type: 'success',
|
||||
duration: 3000
|
||||
});
|
||||
const displayMessage = data.value.split('|')[0].trim();
|
||||
// ElNotification({
|
||||
// title: '消息',
|
||||
// message: displayMessage,
|
||||
// type: 'success',
|
||||
// duration: 3000
|
||||
// });
|
||||
noticeStore.state.isNotice = true;
|
||||
data.value = null;
|
||||
});
|
||||
};
|
||||
|
||||
256
src/utils/video.ts
Normal file
256
src/utils/video.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import Hls from 'hls.js';
|
||||
|
||||
/**
|
||||
* 每个播放器实例维护自己的状态,避免全局单例互相踩踏
|
||||
*/
|
||||
interface HlsPlayerState {
|
||||
hls: Hls;
|
||||
video: HTMLVideoElement;
|
||||
/** live edge 追赶定时器 */
|
||||
syncTimer: ReturnType<typeof setInterval> | null;
|
||||
/** 卡死检测定时器 */
|
||||
stallTimer: ReturnType<typeof setInterval> | null;
|
||||
/** 连续加载失败计数 */
|
||||
consecutiveFailures: number;
|
||||
/** 是否已销毁 */
|
||||
destroyed: boolean;
|
||||
}
|
||||
|
||||
const playerStates = new WeakMap<HTMLVideoElement, HlsPlayerState>();
|
||||
|
||||
/**
|
||||
* 直播流追赶逻辑:检测播放位置与 live edge 的差距,超过阈值就 seek
|
||||
*/
|
||||
function startLiveSync(state: HlsPlayerState) {
|
||||
const { hls, video } = state;
|
||||
state.syncTimer = setInterval(() => {
|
||||
if (state.destroyed) return;
|
||||
try {
|
||||
const buffered = video.buffered;
|
||||
if (buffered.length === 0) return;
|
||||
// 取最后一个 buffer range 的 end 作为 live edge
|
||||
const liveEdge = buffered.end(buffered.length - 1);
|
||||
const currentTime = video.currentTime;
|
||||
const gap = liveEdge - currentTime;
|
||||
// 超过 6 秒就 seek 到 live edge 附近
|
||||
if (gap > 300) {
|
||||
video.currentTime = liveEdge - 2;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, 60 * 1000);
|
||||
}
|
||||
|
||||
function stopLiveSync(state: HlsPlayerState) {
|
||||
if (state.syncTimer) {
|
||||
clearInterval(state.syncTimer);
|
||||
state.syncTimer = null;
|
||||
}
|
||||
if (state.stallTimer) {
|
||||
clearInterval(state.stallTimer);
|
||||
state.stallTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const initHls = (monitorVideoUrl, monitorVideoRef, isDialogVisible?: () => boolean, onStall?: () => void) => {
|
||||
console.log('isDialogVisible', isDialogVisible);
|
||||
const url = monitorVideoUrl.value;
|
||||
const video = monitorVideoRef.value;
|
||||
if (!url || !video) return;
|
||||
|
||||
// 先销毁该 video 元素上已有的实例(不再操作全局变量)
|
||||
const existingState = playerStates.get(video);
|
||||
if (existingState) {
|
||||
try {
|
||||
stopLiveSync(existingState);
|
||||
existingState.destroyed = true;
|
||||
existingState.hls.destroy();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
playerStates.delete(video);
|
||||
}
|
||||
|
||||
if (Hls.isSupported()) {
|
||||
const hls = new Hls({
|
||||
// ★ ZLM 注册流需要时间,配置自动重试
|
||||
manifestLoadingMaxRetry: 5,
|
||||
manifestLoadingRetryDelay: 1000,
|
||||
manifestLoadingTimeOut: 5000,
|
||||
levelLoadingMaxRetry: 5,
|
||||
levelLoadingRetryDelay: 500,
|
||||
lowLatencyMode: false,
|
||||
startLevel: -1,
|
||||
// ★ 直播流关键配置
|
||||
liveSyncDurationCount: 3,
|
||||
liveDurationInfinity: true,
|
||||
// 缓冲区上限,防止无限增长导致内存爆掉
|
||||
maxBufferLength: 5,
|
||||
maxMaxBufferLength: 10,
|
||||
// manifest 加载:最多重试 5 次,每次间隔 1 秒,单次超时 10 秒
|
||||
// manifestLoadingMaxRetry: 5,
|
||||
// manifestLoadingRetryDelay: 1000,
|
||||
// manifestLoadingTimeOut: 10000,
|
||||
// // level(清晰度)加载:最多重试 3 次,每次间隔 1 秒,单次超时 15 秒
|
||||
// levelLoadingMaxRetry: 3,
|
||||
// levelLoadingRetryDelay: 1000,
|
||||
// levelLoadingTimeOut: 15000,
|
||||
// lowLatencyMode: true,
|
||||
// startLevel: -1,
|
||||
// // 直播同步:滞后 2 个分片时自动追赶 live edge
|
||||
// liveSyncDurationCount: 2,
|
||||
// liveDurationInfinity: true,
|
||||
// // 不保留已播放过的旧缓冲,减少内存占用
|
||||
backBufferLength: 0
|
||||
// // 缓冲区上限 5~10 秒,限制同时发出的分片请求数,防止占满连接池
|
||||
// maxBufferLength: 5,
|
||||
// maxMaxBufferLength: 10,
|
||||
// // 分片加载:最多重试 3 次,每次间隔 2 秒,单次超时 15 秒
|
||||
// fragLoadingMaxRetry: 3,
|
||||
// fragLoadingRetryDelay: 2000,
|
||||
// fragLoadingTimeOut: 15000
|
||||
// // 允许缓冲区内最大 0.5 秒的空洞,避免小卡顿触发不必要的重载
|
||||
// maxBufferHole: 0.5
|
||||
});
|
||||
|
||||
hls.loadSource(url);
|
||||
hls.attachMedia(video);
|
||||
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
try {
|
||||
video.play().catch(() => {});
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
const state: HlsPlayerState = {
|
||||
hls,
|
||||
video,
|
||||
syncTimer: null,
|
||||
stallTimer: null,
|
||||
consecutiveFailures: 0,
|
||||
destroyed: false
|
||||
};
|
||||
playerStates.set(video, state);
|
||||
|
||||
/**
|
||||
* 完善的错误处理
|
||||
*/
|
||||
hls.on(Hls.Events.ERROR, (event, data) => {
|
||||
// 对话框已关闭则不再恢复
|
||||
if (isDialogVisible && !isDialogVisible()) return;
|
||||
|
||||
if (data.fatal) {
|
||||
switch (data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
if (data.details === 'manifestLoadError' || data.details === 'manifestLoadTimeOut') {
|
||||
//console.error('网络错误');
|
||||
ElMessage.error('[HLS]网络错误,尝试恢复...');
|
||||
onStall();
|
||||
} else {
|
||||
ElMessage.error('[HLS] 网络错误,自动恢复...');
|
||||
hls.startLoad();
|
||||
}
|
||||
break;
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
ElMessage.error('[HLS] 媒体解码错误,尝试恢复...');
|
||||
hls.recoverMediaError();
|
||||
break;
|
||||
default:
|
||||
ElMessage.error('[HLS] 不可恢复的错误,销毁实例');
|
||||
state.destroyed = true;
|
||||
stopLiveSync(state);
|
||||
hls.destroy();
|
||||
playerStates.delete(video);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// ★ 非致命错误也处理,防止问题累积
|
||||
// 记录加载失败
|
||||
state.consecutiveFailures++;
|
||||
switch (data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
// 分片加载失败等 → 重新加载
|
||||
if (data.details === 'fragLoadError' || data.details === 'fragLoadTimeOut') {
|
||||
ElMessage.error(`[HLS] 分片加载问题 (${data.details}),第 ${state.consecutiveFailures} 次失败,尝试恢复...`);
|
||||
hls.startLoad(hls.currentLevel);
|
||||
}
|
||||
break;
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
if (data.details === 'bufferAppendError' || data.details === 'bufferFullError') {
|
||||
ElMessage.error(`[HLS] 缓冲区问题 (${data.details}),尝试恢复...`);
|
||||
hls.recoverMediaError();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
ElMessage.error(`[HLS] 非致命错误: ${data.type} - ${data.details}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 卡死检测:每 10 秒检查一次是否还在正常加载分片
|
||||
* 如果连续多次加载失败,触发 onStall 回调(自动刷新)
|
||||
*/
|
||||
state.stallTimer = setInterval(() => {
|
||||
if (state.destroyed || !onStall) return;
|
||||
if (state.consecutiveFailures >= 3) {
|
||||
ElMessage.error(`[HLS] 连续 ${state.consecutiveFailures} 次加载失败,触发自动刷新`);
|
||||
state.consecutiveFailures = 0;
|
||||
onStall();
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
// 播放开始后启动 live edge 追赶
|
||||
hls.on(Hls.Events.FRAG_BUFFERED, () => {
|
||||
// 加载成功,重置失败计数
|
||||
state.consecutiveFailures = 0;
|
||||
if (!state.syncTimer) {
|
||||
startLiveSync(state);
|
||||
}
|
||||
});
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
// Safari 原生 HLS
|
||||
video.src = url;
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
try {
|
||||
video.play().catch(() => {});
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessage.error('当前浏览器不支持 HLS 播放');
|
||||
}
|
||||
};
|
||||
|
||||
export const destroyHls = (monitorVideoRef) => {
|
||||
const video = monitorVideoRef.value;
|
||||
if (video) {
|
||||
// 销毁该 video 元素上的 HLS 实例
|
||||
const state = playerStates.get(video);
|
||||
if (state) {
|
||||
try {
|
||||
stopLiveSync(state);
|
||||
state.destroyed = true;
|
||||
state.hls.destroy();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
playerStates.delete(video);
|
||||
}
|
||||
// 清理原生 src,避免残留
|
||||
try {
|
||||
video.pause();
|
||||
video.removeAttribute('src');
|
||||
if (typeof video.load === 'function') video.load();
|
||||
} catch (e) {
|
||||
console.log('Error in destroyHls:', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default { initHls, destroyHls };
|
||||
@@ -7,9 +7,9 @@
|
||||
<el-form-item label="报警编号" prop="alarmCode">
|
||||
<el-input v-model="queryParams.alarmCode" placeholder="请输入报警编号" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="设备选择" prop="userName">
|
||||
<el-select v-model="queryParams.userName" placeholder="请选择" clearable style="width: 160px">
|
||||
<el-option label="绑定的设备" :value="0" />
|
||||
<el-form-item label="设备选择" prop="deviceId">
|
||||
<el-select v-model="queryParams.deviceId" placeholder="请选择" clearable style="width: 160px">
|
||||
<el-option v-for="item in deviceInfo" :key="item.id || item.code" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="报警时间" prop="queryTime">
|
||||
@@ -39,19 +39,31 @@
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="flex justify-end mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['demo:demo:edit']" type="success" plain icon="Edit" :disabled="multiple" @click="handleBatchProcess"
|
||||
>批量处理</el-button
|
||||
>
|
||||
<el-button type="success" plain icon="Edit" :disabled="multiple" @click="handleBatchProcess">批量处理</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="demoList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="selection" width="55" align="center" :selectable="checkSelectable" />
|
||||
<el-table-column label="报警编号" align="center" prop="alarmCode" width="180" />
|
||||
<el-table-column label="报警时间" align="center" prop="alarmTime" min-width="170" />
|
||||
<el-table-column label="所属设备" align="center" prop="deviceName" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="报警区域" align="center" prop="monitoringArea" min-width="120" show-overflow-tooltip />
|
||||
<!-- pay content -->
|
||||
<!-- <el-table-column label="报警图片" align="center" prop="alarmImage" min-width="120">
|
||||
<template #default="scope">
|
||||
<el-image
|
||||
v-if="scope.row.alarmImage"
|
||||
:src="scope.row.alarmImage"
|
||||
:preview-src-list="[scope.row.alarmImage]"
|
||||
preview-teleported
|
||||
fit="cover"
|
||||
style="width: 60px; height: 60px; border-radius: 4px"
|
||||
/>
|
||||
<span v-else>暂无图片</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column label="处理状态" align="center" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.status === 0" type="warning">未处理</el-tag>
|
||||
@@ -118,6 +130,17 @@
|
||||
<script setup name="WarningList" lang="ts">
|
||||
import { listDemo, updateDemo } from '@/api/WarningList/index';
|
||||
//import { DemoVO, DemoQuery, DemoForm } from '@/api/WarningList/types';
|
||||
import { useDeviceStore } from '@/store/modules/equipmentList';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useWarningListStore } from '@/store/modules/warnningList';
|
||||
|
||||
const warningListStore = useWarningListStore();
|
||||
const { getWarningList, handleAction } = warningListStore;
|
||||
const { warningListTotal, warningList } = storeToRefs(warningListStore);
|
||||
|
||||
const deviceStore = useDeviceStore();
|
||||
const { deviceList } = storeToRefs(deviceStore);
|
||||
const { getdeviceList } = deviceStore;
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
@@ -149,6 +172,7 @@ const initFormData: any = {
|
||||
processingTime: undefined
|
||||
};
|
||||
/* ?id=&deviceId=&pageSize=1&pageNum=1&beginTime=&endTime= */
|
||||
/** 查询参数 */
|
||||
const data = reactive({
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
@@ -169,27 +193,35 @@ const data = reactive({
|
||||
|
||||
const { queryParams, form, rules, queryTime } = toRefs(data);
|
||||
|
||||
/** 获取列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await listDemo(queryParams.value);
|
||||
demoList.value = res.data.rows;
|
||||
total.value = res.total;
|
||||
total.value = res.data.total;
|
||||
await getWarningList({
|
||||
status: 0,
|
||||
pageNum: 1,
|
||||
pageSize: 300
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
reset();
|
||||
dialog.visible = false;
|
||||
};
|
||||
|
||||
/** 重置按钮 */
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
demoFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/** 查询按钮 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
queryParams.value.beginTime = queryTime.value[0];
|
||||
@@ -197,11 +229,13 @@ const handleQuery = () => {
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置按钮 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryTime.value = [];
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: any) => {
|
||||
ids.value = selection.map((item) => item.id as string | number);
|
||||
multiple.value = !selection.length;
|
||||
@@ -213,7 +247,7 @@ const handleUpdate = async (row: any) => {
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
await updateDemo(row.id);
|
||||
await handleAction(row.id);
|
||||
proxy?.$modal.msgSuccess('处理成功');
|
||||
await getList();
|
||||
} finally {
|
||||
@@ -221,6 +255,11 @@ const handleUpdate = async (row: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
/** 检查行是否可选(已处理不可选) */
|
||||
const checkSelectable = (row: any) => {
|
||||
return row.status !== 1;
|
||||
};
|
||||
|
||||
/** 批量:确认后直接提交 */
|
||||
const handleBatchProcess = async () => {
|
||||
if (!ids.value.length) {
|
||||
@@ -230,7 +269,7 @@ const handleBatchProcess = async () => {
|
||||
await proxy?.$modal.confirm('是否确认批量处理已选中的报警?');
|
||||
loading.value = true;
|
||||
try {
|
||||
await updateDemo(ids.value);
|
||||
await handleAction(ids.value);
|
||||
proxy?.$modal.msgSuccess('处理成功');
|
||||
await getList();
|
||||
} finally {
|
||||
@@ -238,6 +277,7 @@ const handleBatchProcess = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = async () => {
|
||||
if (form.value.id == null) {
|
||||
proxy?.$modal.msgWarning('数据异常');
|
||||
@@ -254,7 +294,28 @@ const submitForm = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
//获取设备名称-搜索_设备选择
|
||||
const deviceInfo = ref([]);
|
||||
const getDeviceName = async () => {
|
||||
try {
|
||||
await getdeviceList();
|
||||
deviceInfo.value = deviceList.value;
|
||||
console.log('设备列表:', deviceInfo.value);
|
||||
} catch (error) {
|
||||
proxy?.$modal.msgError('获取设备列表失败');
|
||||
}
|
||||
// const res = await getdeviceList();
|
||||
// deviceInfo.value = res?.rows;
|
||||
};
|
||||
onMounted(() => {
|
||||
getList();
|
||||
getDeviceName();
|
||||
});
|
||||
|
||||
//监听数据源变化,自动刷新列表
|
||||
watch(warningListTotal, async (newVal) => {
|
||||
//console.log('监听到报警列表变化,自动刷新列表', newVal);
|
||||
//数据源不同, 接口复用,请求多了一遍(不属于bug)
|
||||
await getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<el-select v-model="queryParams.status" placeholder="请选择设备状态" clearable style="width: 160px">
|
||||
<el-option label="在线" :value="1" />
|
||||
<el-option label="离线" :value="2" />
|
||||
<el-option label="低帧" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
@@ -56,31 +57,149 @@
|
||||
<el-table-column v-if="true" label="设备编号" align="center" prop="code" />
|
||||
<el-table-column label="设备名称" align="center" prop="name" />
|
||||
<el-table-column label="监控区域" align="center" prop="monitoringArea" />
|
||||
<el-table-column label="设备状态" align="center" prop="status" width="100">
|
||||
<el-table-column label="设备状态" align="center" prop="statusName" width="100">
|
||||
<!-- <template #default="scope">
|
||||
<el-tag v-if="scope.row.status === 1 || scope.row.status === '1'">在线</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === 2 || scope.row.status === '2'" type="warning">离线</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === 3 || scope.row.status === '3'" type="info">低帧</el-tag>
|
||||
<el-tag v-else>{{ scope.row.status ?? '-' }}</el-tag>
|
||||
</template> -->
|
||||
</el-table-column>
|
||||
<!-- <el-table-column label="电池电量" align="center" prop="batteryLevel1" width="150">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.status === 1 || scope.row.status === '1'">在线</span>
|
||||
<span v-else-if="scope.row.status === 2 || scope.row.status === '2'">离线</span>
|
||||
<span v-else>{{ scope.row.status ?? '-' }}</span>
|
||||
<el-tag v-if="scope.row.batteryLevel1">左{{ scope.row.batteryLevel1 + '%' }}</el-tag>
|
||||
<el-tag v-if="scope.row.batteryLevel2" :style="scope.row.batteryLevel1 && scope.row.batteryLevel2 ? 'margin-left: 10px' : ''">
|
||||
右{{ scope.row.batteryLevel2 + '%' }}
|
||||
</el-tag>
|
||||
<el-tag v-if="!scope.row.batteryLevel1 && !scope.row.batteryLevel2"> 未获取 </el-tag>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column label="电池电量" align="center" width="160">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.batteryLevel1 != null || scope.row.batteryLevel2 != null" class="battery-group">
|
||||
<!-- 左电池 -->
|
||||
<div v-if="scope.row.batteryLevel1 != null" class="battery-item">
|
||||
<svg viewBox="0 0 56 20" width="56" height="20">
|
||||
<rect
|
||||
x="0"
|
||||
y="1"
|
||||
width="44"
|
||||
height="18"
|
||||
rx="4"
|
||||
ry="4"
|
||||
fill="none"
|
||||
:stroke="getBatteryColor(scope.row.batteryLevel1)"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<rect x="44" y="6" width="4" height="8" rx="2" ry="2" :fill="getBatteryColor(scope.row.batteryLevel1)" />
|
||||
<rect
|
||||
x="2"
|
||||
y="3"
|
||||
:width="Math.max((40 * scope.row.batteryLevel1) / 100, 0)"
|
||||
height="14"
|
||||
rx="2"
|
||||
ry="2"
|
||||
:fill="getBatteryColor(scope.row.batteryLevel1)"
|
||||
opacity="0.25"
|
||||
/>
|
||||
<text
|
||||
x="24"
|
||||
y="11.5"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
font-size="2"
|
||||
:fill="getBatteryColor(scope.row.batteryLevel1)"
|
||||
>
|
||||
左{{ scope.row.batteryLevel1 }}%
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 右电池 -->
|
||||
<div v-if="scope.row.batteryLevel2 != null" class="battery-item">
|
||||
<svg viewBox="0 0 56 20" width="56" height="20">
|
||||
<rect
|
||||
x="0"
|
||||
y="1"
|
||||
width="44"
|
||||
height="18"
|
||||
rx="4"
|
||||
ry="4"
|
||||
fill="none"
|
||||
:stroke="getBatteryColor(scope.row.batteryLevel2)"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<rect x="44" y="6" width="4" height="8" rx="2" ry="2" :fill="getBatteryColor(scope.row.batteryLevel2)" />
|
||||
<rect
|
||||
x="2"
|
||||
y="3"
|
||||
:width="Math.max((40 * scope.row.batteryLevel2) / 100, 0)"
|
||||
height="14"
|
||||
rx="2"
|
||||
ry="2"
|
||||
:fill="getBatteryColor(scope.row.batteryLevel2)"
|
||||
opacity="0.25"
|
||||
/>
|
||||
<text
|
||||
x="24"
|
||||
y="11.5"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
font-size="2"
|
||||
:fill="getBatteryColor(scope.row.batteryLevel2)"
|
||||
>
|
||||
右{{ scope.row.batteryLevel2 }}%
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else>未获取</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column align="center" label="电量" prop="powerLevel" width="160">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.powerLevel !== undefined && scope.row.powerLevel !== null && scope.row.powerLevel !== ''" class="power-level">
|
||||
<svg class="power-level__icon" :class="getPowerLevelClass(scope.row.powerLevel)" viewBox="0 0 48 24" width="48" height="24">
|
||||
<rect x="0" y="2" width="42" height="20" rx="4" ry="4" fill="none" stroke="currentColor" stroke-width="2" />
|
||||
|
||||
<rect x="42" y="8" width="4" height="8" rx="2" ry="2" fill="currentColor" />
|
||||
|
||||
<rect
|
||||
x="3"
|
||||
y="5"
|
||||
:width="Math.max((36 * formatPowerLevel(scope.row.powerLevel)) / 100, 0)"
|
||||
height="14"
|
||||
rx="2"
|
||||
ry="2"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span class="power-level__text">{{ formatPowerLevel(scope.row.powerLevel) }}%</span>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column label="设备型号" align="center" prop="model" />
|
||||
<el-table-column label="固件版本" align="center" prop="id" />
|
||||
<el-table-column label="固件版本" align="center" prop="version" />
|
||||
<el-table-column label="是否绘制区域" align="center" prop="isDrawArea">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.isDrawArea === 1 || scope.row.isDrawArea === '1'">是</span>
|
||||
<span v-else-if="scope.row.isDrawArea === 0 || scope.row.isDrawArea === '0'">否</span>
|
||||
<span v-else>{{ scope.row.isDrawArea ?? '-' }}</span>
|
||||
<el-tag v-if="scope.row.isDrawArea === 1 || scope.row.isDrawArea === '1'">是</el-tag>
|
||||
<el-tag v-else-if="scope.row.isDrawArea === 0 || scope.row.isDrawArea === '0'" type="info">否</el-tag>
|
||||
<el-tag v-else>{{ scope.row.isDrawArea ?? '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="绑定时间" align="center" prop="isDelete" />
|
||||
<el-table-column label="绑定时间" align="center" prop="activateTime" />
|
||||
<el-table-column label="设备异常说明" align="center" prop="remark" />
|
||||
<el-table-column width="400" label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" @click="handleUpdate(scope.row)">编辑</el-button>
|
||||
<el-button type="primary" @click="handleSee(scope.row)">查看监控</el-button>
|
||||
<el-button :disabled="scope.row.status === 1 || scope.row.status === '1'" type="primary" @click="handleActivate(scope.row)"
|
||||
<el-button :disabled="scope.row.status == 2 && scope.row.status == '2'" type="primary" @click="handleSee(scope.row)">查看监控</el-button>
|
||||
<el-button
|
||||
:disabled="scope.row.status === 1 || scope.row.status === 2 || scope.row.status === '1' || scope.row.status === '2'"
|
||||
type="primary"
|
||||
@click="handleActivate(scope.row)"
|
||||
>唤醒设备</el-button
|
||||
>
|
||||
<el-button type="primary" @click="handleDelete(scope.row)">删除</el-button>
|
||||
<el-button :disabled="scope.row.status !== 2 && scope.row.status !== '2'" type="primary" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -111,8 +230,10 @@
|
||||
<el-dialog v-model="dialog1.visible" class="monitor-dialog" :before-close="handleMonitorDialogClose" fullscreen destroy-on-close>
|
||||
<template #header>
|
||||
<div class="flex w-full items-center justify-between gap-2 min-w-0 pr-8">
|
||||
<span class="truncate">实时监控-北门出入门监控</span>
|
||||
<span class="truncate">{{ videoName }}-{{ monitoringArea }}监控</span>
|
||||
<div>
|
||||
<!-- <el-button type="primary" class="shrink-0" @click="handleVideo(videosInfo)">切换摄像头</el-button> -->
|
||||
<el-button type="primary" class="shrink-0" @click="handleRefresh">刷新</el-button>
|
||||
<el-button v-if="!seeSee" type="primary" class="shrink-0" @click="drawImage()">绘制区域</el-button>
|
||||
<el-button v-if="seeSee" type="primary" class="shrink-0" @click="clearImage()">清空区域</el-button>
|
||||
<el-button v-if="seeSee" type="primary" class="shrink-0" @click="saveImage()">保存区域</el-button>
|
||||
@@ -121,7 +242,7 @@
|
||||
</template>
|
||||
<div class="monitor-video-wrap">
|
||||
<div class="monitor-video-stack">
|
||||
<video ref="monitorVideoRef" class="monitor-video" controls preload="metadata" :src="monitorVideoUrl">您的浏览器不支持视频播放</video>
|
||||
<video ref="monitorVideoRef" class="monitor-video" preload="metadata">您的浏览器不支持视频播放</video>
|
||||
<div v-show="monitorDrawCollecting" class="monitor-draw-overlay" @click.stop="onMonitorDrawClick" />
|
||||
<div v-if="monitorRectPx" class="monitor-draw-rect" :style="drawRectStyle" />
|
||||
<div v-if="monitorRectPx" class="monitor-draw-label" :style="drawLabelStyle">当前报警区域</div>
|
||||
@@ -134,11 +255,14 @@
|
||||
<script setup name="Demo" lang="ts">
|
||||
import { addEquipment, getDevicetList, getDeviceInfo, delDevice, actDevice, updateDeviceInfo, drawInfo } from '@/api/equipmentList/index';
|
||||
import { EquipmentList, DeviceInfoForm, AlarmAreaData } from '@/api/equipmentList/types';
|
||||
import { listDemo, getDemo, delDemo, addDemo, updateDemo } from '@/api/demo/demo';
|
||||
import { DemoVO, DemoQuery, DemoForm } from '@/api/demo/demo/types';
|
||||
import { log } from 'node:console';
|
||||
import Hls from 'hls.js';
|
||||
import { initHls, destroyHls } from '@/utils/video';
|
||||
import { nextTick } from 'vue';
|
||||
import { useVideosStore } from '@/store/modules/video';
|
||||
import { addDemo } from '@/api/video/index';
|
||||
import { AnyARecord } from 'dns';
|
||||
|
||||
const videosStore = useVideosStore();
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const demoList = ref([]);
|
||||
@@ -150,6 +274,7 @@ const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
const seeSee = ref(0);
|
||||
const haveCode = ref(false);
|
||||
const videoName = ref<string>('实时监控');
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const demoFormRef = ref<ElFormInstance>();
|
||||
@@ -163,13 +288,17 @@ const dialog1 = reactive<DialogOption>({
|
||||
title: ''
|
||||
});
|
||||
|
||||
const monitorVideoUrl = 'http://192.168.0.214:8088/5a0db047943df198aff98b650ac9e001.mp4';
|
||||
const monitorVideoUrl = ref('');
|
||||
const monitorVideoRef = ref<HTMLVideoElement>();
|
||||
let visibilityHandler: (() => void) | null = null;
|
||||
const monitoringArea = ref<string>('');
|
||||
|
||||
type MonitorPoint = { x: number; y: number };
|
||||
const monitorDrawCollecting = ref(false);
|
||||
const monitorDrawPoints = ref<MonitorPoint[]>([]);
|
||||
const monitorRectPx = ref<{ left: number; top: number; width: number; height: number } | null>(null);
|
||||
const currentDrawArea = ref<AlarmAreaData | null>(null);
|
||||
const videoResizeObserver = ref<ResizeObserver | null>(null);
|
||||
|
||||
const drawRectStyle = computed<Record<string, string>>(() => {
|
||||
const r = monitorRectPx.value;
|
||||
@@ -316,23 +445,194 @@ const resetMonitorDraw = () => {
|
||||
monitorDrawCollecting.value = false;
|
||||
monitorDrawPoints.value = [];
|
||||
monitorRectPx.value = null;
|
||||
currentDrawArea.value = null;
|
||||
};
|
||||
// const videosInfo = ref();
|
||||
const handleSee = async (row?: any) => {
|
||||
console.log('1111111111111111', row, row.drawArea);
|
||||
// videosInfo.value = row;
|
||||
reset();
|
||||
resetMonitorDraw();
|
||||
seeSee.value = row?.isDrawArea === 1 ? 1 : 0;
|
||||
monitoringArea.value = row?.monitoringArea;
|
||||
const _id = row?.id || ids.value[0];
|
||||
const res = await getDeviceInfo(_id);
|
||||
Object.assign(form.value, res.data);
|
||||
dialog1.visible = true;
|
||||
dialog1.title = '修改设备';
|
||||
|
||||
destroyHls(monitorVideoRef);
|
||||
monitorVideoUrl.value = '';
|
||||
await videosStore.getVideos(row.id);
|
||||
monitorVideoUrl.value = videosStore.hisUrl || '';
|
||||
|
||||
if (!monitorVideoUrl.value) {
|
||||
//proxy?.$modal.msgWarning('该设备未登录或无法获取直播地址,已停止播放旧画面');
|
||||
initVideoResizeObserver();
|
||||
return;
|
||||
}
|
||||
|
||||
// 等待 DOM 更新后初始化 Hls
|
||||
await nextTick();
|
||||
initHls(monitorVideoUrl, monitorVideoRef, () => dialog1.visible, handleRefresh);
|
||||
// 等待视频加载完成后初始化 ResizeObserver,以便能正确获取视频尺寸
|
||||
initVideoResizeObserver();
|
||||
|
||||
// 如果有 drawArea,直接使用它显示矩形区域,不允许点击绘制
|
||||
if (row?.drawArea) {
|
||||
try {
|
||||
// 将 drawArea 从字符串转为对象
|
||||
const drawArea = typeof row.drawArea === 'string' ? JSON.parse(row.drawArea) : row.drawArea;
|
||||
console.log('[查看监控] 解析后的 drawArea:', drawArea);
|
||||
|
||||
if (drawArea && drawArea.x1 !== undefined && drawArea.y1 !== undefined && drawArea.x2 !== undefined && drawArea.y2 !== undefined) {
|
||||
currentDrawArea.value = drawArea;
|
||||
// 等待视频加载完成
|
||||
await nextTick();
|
||||
const video = monitorVideoRef.value;
|
||||
|
||||
// 如果视频已加载,立即显示矩形
|
||||
if (video && video.videoWidth > 0 && video.videoHeight > 0) {
|
||||
displayDrawArea(drawArea, video);
|
||||
} else if (video) {
|
||||
// 如果视频还未加载,等待 loadedmetadata 事件
|
||||
video.addEventListener(
|
||||
'loadedmetadata',
|
||||
() => {
|
||||
displayDrawArea(drawArea, video);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[查看监控] 解析 drawArea 失败:', error);
|
||||
}
|
||||
}
|
||||
// 后台运行的时候恢复播放
|
||||
cleanupVisibilityChange();
|
||||
const handleVisibilityChange = () => {
|
||||
if (!document.hidden) {
|
||||
const video = monitorVideoRef.value;
|
||||
if (video && video.paused) {
|
||||
video.play().catch((e) => console.warn('恢复播放失败:', e));
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
visibilityHandler = handleVisibilityChange;
|
||||
};
|
||||
|
||||
// 提取显示报警区域的逻辑为独立函数
|
||||
const displayDrawArea = (drawArea: any, video: HTMLVideoElement) => {
|
||||
const VW = video.videoWidth;
|
||||
const VH = video.videoHeight;
|
||||
if (!VW || !VH) return;
|
||||
|
||||
// 计算视频原始像素坐标(x 是距离右侧的千分比,y 是距离顶部的千分比)
|
||||
const x1Video = VW - (drawArea.x1 / 1000) * VW;
|
||||
const y1Video = (drawArea.y1 / 1000) * VH;
|
||||
const x2Video = VW - (drawArea.x2 / 1000) * VW;
|
||||
const y2Video = (drawArea.y2 / 1000) * VH;
|
||||
|
||||
const rect = video.getBoundingClientRect();
|
||||
const dw = rect.width;
|
||||
const dh = rect.height;
|
||||
if (dw <= 0 || dh <= 0) return;
|
||||
|
||||
const scale = Math.min(dw / VW, dh / VH);
|
||||
const contentW = VW * scale;
|
||||
const contentH = VH * scale;
|
||||
const offsetX = (dw - contentW) / 2;
|
||||
const offsetY = (dh - contentH) / 2;
|
||||
|
||||
const x1Px = offsetX + x1Video * scale;
|
||||
const y1Px = offsetY + y1Video * scale;
|
||||
const x2Px = offsetX + x2Video * scale;
|
||||
const y2Px = offsetY + y2Video * scale;
|
||||
|
||||
const left = Math.min(x1Px, x2Px);
|
||||
const top = Math.min(y1Px, y2Px);
|
||||
const width = Math.max(Math.abs(x2Px - x1Px), 2);
|
||||
const height = Math.max(Math.abs(y2Px - y1Px), 2);
|
||||
|
||||
monitorRectPx.value = { left, top, width, height };
|
||||
currentDrawArea.value = drawArea;
|
||||
monitorDrawCollecting.value = false; // 禁止点击绘制
|
||||
|
||||
// 同时添加到 payload 用于保存
|
||||
payload.value = [
|
||||
{
|
||||
deviceId: form.value.id,
|
||||
x1: drawArea.x1,
|
||||
y1: drawArea.y1,
|
||||
x2: drawArea.x2,
|
||||
y2: drawArea.y2
|
||||
}
|
||||
];
|
||||
|
||||
console.log('[查看监控] 已加载已有报警区域', payload.value);
|
||||
};
|
||||
// 在窗口调整大小时更新绘制区域位置和大小
|
||||
const updateDrawAreaOnResize = () => {
|
||||
const drawArea = currentDrawArea.value;
|
||||
const video = monitorVideoRef.value;
|
||||
if (!drawArea || !video || video.videoWidth <= 0 || video.videoHeight <= 0) return;
|
||||
displayDrawArea(drawArea, video);
|
||||
};
|
||||
// 初始化 ResizeObserver 监听视频元素尺寸变化,以便在窗口调整大小时更新绘制区域位置和大小
|
||||
const initVideoResizeObserver = () => {
|
||||
if (!window.ResizeObserver || videoResizeObserver.value) return;
|
||||
const video = monitorVideoRef.value;
|
||||
if (!video) return;
|
||||
videoResizeObserver.value = new ResizeObserver(() => {
|
||||
updateDrawAreaOnResize();
|
||||
});
|
||||
videoResizeObserver.value.observe(video);
|
||||
};
|
||||
// 销毁 ResizeObserver
|
||||
const destroyVideoResizeObserver = () => {
|
||||
if (videoResizeObserver.value) {
|
||||
videoResizeObserver.value.disconnect();
|
||||
videoResizeObserver.value = null;
|
||||
}
|
||||
};
|
||||
// 清理 visibilitychange 事件监听
|
||||
const cleanupVisibilityChange = () => {
|
||||
if (visibilityHandler) {
|
||||
document.removeEventListener('visibilitychange', visibilityHandler);
|
||||
visibilityHandler = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleMonitorDialogClose = (done: () => void) => {
|
||||
monitorVideoRef.value?.pause();
|
||||
monitorVideoRef.value && (monitorVideoRef.value.currentTime = 0);
|
||||
try {
|
||||
addDemo(form.value.id).then(() => {
|
||||
console.log('关闭视频111111');
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('关闭视频22222222', error);
|
||||
}
|
||||
// 卸载 Hls 与清理 video
|
||||
destroyHls(monitorVideoRef);
|
||||
monitorVideoUrl.value = '';
|
||||
destroyVideoResizeObserver();
|
||||
resetMonitorDraw();
|
||||
cleanupVisibilityChange();
|
||||
done();
|
||||
//刷新列表
|
||||
getList();
|
||||
};
|
||||
|
||||
const handleMonitorDialogOk = async () => {
|
||||
if (monitorDrawCollecting.value) {
|
||||
proxy?.$modal.msgWarning('请先完成报警区域绘制');
|
||||
return;
|
||||
}
|
||||
if (!currentDrawArea.value) {
|
||||
proxy?.$modal.msgWarning('请先绘制报警区域');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/** 显示坐标(相对 video 元素) → 视频原始像素坐标,含 object-fit: contain 留白换算 */
|
||||
@@ -362,6 +662,7 @@ const payload = ref<AlarmAreaData[]>([]);
|
||||
const drawImage = () => {
|
||||
resetMonitorDraw();
|
||||
monitorDrawCollecting.value = true;
|
||||
videoName.value = '绘制区域';
|
||||
proxy?.$modal.msgSuccess('请在视频画面上连续点击两个点,生成报警区域');
|
||||
};
|
||||
|
||||
@@ -412,18 +713,19 @@ const onMonitorDrawClick = (e: MouseEvent) => {
|
||||
const point2RightPctOfWidth = ((VW - v2.vx) / VW) * 1000;
|
||||
const point2TopPctOfHeight = (v2.vy / VH) * 1000;
|
||||
|
||||
// 添加新的报警区域到数组
|
||||
const clamp = (value: number) => Math.max(0, Math.min(1000, Math.round(value)));
|
||||
|
||||
const newArea: AlarmAreaData = {
|
||||
deviceId: form.value.id,
|
||||
x1: Number(point1RightPctOfWidth.toFixed(4)),
|
||||
y1: Number(point1TopPctOfHeight.toFixed(4)),
|
||||
x2: Number(point2RightPctOfWidth.toFixed(4)),
|
||||
y2: Number(point2TopPctOfHeight.toFixed(4))
|
||||
x1: clamp(point1RightPctOfWidth),
|
||||
y1: clamp(point1TopPctOfHeight),
|
||||
x2: clamp(point2RightPctOfWidth),
|
||||
y2: clamp(point2TopPctOfHeight)
|
||||
};
|
||||
payload.value.push(newArea);
|
||||
currentDrawArea.value = newArea;
|
||||
console.log('[监控区域] 两点千分比坐标', newArea);
|
||||
console.log('[监控区域] 所有报警区域', payload.value);
|
||||
//proxy?.$modal.msgSuccess(`已采集(相对原视频):x右距${payload.value.x}% y顶距${payload.value.y}% w框宽${payload.value.w}% h框高${payload.value.h}%`);
|
||||
seeSee.value = 1;
|
||||
};
|
||||
|
||||
@@ -435,14 +737,33 @@ const clearImage = () => {
|
||||
// 清除绘制的矩形图案
|
||||
monitorRectPx.value = null;
|
||||
monitorDrawPoints.value = [];
|
||||
monitorDrawCollecting.value = false; // 重置为不允许点击绘制状态
|
||||
};
|
||||
|
||||
const saveImage = async () => {
|
||||
console.log('[保存区域] 准备保存的报警区域列表', payload.value);
|
||||
|
||||
await drawInfo(payload.value);
|
||||
drawStart.value = false;
|
||||
seeSee.value = 0;
|
||||
try {
|
||||
const response = await drawInfo(payload.value);
|
||||
proxy?.$modal.msgSuccess('保存成功');
|
||||
console.log('[保存区域] 保存结果', response);
|
||||
seeSee.value = 0;
|
||||
dialog1.visible = false;
|
||||
//重新获取数据
|
||||
await getList();
|
||||
} catch (error) {
|
||||
proxy?.$modal.msgSuccess('保存成功, 请联系管理员');
|
||||
console.error('[保存区域] 保存失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMonitorDialogOpen = () => {
|
||||
// 初始化 Hls 与 video
|
||||
initHls(monitorVideoUrl, monitorVideoRef, () => dialog1.visible);
|
||||
initVideoResizeObserver();
|
||||
monitorDrawCollecting.value = false;
|
||||
monitorDrawPoints.value = [];
|
||||
monitorRectPx.value = null;
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
@@ -468,6 +789,46 @@ const handleExport = () => {
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => seeSee.value,
|
||||
(newValue) => {
|
||||
videoName.value = newValue === 1 ? '绘制区域' : '实时监控';
|
||||
}
|
||||
);
|
||||
/** 刷新按钮操作 */
|
||||
// const handleVideo = async (row?: any) => {
|
||||
// console.log('11111111111', row);
|
||||
// handleSee(row);
|
||||
// };
|
||||
|
||||
/** 刷新按钮操作 */
|
||||
const handleRefresh = async () => {
|
||||
if (!form.value.id) return;
|
||||
destroyHls(monitorVideoRef);
|
||||
monitorVideoUrl.value = '';
|
||||
await videosStore.getVideos(form.value.id);
|
||||
monitorVideoUrl.value = videosStore.hisUrl || '';
|
||||
if (monitorVideoUrl.value) {
|
||||
await nextTick();
|
||||
initHls(monitorVideoUrl, monitorVideoRef, () => dialog1.visible, handleRefresh);
|
||||
}
|
||||
};
|
||||
|
||||
/** 电池颜色:>50% 绿,30-50% 黄,<30% 红 */
|
||||
const getBatteryColor = (val: number) => {
|
||||
if (val > 50) return '#52c41a';
|
||||
if (val >= 30) return '#faad14';
|
||||
return '#ff4d4f';
|
||||
};
|
||||
|
||||
const formatPowerLevel = (val: any) => Number(val) || 0;
|
||||
const getPowerLevelClass = (val: any) => {
|
||||
const v = Number(val);
|
||||
if (v > 50) return 'power--high';
|
||||
if (v >= 30) return 'power--mid';
|
||||
return 'power--low';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -552,4 +913,32 @@ onMounted(() => {
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.battery-group {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.battery-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
.battery-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
/* 电量电池颜色 */
|
||||
.power--high {
|
||||
color: #52c41a;
|
||||
}
|
||||
.power--mid {
|
||||
color: #faad14;
|
||||
}
|
||||
.power--low {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="设备选择" prop="userName">
|
||||
<el-select v-model="queryParams.userName" placeholder="请选择" clearable style="width: 160px">
|
||||
<el-option label="绑定的设备" :value="0" />
|
||||
<el-form-item label="设备选择" prop="deviceId">
|
||||
<el-select v-model="queryParams.deviceId" placeholder="请选择" clearable style="width: 160px">
|
||||
<el-option v-for="item in deviceInfo" :key="item.id || item.code" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="报警编号" prop="alarmCode">
|
||||
@@ -14,7 +14,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="报警时间" prop="queryTime">
|
||||
<el-date-picker
|
||||
v-model="queryTime"
|
||||
v-model="queryParams.queryTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
@@ -51,7 +51,7 @@
|
||||
</el-row>
|
||||
</template> -->
|
||||
|
||||
<el-table class="monitor-dialog" v-loading="loading" border :data="demoList" @selection-change="handleSelectionChange">
|
||||
<el-table v-loading="loading" border :data="demoList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="录像编号" align="center" prop="code" width="180" />
|
||||
<el-table-column label="文件名称" align="center" prop="videoName" min-width="170" />
|
||||
@@ -71,10 +71,10 @@
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
<!-- 添加或修改测试单对话框 -->
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body fullscreen destroy-on-close>
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" class="monitor-dialog" fullscreen destroy-on-close>
|
||||
<template #header>
|
||||
<div class="flex w-full items-center justify-between gap-2 min-w-0 pr-8">
|
||||
<span class="truncate">实时监控-北门出入门监控</span>
|
||||
<span class="truncate">播放-{{ videoName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="monitor-video-wrap">
|
||||
@@ -89,7 +89,13 @@
|
||||
<script setup name="Demo" lang="ts">
|
||||
import { listDemo, getDemo, delDemo, addDemo, updateDemo } from '@/api/fileList/index';
|
||||
import { download } from '@/utils/request';
|
||||
import { onBeforeRouteLeave } from 'vue-router';
|
||||
//import { DemoVO, DemoQuery, DemoForm } from '@/api/demo/demo/types';
|
||||
import { useDeviceStore } from '@/store/modules/equipmentList';
|
||||
import { storeToRefs } from 'pinia';
|
||||
const deviceStore = useDeviceStore();
|
||||
const { deviceList } = storeToRefs(deviceStore);
|
||||
const { getdeviceList } = deviceStore;
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
@@ -103,6 +109,7 @@ const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
const monitorVideoUrl = ref();
|
||||
const downloadUrl = ref();
|
||||
const videoName = ref<string>('录像文件');
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const demoFormRef = ref<ElFormInstance>();
|
||||
@@ -127,7 +134,9 @@ const data = reactive<any>({
|
||||
pageSize: 10,
|
||||
userName: undefined,
|
||||
deviceId: undefined,
|
||||
queryTime: undefined
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
queryTime: []
|
||||
},
|
||||
rules: {
|
||||
id: [{ required: true, message: '主键不能为空', trigger: 'blur' }],
|
||||
@@ -142,7 +151,7 @@ const data = reactive<any>({
|
||||
const { queryParams, form, rules } = toRefs(data);
|
||||
|
||||
/** 查询测试单列表 */
|
||||
const getList = async () => {
|
||||
const getList = async (params?: any) => {
|
||||
loading.value = true;
|
||||
const res = await listDemo(queryParams.value);
|
||||
demoList.value = res.rows;
|
||||
@@ -165,12 +174,20 @@ const reset = () => {
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
queryParams.value.startTime = queryParams.value.queryTime?.[0];
|
||||
queryParams.value.endTime = queryParams.value.queryTime?.[1];
|
||||
// queryParams.value.queryTime = undefined;
|
||||
// getList();
|
||||
const { queryTime, ...reqParams } = queryParams.value;
|
||||
getList(reqParams);
|
||||
};
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.value.startTime = undefined;
|
||||
queryParams.value.endTime = undefined;
|
||||
queryParams.value.queryTime = [];
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
@@ -202,9 +219,11 @@ const handlePlay = async (row?: any) => {
|
||||
return;
|
||||
}
|
||||
monitorVideoUrl.value = row.videoUrl;
|
||||
videoName.value = row.videoName;
|
||||
dialog.visible = true;
|
||||
};
|
||||
const handleDown = async (row?: any) => {
|
||||
console.log('111111111', row);
|
||||
if (!row || !row.zipUrl) {
|
||||
proxy?.$modal.msgError('下载地址不存在');
|
||||
return;
|
||||
@@ -266,13 +285,88 @@ const handleExport = () => {
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
dialog.visible = false;
|
||||
monitorVideoUrl.value = '';
|
||||
});
|
||||
|
||||
//获取设备名称-搜索_设备选择
|
||||
const deviceInfo = ref([]);
|
||||
const getDeviceName = async () => {
|
||||
try {
|
||||
await getdeviceList();
|
||||
deviceInfo.value = deviceList.value;
|
||||
console.log('设备列表:', deviceInfo.value);
|
||||
} catch (error) {
|
||||
proxy?.$modal.msgError('获取设备列表失败');
|
||||
}
|
||||
// const res = await getdeviceList();
|
||||
// deviceInfo.value = res?.rows;
|
||||
};
|
||||
onMounted(() => {
|
||||
getDeviceName();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 全屏监控弹窗:占满视口、禁止外层滚动,视频在可视区内尽可能放大 */
|
||||
.monitor-video-stack {
|
||||
:deep(.monitor-dialog.el-dialog) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.monitor-dialog .el-dialog__header) {
|
||||
flex-shrink: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
:deep(.monitor-dialog .el-dialog__body) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.monitor-video-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.monitor-video-stack {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monitor-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain; //cover裁剪
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.monitor-draw-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
cursor: crosshair;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="app-container home">
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :lg="12" style="padding-left: 20px">
|
||||
<h2>RuoYi-Vue-Plus多租户管理系统</h2>
|
||||
<h2>可视化电子围栏管理系统</h2>
|
||||
<p>
|
||||
RuoYi-Vue-Plus 是基于 RuoYi-Vue 针对 分布式集群 场景升级(不兼容原框架)
|
||||
<br />
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="login-brand__logo-wrap">
|
||||
<img class="login-brand__logo" :src="logoUrl" alt="" />
|
||||
</div>
|
||||
<h1 class="login-brand__title">可视化电子围栏管理系统</h1>
|
||||
<h1 class="login-brand__title">{{ logoName }}</h1>
|
||||
<p class="login-brand__subtitle">智能安防 · 实时监控 · 高效管理</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,7 +39,7 @@
|
||||
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="captchaEnabled" prop="code">
|
||||
<el-form-item label="验证码" v-if="captchaEnabled" prop="code">
|
||||
<el-input
|
||||
v-model="loginForm.code"
|
||||
size="large"
|
||||
@@ -80,7 +80,8 @@ import { LoginData, TenantVO } from '@/api/types';
|
||||
import { to } from 'await-to-js';
|
||||
import { HttpStatus } from '@/enums/RespEnum';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import logoUrl from '@/assets/logo/logo.png';
|
||||
import logo from '@/assets/logo/logo.png';
|
||||
import { getDemo } from '@/api/systemList/index';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
@@ -90,8 +91,8 @@ const { t } = useI18n();
|
||||
|
||||
const loginForm = ref<LoginData>({
|
||||
tenantId: '000000',
|
||||
username: 'admin',
|
||||
password: 'admin123',
|
||||
username: '',
|
||||
password: '',
|
||||
rememberMe: false,
|
||||
code: '',
|
||||
uuid: ''
|
||||
@@ -224,6 +225,18 @@ onMounted(() => {
|
||||
initTenantList();
|
||||
getLoginData();
|
||||
});
|
||||
|
||||
// 获取Logo图片
|
||||
const logoUrl = ref();
|
||||
const logoName = ref();
|
||||
onMounted(() => {
|
||||
getDemo().then((res) => {
|
||||
console.log('系统11111111111', res);
|
||||
|
||||
logoName.value = res.data.name;
|
||||
logoUrl.value = res.data.url;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -252,8 +265,8 @@ onMounted(() => {
|
||||
|
||||
.login-brand {
|
||||
flex: 0 0 auto;
|
||||
width: min(420px, 90vw);
|
||||
max-width: 420px;
|
||||
width: min(450px, 90vw);
|
||||
max-width: 450px;
|
||||
height: 450px;
|
||||
max-height: 450px;
|
||||
box-sizing: border-box;
|
||||
@@ -320,8 +333,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
width: min(420px, 100%);
|
||||
max-width: 420px;
|
||||
width: min(450px, 100%);
|
||||
max-width: 450px;
|
||||
height: 450px;
|
||||
max-height: 450px;
|
||||
border-radius: var(--app-radius-lg);
|
||||
@@ -365,7 +378,7 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
min-height: 0;
|
||||
width: min(420px, 90vw);
|
||||
width: min(450px, 90vw);
|
||||
height: 450px;
|
||||
max-height: 450px;
|
||||
padding: 44px 48px 36px;
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
<svg-icon icon-class="email" />用户邮箱
|
||||
<div class="pull-right">{{ state.user.email }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<!-- <li class="list-group-item">
|
||||
<svg-icon icon-class="tree" />所属部门
|
||||
<div v-if="state.user.deptName" class="pull-right">{{ state.user.deptName }} / {{ state.postGroup }}</div>
|
||||
</li>
|
||||
</li> -->
|
||||
<li class="list-group-item">
|
||||
<svg-icon icon-class="peoples" />所属角色
|
||||
<div class="pull-right">{{ state.roleGroup }}</div>
|
||||
@@ -55,9 +55,9 @@
|
||||
<el-tab-pane label="修改密码" name="resetPwd">
|
||||
<resetPwd />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="第三方应用" name="thirdParty">
|
||||
<!-- <el-tab-pane label="第三方应用" name="thirdParty">
|
||||
<thirdParty :auths="state.auths" />
|
||||
</el-tab-pane>
|
||||
</el-tab-pane> -->
|
||||
<el-tab-pane label="在线设备" name="onlineDevice">
|
||||
<onlineDevice :devices="state.devices" />
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<p class="title">系统名称与LOGO配置</p>
|
||||
<el-form ref="ruleFormRef" :model="form">
|
||||
<el-form-item label="系统名称" prop="name">
|
||||
<el-input v-model="form.name" />
|
||||
<el-input v-model="form.name" maxlength="11" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="系统图标" prop="logo">
|
||||
<div class="text-center">
|
||||
@@ -16,8 +16,10 @@
|
||||
:action="uploadUrl"
|
||||
:on-change="handleChange"
|
||||
:before-upload="beforeUpload"
|
||||
:on-exceed="handleExceed"
|
||||
:auto-upload="false"
|
||||
accept="image/jpeg,image/png"
|
||||
:on-preview="handlePictureCardPreview"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-upload>
|
||||
@@ -32,8 +34,8 @@
|
||||
</el-form-item>
|
||||
<el-form-item class="button-item">
|
||||
<div class="flex justify-center gap-4">
|
||||
<el-button @click="cancel" style="width: 120px">取 消</el-button>
|
||||
<el-button type="primary" @click="save" style="width: 120px">保存配置</el-button>
|
||||
<el-button @click="cancel" class="w-120px mr-100px -ml-100px">取 消</el-button>
|
||||
<el-button type="primary" @click="save" class="w-120px">保存配置</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -42,20 +44,18 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import type { UploadProps, UploadUserFile, UploadRequestOptions } from 'element-plus';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import type { UploadProps, UploadUserFile } from 'element-plus';
|
||||
import { uploadDemo, getDemo, updateDemo } from '@/api/systemList/index';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { on } from 'node:cluster';
|
||||
import { get } from 'node:http';
|
||||
import router from '@/router';
|
||||
|
||||
const uploadUrl = import.meta.env.VITE_BASE_API + '/system/logo/upload';
|
||||
|
||||
const form = reactive({
|
||||
const form = ref({
|
||||
name: '可视化电子围栏管理系统',
|
||||
remark: '',
|
||||
logoUrl: ''
|
||||
url: ''
|
||||
});
|
||||
|
||||
const getList = async () => {
|
||||
@@ -70,25 +70,28 @@ const getList = async () => {
|
||||
name: '系统图标'
|
||||
}
|
||||
];
|
||||
form.logoUrl = res.data.url;
|
||||
form.name = res.data.name;
|
||||
form.value.url = res.data.url;
|
||||
form.value.name = res.data.name;
|
||||
}
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
fileList.value = [];
|
||||
form.logoUrl = '';
|
||||
form.name = '';
|
||||
form.value.url = '';
|
||||
form.value.name = '';
|
||||
};
|
||||
const save = async () => {
|
||||
updateDemo(form)
|
||||
console.log('保存数据', form.value);
|
||||
updateDemo(form.value)
|
||||
.then((res) => {
|
||||
console.log('保存成功', res);
|
||||
//ElMessage.success('保存成功');
|
||||
ElMessage.success('保存成功');
|
||||
// 刷新当前路由
|
||||
router.go(0);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('保存失败', err);
|
||||
//ElMessage.error('保存失败');
|
||||
// ElMessage.error('保存失败');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -101,7 +104,12 @@ const dialogVisible = ref(false);
|
||||
|
||||
const handleRemove: UploadProps['onRemove'] = (uploadFile, uploadFiles) => {
|
||||
// 清空 logo 字段
|
||||
form.logoUrl = '';
|
||||
form.value.url = '';
|
||||
};
|
||||
|
||||
// 处理超出限制的情况
|
||||
const handleExceed: UploadProps['onExceed'] = () => {
|
||||
ElMessage.warning('最多只能上传1张图片,请先删除现有图片后再上传');
|
||||
};
|
||||
|
||||
// 自定义 change 处理函数
|
||||
@@ -115,12 +123,10 @@ const handleChange = (file: any) => {
|
||||
uploadDemo(realFile)
|
||||
.then((res) => {
|
||||
console.log('上传成功', res);
|
||||
const logoImage = {
|
||||
name: res.data.name,
|
||||
url: res.data.url
|
||||
};
|
||||
fileList.value = [logoImage];
|
||||
getList(); // 刷新列表获取最新的 logo URL
|
||||
form.value.url = res.data.imgUrl;
|
||||
console.log('上传设备', form.value);
|
||||
fileList.value = [form.value];
|
||||
//getList(); // 刷新列表获取最新的 logo URL
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('上传失败', err);
|
||||
|
||||
@@ -70,7 +70,7 @@ const rules = ref({
|
||||
const submit = async () => {
|
||||
const res = await getUserProfile();
|
||||
console.log('当前用户信息', res);
|
||||
if (!user.value.username != res.data.userName) {
|
||||
if (user.value.username != res.data.user?.userName) {
|
||||
proxy?.$modal.msgError('账号填写错误');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="flex justify-end mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:role:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
|
||||
</el-col>
|
||||
<!-- <el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:role:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
|
||||
@@ -37,23 +37,23 @@
|
||||
|
||||
<el-table ref="roleTableRef" border v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="false" label="角色编号" prop="roleId" width="120" />
|
||||
<el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" width="150" />
|
||||
<el-table-column label="角色描述" prop="roleKey" :show-overflow-tooltip="true" width="200" />
|
||||
<el-table-column label="操作人(需接口)" prop="roleKey" :show-overflow-tooltip="true" width="200" />
|
||||
<el-table-column label="排序" prop="roleSort" width="100" />
|
||||
<el-table-column v-if="false" label="角色编号" prop="roleId" />
|
||||
<el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="角色描述" prop="remark" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="操作人" prop="createUserName" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="排序" prop="roleSort" />
|
||||
<el-table-column label="状态" align="center" width="100">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime">
|
||||
<!-- <el-table-column label="创建时间" align="center" prop="createTime">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column> -->
|
||||
|
||||
<el-table-column fixed="right" label="操作" width="280" align="center">
|
||||
<el-table-column fixed="right" label="操作" width="380" align="center">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" @click="handleUpdate(scope.row)">权限配置</el-button>
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
<el-form-item label="角色名称" prop="roleName">
|
||||
<el-input v-model="form.roleName" placeholder="请输入角色名称" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="roleKey">
|
||||
<!-- <el-form-item prop="roleKey">
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content="控制器中定义的权限字符,如:@SaCheckRole('admin')" placement="top">
|
||||
@@ -95,7 +95,7 @@
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="form.roleKey" placeholder="请输入权限字符" />
|
||||
</el-form-item>
|
||||
</el-form-item> -->
|
||||
<el-form-item label="角色顺序" prop="roleSort">
|
||||
<el-input-number v-model="form.roleSort" controls-position="right" :min="0" />
|
||||
</el-form-item>
|
||||
@@ -137,9 +137,9 @@
|
||||
<el-form-item label="角色名称">
|
||||
<el-input v-model="form.roleName" :disabled="true" />
|
||||
</el-form-item>
|
||||
<el-form-item label="权限字符">
|
||||
<!-- <el-form-item label="权限字符">
|
||||
<el-input v-model="form.roleKey" :disabled="true" />
|
||||
</el-form-item>
|
||||
</el-form-item> -->
|
||||
<el-form-item label="权限范围">
|
||||
<el-select v-model="form.dataScope" @change="dataScopeSelectChange">
|
||||
<el-option v-for="item in dataScopeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
|
||||
@@ -431,6 +431,11 @@ const getMenuAllCheckedKeys = (): any => {
|
||||
const submitForm = () => {
|
||||
roleFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
// 如果 rolekey 为空,自动生成唯一标识
|
||||
if (!form.value.roleKey) {
|
||||
form.value.roleKey = `role_${Date.now()}_${Math.random().toString(36)}`;
|
||||
}
|
||||
|
||||
form.value.menuIds = getMenuAllCheckedKeys();
|
||||
form.value.roleId ? await updateRole(form.value) : await addRole(form.value);
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="用户名称" prop="userName">
|
||||
<el-form-item label="用户账号" prop="userName">
|
||||
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="用户昵称" prop="nickName">
|
||||
<el-input v-model="queryParams.nickName" placeholder="请输入用户昵称" clearable @keyup.enter="handleQuery" />
|
||||
<!-- <el-form-item label="用户账号" prop="nickName">
|
||||
<el-input v-model="queryParams.nickName" placeholder="请输入用户账号" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号码" prop="phonenumber">
|
||||
<el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable @keyup.enter="handleQuery" />
|
||||
@@ -62,7 +62,7 @@
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="flex justify-end mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-has-permi="['system:user:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
|
||||
</el-col>
|
||||
<!-- <el-col :span="1.5">
|
||||
<el-button v-has-permi="['system:user:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">
|
||||
@@ -96,30 +96,37 @@
|
||||
|
||||
<el-table v-loading="loading" border :data="userList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column v-if="columns[0].visible" key="userId" label="用户编号" align="center" prop="userId" />
|
||||
<el-table-column v-if="columns[1].visible" key="userName" label="用户名称" align="center" prop="userName" :show-overflow-tooltip="true" />
|
||||
<el-table-column v-if="columns[2].visible" key="nickName" label="用户昵称" align="center" prop="nickName" :show-overflow-tooltip="true" />
|
||||
<el-table-column v-if="columns[3].visible" key="deptName" label="部门" align="center" prop="deptName" :show-overflow-tooltip="true" />
|
||||
<el-table-column v-if="columns[4].visible" key="phonenumber" label="手机号码" align="center" prop="phonenumber" width="120" />
|
||||
<el-table-column key="userName" label="用户账号" align="center" prop="userName" />
|
||||
<el-table-column v-if="columns[1].visible" key="roleName" label="角色名称" align="center" prop="roleName" :show-overflow-tooltip="true" />
|
||||
<el-table-column v-if="columns[2].visible" key="nickName" label="操作人" align="center" prop="nickName" :show-overflow-tooltip="true" />
|
||||
<el-table-column
|
||||
v-if="columns[3].visible"
|
||||
key="createTime"
|
||||
label="添加时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:show-overflow-tooltip="true"
|
||||
/>
|
||||
<el-table-column v-if="columns[4].visible" key="sort" label="排序" align="center" prop="sort" width="120" />
|
||||
<el-table-column v-if="columns[5].visible" key="status" label="状态" align="center">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="columns[6].visible" label="创建时间" align="center" prop="createTime" width="160">
|
||||
<!-- <el-table-column v-if="columns[6].visible" label="创建时间" align="center" prop="createTime" width="160">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.createTime }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column> -->
|
||||
|
||||
<el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<!-- <el-tooltip v-if="scope.row.userId !== 1" content="修改" placement="top"> -->
|
||||
<el-button v-hasPermi="['system:user:edit']" type="primary" @click="handleUpdate(scope.row)">编辑</el-button>
|
||||
<el-button type="primary" @click="handleUpdate(scope.row)">编辑</el-button>
|
||||
<!-- </el-tooltip>
|
||||
<el-tooltip v-if="scope.row.userId !== 1" content="删除" placement="top"> -->
|
||||
<el-button v-hasPermi="['system:user:remove']" type="primary" @click="handleDelete(scope.row)">删除</el-button>
|
||||
<el-button type="primary" @click="handleDelete(scope.row)">删除</el-button>
|
||||
<!-- </el-tooltip> -->
|
||||
|
||||
<!-- <el-tooltip v-if="scope.row.userId !== 1" content="重置密码" placement="top">
|
||||
@@ -149,11 +156,11 @@
|
||||
<el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="用户昵称" prop="nickName">
|
||||
<el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30" />
|
||||
<el-form-item label="用户名称" prop="nickName">
|
||||
<el-input v-model="form.nickName" placeholder="请输入用户账号" maxlength="30" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId">
|
||||
<!-- <el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId">
|
||||
<el-form-item label="归属部门" prop="deptId">
|
||||
<el-tree-select
|
||||
v-model="form.deptId"
|
||||
@@ -165,9 +172,9 @@
|
||||
@change="handleDeptChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
</el-col> -->
|
||||
<!-- </el-row>
|
||||
<el-row> -->
|
||||
<el-col :span="12">
|
||||
<el-form-item label="手机号码" prop="phonenumber">
|
||||
<el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
|
||||
@@ -178,20 +185,20 @@
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName">
|
||||
<!-- </el-row>
|
||||
<el-row> -->
|
||||
<el-col :span="12" v-if="form.userId == undefined">
|
||||
<el-form-item label="用户账号" prop="userName">
|
||||
<el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item v-if="form.userId == undefined" label="用户密码" prop="password">
|
||||
<el-col :span="12" v-if="form.userId == undefined">
|
||||
<el-form-item label="用户密码" prop="password">
|
||||
<el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<!-- </el-row>
|
||||
<el-row> -->
|
||||
<el-col :span="12">
|
||||
<el-form-item label="用户性别">
|
||||
<el-select v-model="form.sex" placeholder="请选择">
|
||||
@@ -206,9 +213,9 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId">
|
||||
<!-- </el-row>
|
||||
<el-row> -->
|
||||
<!-- <el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId">
|
||||
<el-form-item label="岗位">
|
||||
<el-select v-model="form.postIds" multiple placeholder="请选择">
|
||||
<el-option
|
||||
@@ -220,7 +227,7 @@
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-col> -->
|
||||
<el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId">
|
||||
<el-form-item label="角色" prop="roleIds">
|
||||
<el-select v-model="form.roleIds" filterable multiple placeholder="请选择">
|
||||
@@ -234,8 +241,8 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<!-- </el-row>
|
||||
<el-row> -->
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
|
||||
@@ -335,7 +342,7 @@ const upload = reactive<ImportOption>({
|
||||
const columns = ref<FieldOption[]>([
|
||||
{ key: 0, label: `用户编号`, visible: false, children: [] },
|
||||
{ key: 1, label: `用户名称`, visible: true, children: [] },
|
||||
{ key: 2, label: `用户昵称`, visible: true, children: [] },
|
||||
{ key: 2, label: `用户账号`, visible: true, children: [] },
|
||||
{ key: 3, label: `部门`, visible: true, children: [] },
|
||||
{ key: 4, label: `手机号码`, visible: true, children: [] },
|
||||
{ key: 5, label: `状态`, visible: true, children: [] },
|
||||
@@ -389,7 +396,7 @@ const initData: PageData<UserForm, UserQuery> = {
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
nickName: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }],
|
||||
nickName: [{ required: true, message: '用户账号不能为空', trigger: 'blur' }],
|
||||
password: [
|
||||
{ required: true, message: '用户密码不能为空', trigger: 'blur' },
|
||||
{
|
||||
|
||||
25
start.sh
Normal file
25
start.sh
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 启动命令 : ./start.sh
|
||||
echo "📦 正在安装依赖..."
|
||||
pnpm install
|
||||
|
||||
echo "🚀 正在启动开发服务器..."
|
||||
pnpm run dev
|
||||
|
||||
echo "⏳ 等待服务器启动..."
|
||||
sleep 5
|
||||
|
||||
echo "🌐 正在打开浏览器..."
|
||||
if command -v start &> /dev/null; then
|
||||
# Windows
|
||||
start http://localhost:80
|
||||
elif command -v xdg-open &> /dev/null; then
|
||||
# Linux
|
||||
xdg-open http://localhost:80
|
||||
elif command -v open &> /dev/null; then
|
||||
# macOS
|
||||
open http://localhost:80
|
||||
else
|
||||
echo "⚠️ 请手动打开浏览器访问: http://localhost:80"
|
||||
fi
|
||||
@@ -24,7 +24,8 @@ export default defineConfig(({ mode, command }) => {
|
||||
open: true,
|
||||
proxy: {
|
||||
[env.VITE_APP_BASE_API]: {
|
||||
target: 'http://192.168.0.214:8089',
|
||||
// target: 'http://47.104.208.94:8089',
|
||||
target: 'http://47.104.215.165:8089',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
|
||||
|
||||
Reference in New Issue
Block a user