Compare commits
No commits in common. "b80c9913781f7d331838c3750d42f536e65f70d3" and "1d550b8952c493f1b0059ea0af052981c02d7372" have entirely different histories.
b80c991378
...
1d550b8952
|
@ -5,7 +5,7 @@
|
||||||
<link rel="icon" href="/icon.ico">
|
<link rel="icon" href="/icon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>告警管理</title>
|
<title>告警管理</title>
|
||||||
<script src="/static/jsmpeg-master/jsmpeg.min.js"></script>
|
<script src="public\static\jsmpeg-master\jsmpeg.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
@ -22,11 +22,7 @@
|
||||||
<el-icon><Avatar /></el-icon>
|
<el-icon><Avatar /></el-icon>
|
||||||
<template #title><span>用户管理</span></template>
|
<template #title><span>用户管理</span></template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/settings">
|
<el-sub-menu index="1">
|
||||||
<el-icon><Setting /></el-icon>
|
|
||||||
<template #title><span>告警设置</span></template>
|
|
||||||
</el-menu-item>
|
|
||||||
<!-- <el-sub-menu index="1">
|
|
||||||
<template #title>
|
<template #title>
|
||||||
<el-icon><Location /></el-icon>
|
<el-icon><Location /></el-icon>
|
||||||
<span>面板测试</span>
|
<span>面板测试</span>
|
||||||
|
@ -47,7 +43,7 @@
|
||||||
<el-icon><Document /></el-icon>
|
<el-icon><Document /></el-icon>
|
||||||
<template #title><span>功能点3测试</span></template>
|
<template #title><span>功能点3测试</span></template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-sub-menu> -->
|
</el-sub-menu>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
|
||||||
|
@ -68,7 +64,7 @@
|
||||||
</span>
|
</span>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item @click="goToUserManagement">
|
<el-dropdown-item>
|
||||||
<el-icon><User /></el-icon> 用户中心
|
<el-icon><User /></el-icon> 用户中心
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item @click="onLogout">
|
<el-dropdown-item @click="onLogout">
|
||||||
|
@ -96,7 +92,7 @@ import { ref, watch, onMounted } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import {
|
import {
|
||||||
Document, WarningFilled, Location, User, SwitchButton,
|
Document, WarningFilled, Location, User, SwitchButton,
|
||||||
House, Management, TrendCharts, Avatar, Fold, Expand ,Setting
|
House, Management, TrendCharts, Avatar, Fold, Expand
|
||||||
} from '@element-plus/icons-vue';
|
} from '@element-plus/icons-vue';
|
||||||
import { BoxApi } from '@/utils/boxApi';
|
import { BoxApi } from '@/utils/boxApi';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
@ -107,10 +103,6 @@ const activeIndex = ref(route.path);
|
||||||
const isCollapse = ref(false); // 控制侧边栏收缩状态
|
const isCollapse = ref(false); // 控制侧边栏收缩状态
|
||||||
const username = ref(''); // 存储用户名
|
const username = ref(''); // 存储用户名
|
||||||
|
|
||||||
const goToUserManagement = () => {
|
|
||||||
router.push('/userList'); // 跳转到用户管理页面
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const storedUsername = localStorage.getItem('username');
|
const storedUsername = localStorage.getItem('username');
|
||||||
username.value = storedUsername ? storedUsername : '用户';
|
username.value = storedUsername ? storedUsername : '用户';
|
||||||
|
@ -249,18 +241,14 @@ const onLogout = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-footer {
|
.nav-footer {
|
||||||
background-color: #001529;
|
background-color: #fff;
|
||||||
/* background: linear-gradient(to top, rgb(3, 158, 185,0.7), rgb(123, 3, 153,0.7)); */
|
font-size: 16px;
|
||||||
font-size: 12px;
|
color: #333;
|
||||||
color: #fff;
|
height: 5vh;
|
||||||
font-weight: bold;
|
|
||||||
height: 2vh;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
align-items: center;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 1px solid #001529;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,152 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="settings-container">
|
|
||||||
<el-row class="popup-row">
|
|
||||||
<el-checkbox v-model="isPopupEnabled" @change="handleCheckboxChange">开启弹窗</el-checkbox>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
|
||||||
import { ElMessageBox, ElMessage } from 'element-plus';
|
|
||||||
|
|
||||||
// 用于控制复选框的勾选状态
|
|
||||||
const isPopupEnabled = ref(false);
|
|
||||||
let websocket: WebSocket | null = null;
|
|
||||||
let heartbeatInterval: number | null = null; // 用于心跳检测的定时器
|
|
||||||
|
|
||||||
// 从 localStorage 中读取勾选状态
|
|
||||||
onMounted(() => {
|
|
||||||
const storedState = localStorage.getItem('isPopupEnabled');
|
|
||||||
isPopupEnabled.value = storedState === 'true';
|
|
||||||
|
|
||||||
if (isPopupEnabled.value) {
|
|
||||||
connectWebSocket();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 当页面离开或组件卸载时,清理 WebSocket 连接和心跳检测
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
closeWebSocket();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 当用户勾选或取消勾选时触发的函数
|
|
||||||
const handleCheckboxChange = () => {
|
|
||||||
if (isPopupEnabled.value) {
|
|
||||||
// 如果复选框被勾选,弹出确认对话框
|
|
||||||
ElMessageBox.confirm('是否开启弹窗提示?', '提示', {
|
|
||||||
confirmButtonText: '是',
|
|
||||||
cancelButtonText: '否',
|
|
||||||
type: 'warning',
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
// 用户点击确认,开启 WebSocket
|
|
||||||
localStorage.setItem('isPopupEnabled', 'true'); // 保存状态到 localStorage
|
|
||||||
connectWebSocket();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// 用户取消,重置勾选状态
|
|
||||||
isPopupEnabled.value = false;
|
|
||||||
localStorage.setItem('isPopupEnabled', 'false'); // 更新 localStorage
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 取消勾选时关闭 WebSocket 连接
|
|
||||||
closeWebSocket();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// WebSocket 连接函数
|
|
||||||
const connectWebSocket = () => {
|
|
||||||
websocket = new WebSocket('ws://192.168.28.11:8080/event/ws');
|
|
||||||
|
|
||||||
websocket.onopen = () => {
|
|
||||||
ElMessage.success('弹窗告警开启成功');
|
|
||||||
startHeartbeat(); // 启动心跳检测
|
|
||||||
};
|
|
||||||
|
|
||||||
websocket.onmessage = (event) => {
|
|
||||||
// 接收到消息时,显示浏览器通知
|
|
||||||
showNotification(event.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
websocket.onclose = () => {
|
|
||||||
ElMessage.warning('WebSocket 已关闭');
|
|
||||||
isPopupEnabled.value = false; // 如果连接异常关闭,取消勾选状态
|
|
||||||
localStorage.setItem('isPopupEnabled', 'false'); // 更新 localStorage
|
|
||||||
stopHeartbeat(); // 停止心跳检测
|
|
||||||
};
|
|
||||||
|
|
||||||
websocket.onerror = () => {
|
|
||||||
ElMessage.error('WebSocket 连接出错');
|
|
||||||
isPopupEnabled.value = false;
|
|
||||||
localStorage.setItem('isPopupEnabled', 'false');
|
|
||||||
stopHeartbeat(); // 停止心跳检测
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 关闭 WebSocket 连接
|
|
||||||
const closeWebSocket = () => {
|
|
||||||
if (websocket) {
|
|
||||||
websocket.close();
|
|
||||||
ElMessage.info('WebSocket 连接已关闭');
|
|
||||||
localStorage.setItem('isPopupEnabled', 'false'); // 更新 localStorage
|
|
||||||
stopHeartbeat(); // 停止心跳检测
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 显示浏览器通知
|
|
||||||
const showNotification = (message: string) => {
|
|
||||||
if (Notification.permission === 'granted') {
|
|
||||||
new Notification('新消息', {
|
|
||||||
body: message,
|
|
||||||
icon: '/path/to/icon.png', // 可以指定通知的图标
|
|
||||||
});
|
|
||||||
} else if (Notification.permission !== 'denied') {
|
|
||||||
// 请求用户授权通知
|
|
||||||
Notification.requestPermission().then((permission) => {
|
|
||||||
if (permission === 'granted') {
|
|
||||||
new Notification('新消息', {
|
|
||||||
body: message,
|
|
||||||
icon: '/path/to/icon.png',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 心跳检测,定时发送消息或 ping WebSocket
|
|
||||||
const startHeartbeat = () => {
|
|
||||||
if (heartbeatInterval) return;
|
|
||||||
|
|
||||||
heartbeatInterval = window.setInterval(() => {
|
|
||||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
|
||||||
websocket.send('ping'); // 发送心跳消息
|
|
||||||
}
|
|
||||||
}, 5000); // 每5秒发送一次心跳消息
|
|
||||||
};
|
|
||||||
|
|
||||||
// 停止心跳检测
|
|
||||||
const stopHeartbeat = () => {
|
|
||||||
if (heartbeatInterval) {
|
|
||||||
clearInterval(heartbeatInterval);
|
|
||||||
heartbeatInterval = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检查通知权限
|
|
||||||
onMounted(() => {
|
|
||||||
if (Notification.permission !== 'granted') {
|
|
||||||
Notification.requestPermission();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.settings-container {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.popup-row {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -85,17 +85,15 @@ onMounted(async () => {
|
||||||
/* margin-bottom: 10px; */
|
/* margin-bottom: 10px; */
|
||||||
/* display: flex; */
|
/* display: flex; */
|
||||||
/* gap: 5px; */
|
/* gap: 5px; */
|
||||||
/* background-color: #fff; */
|
background-color: #fff;
|
||||||
/* background-color: #1E2E4A; */
|
|
||||||
/* overflow: hidden; */
|
/* overflow: hidden; */
|
||||||
/* height: 55vh; */
|
/* height: 55vh; */
|
||||||
/* max-height: 450px; */
|
/* max-height: 450px; */
|
||||||
/* padding-left: 1vh; */
|
/* padding-left: 1vh; */
|
||||||
/* padding-right:1vh ; */
|
/* padding-right:1vh ; */
|
||||||
/* overflow: hidden; */
|
/* overflow: hidden; */
|
||||||
height: 56vh;
|
height: 55vh;
|
||||||
margin-top: 60px;
|
margin-top: 60px;
|
||||||
/* border: 1px solid #1E2E4A; */
|
|
||||||
}
|
}
|
||||||
.bottom-pan{
|
.bottom-pan{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -1,15 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="camera-container">
|
<div class="camera-container">
|
||||||
<!-- 左侧:摄像头列表 -->
|
<!-- 左侧摄像头列表 -->
|
||||||
<div class="camera-list">
|
<div class="camera-list">
|
||||||
<el-input
|
<el-card v-for="camera in cameras" :key="camera.id" class="camera-item" @click="selectCamera(camera)">
|
||||||
v-model="searchQuery"
|
|
||||||
placeholder="搜索摄像头名称"
|
|
||||||
prefix-icon="el-icon-search"
|
|
||||||
clearable
|
|
||||||
class="search-input"
|
|
||||||
/>
|
|
||||||
<el-card v-for="camera in filteredCameras" :key="camera.id" class="camera-item" @click="selectCamera(camera)">
|
|
||||||
<div class="camera-header">
|
<div class="camera-header">
|
||||||
<span>ID: {{ camera.id }}</span>
|
<span>ID: {{ camera.id }}</span>
|
||||||
<span class="status" :class="{ 'online': camera.status === 'online', 'offline': camera.status !== 'online' }">
|
<span class="status" :class="{ 'online': camera.status === 'online', 'offline': camera.status !== 'online' }">
|
||||||
|
@ -23,232 +16,208 @@
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧:摄像头详情栅格 -->
|
<!-- 右侧摄像头详情和播放控制 -->
|
||||||
<div class="camera-grid">
|
<div class="camera-details" v-if="selectedCamera">
|
||||||
<div v-for="camera in selectedCameras" :key="camera.id" class="camera-details">
|
<el-card>
|
||||||
<el-card class="camera-card">
|
|
||||||
<div class="stream-control">
|
<div class="stream-control">
|
||||||
<p class="camera-name-title">{{ camera.name }}</p>
|
<p>{{ selectedCamera.name }}</p>
|
||||||
<el-button @click="closeStream(camera)" class="close-button" circle size="mini">X</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 视频播放和快照部分 -->
|
<!-- 在未播放时显示播放按钮 -->
|
||||||
<div class="play-button-container" @mouseenter="showButton = true" @mouseleave="showButton = false">
|
<div class="play-button-container" @mouseenter="showButton = true" @mouseleave="showButton = false">
|
||||||
<!-- 未播放时显示快照或占位符 -->
|
<img v-show="!playing && selectedCamera.snapshot" :src="selectedCamera.snapshot" alt="camera snapshot"
|
||||||
<div class="camera-placeholder" v-if="!camera.playing && !camera.snapshot">
|
|
||||||
<el-icon size="48">
|
|
||||||
<VideoCameraFilled />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<img v-if="!camera.playing && camera.snapshot" :src="camera.snapshot" alt="camera snapshot"
|
|
||||||
class="camera-snapshot" />
|
class="camera-snapshot" />
|
||||||
|
<canvas v-show="playing" ref="canvasRef" class="camera-large"></canvas>
|
||||||
|
|
||||||
<!-- 播放视频流的 canvas -->
|
<!-- 播放按钮 -->
|
||||||
<canvas v-show="camera.playing" :ref="el => setCanvasRef(camera.id, el)" class="camera-large"></canvas>
|
<el-button v-show="!playing || showButton" class="play-button" type="primary" circle size="large"
|
||||||
|
@click="handlePlayPause">
|
||||||
<!-- 播放和暂停按钮 -->
|
|
||||||
<el-button v-show="!camera.playing || showButton" class="play-button" type="primary" circle size="large"
|
|
||||||
@click="handlePlayPause(camera)">
|
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<VideoPlay v-if="!camera.playing" />
|
<VideoPlay v-if="!playing" />
|
||||||
<VideoPause v-if="camera.playing" />
|
<VideoPause v-if="playing" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount, nextTick,computed } from 'vue';
|
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||||
import { BoxApi } from '@/utils/boxApi.ts';
|
import { BoxApi } from '@/utils/boxApi.ts';
|
||||||
import { VideoPlay, VideoPause, VideoCameraFilled } from '@element-plus/icons-vue';
|
import { VideoPlay, VideoPause } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
// 存储所有摄像头列表
|
|
||||||
const cameras = ref([]);
|
const cameras = ref([]);
|
||||||
// 存储已选择并展示的摄像头
|
const selectedCamera = ref(null);
|
||||||
const selectedCameras = ref([]);
|
const playing = ref(false);
|
||||||
// 控制播放按钮的显示状态
|
const streamPort = ref(null);
|
||||||
const showButton = ref(false);
|
const canvasRef = ref(null);
|
||||||
// API 实例
|
const playerRef = ref(null);
|
||||||
|
const showButton = ref(false); // 用于控制按钮的显示和隐藏
|
||||||
|
|
||||||
const apiInstance = new BoxApi();
|
const apiInstance = new BoxApi();
|
||||||
// 存储 canvas 引用
|
|
||||||
const canvasRefs = ref({});
|
|
||||||
|
|
||||||
// 搜索输入的查询字符串
|
// 请求摄像头数据
|
||||||
const searchQuery = ref('');
|
|
||||||
|
|
||||||
// 计算属性:根据搜索关键词过滤摄像头列表
|
|
||||||
const filteredCameras = computed(() =>
|
|
||||||
cameras.value.filter(camera =>
|
|
||||||
camera.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 获取摄像头列表
|
|
||||||
const fetchCameras = async () => {
|
const fetchCameras = async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('alertToken');
|
const token = localStorage.getItem('alertToken');
|
||||||
const cameraData = await apiInstance.getMinCameras(token);
|
const cameraData = await apiInstance.getMinCameras(token); // 调用 getMinCameras 方法
|
||||||
cameras.value = cameraData;
|
cameras.value = cameraData; // 保存摄像头列表数据
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取摄像头列表失败:', error);
|
console.error('Error fetching cameras:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 选择摄像头并展示详情
|
// 选择摄像头
|
||||||
const selectCamera = (camera) => {
|
const selectCamera = (camera) => {
|
||||||
if (!selectedCameras.value.some(c => c.id === camera.id)) {
|
selectedCamera.value = camera; // 将点击的摄像头设为选中的摄像头
|
||||||
selectedCameras.value.push({ ...camera, playing: false, streamPort: null });
|
playing.value = false; // 重置播放状态
|
||||||
}
|
streamPort.value = null; // 重置流端口
|
||||||
|
showButton.value = true; // 重置按钮显示状态
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设置 canvas 引用
|
// 启动或暂停播放
|
||||||
const setCanvasRef = (cameraId, el) => {
|
const handlePlayPause = async () => {
|
||||||
if (el) {
|
if (playing.value) {
|
||||||
canvasRefs.value[cameraId] = el;
|
handleStopStream();
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 播放/暂停摄像头视频流
|
|
||||||
const handlePlayPause = async (camera) => {
|
|
||||||
if (camera.playing) {
|
|
||||||
handleStopStream(camera);
|
|
||||||
} else {
|
} else {
|
||||||
handleStartStream(camera);
|
handleStartStream();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 启动视频流
|
// 启动流播放
|
||||||
const handleStartStream = async (camera) => {
|
const handleStartStream = async () => {
|
||||||
await nextTick(); // 确保 DOM 已渲染
|
console.log('Button clicked and playing is:', playing.value);
|
||||||
const canvas = canvasRefs.value[camera.id];
|
if (!selectedCamera.value || !canvasRef.value) {
|
||||||
|
|
||||||
if (!camera || !canvas) {
|
|
||||||
console.error('未找到对应的 canvas');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem('alertToken');
|
const token = localStorage.getItem('alertToken');
|
||||||
try {
|
try {
|
||||||
const response = await apiInstance.startCameraStream(token, camera.id);
|
const response = await apiInstance.startCameraStream(token, selectedCamera.value.id);
|
||||||
camera.streamPort = response.port;
|
streamPort.value = response.port;
|
||||||
camera.playing = true;
|
playing.value = true;
|
||||||
|
|
||||||
const rememberedAddress = localStorage.getItem('rememberedAddress') || '127.0.0.1';
|
// 创建播放器
|
||||||
const url = `ws://${rememberedAddress}:${camera.streamPort}/`;
|
const url = `ws://192.168.28.33:${streamPort.value}/`;
|
||||||
// const url = `ws://192.168.28.33:${camera.streamPort}/`;
|
console.log('Playing set to true:', playing.value);
|
||||||
console.log('播放路径:', url);
|
|
||||||
|
if (playerRef.value) {
|
||||||
|
playerRef.value.destroy(); // 销毁旧播放器
|
||||||
|
}
|
||||||
|
|
||||||
if (window.JSMpeg) {
|
if (window.JSMpeg) {
|
||||||
const player = new window.JSMpeg.Player(url, {
|
playerRef.value = new window.JSMpeg.Player(url, {
|
||||||
canvas: canvas,
|
canvas: canvasRef.value,
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
videoBufferSize: 15 * 1024 * 1024,
|
videoBufferSize: 15 * 1024 * 1024,
|
||||||
audioBufferSize: 5 * 1024 * 1024,
|
audioBufferSize: 5 * 1024 * 1024,
|
||||||
});
|
});
|
||||||
camera.player = player;
|
|
||||||
} else {
|
} else {
|
||||||
console.error('JSMpeg 未加载');
|
console.error('JSMpeg is not available on window object.');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('启动视频流失败:', error);
|
console.error('Error starting stream:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 停止视频流
|
// 停止流播放
|
||||||
const handleStopStream = async (camera) => {
|
const handleStopStream = async () => {
|
||||||
|
if (!selectedCamera.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem('alertToken');
|
const token = localStorage.getItem('alertToken');
|
||||||
try {
|
try {
|
||||||
await apiInstance.stopCameraStream(token, camera.id);
|
await apiInstance.stopCameraStream(token, selectedCamera.value.id);
|
||||||
camera.playing = false;
|
playing.value = false;
|
||||||
|
|
||||||
if (camera.player) {
|
// 销毁播放器
|
||||||
camera.player.destroy();
|
if (playerRef.value) {
|
||||||
camera.player = null;
|
playerRef.value.destroy();
|
||||||
|
playerRef.value = null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('停止视频流失败:', error);
|
console.error('Error stopping stream:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 关闭摄像头流视图
|
// 组件卸载时清理播放器
|
||||||
const closeStream = (camera) => {
|
onBeforeUnmount(() => {
|
||||||
handleStopStream(camera);
|
if (playerRef.value) {
|
||||||
selectedCameras.value = selectedCameras.value.filter(c => c.id !== camera.id);
|
playerRef.value.destroy();
|
||||||
};
|
playerRef.value = null;
|
||||||
|
}
|
||||||
// 组件挂载时获取摄像头列表
|
|
||||||
onMounted(() => {
|
|
||||||
fetchCameras();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 组件卸载时清理资源
|
onMounted(() => {
|
||||||
onBeforeUnmount(() => {
|
fetchCameras(); // 页面加载时请求摄像头数据
|
||||||
selectedCameras.value.forEach(camera => {
|
|
||||||
if (camera.player) {
|
|
||||||
camera.player.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.camera-container {
|
.camera-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh;
|
|
||||||
background-color: #1E2E4A;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 左侧摄像头列表 */
|
|
||||||
.camera-list {
|
.camera-list {
|
||||||
width: 20%;
|
width: 20%;
|
||||||
min-width: 215px;
|
min-width: 215px;
|
||||||
max-height: 100vh;
|
max-height: 90vh;
|
||||||
/* 限制高度为一屏 */
|
/* 限制高度为一屏 */
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
/* 超出时滚动 */
|
/* 超出时滚动 */
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-right: 1px solid #1E2E4A;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 每个摄像头项目的样式 */
|
|
||||||
.camera-item {
|
.camera-item {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 12px;
|
padding: 10px;
|
||||||
border: 1px solid #458388;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: background-color 0.3s, box-shadow 0.3s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.camera-item:hover {
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 摄像头项目头部 */
|
|
||||||
.camera-header {
|
.camera-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 摄像头状态标签 */
|
|
||||||
.status {
|
.status {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
font-weight: bold;
|
}
|
||||||
|
|
||||||
|
.camera-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-thumbnail {
|
||||||
|
width: 60px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-name {
|
||||||
|
flex: 1;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-details {
|
||||||
|
width: 80%;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-snapshot {
|
||||||
|
width: 100%;
|
||||||
|
height: 68vh;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-large {
|
||||||
|
width: 100%;
|
||||||
|
height: 68vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.online {
|
.online {
|
||||||
|
@ -259,141 +228,28 @@ onBeforeUnmount(() => {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 摄像头内容:缩略图和名称 */
|
|
||||||
.camera-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.camera-thumbnail {
|
|
||||||
width: 70px;
|
|
||||||
height: 50px;
|
|
||||||
margin-right: 10px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 2px solid #12d1df;
|
|
||||||
}
|
|
||||||
|
|
||||||
.camera-name {
|
|
||||||
flex: 1;
|
|
||||||
word-break: break-word;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 右侧摄像头详情展示 */
|
|
||||||
.camera-grid {
|
|
||||||
width: 80%;
|
|
||||||
height: 95vh; /* 占满页面右侧区域 */
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr); /* 两列 */
|
|
||||||
grid-template-rows: repeat(2, 1fr); /* 两行 */
|
|
||||||
gap: 5px; /* 栅格块之间的间距 */
|
|
||||||
padding: 10px;
|
|
||||||
/* background-color: #1E2E4A; */
|
|
||||||
background: linear-gradient(to top, rgba(0, 3, 3, 0.4), rgba(9, 21, 196, 0.3));
|
|
||||||
/* background: linear-gradient(to top, rgba(8, 53, 61, 0.4), rgba(9, 21, 196, 0.3)); */
|
|
||||||
/* border: 2px solid #ece9e9; */
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.camera-details {
|
|
||||||
height: 45vh;
|
|
||||||
/* background-color: #3b2c2c; */
|
|
||||||
/* background: linear-gradient(to top, rgba(8, 53, 61, 0.4), rgba(9, 21, 196, 0.3)); */
|
|
||||||
border-radius: 4px;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
/* overflow-y: auto; */
|
|
||||||
}
|
|
||||||
.camera-card {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
/* background-color: #1E2E4A; */
|
|
||||||
background: linear-gradient(to top, rgba(64, 226, 255, 0.7), rgba(211, 64, 248, 0.7));
|
|
||||||
/* background: linear-gradient(to top, rgba(0, 7, 8, 0.9), rgba(12, 2, 155, 0.7)); */
|
|
||||||
border: 2px solid #0b4c5f;
|
|
||||||
border-radius: 8px;
|
|
||||||
position: relative;
|
|
||||||
/* padding: 10px; */
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.stream-control {
|
.stream-control {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
margin-top: 20px;
|
||||||
align-items: center;
|
text-align: center;
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .stream-control p {
|
|
||||||
color: #f5f7fa;
|
|
||||||
font-size: 18px;
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1;
|
|
||||||
font-weight: bold;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.camera-name-title {
|
|
||||||
font-size: 18px;
|
|
||||||
color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-button {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 20px;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.camera-placeholder {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
color: #6b6b6b;
|
|
||||||
border: 2px dashed rgba(109, 109, 109, 0.7);
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
|
||||||
background-color: rgba(50, 50, 50, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 播放中的 canvas 样式 */
|
|
||||||
.camera-large {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 8px;
|
|
||||||
object-fit: cover;
|
|
||||||
border: 2px solid rgba(109, 109, 109, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 摄像头大图和快照 */
|
|
||||||
.camera-snapshot {
|
|
||||||
width: 100%;
|
|
||||||
height: 40vh;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 2px solid #dcdcdc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 播放按钮容器 */
|
.stream-control p {
|
||||||
|
margin-right: 20px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-control el-button {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
.play-button-container {
|
.play-button-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 播放按钮 */
|
|
||||||
.play-button {
|
.play-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
|
|
|
@ -7,18 +7,12 @@ import { User, Lock } from '@element-plus/icons-vue';
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
// 获取主机地址(去掉端口)
|
|
||||||
const getHostAddress = () => window.location.hostname;
|
|
||||||
const getHostPort = () => '8000';
|
|
||||||
|
|
||||||
|
|
||||||
const loginForm = reactive({
|
const loginForm = reactive({
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
address: localStorage.getItem('rememberedAddress') || getHostAddress(),
|
address: '',
|
||||||
port: localStorage.getItem('rememberedPort') || getHostPort(),
|
port: '',
|
||||||
remember: false,
|
remember: false // 新增 "记住密码" 选项
|
||||||
serverSettings: false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
|
@ -29,48 +23,50 @@ const cookless = ref(false);
|
||||||
// 创建实例
|
// 创建实例
|
||||||
let apiInstance: BoxApi | null = null;
|
let apiInstance: BoxApi | null = null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 加载页面时检查是否有记住的密码
|
// 加载页面时检查是否有记住的密码
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const savedUsername = localStorage.getItem('username');
|
const savedUsername = localStorage.getItem('username');
|
||||||
const savedPassword = localStorage.getItem('rememberedPassword');
|
const savedPassword = localStorage.getItem('rememberedPassword');
|
||||||
// const savedAddress = localStorage.getItem('rememberedAddress');
|
const savedAddress = localStorage.getItem('rememberedAddress');
|
||||||
// const savedPort = localStorage.getItem('rememberedPort');
|
const savedPort = localStorage.getItem('rememberedPort');
|
||||||
if (savedUsername && savedPassword) {
|
|
||||||
|
if (savedUsername && savedPassword && savedAddress && savedPort) {
|
||||||
loginForm.username = savedUsername;
|
loginForm.username = savedUsername;
|
||||||
loginForm.password = savedPassword;
|
loginForm.password = savedPassword;
|
||||||
|
loginForm.address = savedAddress;
|
||||||
|
loginForm.port = savedPort;
|
||||||
loginForm.remember = true; // 如果有记住的数据,将复选框设为选中状态
|
loginForm.remember = true; // 如果有记住的数据,将复选框设为选中状态
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
// if (!loginForm.address || !loginForm.port) {
|
// 检查服务器地址和端口是否填写
|
||||||
// errorMessage.value = '请输入服务器地址和端口';
|
if (!loginForm.address || !loginForm.port) {
|
||||||
// return;
|
errorMessage.value = '请输入服务器地址和端口';
|
||||||
// }
|
return;
|
||||||
const address = loginForm.serverSettings ? loginForm.address : getHostAddress();
|
}
|
||||||
const port = loginForm.serverSettings ? loginForm.port : getHostPort();
|
|
||||||
|
|
||||||
// 实例化 SuperboxApi
|
// 实例化 SuperboxApi
|
||||||
apiInstance = new BoxApi(address, parseInt(port));
|
apiInstance = new BoxApi(loginForm.address, parseInt(loginForm.port));
|
||||||
|
|
||||||
// 调用 SuperboxApi 的 login 方法,传入 cookless 为 false
|
// 调用 SuperboxApi 的 login 方法,传入 cookless 为 false
|
||||||
const apiToken = await apiInstance.login(loginForm.username, loginForm.password, false);
|
const apiToken = await apiInstance.login(loginForm.username, loginForm.password,false);
|
||||||
|
|
||||||
// 保存 token 到本地存储
|
// 保存 token 到本地存储
|
||||||
localStorage.setItem('alertToken', apiToken);
|
localStorage.setItem('alertToken', apiToken);
|
||||||
localStorage.setItem('username', loginForm.username);
|
|
||||||
localStorage.setItem('rememberedAddress', address);
|
|
||||||
localStorage.setItem('rememberedPort', port);
|
|
||||||
|
|
||||||
// 如果勾选了“记住密码”,将用户名、密码、地址和端口存入 localStorage
|
// 如果勾选了“记住密码”,将用户名、密码、地址和端口存入 localStorage
|
||||||
if (loginForm.remember) {
|
if (loginForm.remember) {
|
||||||
|
localStorage.setItem('username', loginForm.username);
|
||||||
localStorage.setItem('rememberedPassword', loginForm.password);
|
localStorage.setItem('rememberedPassword', loginForm.password);
|
||||||
|
localStorage.setItem('rememberedAddress', loginForm.address);
|
||||||
|
localStorage.setItem('rememberedPort', loginForm.port);
|
||||||
} else {
|
} else {
|
||||||
// 如果未勾选“记住密码”,清除之前记住的数据
|
// 如果未勾选“记住密码”,清除之前记住的数据
|
||||||
localStorage.removeItem('rememberedPassword');
|
localStorage.removeItem('rememberedPassword');
|
||||||
|
localStorage.removeItem('rememberedAddress');
|
||||||
|
localStorage.removeItem('rememberedPort');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转到首页
|
// 跳转到首页
|
||||||
|
@ -88,12 +84,9 @@ const onSubmit = async () => {
|
||||||
const onReset = () => {
|
const onReset = () => {
|
||||||
loginForm.username = '';
|
loginForm.username = '';
|
||||||
loginForm.password = '';
|
loginForm.password = '';
|
||||||
// loginForm.address = '';
|
loginForm.address = '';
|
||||||
// loginForm.port = '';
|
loginForm.port = '';
|
||||||
loginForm.address = getHostAddress(); // 重置为当前主机地址
|
|
||||||
loginForm.port = getHostPort(); // 重置为当前端口号
|
|
||||||
loginForm.remember = false; // 重置记住密码复选框
|
loginForm.remember = false; // 重置记住密码复选框
|
||||||
loginForm.serverSettings = false;
|
|
||||||
errorMessage.value = '';
|
errorMessage.value = '';
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -109,10 +102,10 @@ const onReset = () => {
|
||||||
</div>
|
</div>
|
||||||
<div class="login-body">
|
<div class="login-body">
|
||||||
<el-form :model="loginForm" style="max-width: 600px;">
|
<el-form :model="loginForm" style="max-width: 600px;">
|
||||||
<el-form-item v-show="loginForm.serverSettings">
|
<el-form-item>
|
||||||
<el-input v-model="loginForm.address" placeholder="服务器地址" />
|
<el-input v-model="loginForm.address" placeholder="服务器地址" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-show="loginForm.serverSettings">
|
<el-form-item>
|
||||||
<el-input v-model="loginForm.port" placeholder="端口" type="number" />
|
<el-input v-model="loginForm.port" placeholder="端口" type="number" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
|
@ -133,14 +126,10 @@ const onReset = () => {
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-row class="setting-row">
|
<!-- 添加记住密码复选框 -->
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-checkbox v-model="loginForm.remember">记住密码</el-checkbox>
|
<el-checkbox v-model="loginForm.remember">记住密码</el-checkbox>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
|
||||||
<el-checkbox v-model="loginForm.serverSettings">服务器设置</el-checkbox>
|
|
||||||
</el-form-item>
|
|
||||||
</el-row>
|
|
||||||
<el-row justify="end">
|
<el-row justify="end">
|
||||||
<el-form-item size="small" :gutter="30">
|
<el-form-item size="small" :gutter="30">
|
||||||
<el-button type="primary" @click="onReset" plain>重置</el-button>
|
<el-button type="primary" @click="onReset" plain>重置</el-button>
|
||||||
|
@ -212,8 +201,4 @@ const onReset = () => {
|
||||||
.error-message {
|
.error-message {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-row {
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -10,7 +10,6 @@ import UserList from '@/html/UserList.vue';
|
||||||
import Home from '@/html/Home.vue';
|
import Home from '@/html/Home.vue';
|
||||||
import DataStatistics from '@/html/DataStatistics.vue';
|
import DataStatistics from '@/html/DataStatistics.vue';
|
||||||
import Cameras from '@/components/Cameras.vue';
|
import Cameras from '@/components/Cameras.vue';
|
||||||
import Settings from '@/components/Settings.vue';
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
|
@ -66,12 +65,6 @@ const routes = [
|
||||||
name: 'Cameras',
|
name: 'Cameras',
|
||||||
component: Cameras,
|
component: Cameras,
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
},
|
|
||||||
{
|
|
||||||
path:'/settings',
|
|
||||||
name: 'Settings',
|
|
||||||
component: Settings,
|
|
||||||
meta: { requiresAuth: true }
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -382,24 +382,17 @@ class BoxApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _authHeader(token: string | null = null): object {
|
private _authHeader(token: string | null = null): object {
|
||||||
const accessToken = token || this.token || localStorage.getItem('alertToken') || "";
|
// const access_token = token === "" ? this.token : token;
|
||||||
|
const alertToken = localStorage.getItem(`alertToken`) || token || this.token || "" || "";
|
||||||
return {
|
return {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
'Authorization': `Bearer ${alertToken}`,
|
||||||
// 'X-CSRFToken': this.getCsrfToken()
|
// 'Cookie': `jwt=${access_token}`
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
private getCsrfToken(): string {
|
|
||||||
const cookie = document.cookie.split(';').find(cookie => cookie.trim().startsWith('csrftoken='));
|
|
||||||
if (cookie) {
|
|
||||||
return cookie.split('=')[1];
|
|
||||||
}
|
}
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private _boxAddr() {
|
private _boxAddr() {
|
||||||
return window.location.host.replace(/:\d+/, "");
|
return window.location.host.replace(/:\d+/, "");
|
||||||
|
|
Loading…
Reference in New Issue