Compare commits
46 Commits
| 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 |
@@ -35,4 +35,6 @@ VITE_APP_WEBSOCKET = false
|
|||||||
# sse 开关
|
# sse 开关
|
||||||
VITE_APP_SSE = true
|
VITE_APP_SSE = true
|
||||||
|
|
||||||
VITE_BASE_API = "http://47.104.208.94:8089"
|
# http://192.168.0.214:8089
|
||||||
|
# VITE_BASE_API = "http://47.104.208.94:8089"
|
||||||
|
VITE_BASE_API = "http://47.104.215.165:8080"
|
||||||
|
|||||||
@@ -38,4 +38,4 @@ VITE_APP_WEBSOCKET = false
|
|||||||
# sse 开关
|
# sse 开关
|
||||||
VITE_APP_SSE = true
|
VITE_APP_SSE = true
|
||||||
|
|
||||||
VITE_BASE_API = "http://47.104.208.94:8089"
|
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
|
# Editor directories and files
|
||||||
.claude
|
.claude
|
||||||
|
.mimocode
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
*.suo
|
*.suo
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -7,27 +7,20 @@
|
|||||||
## 配套后端代码仓库地址
|
## 配套后端代码仓库地址
|
||||||
|
|
||||||
| 介绍 | 项目名 | 项目地址 |
|
| 介绍 | 项目名 | 项目地址 |
|
||||||
|------------|:-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
| ----------------- | :--------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| 🔥 分布式集群框架 | 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-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分支(开发分支 开发过程中使用)
|
- dev分支(开发分支 开发过程中使用)
|
||||||
|
|
||||||
## 前端运行
|
## 前端运行
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 安装依赖
|
# 启动命令
|
||||||
npm install --registry=https://registry.npmmirror.com
|
./start.sh
|
||||||
|
|
||||||
# 启动服务
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# 构建生产环境
|
|
||||||
npm run build:prod
|
|
||||||
|
|
||||||
# 前端访问地址 http://localhost:80
|
# 前端访问地址 http://localhost:80
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@10.34.1",
|
"packageManager": "pnpm@10.34.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"preinstall": "npx only-allow pnpm",
|
||||||
"dev": "vite serve --mode development",
|
"dev": "vite serve --mode development",
|
||||||
"build:prod": "vite build --mode production",
|
"build:prod": "vite build --mode production",
|
||||||
"build:dev": "vite build --mode development",
|
"build:dev": "vite build --mode development",
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export const listDemo = (deviceId?: string | number) => {
|
|||||||
*/
|
*/
|
||||||
export const addDemo = (data: any) => {
|
export const addDemo = (data: any) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/haikang/isup/stream/live/stop',
|
url: `/haikang/isup/stream/live/stop?id=${data}`,
|
||||||
method: 'get',
|
method: 'get'
|
||||||
params: data
|
// params: data
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,28 +13,9 @@
|
|||||||
</template>
|
</template>
|
||||||
<div class="right-menu flex align-center">
|
<div class="right-menu flex align-center">
|
||||||
<div v-if="appStore.device !== 'mobile'">
|
<div 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> -->
|
|
||||||
|
|
||||||
<search-menu ref="searchMenuRef" />
|
<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>
|
</div>
|
||||||
</el-tooltip> -->
|
<!-- 消息 - 移到 v-if 外面,手机端也能显示 -->
|
||||||
<!-- 消息 -->
|
|
||||||
<el-tooltip content="警报" effect="dark" placement="bottom">
|
<el-tooltip content="警报" effect="dark" placement="bottom">
|
||||||
<div style="display: flex; align-items: center" class="mr-[30px]">
|
<div style="display: flex; align-items: center" class="mr-[30px]">
|
||||||
<el-popover
|
<el-popover
|
||||||
@@ -46,7 +27,7 @@
|
|||||||
v-model:visible="popoverVisible"
|
v-model:visible="popoverVisible"
|
||||||
>
|
>
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-badge :value="infoData" :max="999">
|
<el-badge :value="infoData" :max="9999">
|
||||||
<div class="right-menu-item hover-effect" @click="goToWarningList">
|
<div class="right-menu-item hover-effect" @click="goToWarningList">
|
||||||
<el-icon><BellFilled /></el-icon>
|
<el-icon><BellFilled /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,26 +39,6 @@
|
|||||||
</el-popover>
|
</el-popover>
|
||||||
</div>
|
</div>
|
||||||
</el-tooltip>
|
</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> -->
|
|
||||||
</div>
|
|
||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<el-dropdown class="right-menu-item hover-effect" trigger="click" @command="handleCommand">
|
<el-dropdown class="right-menu-item hover-effect" trigger="click" @command="handleCommand">
|
||||||
<div class="avatar-wrapper">
|
<div class="avatar-wrapper">
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
<div class="sidebar-logo-container" :class="{ collapse: collapse }">
|
<div class="sidebar-logo-container" :class="{ collapse: collapse }">
|
||||||
<transition :enter-active-class="proxy?.animate.logoAnimate.enter" mode="out-in">
|
<transition :enter-active-class="proxy?.animate.logoAnimate.enter" mode="out-in">
|
||||||
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
||||||
<img v-if="logo" :src="logoUrl" class="sidebar-logo" />
|
<img v-if="showLogoImage" :src="logoUrl" class="sidebar-logo" />
|
||||||
<h1 v-else class="sidebar-title">{{ logoName }}</h1>
|
<h1 v-else class="sidebar-title">{{ logoName }}</h1>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
||||||
<img v-if="logo" :src="logoUrl" class="sidebar-logo" />
|
<img v-if="showLogoImage" :src="logoUrl" class="sidebar-logo" />
|
||||||
<h1 class="sidebar-title">{{ logoName }}</h1>
|
<h1 class="sidebar-title">{{ logoName }}</h1>
|
||||||
</router-link>
|
</router-link>
|
||||||
</transition>
|
</transition>
|
||||||
@@ -15,11 +15,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import variables from '@/assets/styles/variables.module.scss';
|
import variables from '@/assets/styles/variables.module.scss';
|
||||||
import logo from '@/assets/logo/logo.png';
|
|
||||||
import { useSettingsStore } from '@/store/modules/settings';
|
import { useSettingsStore } from '@/store/modules/settings';
|
||||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
import { NavTypeEnum } from '@/enums/NavTypeEnum';
|
import { NavTypeEnum } from '@/enums/NavTypeEnum';
|
||||||
import { getDemo } from '@/api/systemList/index';
|
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
collapse: {
|
collapse: {
|
||||||
@@ -28,13 +26,15 @@ defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const title = import.meta.env.VITE_APP_LOGO_TITLE;
|
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const sideTheme = computed(() => settingsStore.sideTheme);
|
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背景色
|
// 获取Logo背景色
|
||||||
const getLogoBackground = computed(() => {
|
const getLogoBackground = computed(() => {
|
||||||
if (settingsStore.isDark) {
|
if (settingsStore.dark) {
|
||||||
return 'var(--sidebar-bg)';
|
return 'var(--sidebar-bg)';
|
||||||
}
|
}
|
||||||
if (settingsStore.navType == NavTypeEnum.TOP) {
|
if (settingsStore.navType == NavTypeEnum.TOP) {
|
||||||
@@ -45,7 +45,7 @@ const getLogoBackground = computed(() => {
|
|||||||
|
|
||||||
// 获取Logo文字颜色
|
// 获取Logo文字颜色
|
||||||
const getLogoTextColor = computed(() => {
|
const getLogoTextColor = computed(() => {
|
||||||
if (settingsStore.isDark) {
|
if (settingsStore.dark) {
|
||||||
return 'var(--sidebar-text)';
|
return 'var(--sidebar-text)';
|
||||||
}
|
}
|
||||||
if (settingsStore.navType == NavTypeEnum.TOP) {
|
if (settingsStore.navType == NavTypeEnum.TOP) {
|
||||||
@@ -53,18 +53,6 @@ const getLogoTextColor = computed(() => {
|
|||||||
}
|
}
|
||||||
return sideTheme.value === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor;
|
return sideTheme.value === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<div class="monitor-video-wrap">
|
<div class="monitor-video-wrap">
|
||||||
<div class="monitor-video-stack">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -53,10 +53,11 @@ import { initHls, destroyHls } from '@/utils/video';
|
|||||||
import { onBeforeRouteLeave } from 'vue-router';
|
import { onBeforeRouteLeave } from 'vue-router';
|
||||||
import { useWarningListStore } from '@/store/modules/warnningList';
|
import { useWarningListStore } from '@/store/modules/warnningList';
|
||||||
import type { WarningListResult } from '@/api/WarningList/types';
|
import type { WarningListResult } from '@/api/WarningList/types';
|
||||||
|
import { addDemo } from '@/api/video/index';
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
|
|
||||||
import { nextTick } from 'vue';
|
import { nextTick, onUnmounted } from 'vue';
|
||||||
import { useVideosStore } from '@/store/modules/video';
|
import { useVideosStore } from '@/store/modules/video';
|
||||||
const videosStore = useVideosStore();
|
const videosStore = useVideosStore();
|
||||||
const monitoringArea = ref<string>('');
|
const monitoringArea = ref<string>('');
|
||||||
@@ -109,7 +110,23 @@ const parseNoticeItem = (notice: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
//查看画面
|
//查看画面
|
||||||
|
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) => {
|
const handleSee = async (row: any) => {
|
||||||
|
currentDeviceId.value = row?.deviceId;
|
||||||
const videoId = row?.deviceId;
|
const videoId = row?.deviceId;
|
||||||
console.log('查看画面', row, 'deviceId', videoId);
|
console.log('查看画面', row, 'deviceId', videoId);
|
||||||
if (!videoId) {
|
if (!videoId) {
|
||||||
@@ -121,12 +138,19 @@ const handleSee = async (row: any) => {
|
|||||||
monitoringArea.value = row.monitoringArea;
|
monitoringArea.value = row.monitoringArea;
|
||||||
dialog.visible = true;
|
dialog.visible = true;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
initHls(monitorVideoUrl, monitorVideoRef);
|
initHls(monitorVideoUrl, monitorVideoRef, () => dialog.visible, handleRefresh);
|
||||||
await handleAction(row.id);
|
await handleAction(row.id);
|
||||||
await getList();
|
await getList();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMonitorDialogClose = async (done: () => void) => {
|
const handleMonitorDialogClose = async (done: () => void) => {
|
||||||
|
try {
|
||||||
|
addDemo(currentDeviceId.value as string).then(() => {
|
||||||
|
console.log('关闭视频111111');
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log('关闭视频22222222', error);
|
||||||
|
}
|
||||||
// 卸载 Hls 与清理 video
|
// 卸载 Hls 与清理 video
|
||||||
destroyHls(monitorVideoRef);
|
destroyHls(monitorVideoRef);
|
||||||
done();
|
done();
|
||||||
@@ -165,7 +189,7 @@ watch(
|
|||||||
(newNotices) => {
|
(newNotices) => {
|
||||||
const filteredNotices = [...newNotices].map(parseNoticeItem);
|
const filteredNotices = [...newNotices].map(parseNoticeItem);
|
||||||
newsList.value = filteredNotices;
|
newsList.value = filteredNotices;
|
||||||
console.log('监听消息队列', newsList.value);
|
// console.log('监听消息队列', newsList.value);
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
);
|
);
|
||||||
@@ -209,9 +233,14 @@ onMounted(() => {
|
|||||||
|
|
||||||
onBeforeRouteLeave(() => {
|
onBeforeRouteLeave(() => {
|
||||||
dialog.visible = false;
|
dialog.visible = false;
|
||||||
|
destroyHls(monitorVideoRef);
|
||||||
monitorVideoUrl.value = '';
|
monitorVideoUrl.value = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
destroyHls(monitorVideoRef);
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getList();
|
getList();
|
||||||
console.log('111111111111111111111_warningList.value', warningList.value);
|
console.log('111111111111111111111_warningList.value', warningList.value);
|
||||||
|
|||||||
22
src/main.ts
22
src/main.ts
@@ -30,6 +30,8 @@ import './permission';
|
|||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
import i18n from '@/lang/index';
|
import i18n from '@/lang/index';
|
||||||
|
import { useSettingsStore } from '@/store/modules/settings';
|
||||||
|
import { getDemo } from '@/api/systemList/index';
|
||||||
|
|
||||||
// vxeTable
|
// vxeTable
|
||||||
import VXETable from 'vxe-table';
|
import VXETable from 'vxe-table';
|
||||||
@@ -54,4 +56,24 @@ app.use(plugins);
|
|||||||
// 自定义指令
|
// 自定义指令
|
||||||
directive(app);
|
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');
|
app.mount('#app');
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export const useSettingsStore = defineStore('setting', () => {
|
|||||||
radiusBase: defaultSettings.radiusBase
|
radiusBase: defaultSettings.radiusBase
|
||||||
});
|
});
|
||||||
const title = ref<string>(defaultSettings.title);
|
const title = ref<string>(defaultSettings.title);
|
||||||
|
const appTitle = ref<string>(defaultSettings.title);
|
||||||
|
const appLogoUrl = ref<string>('');
|
||||||
const theme = ref<string>(storageSetting.value.theme);
|
const theme = ref<string>(storageSetting.value.theme);
|
||||||
const sideTheme = ref<string>(storageSetting.value.sideTheme);
|
const sideTheme = ref<string>(storageSetting.value.sideTheme);
|
||||||
const showSettings = ref<boolean>(defaultSettings.showSettings);
|
const showSettings = ref<boolean>(defaultSettings.showSettings);
|
||||||
@@ -36,8 +38,17 @@ export const useSettingsStore = defineStore('setting', () => {
|
|||||||
title.value = value;
|
title.value = value;
|
||||||
useDynamicTitle();
|
useDynamicTitle();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setSystemInfo = (name: string, url: string) => {
|
||||||
|
appTitle.value = name || defaultSettings.title;
|
||||||
|
appLogoUrl.value = url || '';
|
||||||
|
useDynamicTitle();
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
appTitle,
|
||||||
|
appLogoUrl,
|
||||||
theme,
|
theme,
|
||||||
sideTheme,
|
sideTheme,
|
||||||
showSettings,
|
showSettings,
|
||||||
@@ -50,6 +61,7 @@ export const useSettingsStore = defineStore('setting', () => {
|
|||||||
dark,
|
dark,
|
||||||
navType,
|
navType,
|
||||||
radiusBase,
|
radiusBase,
|
||||||
setTitle
|
setTitle,
|
||||||
|
setSystemInfo
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import { useSettingsStore } from '@/store/modules/settings';
|
|||||||
*/
|
*/
|
||||||
export const useDynamicTitle = () => {
|
export const useDynamicTitle = () => {
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
const pageTitle = settingsStore.title || (defaultSettings.title as string);
|
||||||
|
const appTitle = settingsStore.appTitle || (defaultSettings.title as string);
|
||||||
|
|
||||||
if (settingsStore.dynamicTitle) {
|
if (settingsStore.dynamicTitle) {
|
||||||
document.title = settingsStore.title + ' - ' + import.meta.env.VITE_APP_TITLE;
|
document.title = `${pageTitle} - ${appTitle}`;
|
||||||
} else {
|
} else {
|
||||||
document.title = defaultSettings.title as string;
|
document.title = appTitle;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ export const initSSE = (url: any) => {
|
|||||||
if (!data.value) return;
|
if (!data.value) return;
|
||||||
|
|
||||||
// 打印接收到的 SSE 消息
|
// 打印接收到的 SSE 消息
|
||||||
console.log('=== SSE 接收到消息 ===');
|
// console.log('=== SSE 接收到消息 ===');
|
||||||
console.log('消息内容:', data.value);
|
// console.log('消息内容:', data.value);
|
||||||
console.log('消息类型:', typeof data.value);
|
// console.log('消息类型:', typeof data.value);
|
||||||
console.log('接收时间:', new Date().toLocaleString());
|
// console.log('接收时间:', new Date().toLocaleString());
|
||||||
console.log('=====================');
|
// console.log('=====================');
|
||||||
|
|
||||||
useNoticeStore().addNotice({
|
useNoticeStore().addNotice({
|
||||||
message: data.value,
|
message: data.value,
|
||||||
|
|||||||
@@ -1,62 +1,255 @@
|
|||||||
import Hls from 'hls.js';
|
import Hls from 'hls.js';
|
||||||
let hlsInstance: Hls | null = null;
|
|
||||||
|
|
||||||
export const initHls = (monitorVideoUrl, monitorVideoRef) => {
|
/**
|
||||||
|
* 每个播放器实例维护自己的状态,避免全局单例互相踩踏
|
||||||
|
*/
|
||||||
|
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 url = monitorVideoUrl.value;
|
||||||
const video = monitorVideoRef.value;
|
const video = monitorVideoRef.value;
|
||||||
if (!url || !video) return;
|
if (!url || !video) return;
|
||||||
|
|
||||||
// 销毁已有实例
|
// 先销毁该 video 元素上已有的实例(不再操作全局变量)
|
||||||
if (hlsInstance) {
|
const existingState = playerStates.get(video);
|
||||||
|
if (existingState) {
|
||||||
try {
|
try {
|
||||||
hlsInstance.destroy();
|
stopLiveSync(existingState);
|
||||||
} catch (e) {
|
existingState.destroyed = true;
|
||||||
|
existingState.hls.destroy();
|
||||||
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
hlsInstance = null;
|
playerStates.delete(video);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Hls.isSupported()) {
|
if (Hls.isSupported()) {
|
||||||
const hls = new Hls();
|
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.loadSource(url);
|
||||||
hls.attachMedia(video);
|
hls.attachMedia(video);
|
||||||
|
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||||
// 自动播放(若浏览器允许)
|
|
||||||
try {
|
try {
|
||||||
video.play().catch(() => {});
|
video.play().catch(() => {});
|
||||||
} catch (e) {}
|
} 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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
hlsInstance = hls;
|
|
||||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
// Safari/原生支持 HLS
|
// Safari 原生 HLS
|
||||||
video.src = url;
|
video.src = url;
|
||||||
video.addEventListener('loadedmetadata', () => {
|
video.addEventListener('loadedmetadata', () => {
|
||||||
try {
|
try {
|
||||||
video.play().catch(() => {});
|
video.play().catch(() => {});
|
||||||
} catch (e) {}
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
//proxy?.$modal.msgWarning('当前浏览器不支持 HLS 播放');
|
|
||||||
ElMessage.error('当前浏览器不支持 HLS 播放');
|
ElMessage.error('当前浏览器不支持 HLS 播放');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const destroyHls = (monitorVideoRef) => {
|
export const destroyHls = (monitorVideoRef) => {
|
||||||
try {
|
|
||||||
if (hlsInstance) {
|
|
||||||
hlsInstance.destroy();
|
|
||||||
hlsInstance = null;
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
const video = monitorVideoRef.value;
|
const video = monitorVideoRef.value;
|
||||||
if (video) {
|
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,避免残留
|
// 清理原生 src,避免残留
|
||||||
try {
|
try {
|
||||||
video.pause();
|
video.pause();
|
||||||
video.removeAttribute('src');
|
video.removeAttribute('src');
|
||||||
// 对某些浏览器,需要调用 load() 来重置 media element
|
|
||||||
if (typeof video.load === 'function') video.load();
|
if (typeof video.load === 'function') video.load();
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
console.log('Error in destroyHls:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
<el-table-column label="所属设备" align="center" prop="deviceName" min-width="120" show-overflow-tooltip />
|
<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 />
|
<el-table-column label="报警区域" align="center" prop="monitoringArea" min-width="120" show-overflow-tooltip />
|
||||||
<!-- pay content -->
|
<!-- pay content -->
|
||||||
<el-table-column label="报警图片" align="center" prop="alarmImage" min-width="120">
|
<!-- <el-table-column label="报警图片" align="center" prop="alarmImage" min-width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-image
|
<el-image
|
||||||
v-if="scope.row.alarmImage"
|
v-if="scope.row.alarmImage"
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
/>
|
/>
|
||||||
<span v-else>暂无图片</span>
|
<span v-else>暂无图片</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column> -->
|
||||||
<el-table-column label="处理状态" align="center" prop="status" width="100">
|
<el-table-column label="处理状态" align="center" prop="status" width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="scope.row.status === 0" type="warning">未处理</el-tag>
|
<el-tag v-if="scope.row.status === 0" type="warning">未处理</el-tag>
|
||||||
|
|||||||
@@ -65,23 +65,121 @@
|
|||||||
<el-tag v-else>{{ scope.row.status ?? '-' }}</el-tag>
|
<el-tag v-else>{{ scope.row.status ?? '-' }}</el-tag>
|
||||||
</template> -->
|
</template> -->
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="电池电量" align="center" prop="batteryLevel1" width="150">
|
<!-- <el-table-column label="电池电量" align="center" prop="batteryLevel1" width="150">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<!-- <el-button type="primary" v-if="scope.row.batteryLevel1" plain size="small">{{ scope.row.batteryLevel1 + '%' }}</el-button>
|
<el-tag v-if="scope.row.batteryLevel1">左{{ scope.row.batteryLevel1 + '%' }}</el-tag>
|
||||||
<el-button type="primary" v-if="scope.row.batteryLevel2" plain size="small">
|
|
||||||
{{ scope.row.batteryLevel2 + '%' }}
|
|
||||||
</el-button> -->
|
|
||||||
<!-- <el-button type="primary" v-else plain size="small"> 未获取 </el-button> -->
|
|
||||||
<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' : ''">
|
<el-tag v-if="scope.row.batteryLevel2" :style="scope.row.batteryLevel1 && scope.row.batteryLevel2 ? 'margin-left: 10px' : ''">
|
||||||
{{ scope.row.batteryLevel2 + '%' }}
|
右{{ scope.row.batteryLevel2 + '%' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<!-- <el-tag v-else type="info"> 未获取 </el-tag> -->
|
|
||||||
<el-tag v-if="!scope.row.batteryLevel1 && !scope.row.batteryLevel2"> 未获取 </el-tag>
|
<el-tag v-if="!scope.row.batteryLevel1 && !scope.row.batteryLevel2"> 未获取 </el-tag>
|
||||||
</template>
|
</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>
|
||||||
|
<!-- <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="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">
|
<el-table-column label="是否绘制区域" align="center" prop="isDrawArea">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="scope.row.isDrawArea === 1 || scope.row.isDrawArea === '1'">是</el-tag>
|
<el-tag v-if="scope.row.isDrawArea === 1 || scope.row.isDrawArea === '1'">是</el-tag>
|
||||||
@@ -101,7 +199,7 @@
|
|||||||
@click="handleActivate(scope.row)"
|
@click="handleActivate(scope.row)"
|
||||||
>唤醒设备</el-button
|
>唤醒设备</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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -134,6 +232,8 @@
|
|||||||
<div class="flex w-full items-center justify-between gap-2 min-w-0 pr-8">
|
<div class="flex w-full items-center justify-between gap-2 min-w-0 pr-8">
|
||||||
<span class="truncate">{{ videoName }}-{{ monitoringArea }}监控</span>
|
<span class="truncate">{{ videoName }}-{{ monitoringArea }}监控</span>
|
||||||
<div>
|
<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="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="clearImage()">清空区域</el-button>
|
||||||
<el-button v-if="seeSee" type="primary" class="shrink-0" @click="saveImage()">保存区域</el-button>
|
<el-button v-if="seeSee" type="primary" class="shrink-0" @click="saveImage()">保存区域</el-button>
|
||||||
@@ -142,7 +242,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<div class="monitor-video-wrap">
|
<div class="monitor-video-wrap">
|
||||||
<div class="monitor-video-stack">
|
<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-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-rect" :style="drawRectStyle" />
|
||||||
<div v-if="monitorRectPx" class="monitor-draw-label" :style="drawLabelStyle">当前报警区域</div>
|
<div v-if="monitorRectPx" class="monitor-draw-label" :style="drawLabelStyle">当前报警区域</div>
|
||||||
@@ -155,12 +255,12 @@
|
|||||||
<script setup name="Demo" lang="ts">
|
<script setup name="Demo" lang="ts">
|
||||||
import { addEquipment, getDevicetList, getDeviceInfo, delDevice, actDevice, updateDeviceInfo, drawInfo } from '@/api/equipmentList/index';
|
import { addEquipment, getDevicetList, getDeviceInfo, delDevice, actDevice, updateDeviceInfo, drawInfo } from '@/api/equipmentList/index';
|
||||||
import { EquipmentList, DeviceInfoForm, AlarmAreaData } from '@/api/equipmentList/types';
|
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 { DemoVO, DemoQuery, DemoForm } from '@/api/demo/demo/types';
|
||||||
import { log } from 'node:console';
|
import { initHls, destroyHls } from '@/utils/video';
|
||||||
import Hls from 'hls.js';
|
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import { useVideosStore } from '@/store/modules/video';
|
import { useVideosStore } from '@/store/modules/video';
|
||||||
|
import { addDemo } from '@/api/video/index';
|
||||||
|
import { AnyARecord } from 'dns';
|
||||||
|
|
||||||
const videosStore = useVideosStore();
|
const videosStore = useVideosStore();
|
||||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
@@ -190,68 +290,9 @@ const dialog1 = reactive<DialogOption>({
|
|||||||
|
|
||||||
const monitorVideoUrl = ref('');
|
const monitorVideoUrl = ref('');
|
||||||
const monitorVideoRef = ref<HTMLVideoElement>();
|
const monitorVideoRef = ref<HTMLVideoElement>();
|
||||||
let hlsInstance: Hls | null = null;
|
|
||||||
let visibilityHandler: (() => void) | null = null;
|
let visibilityHandler: (() => void) | null = null;
|
||||||
const monitoringArea = ref<string>('');
|
const monitoringArea = ref<string>('');
|
||||||
|
|
||||||
const initHls = () => {
|
|
||||||
const url = monitorVideoUrl.value;
|
|
||||||
const video = monitorVideoRef.value;
|
|
||||||
if (!url || !video) return;
|
|
||||||
|
|
||||||
// 销毁已有实例
|
|
||||||
if (hlsInstance) {
|
|
||||||
try {
|
|
||||||
hlsInstance.destroy();
|
|
||||||
} catch (e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
hlsInstance = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Hls.isSupported()) {
|
|
||||||
const hls = new Hls();
|
|
||||||
hls.loadSource(url);
|
|
||||||
hls.attachMedia(video);
|
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
||||||
// 自动播放(若浏览器允许)
|
|
||||||
try {
|
|
||||||
video.play().catch(() => {});
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
hlsInstance = hls;
|
|
||||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
||||||
// Safari/原生支持 HLS
|
|
||||||
video.src = url;
|
|
||||||
video.addEventListener('loadedmetadata', () => {
|
|
||||||
try {
|
|
||||||
video.play().catch(() => {});
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
proxy?.$modal.msgWarning('当前浏览器不支持 HLS 播放');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const destroyHls = () => {
|
|
||||||
try {
|
|
||||||
if (hlsInstance) {
|
|
||||||
hlsInstance.destroy();
|
|
||||||
hlsInstance = null;
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
const video = monitorVideoRef.value;
|
|
||||||
if (video) {
|
|
||||||
// 清理原生 src,避免残留
|
|
||||||
try {
|
|
||||||
video.pause();
|
|
||||||
video.removeAttribute('src');
|
|
||||||
// 对某些浏览器,需要调用 load() 来重置 media element
|
|
||||||
if (typeof video.load === 'function') video.load();
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type MonitorPoint = { x: number; y: number };
|
type MonitorPoint = { x: number; y: number };
|
||||||
const monitorDrawCollecting = ref(false);
|
const monitorDrawCollecting = ref(false);
|
||||||
const monitorDrawPoints = ref<MonitorPoint[]>([]);
|
const monitorDrawPoints = ref<MonitorPoint[]>([]);
|
||||||
@@ -406,9 +447,10 @@ const resetMonitorDraw = () => {
|
|||||||
monitorRectPx.value = null;
|
monitorRectPx.value = null;
|
||||||
currentDrawArea.value = null;
|
currentDrawArea.value = null;
|
||||||
};
|
};
|
||||||
|
// const videosInfo = ref();
|
||||||
const handleSee = async (row?: any) => {
|
const handleSee = async (row?: any) => {
|
||||||
console.log('1111111111111111', row, row.drawArea);
|
console.log('1111111111111111', row, row.drawArea);
|
||||||
|
// videosInfo.value = row;
|
||||||
reset();
|
reset();
|
||||||
resetMonitorDraw();
|
resetMonitorDraw();
|
||||||
seeSee.value = row?.isDrawArea === 1 ? 1 : 0;
|
seeSee.value = row?.isDrawArea === 1 ? 1 : 0;
|
||||||
@@ -419,7 +461,7 @@ const handleSee = async (row?: any) => {
|
|||||||
dialog1.visible = true;
|
dialog1.visible = true;
|
||||||
dialog1.title = '修改设备';
|
dialog1.title = '修改设备';
|
||||||
|
|
||||||
destroyHls();
|
destroyHls(monitorVideoRef);
|
||||||
monitorVideoUrl.value = '';
|
monitorVideoUrl.value = '';
|
||||||
await videosStore.getVideos(row.id);
|
await videosStore.getVideos(row.id);
|
||||||
monitorVideoUrl.value = videosStore.hisUrl || '';
|
monitorVideoUrl.value = videosStore.hisUrl || '';
|
||||||
@@ -432,7 +474,7 @@ const handleSee = async (row?: any) => {
|
|||||||
|
|
||||||
// 等待 DOM 更新后初始化 Hls
|
// 等待 DOM 更新后初始化 Hls
|
||||||
await nextTick();
|
await nextTick();
|
||||||
initHls();
|
initHls(monitorVideoUrl, monitorVideoRef, () => dialog1.visible, handleRefresh);
|
||||||
// 等待视频加载完成后初始化 ResizeObserver,以便能正确获取视频尺寸
|
// 等待视频加载完成后初始化 ResizeObserver,以便能正确获取视频尺寸
|
||||||
initVideoResizeObserver();
|
initVideoResizeObserver();
|
||||||
|
|
||||||
@@ -564,8 +606,15 @@ const cleanupVisibilityChange = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMonitorDialogClose = (done: () => void) => {
|
const handleMonitorDialogClose = (done: () => void) => {
|
||||||
|
try {
|
||||||
|
addDemo(form.value.id).then(() => {
|
||||||
|
console.log('关闭视频111111');
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log('关闭视频22222222', error);
|
||||||
|
}
|
||||||
// 卸载 Hls 与清理 video
|
// 卸载 Hls 与清理 video
|
||||||
destroyHls();
|
destroyHls(monitorVideoRef);
|
||||||
monitorVideoUrl.value = '';
|
monitorVideoUrl.value = '';
|
||||||
destroyVideoResizeObserver();
|
destroyVideoResizeObserver();
|
||||||
resetMonitorDraw();
|
resetMonitorDraw();
|
||||||
@@ -710,7 +759,7 @@ const saveImage = async () => {
|
|||||||
|
|
||||||
const handleMonitorDialogOpen = () => {
|
const handleMonitorDialogOpen = () => {
|
||||||
// 初始化 Hls 与 video
|
// 初始化 Hls 与 video
|
||||||
initHls();
|
initHls(monitorVideoUrl, monitorVideoRef, () => dialog1.visible);
|
||||||
initVideoResizeObserver();
|
initVideoResizeObserver();
|
||||||
monitorDrawCollecting.value = false;
|
monitorDrawCollecting.value = false;
|
||||||
monitorDrawPoints.value = [];
|
monitorDrawPoints.value = [];
|
||||||
@@ -747,6 +796,39 @@ watch(
|
|||||||
videoName.value = newValue === 1 ? '绘制区域' : '实时监控';
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -832,18 +914,31 @@ watch(
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 隐藏视频进度条,保留其他控制按钮 */
|
.battery-group {
|
||||||
.monitor-video::-webkit-media-controls-timeline {
|
display: flex;
|
||||||
display: none !important;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.monitor-video::-webkit-media-controls-current-time-display,
|
.battery-item {
|
||||||
.monitor-video::-webkit-media-controls-time-remaining-display {
|
display: flex;
|
||||||
display: none !important;
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
}
|
}
|
||||||
.monitor-video::-moz-range-track {
|
.battery-label {
|
||||||
display: none !important;
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
}
|
}
|
||||||
.monitor-video::-moz-progress-bar {
|
|
||||||
display: none !important;
|
/* 电量电池颜色 */
|
||||||
|
.power--high {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
.power--mid {
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
|
.power--low {
|
||||||
|
color: #ff4d4f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -91,8 +91,8 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
const loginForm = ref<LoginData>({
|
const loginForm = ref<LoginData>({
|
||||||
tenantId: '000000',
|
tenantId: '000000',
|
||||||
username: 'admin',
|
username: '',
|
||||||
password: 'admin123',
|
password: '',
|
||||||
rememberMe: false,
|
rememberMe: false,
|
||||||
code: '',
|
code: '',
|
||||||
uuid: ''
|
uuid: ''
|
||||||
@@ -265,8 +265,8 @@ onMounted(() => {
|
|||||||
|
|
||||||
.login-brand {
|
.login-brand {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: min(420px, 90vw);
|
width: min(450px, 90vw);
|
||||||
max-width: 420px;
|
max-width: 450px;
|
||||||
height: 450px;
|
height: 450px;
|
||||||
max-height: 450px;
|
max-height: 450px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -333,8 +333,8 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-brand {
|
.login-brand {
|
||||||
width: min(420px, 100%);
|
width: min(450px, 100%);
|
||||||
max-width: 420px;
|
max-width: 450px;
|
||||||
height: 450px;
|
height: 450px;
|
||||||
max-height: 450px;
|
max-height: 450px;
|
||||||
border-radius: var(--app-radius-lg);
|
border-radius: var(--app-radius-lg);
|
||||||
@@ -378,7 +378,7 @@ onMounted(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
width: min(420px, 90vw);
|
width: min(450px, 90vw);
|
||||||
height: 450px;
|
height: 450px;
|
||||||
max-height: 450px;
|
max-height: 450px;
|
||||||
padding: 44px 48px 36px;
|
padding: 44px 48px 36px;
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const save = async () => {
|
|||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('保存失败', err);
|
console.error('保存失败', err);
|
||||||
ElMessage.error('保存失败');
|
// ElMessage.error('保存失败');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<template #header>
|
<template #header>
|
||||||
<el-row :gutter="10" class="flex justify-end mb8">
|
<el-row :gutter="10" class="flex justify-end mb8">
|
||||||
<el-col :span="1.5">
|
<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>
|
||||||
<!-- <el-col :span="1.5">
|
<!-- <el-col :span="1.5">
|
||||||
<el-button v-hasPermi="['system:role:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
|
<el-button v-hasPermi="['system:role:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<div v-show="showSearch" class="mb-[10px]">
|
<div v-show="showSearch" class="mb-[10px]">
|
||||||
<el-card shadow="hover">
|
<el-card shadow="hover">
|
||||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
<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-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<!-- <el-form-item label="用户账号" prop="nickName">
|
<!-- <el-form-item label="用户账号" prop="nickName">
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<template #header>
|
<template #header>
|
||||||
<el-row :gutter="10" class="flex justify-end mb8">
|
<el-row :gutter="10" class="flex justify-end mb8">
|
||||||
<el-col :span="1.5">
|
<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>
|
||||||
<!-- <el-col :span="1.5">
|
<!-- <el-col :span="1.5">
|
||||||
<el-button v-has-permi="['system:user:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">
|
<el-button v-has-permi="['system:user:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">
|
||||||
@@ -123,10 +123,10 @@
|
|||||||
<el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
|
<el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<!-- <el-tooltip v-if="scope.row.userId !== 1" content="修改" placement="top"> -->
|
<!-- <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>
|
||||||
<el-tooltip v-if="scope.row.userId !== 1" content="删除" placement="top"> -->
|
<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> -->
|
||||||
|
|
||||||
<!-- <el-tooltip v-if="scope.row.userId !== 1" content="重置密码" placement="top">
|
<!-- <el-tooltip v-if="scope.row.userId !== 1" content="重置密码" placement="top">
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
<el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px">
|
<el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px">
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="用户账号" prop="nickName">
|
<el-form-item label="用户名称" prop="nickName">
|
||||||
<el-input v-model="form.nickName" placeholder="请输入用户账号" maxlength="30" />
|
<el-input v-model="form.nickName" placeholder="请输入用户账号" maxlength="30" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
<!-- </el-row>
|
<!-- </el-row>
|
||||||
<el-row> -->
|
<el-row> -->
|
||||||
<el-col :span="12" v-if="form.userId == undefined">
|
<el-col :span="12" v-if="form.userId == undefined">
|
||||||
<el-form-item label="用户名称" prop="userName">
|
<el-form-item label="用户账号" prop="userName">
|
||||||
<el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
|
<el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|||||||
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
|
||||||
@@ -25,7 +25,7 @@ export default defineConfig(({ mode, command }) => {
|
|||||||
proxy: {
|
proxy: {
|
||||||
[env.VITE_APP_BASE_API]: {
|
[env.VITE_APP_BASE_API]: {
|
||||||
// target: 'http://47.104.208.94:8089',
|
// target: 'http://47.104.208.94:8089',
|
||||||
target: 'http://192.168.0.214:8089',
|
target: 'http://47.104.215.165:8089',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true,
|
ws: true,
|
||||||
rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
|
rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
|
||||||
|
|||||||
Reference in New Issue
Block a user