Commit 81b7a05a authored by 柳 佳乐's avatar 柳 佳乐
Browse files

头版

parents
Pipeline #81 failed with stages
in 0 seconds
<template>
<div class="camera-container">
<!-- 相机模块 -->
<el-card class="camera-module">
<div class="header-container">
<div class="header-title">相机1模块</div>
<div class="header-buttons">
<el-button type="primary" @click="getCameraList">获取相机列表</el-button>
</div>
</div>
<template v-if="cameraList.length > 0">
<!-- 相机参数模块 - 紧凑版 -->
<div class="camera-params compact">
<div class="section-title">相机参数设置</div>
<el-form inline class="params-form">
<el-form-item label="分辨率">
<el-select v-model="globalParams.resolution" style="width: 140px">
<el-option label="2560×1440" value="2560x1440"></el-option>
<el-option label="1920×1080" value="1920x1080"></el-option>
<el-option label="1280×960" value="1280x960"></el-option>
</el-select>
</el-form-item>
<el-form-item label="帧率">
<el-select v-model="globalParams.fps" style="width: 80px">
<el-option label="15" :value="15"></el-option>
<el-option label="30" :value="30"></el-option>
<el-option label="60" :value="60"></el-option>
</el-select>
</el-form-item>
<el-form-item label="保存路径">
<el-input v-model="globalParams.savePath" placeholder="/images" style="width: 180px"></el-input>
</el-form-item>
<el-form-item>
<el-button-group>
<el-button type="primary" @click="initAllCameras" icon="el-icon-refresh">初始化</el-button>
<el-switch v-model="globalParams.lowFpsMode" active-text="低帧率" inactive-text="原始帧"
@change="toggleFpsMode" style="margin-left: 10px;" />
<el-button @click="closeAllCameras" size="mini" icon="el-icon-close"
style="margin-left: 10px;">关闭所有</el-button>
</el-button-group>
</el-form-item>
</el-form>
</div>
<!-- 视频帧显示区 -->
<div class="section-title">视频帧显示</div>
<el-row :gutter="20" class="frame-display-row">
<el-col :span="8" v-for="camera in cameraList" :key="camera.id" class="frame-display-wrapper">
<el-card class="frame-display">
<div slot="header" class="clearfix">
<div class="frame-title">
<span class="camera-id">相机 {{ camera.id }}_{{ camera.name }}</span>
<span class="camera-spec">({{ camera.width }}×{{ camera.height }} @{{ camera.fps }}fps)</span>
</div>
</div>
<div class="frame-header">
<el-select v-model="camera.cameraOrientation" style="width: 120px">
<el-option label="不指定" value="不指定"></el-option>
<el-option label="后侧" value="后侧"></el-option>
<el-option label="左侧" value="左侧"></el-option>
<el-option label="右侧" value="右侧"></el-option>
</el-select>
<el-button-group style="margin-left: 10px;">
<el-button @click="configCamera(camera)" size="mini">配置</el-button>
<el-button type="success" @click="startStream(camera)" size="mini"
icon="el-icon-video-play">链接</el-button>
<el-button type="danger" @click="stopStream(camera)" size="mini"
icon="el-icon-switch-button">断链</el-button>
</el-button-group>
</div>
<div v-if="!camera.imageData" ref="frameDisplay" style="width: 100%; height: 280px; background: #000;">
</div>
<canvas v-else :ref="'canvas_' + camera.id" style="width: 100%; height: 100%; background: #000;"></canvas>
<div class="status-info">
<el-tag v-if="camera.statusInfo" type="info" size="small">{{ camera.statusInfo }}</el-tag>
</div>
<div class="status-log">
<el-alert v-if="camera.captureLog" :title="camera.captureLog" type="info" :closable="false" show-icon
style="padding: 5px 10px;" />
</div>
</el-card>
</el-col>
</el-row>
<!-- 全局控制模块 -->
<div class="section-title">全局控制</div>
<div class="global-controls">
<el-button-group>
<el-button type="success" @click="captureImage">捕获</el-button>
</el-button-group>
</div>
</template>
<div v-else class="empty-tip">
<el-empty description="暂无相机,请点击右上角按钮获取相机列表"></el-empty>
</div>
</el-card>
</div>
</template>
<script>
import {
baseURLConsole
} from '@/utils/url'
import { getStillCameraList, initializeStillCamera, setStillCameraOrientation, lowStillCamera, getWebSocketLink, closeStillCamera, captureStillCamera, disconnectStillCamera } from '@/api/stillCamera'
export default {
name:'StillCamera',
data() {
return {
globalParams: {
fps: 30,//帧率
resolution: '2560x1440',//分别率
savePath: '/images/captures',//存储路径
lowFpsMode: false,//低帧率模式开关
},
cameraList: [],//相机列表
}
},
methods: {
//获取相机列表
getCameraList() {
// 调用接口获取相机列表
getStillCameraList().then(res => {
console.log(res)
if (res.data.length > 0) {
let list = [];
res.data.forEach(item => {
list.push({
id: item.camera_id,
name: item.name,
width: 0,
height: 0,
fps: 0,
cameraOrientation: '不指定',//方位
websocket: null,
reconnectAttempts: 0,//重连次数限制
imageData: null,
objectURL: null,
statusInfo: null,
captureLog: '等待连接',
})
})
this.cameraList = list;
this.$message.success(`获取到 ${this.cameraList.length} 个相机`)
//查询相机原有的webScoket链接并关闭
getWebSocketLink().then(res => {
console.log(res)
if (res.data.connected_cameras.length > 0) {
res.data.connected_cameras.forEach(item => {
disconnectStillCamera(item)
})
}
})
} else {
this.$message.error('当前没有相机')
}
})
},
//初始化所有相机
initAllCameras() {
// 调用接口初始化所有相机
let cameraIds = [];
const [width, height] = this.globalParams.resolution.split('x')
//设置相机
this.cameraList.forEach(camera => {
cameraIds.push(camera.id)
})
//设置参数
let param = {
camera_ids: cameraIds.join(","),
init_params: {
resolution_width: parseInt(width),
resolution_height: parseInt(height),
fps: this.globalParams.fps
}
}
initializeStillCamera(param).then(res => {
console.log(res)
this.cameraList.forEach(camera => {
camera.width = parseInt(width)
camera.height = parseInt(height)
camera.fps = this.globalParams.fps
})
this.$message.success(`初始化所有相机成功`)
})
},
// 切换帧率模式
toggleFpsMode(isLowFps) {
// 调用接口设置帧率模式
let cameraIds = [];
this.cameraList.forEach(camera => {
cameraIds.push(camera.id)
});
let param = {
camera_ids: cameraIds.join(','),
fps: isLowFps ? 5 : 25
};
lowStillCamera(param).then(res => {
this.$message.success(isLowFps ? '已启用低帧率模式' : '已恢复原始帧率');
})
},
//配置相机
configCamera(camera) {
let param = {
camera_id: camera.id,
orientation: camera.cameraOrientation
}
//调用接口配置相机
setStillCameraOrientation(param).then(res => {
console.log(res)
this.$message.success(`配置相机成功`)
})
},
//开始链接webScoke
startStream(camera) {
// 创建canvas用于渲染帧数据
if (camera.websocket) {
camera.websocket.close();
camera.websocket = null;
// 等待一小段时间确保连接完全关闭
setTimeout(() => {
this.establishWebSocketConnection(camera);
}, 100);
} else {
this.establishWebSocketConnection(camera);
}
},
establishWebSocketConnection(camera) {
const wsUrl = `ws://${baseURLConsole}/api/cameras/hd_static/ws/connect/${camera.id}`;
camera.websocket = new WebSocket(wsUrl);
camera.reconnectAttempts = 0;
camera.websocket.onopen = (event) => {
camera.captureLog = 'WebSocket已连接,等待相机响应...'
//心跳检测
setInterval(() => {
if (camera.websocket.readyState === WebSocket.OPEN) {
camera.websocket.send('ping');
}
}, 30000);
};
camera.websocket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// 处理连接状态消息
if (data.status) {
camera.captureLog = `连接成功: ${data.message}`
return;
}
// 处理错误消息
if (data.error) {
if (data.error.includes('未初始化')) {
camera.captureLog = '请先在"相机控制"部分初始化相机,然后再尝试连接视频流'
}
if (data.error.includes('已在流式传输中')) {
camera.captureLog = '相机正在使用中,请等待一会儿后再尝试连接'
}
camera.captureLog = `错误: ${data.error}`
return;
}
// 处理视频帧数据
if (data.data) {
// 创建Image对象加载图像
const img = new Image();
img.onload = () => {
// 获取对应相机的canvas元素
const canvasRef = `canvas_${camera.id}`;
const canvas = this.$refs[canvasRef]?.[0];
if (!canvas) return;
// 获取容器尺寸
const containerWidth = canvas.parentElement.clientWidth;
const containerHeight = canvas.parentElement.clientHeight;
// 计算保持比例的尺寸
let width = img.width;
let height = img.height;
const ratio = width / height;
if (width > containerWidth) {
width = containerWidth;
height = width / ratio;
}
if (height > containerHeight) {
height = containerHeight;
width = height * ratio;
}
// 设置canvas尺寸
canvas.width = width;
canvas.height = height;
// 绘制图像
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, width, height);
// 更新状态信息
let statusInfo = `分辨率: ${img.width}x${img.height} | 显示尺寸: ${Math.round(width)}x${Math.round(height)}`;
if (data.performance) {
statusInfo += ` | 实际FPS: ${data.performance.actual_fps} | 目标FPS: ${data.performance.target_fps}`;
}
if (data.encode_time_ms) {
statusInfo += ` | 编码耗时: ${data.encode_time_ms}ms`;
}
if (data.data_size_kb) {
statusInfo += ` | 数据大小: ${data.data_size_kb}KB`;
}
camera.statusInfo = statusInfo;
img.src = '';
};
img.src = `data:image/jpeg;base64,${data.data}`;
camera.imageData = `data:image/jpeg;base64,${data.data}`;
}
} catch (error) {
camera.captureLog = `解析消息数据时出错: ${error}`
}
}
camera.websocket.onerror = (error) => {
console.log(error)
camera.captureLog = `WebSocket连接错误`
};
camera.websocket.onclose = (event) => {
console.log(`WebSocket连接已关闭 (代码: ${event.code}, 原因: ${event.reason || '未知'})`);
// 不同的关闭代码有不同的含义
let closeReason = '';
switch (event.code) {
case 1000:
closeReason = '正常关闭';
break;
case 1001:
closeReason = '端点离开';
break;
case 1002:
closeReason = '协议错误';
break;
case 1003:
closeReason = '不支持的数据类型';
break;
case 1006:
closeReason = '异常关闭';
break;
case 1011:
closeReason = '服务器错误';
break;
default:
closeReason = `未知错误 (${event.code})`;
}
camera.captureLog = `WebSocket连接已关闭: ${closeReason}`
// 只有在非正常关闭的情况下才自动重连
if (event.code !== 1000 && camera.reconnectAttempts < 5) {
camera.reconnectAttempts++;
const retryDelay = Math.min(1000 * Math.pow(2, camera.reconnectAttempts - 1), 10000); // 指数退避,最大10秒
console.log(`连接异常关闭,${retryDelay / 1000}秒后尝试重连 (${camera.reconnectAttempts}/5)...`);
camera.captureLog = `连接异常关闭,${retryDelay / 1000}秒后尝试重连 (${camera.reconnectAttempts}/5)...`
setTimeout(() => this.startStream(camera), retryDelay);
} else if (event.code === 1000) {
console.log('WebSocket正常关闭,不进行重连');
} else {
console.log('重连次数已达上限,停止重连');
camera.captureLog = '重连失败,请检查相机状态后手动重连'
}
};
},
//断开链接
stopStream(camera) {
//调用接口断开链接
disconnectStillCamera(camera.id).then(res => {
this.$message.success(`断开链接成功`)
if (camera.websocket) {
camera.websocket.close()
this.$nextTick(() => {
camera.websocket = null
camera.reconnectAttempts = 0
camera.imageData = null
camera.objectURL = null
camera.statusInfo = null
camera.captureLog = '等待链接'
})
}
})
},
//关闭所有相机
closeAllCameras() {
let cameraIds = [];
this.cameraList.forEach(camera => {
cameraIds.push(camera.id);
if (camera.websocket) {
camera.websocket.close();
camera.websocket = null;
}
});
let param = {
camera_ids: cameraIds.join(',')
};
closeStillCamera(param).then(res => {
console.log(res);
this.cameraList = [];
this.globalParams = {
fps: 30,//帧率
resolution: '2560x1440',//分别率
savePath: '/images/captures',//存储路径
lowFpsMode: false,//低帧率模式开关
},
this.$message.success('已关闭所有相机');
})
},
//捕捉帧
captureImage(camera) {
let cameraIds = [];
this.cameraList.forEach(camera => {
cameraIds.push(camera.id);
});
//调用接口捕获帧
let param = {
camera_ids: cameraIds.join(','),
user_name: this.$store.getters.username,
save_dir: this.globalParams.savePath,
}
captureStillCamera(param).then(res => {
this.$message.success(`捕获图像成功`)
})
},
base64ToBlob(base64) {
const byteString = atob(base64.split(',')[1]);
const mimeType = base64.match(/:(.*?);/)[1];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], { type: mimeType });
}
},
// 2. 组件销毁时清理
beforeDestroy() {
this.cameraList.forEach(camera => {
if (camera.objectURL) URL.revokeObjectURL(camera.objectURL);
if (camera.websocket) {
camera.websocket.close()
disconnectStillCamera(camera.id)
}
});
}
}
</script>
<style scoped>
.camera-container {
padding: 20px;
background-color: #f5f7fa;
}
/* 相机模块样式 */
.camera-module {
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
/* 模块标题样式 */
.section-title {
font-size: 16px;
font-weight: bold;
color: #303133;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 2px solid #409eff;
}
/* 相机参数模块样式 */
.camera-params {
padding: 20px;
margin-bottom: 20px;
background-color: #fff;
border-radius: 6px;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1);
border: 1px solid #ebeef5;
position: relative;
}
.camera-params::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background-color: #409eff;
border-radius: 4px 0 0 4px;
}
/* 紧凑版参数模块 */
.camera-params.compact {
padding: 15px;
}
.camera-params.compact .params-form {
margin-top: 10px;
}
.camera-params.compact .el-form-item {
margin-bottom: 0;
margin-right: 15px;
}
.camera-params.compact .el-form-item__label {
font-size: 12px;
color: #606266;
padding-right: 5px;
}
.camera-params.compact .el-input-number {
line-height: 28px;
}
/* 视频帧显示区样式 */
.frame-display-row {
margin-bottom: 20px;
}
.frame-display {
height: 100%;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
margin-bottom: 15px;
background-color: #fff;
border: 1px solid #ebeef5;
}
/* 全局控制模块样式 */
.global-controls {
margin-top: 20px;
padding: 20px;
background-color: #fff;
border-radius: 6px;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1);
border: 1px solid #ebeef5;
text-align: center;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header-title {
font-size: 24px;
font-weight: bold;
color: #303133;
padding-left: 10px;
border-left: 4px solid #409eff;
}
.header-buttons {
display: flex;
gap: 10px;
}
.patient-info {
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 10px;
font-size: 14px;
}
.info-item label {
width: 80px;
color: #666;
font-weight: 500;
}
.camera-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.camera-item {
display: flex;
justify-content: space-between;
padding: 15px;
border: 1px solid #ebeef5;
border-radius: 6px;
background-color: #fff;
transition: all 0.3s;
margin-bottom: 10px;
}
.camera-item:hover {
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.selected-camera {
border: 2px solid #409eff;
background-color: #f0f7ff;
}
.frame-title {
display: flex;
align-items: center;
font-size: 16px;
}
.camera-id {
font-weight: bold;
margin-right: 8px;
}
.camera-spec {
font-size: 12px;
color: #909399;
}
.frame-header {
padding: 8px 15px;
display: flex;
justify-content: center;
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
}
.camera-info {
flex: 1;
}
.info-row {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.info-row label {
width: 70px;
font-size: 12px;
color: #666;
text-align: right;
padding-right: 8px;
flex-shrink: 0;
}
.camera-actions {
display: flex;
flex-direction: column;
justify-content: center;
gap: 5px;
}
.frame-display-row {
margin-bottom: 20px;
}
.frame-display {
height: 100%;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.frame-display-wrapper {
height: 100%;
}
.global-controls {
margin-top: 20px;
text-align: center;
padding: 15px;
background-color: #f5f7fa;
border-radius: 6px;
}
.capture-controls {
margin-top: 15px;
padding: 15px;
background-color: #f5f7fa;
border-radius: 6px;
}
.button-group {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 15px;
}
</style>
import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import axios from 'axios'
import store from './store'
Vue.config.productionTip = false
Vue.use(ElementUI)
Vue.prototype.$axios = axios
new Vue({
render: h => h(App),
store
}).$mount('#app')
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
patientInfo: {
name: '柳佳乐',//姓名
age: 25,//年龄
gender: '',//性别
phone: '19955807499',//手机号
idCard: '341221200005067836',//身份证号
nativePlace: '',//籍贯
height: 165,//身高
weight: 55,//体重
bookingId: 1,//预约ID
bkBkTime:'2023-08-10 10:00:00',//预约时间
},
},
mutations: {
SET_PATIENT_INFO(state, payload) {
state.patientInfo = {...state.patientInfo, ...payload}
},
CLEAR_PATIENT_INFO(state) {
state.patientInfo = {
name: null,//姓名
age: null,//年龄
gender: null,//性别
phone: null,//手机号
idCard: null,//身份证号
nativePlace: null,//籍贯
height: null,//身高
weight: null,//体重
bookingId: null,//预约ID
bkBkTime:null,//预约时间
}
}
},
actions: {
updatePatientInfo({ commit }, patientData) {
commit('SET_PATIENT_INFO', patientData)
},
clearPatient({ commit }) {
commit('CLEAR_PATIENT_INFO')
}
},
getters: {
patientInfo: state => state.patientInfo,
username: (state) => {
let str = '';
state.patientInfo.name ? str += state.patientInfo.name : str+='null';//姓名
state.patientInfo.age ? str += state.patientInfo.age : str+='null';//年龄
state.patientInfo.gender ? str += state.patientInfo.gender : str+='null';//性别
state.patientInfo.phone ? str += state.patientInfo.phone : str+='null';//手机号
state.patientInfo.idCard ? str += state.patientInfo.idCard : str+='null';//身份证号
state.patientInfo.nativePlace ? str += state.patientInfo.nativePlace : str+='null';//籍贯
return str;
}
}
})
function pad(timeEl, total = 2, str = '0') {
return timeEl.toString().padStart(total, str)
}
export function timeProcessing(data) {
let timer
if(data){
timer = new Date(data)
}else{
timer = new Date()
}
const year = timer.getFullYear()
const month = timer.getMonth() + 1 // 由于月份从0开始,因此需加1
const day = timer.getDate()
const hour = timer.getHours()
const minute = timer.getMinutes()
const second = timer.getSeconds()
return `${pad(year, 4)}-${pad(month)}-${pad(day)} ${pad(hour)}:${pad(minute)}:${pad(second)}`
}
//开始时间
export function startTimeProcessing(data) {
let timer = new Date(data)
const year = timer.getFullYear()
const month = timer.getMonth() + 1 // 由于月份从0开始,因此需加1
const day = timer.getDate()
return `${pad(year, 4)}-${pad(month)}-${pad(day)} 00:00:00`
}
//结束时间
export function endTimeProcessing(data) {
let timer = new Date(data)
const year = timer.getFullYear()
const month = timer.getMonth() + 1 // 由于月份从0开始,因此需加1
const day = timer.getDate()
return `${pad(year, 4)}-${pad(month)}-${pad(day)} 59:59:59`
}
import axios from 'axios'
import { Loading, Message } from 'element-ui'
let loadingInstance = null
// 白名单路径 - 这些请求不会显示loading (使用正则表达式匹配关键部分)
const WHITE_LIST = [
/\/api\/cameras\/rgbd\/rgbd\/\d+\/current-frame/ // RGBD相机当前帧(带ID)
]
// 创建axios实例
const service = axios.create({
timeout: 10000
})
// 请求拦截器
service.interceptors.request.use(
config => {
// 检查请求是否匹配白名单中的任一正则
const isInWhiteList = WHITE_LIST.some(regex => regex.test(config.url))
// 不在白名单中的请求才显示loading
if (!isInWhiteList) {
loadingInstance = Loading.service({
lock: true,
text: '加载中...',
background: 'rgba(0, 0, 0, 0.7)'
})
}
return config
},
error => {
// 关闭loading
loadingInstance && loadingInstance.close()
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
loadingInstance && loadingInstance.close()
const res = response.data
console.log(res)
if(res.code == 200){
return res
}else{
Message.error(res.message)
console.log(res.message)
}
// 可在此根据业务状态码处理不同情况
// if (res.code !== 200) {
// Message.error(res.message || 'Error')
// return Promise.reject(new Error(res.message || 'Error'))
// }
return res
},
error => {
// 关闭loading
loadingInstance && loadingInstance.close()
// 可在此统一处理HTTP错误状态码
// Message.error(error.message)
return Promise.reject(error)
}
)
export default service
export const baseURL = '/api'
export const baseURLConsole ='192.168.0.103:8000'
\ No newline at end of file
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer:{
client:{
overlay: false
}
}
})
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment