Compare commits

..

23 Commits

Author SHA1 Message Date
Ironsp
40503b4f56 feat 默认app_title 2026-08-19 14:19:30 +08:00
Ironsp
000472231c fix 服务器环境配置 2026-08-13 11:48:53 +08:00
Ironsp
aba75fdd7c fix 去掉账号密码 2026-08-13 10:13:01 +08:00
Ironsp
e19a9df909 fix 修改部署环境 2026-08-12 11:49:51 +08:00
Ironsp
7f195f3fc1 fix 直播流追赶逻辑 2026-08-05 11:05:47 +08:00
Ironsp
04a52895d7 fix 验证码显示问题 2026-08-03 17:15:19 +08:00
Ironsp
bf3c8d8228 feat 摄像头需求变更 2026-07-27 17:07:50 +08:00
Ironsp
5ff0a2e048 feat 多像摄像头切换 2026-07-27 09:19:45 +08:00
Ironsp
1b55bf6935 feat 添加新功能多像摄像头切换 2026-07-27 09:05:53 +08:00
Ironsp
8768ec3729 feat 优化直播链路提示词 2026-07-22 14:38:40 +08:00
Ironsp
747e7000cd feat 优化直播链路 2026-07-22 14:37:14 +08:00
Ironsp
ef01ef8394 feat 优化 2026-07-22 14:26:34 +08:00
Ironsp
b638e7eaa0 feat 优化 2026-07-22 14:01:32 +08:00
Ironsp
93f3a48caf feat 优化 2026-07-22 11:52:43 +08:00
Ironsp
bbd23051e7 feat 直播流优化 2026-07-22 11:50:57 +08:00
Ironsp
aaa223f41d feat 视频优化,去掉控件 2026-07-20 12:00:41 +08:00
Ironsp
7f376ce10c feat hls分片加在成功后播放 2026-07-20 10:22:52 +08:00
Ironsp
d78dd8d42e feat 优化架构设计 2026-07-18 11:04:23 +08:00
Ironsp
d193836c70 feat 优化架构 2026-07-18 10:43:00 +08:00
Ironsp
671c5b4200 feat 优化代码 2026-07-18 10:33:33 +08:00
Ironsp
13ebb3e51a feat 优化代码结构添加ignore 2026-07-18 09:57:31 +08:00
Ironsp
43bfa65108 feat 顶部图标修改 2026-07-17 15:18:36 +08:00
Ironsp
98a30948ec feat hls错误自动恢复 2026-07-17 14:57:25 +08:00
14 changed files with 318 additions and 147 deletions

View File

@@ -37,4 +37,4 @@ VITE_APP_SSE = true
# http://192.168.0.214:8089 # http://192.168.0.214:8089
# VITE_BASE_API = "http://47.104.208.94:8089" # VITE_BASE_API = "http://47.104.208.94:8089"
VITE_BASE_API = "http://192.168.0.214:8089" VITE_BASE_API = "http://47.104.215.165:8080"

View File

@@ -1,5 +1,5 @@
# 页面标题 # 页面标题
VITE_APP_TITLE = 可视化电子围栏管理系统11111 VITE_APP_TITLE = 可视化电子围栏管理系统
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
# 生产环境配置 # 生产环境配置
@@ -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"

1
.gitignore vendored
View File

@@ -13,6 +13,7 @@ selenium-debug.log
# Editor directories and files # Editor directories and files
.claude .claude
.mimocode
.idea .idea
.vscode .vscode
*.suo *.suo

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
registry = https://registry.npmmirror.com

View File

@@ -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
``` ```

View File

@@ -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",

View File

@@ -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
@@ -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">

View File

@@ -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>
@@ -57,7 +57,7 @@ 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>('');
@@ -111,6 +111,20 @@ const parseNoticeItem = (notice: any) => {
//查看画面 //查看画面
const currentDeviceId = ref<string | number>(); 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; currentDeviceId.value = row?.deviceId;
const videoId = row?.deviceId; const videoId = row?.deviceId;
@@ -124,7 +138,7 @@ 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();
}; };
@@ -175,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 }
); );
@@ -219,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);

View File

@@ -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,

View File

@@ -1,73 +1,251 @@
import Hls from 'hls.js'; import Hls from 'hls.js';
import { log } from 'node:console';
let hlsInstance: Hls | null = null;
// let hlsRetryCount = 0;
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 注册流需要时间,配置自动重试 // ★ ZLM 注册流需要时间,配置自动重试
manifestLoadingMaxRetry: 5, // m3u8 加载最多重试20次 manifestLoadingMaxRetry: 5,
manifestLoadingRetryDelay: 500, // 每次重试间隔500ms manifestLoadingRetryDelay: 1000,
manifestLoadingTimeOut: 5000, // m3u8 请求超时5秒 manifestLoadingTimeOut: 5000,
levelLoadingMaxRetry: 2, // ts 分片加载最多重试20次 levelLoadingMaxRetry: 5,
levelLoadingRetryDelay: 500, levelLoadingRetryDelay: 500,
lowLatencyMode: false, // 关掉低延迟模式,更稳定 lowLatencyMode: false,
startLevel: -1 // 自动选择码率 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
// hlsRetryCount = 0; }
});
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); console.log('Error in destroyHls:', e);

View File

@@ -232,6 +232,7 @@
<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 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>
@@ -241,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>
@@ -255,11 +256,11 @@
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 { 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 { initHls, destroyHls } from '@/utils/video';
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 { 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;
@@ -446,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;
@@ -472,7 +474,7 @@ const handleSee = async (row?: any) => {
// 等待 DOM 更新后初始化 Hls // 等待 DOM 更新后初始化 Hls
await nextTick(); await nextTick();
initHls(monitorVideoUrl, monitorVideoRef); initHls(monitorVideoUrl, monitorVideoRef, () => dialog1.visible, handleRefresh);
// 等待视频加载完成后初始化 ResizeObserver以便能正确获取视频尺寸 // 等待视频加载完成后初始化 ResizeObserver以便能正确获取视频尺寸
initVideoResizeObserver(); initVideoResizeObserver();
@@ -757,7 +759,7 @@ const saveImage = async () => {
const handleMonitorDialogOpen = () => { const handleMonitorDialogOpen = () => {
// 初始化 Hls 与 video // 初始化 Hls 与 video
initHls(monitorVideoUrl, monitorVideoRef); initHls(monitorVideoUrl, monitorVideoRef, () => dialog1.visible);
initVideoResizeObserver(); initVideoResizeObserver();
monitorDrawCollecting.value = false; monitorDrawCollecting.value = false;
monitorDrawPoints.value = []; monitorDrawPoints.value = [];
@@ -794,6 +796,11 @@ watch(
videoName.value = newValue === 1 ? '绘制区域' : '实时监控'; videoName.value = newValue === 1 ? '绘制区域' : '实时监控';
} }
); );
/** 刷新按钮操作 */
// const handleVideo = async (row?: any) => {
// console.log('11111111111', row);
// handleSee(row);
// };
/** 刷新按钮操作 */ /** 刷新按钮操作 */
const handleRefresh = async () => { const handleRefresh = async () => {
@@ -804,7 +811,7 @@ const handleRefresh = async () => {
monitorVideoUrl.value = videosStore.hisUrl || ''; monitorVideoUrl.value = videosStore.hisUrl || '';
if (monitorVideoUrl.value) { if (monitorVideoUrl.value) {
await nextTick(); await nextTick();
initHls(monitorVideoUrl, monitorVideoRef); initHls(monitorVideoUrl, monitorVideoRef, () => dialog1.visible, handleRefresh);
} }
}; };
@@ -907,21 +914,6 @@ const getPowerLevelClass = (val: any) => {
border-radius: 2px; border-radius: 2px;
} }
/* 隐藏视频进度条,保留其他控制按钮 */
.monitor-video::-webkit-media-controls-timeline {
display: none !important;
}
.monitor-video::-webkit-media-controls-current-time-display,
.monitor-video::-webkit-media-controls-time-remaining-display {
display: none !important;
}
.monitor-video::-moz-range-track {
display: none !important;
}
.monitor-video::-moz-progress-bar {
display: none !important;
}
.battery-group { .battery-group {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@@ -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;

25
start.sh Normal file
View 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

View File

@@ -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), '')