Commit e1caa03f authored by forevertyler's avatar forevertyler

fix:个性化、视频监控

parent 2d3408c6
import request from '@/utils/request'
// 查询【请填写功能名称】列表
export function listSet(query) {
return request({
url: '/slope/set/list',
method: 'get',
params: query
})
}
// 查询【请填写功能名称】详细
export function getSet(id) {
return request({
url: '/system/set/' + id,
method: 'get'
})
}
// 新增【请填写功能名称】
export function addSet(data) {
return request({
url: '/slope/set/add',
method: 'post',
data: data
})
}
// 修改【请填写功能名称】
export function updateSet(data) {
return request({
url: '/slope/set/update',
method: 'put',
data: data
})
}
// 删除【请填写功能名称】
export function delSet(id) {
return request({
url: '/system/set/' + id,
method: 'delete'
})
}
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
> >
<transition name="sidebarLogoFade" class="logo-header"> <transition name="sidebarLogoFade" class="logo-header">
<router-link class="sidebar-logo-link logo-header" to="/"> <router-link class="sidebar-logo-link logo-header" to="/">
<!-- <img :src="logo" class="sidebar-logo" /> --> <img :src="logo" class="sidebar-logo" />
<h1 <h1
class="sidebar-title" class="sidebar-title"
:style="{ :style="{
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
import logoImg from "@/assets/logo/logo_ks.png"; import logoImg from "@/assets/logo/logo_ks.png";
import variables from "@/assets/styles/variables.scss"; import variables from "@/assets/styles/variables.scss";
import { Navbar } from "../../components"; import { Navbar } from "../../components";
import { listSet } from "@/api/system/perSet";
export default { export default {
name: "SidebarLogo", name: "SidebarLogo",
components: { components: {
...@@ -72,6 +72,21 @@ export default { ...@@ -72,6 +72,21 @@ export default {
logo: logoImg, logo: logoImg,
}; };
}, },
created() {
this.getList()
},
methods: {
getList() {
listSet().then(response => {
if(response.code===200&&response&&response.rows&&response.rows[0]){
this.title = response.rows[0].title||"隧道结构稳定性监测系统";
this.logo = this.previewUrl + response.rows[0].url||logoImg;
}
});
},
}
}; };
</script> </script>
......
<template>
<div class="app-container">
<el-form ref="form" :model="form" label-width="80px">
<el-form-item label="标题logo" prop="description">
<div style="width: 50%; margin: 0 auto">
<img
:src="previewUrl + form.url"
style="width: 100px; object-fit: fill"
/>
</div>
</el-form-item>
<el-form-item label="logo" prop="url">
<el-upload
ref="upload"
:action="uploadUrl"
:on-success="
(res, file) => {
handleUploadSuccess(res, file, 'pic');
}
"
:on-error="handleUploadError"
name="file"
:show-file-list="false"
:headers="headers"
accept=".jpg, .jpeg, .png"
>
<el-button size="small">
选择标题logo
<i class="el-icon-upload el-icon--right"></i>
</el-button>
<div slot="tip" class="el-upload__tip">
只能上传jpg/png/jpeg文件
</div>
</el-upload>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入标题" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
</div>
</div>
</template>
<script>
import { listSet, getSet, delSet, addSet, updateSet } from "@/api/system/perSet";
import { getToken } from "@/utils/auth";
export default {
name: "Set",
data() {
return {
previewUrl: process.env.VUE_APP_BASE_API,
uploadUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
headers: {
Authorization: "Bearer " + getToken(),
},
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 【个性化设置】表格数据
setList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
title: null,
url: null
},
// 表单参数
form: {
id:null,
url:null,
title:null,
},
// 表单校验
rules: {
},
upload: {
// 是否显示弹出层(用户导入)
open: false,
title: "",
// 是否禁用上传
isUploading: false,
updateSupport: 0,
// 设置上传的请求头部
headers: { Authorization: "Bearer " + getToken() },
// 上传的地址
url: process.env.VUE_APP_BASE_API + "/common/upload"
},
};
},
created() {
this.getList()
},
methods: {
getList() {
listSet().then(response => {
if(response.code===200&&response&&response.rows&&response.rows[0]){
this.form = response.rows[0];
}else{
this.form ={
id:null,
url:null,
title:null,
}
}
});
},
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.id != null) {
updateSet(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.getList();
});
} else {
addSet(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.getList();
});
}
}
});
},
/**文档预览**/
preView(file) {
console.log(file);
let originUrl = process.env.VUE_APP_BASE_API + file; //要预览文件的访问地址'
window.open(originUrl);
},
//上传图片
// 上传前校检格式和大小
handleBeforeUpload(file) {
// 校检文件大小
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize;
if (!isLt) {
this.$message.error(`上传文件大小不能超过 ${this.fileSize} MB!`);
return false;
}
}
return true;
},
handleUploadSuccess(response, file, type) {
console.log(response, file, type,'response, file, type');
// 获取富文本组件实例
// 如果上传成功
if (response.code == 200) {
if (type === "pic") {
this.form.url = response.fileName;
}
if (type === "pdf") {
this.form.file.push(response.fileName);
this.form.fileName.push(response.originalFilename);
}
// 获取光标所在位置
} else {
this.$message.error("上传失败");
}
},
delName(index) {
// 在点击时删除数组中的相应项
this.form.file.splice(index, 1);
this.form.fileName.splice(index, 1);
},
handleUploadError() {
this.$message.error("上传失败");
},
}
};
</script>
<style lang="scss" scoped>
.app-container{
background-color: #fff;
}
</style>
\ No newline at end of file
<template>
<div class="box-main">
<div class="card">
<div class="top-title">
<span class="line"></span >
<span class="title-text">实时监控</span>
</div>
<el-form
:model="queryParams"
size="small"
:inline="true"
label-width="90px"
class="searchform"
>
<el-form-item label="摄像头编号:" prop="equipmentName">
<el-input v-model="queryParams.equipmentName" placeholder="请输入摄像头编号" />
</el-form-item>
<el-form-item label="时间:" prop="time">
<el-date-picker
:default-time="['00:00:00', '23:59:59']"
style="width: 350px !important;"
v-model="queryParams.time"
type="datetimerange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
clearable
value-format="yyyy-MM-dd HH:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
icon="el-icon-search"
@click="toSearch"
style="margin-left: 20px;"
>搜索</el-button>
</el-form-item>
</el-form>
<div class="box-bottom">
<div class="boxvideo">
<video-player
class="video-player vjs-custom-skin"
ref="videoPlayer"
:playsinline="true"
:options="playerOptions"/>
</div>
<div class="box-table">
<el-table
:data="tableData"
border
>
<el-table-column
prop="equno"
label="摄像头编号"
align="center"
>
</el-table-column>
<el-table-column
prop="time"
label="监控时间"
align="center"
>
</el-table-column>
<el-table-column
prop="address"
label="操作"
align="center"
>
<template slot-scope="scope">
<el-button
size="mini"
type="primary"
icon="el-icon-edit"
@click.native.prevent="addEqupment()"
>查看记录</el-button
>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</div>
</template>
<script>
import 'video.js/dist/video-js.css'
import { videoPlayer } from 'vue-video-player'
export default {
components: {
videoPlayer
},
data () {
return {
queryParams:{
},
tableData: [{
date: '2016-05-02',
equno: '1',
}, {
date: '2016-05-04',
equno: '2',
}, {
date: '2016-05-01',
equno: '3',
}, {
date: '2016-05-03',
equno: '4',
}],
playerOptions: {
playbackRates: [0.5, 1.0, 1.5, 2.0], // 可选的播放速度
autoplay: false, // 如果为true,浏览器准备好时开始回放。
muted: false, // 默认情况下将会消除任何音频。
loop: false, // 是否视频一结束就重新开始。
preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
language: 'zh-CN',
aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
sources: [{
type: "video/mp4", // 类型
src: 'http://vjs.zencdn.net/v/oceans.mp4' // url地址
}],
notSupportedMessage: '此视频暂无法播放,请稍后再试', // 允许覆盖Video.js无法播放媒体源时显示的默认信息。
controlBar: {
timeDivider: true, // 当前时间和持续时间的分隔符
durationDisplay: true, // 显示持续时间
remainingTimeDisplay: false, // 是否显示剩余时间功能
fullscreenToggle: true // 是否显示全屏按钮
}
}
}
},
methods:{
// 播放回调
onPlayerPlay(player) {
console.log('player play!', player)
},
// 暂停回调
onPlayerPause(player) {
console.log('player pause!', player)
},
// 视频播完回调
onPlayerEnded($event) {
console.log(player)
},
// DOM元素上的readyState更改导致播放停止
onPlayerWaiting($event) {
console.log(player)
},
// 已开始播放回调
onPlayerPlaying($event) {
console.log(player)
},
// 当播放器在当前播放位置下载数据时触发
onPlayerLoadeddata($event) {
console.log(player)
},
// 当前播放位置发生变化时触发。
onPlayerTimeupdate($event) {
console.log(player)
},
//媒体的readyState为HAVE_FUTURE_DATA或更高
onPlayerCanplay(player) {
// console.log('player Canplay!', player)
},
//媒体的readyState为HAVE_ENOUGH_DATA或更高。这意味着可以在不缓冲的情况下播放整个媒体文件。
onPlayerCanplaythrough(player) {
// console.log('player Canplaythrough!', player)
},
//播放状态改变回调
playerStateChanged(playerCurrentState) {
console.log('player current update state', playerCurrentState)
},
//将侦听器绑定到组件的就绪状态。与事件监听器的不同之处在于,如果ready事件已经发生,它将立即触发该函数。。
playerReadied(player) {
console.log('example player 1 readied', player);
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scope>
.box-main{
width: 98%;
height: 50rem;
margin: 0 auto;
.card {
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.top-title {
width: 100%;
height: 75px;
display: flex;
justify-content: flex-start;
align-items:center;
padding-left: 50px;
.line {
width: 6px;
height: 22px;
background: #0d85f4;
}
.title-text{
font-size: 18px;
font-weight:bold;
color: #333333;
line-height: 30px;
padding-left: 10px;
}
}
}
.searchform{
width: 100%;
height: 80px;
background-color: #fff;
display: flex;
justify-content: flex-start;
align-items: center;
padding-left: 60px;
.el-form-item{
margin-bottom: 0px!important;
}
}
.box-bottom{
width: 100%;
height: 80%;
display: flex;
align-items: center;
}
.boxvideo{
width: 50%;
height: 80%;
display: flex;
justify-content: center;
align-items: center;
}
.video-player{
width: 98%!important;
height: 98%!important;
}
.vjs-paused .vjs-big-play-button,
.vjs-paused.vjs-has-started .vjs-big-play-button {
display: block;
}
.video-js .vjs-big-play-button {
font-size: 1.5rem;
line-height: 4.5rem;
height: 4.5rem;
width: 4.5rem;
border-radius: 2.5rem;
background-color: #73859f;
background-color: rgba(115, 133, 159, .5);
border-width: 0.15rem;
margin-top: 25%;
margin-left: 45%;
}
/* 中间的播放箭头 */
.vjs-big-play-button .vjs-icon-placeholder {
font-size: 3.63rem;
}
/* 加载圆圈 */
.vjs-loading-spinner {
font-size: 2.5em;
width: 2em;
height: 2em;
border-radius: 1em;
margin-top: -1em;
margin-left: -1.5em;
}
.box-table{
width: 50%;
height: 80%;
.el-table{
width:90%;
height:100%;
margin-left:40px
}
}
}
</style>
<template>
<div class="box-main">
<div class="card">
<div class="top-title">
<span class="line"></span >
<span class="title-text">实时监控</span>
</div>
<el-form
:model="queryParams"
size="small"
:inline="true"
label-width="90px"
class="searchform"
>
<el-form-item label="摄像头编号:" prop="equipmentName">
<el-input v-model="queryParams.equipmentName" placeholder="请输入摄像头编号" />
</el-form-item>
<el-form-item label="时间:" prop="time">
<el-date-picker
:default-time="['00:00:00', '23:59:59']"
style="width: 350px !important;"
v-model="queryParams.time"
type="datetimerange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
clearable
value-format="yyyy-MM-dd HH:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
icon="el-icon-search"
@click="toSearch"
style="margin-left: 20px;"
>搜索</el-button>
</el-form-item>
</el-form>
<div class="box-bottom">
<div class="boxvideo">
<video-player
class="video-player vjs-custom-skin"
ref="videoPlayer"
:playsinline="true"
:options="playerOptions"/>
</div>
<div class="box-table">
<el-table
:data="tableData"
border
>
<el-table-column
prop="equno"
label="摄像头编号"
align="center"
>
</el-table-column>
<el-table-column
prop="time"
label="监控时间"
align="center"
>
</el-table-column>
<el-table-column
prop="address"
label="操作"
align="center"
>
<template slot-scope="scope">
<el-button
size="mini"
type="primary"
icon="el-icon-edit"
@click.native.prevent="addEqupment()"
>查看记录</el-button
>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</div>
</template>
<script>
import 'video.js/dist/video-js.css'
import { videoPlayer } from 'vue-video-player'
export default {
mounted() {
this.$watermark.set(" ",this.$refs.content)
},
beforeDestroy() {
this.$watermark.set("",this.$refs.content);
},
components: {
videoPlayer
},
data () {
return {
queryParams:{
},
tableData: [{
date: '2016-05-02',
equno: '1',
}, {
date: '2016-05-04',
equno: '2',
}, {
date: '2016-05-01',
equno: '3',
}, {
date: '2016-05-03',
equno: '4',
}],
playerOptions: {
playbackRates: [0.5, 1.0, 1.5, 2.0], // 可选的播放速度
autoplay: false, // 如果为true,浏览器准备好时开始回放。
muted: false, // 默认情况下将会消除任何音频。
loop: false, // 是否视频一结束就重新开始。
preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
language: 'zh-CN',
aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
sources: [{
type: "video/mp4", // 类型
src: 'http://vjs.zencdn.net/v/oceans.mp4' // url地址
}],
notSupportedMessage: '此视频暂无法播放,请稍后再试', // 允许覆盖Video.js无法播放媒体源时显示的默认信息。
controlBar: {
timeDivider: true, // 当前时间和持续时间的分隔符
durationDisplay: true, // 显示持续时间
remainingTimeDisplay: false, // 是否显示剩余时间功能
fullscreenToggle: true // 是否显示全屏按钮
}
}
}
},
methods:{
//播放回调
onPlayerPlay(player) {
console.log('player play!', player)
},
// 暂停回调
onPlayerPause(player) {
console.log('player pause!', player)
},
// 视频播完回调
onPlayerEnded($event) {
console.log(player)
},
// DOM元素上的readyState更改导致播放停止
onPlayerWaiting($event) {
console.log(player)
},
// 已开始播放回调
onPlayerPlaying($event) {
console.log(player)
},
// 当播放器在当前播放位置下载数据时触发
onPlayerLoadeddata($event) {
console.log(player)
},
// 当前播放位置发生变化时触发。
onPlayerTimeupdate($event) {
console.log(player)
},
//媒体的readyState为HAVE_FUTURE_DATA或更高
onPlayerCanplay(player) {
},
//媒体的readyState为HAVE_ENOUGH_DATA或更高。这意味着可以在不缓冲的情况下播放整个媒体文件。
onPlayerCanplaythrough(player) {
// console.log('player Canplaythrough!', player)
},
//播放状态改变回调
playerStateChanged(playerCurrentState) {
console.log('player current update state', playerCurrentState)
},
//将侦听器绑定到组件的就绪状态。与事件监听器的不同之处在于,如果ready事件已经发生,它将立即触发该函数。。
playerReadied(player) {
console.log('example player 1 readied', player);
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scope>
.box-main{
width: 98%;
height: 50rem;
margin: 0 auto;
.card {
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.top-title {
width: 100%;
height: 75px;
display: flex;
justify-content: flex-start;
align-items:center;
padding-left: 50px;
.line {
width: 6px;
height: 22px;
background: #0d85f4;
}
.title-text{
font-size: 18px;
font-weight:bold;
color: #333333;
line-height: 30px;
padding-left: 10px;
}
}
}
.searchform{
width: 100%;
height: 80px;
background-color: #fff;
display: flex;
justify-content: flex-start;
align-items: center;
padding-left: 60px;
.el-form-item{
margin-bottom: 0px!important;
}
}
.box-bottom{
width: 100%;
height: 80%;
display: flex;
align-items: center;
}
.boxvideo{
width: 50%;
height: 80%;
display: flex;
justify-content: center;
align-items: center;
}
.video-player{
width: 98%!important;
height: 98%!important;
}
.vjs-paused .vjs-big-play-button,
.vjs-paused.vjs-has-started .vjs-big-play-button {
display: block;
}
.video-js .vjs-big-play-button {
font-size: 1.5rem;
line-height: 4.5rem;
height: 4.5rem;
width: 4.5rem;
border-radius: 2.5rem;
background-color: #73859f;
background-color: rgba(115, 133, 159, .5);
border-width: 0.15rem;
margin-top: 25%;
margin-left: 45%;
}
/* 中间的播放箭头 */
.vjs-big-play-button .vjs-icon-placeholder {
font-size: 3.63rem;
}
/* 加载圆圈 */
.vjs-loading-spinner {
font-size: 2.5em;
width: 2em;
height: 2em;
border-radius: 1em;
margin-top: -1em;
margin-left: -1.5em;
}
.box-table{
width: 50%;
height: 80%;
.el-table{
width:90%;
height:100%;
margin-left:40px
}
}
}
</style>
<template>
<div></div>
</template>
\ No newline at end of file
<template>
<div class="app-container">
<!-- 布局选择器 -->
<el-select v-model="currentGrid" class="grid-select">
<el-option
v-for="grid in [1,4,6,9,16]"
:key="grid"
:label="`${grid}宫格`"
:value="grid"
/>
</el-select>
<!-- 视频容器 -->
<div
class="video-container"
:style="gridStyle"
>
<div
v-for="video in currentVideos"
:key="video.id"
class="video-wrapper"
>
<video
ref="videoPlayers"
controls
class="video-element"
:src="video.url"
muted
playsinline
></video>
</div>
</div>
<!-- 分页控制 -->
<div class="pagination" v-if="totalPages > 1">
<el-button @click="prevPage" :disabled="currentPage === 1">
<i class="el-icon-arrow-left"></i>
</el-button>
<span class="page-text">{{ currentPage }}/{{ totalPages }}</span>
<el-button @click="nextPage" :disabled="currentPage === totalPages">
<i class="el-icon-arrow-right"></i>
</el-button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
currentGrid: 4,
currentPage: 1,
videoList: [
{ id: 1, url: 'http://vjs.zencdn.net/v/oceans.mp4',name:'aaa' },
{ id: 2, url: 'http://vjs.zencdn.net/v/oceans.mp4',name:'bbb' },
{ id: 3, url: 'http://vjs.zencdn.net/v/oceans.mp4',name:'ccc' },
{ id: 4, url: 'http://vjs.zencdn.net/v/oceans.mp4',name:'ddd' },
{ id: 5, url: 'http://vjs.zencdn.net/v/oceans.mp4',name:'eee' },
{ id: 6, url: 'http://vjs.zencdn.net/v/oceans.mp4',name:'fff' },
{ id: 7, url: 'http://vjs.zencdn.net/v/oceans.mp4',name:'ggg' },
],
gridLayoutMap: {
1: { cols: 1, rows: 1 },
4: { cols: 2, rows: 2 },
6: { cols: 3, rows: 2 },
9: { cols: 3, rows: 3 },
16: { cols: 4, rows: 4 }
}
}
},
computed: {
gridStyle() {
const { cols, rows } = this.gridLayoutMap[this.currentGrid]
return {
gridTemplateColumns: `repeat(${cols}, 1fr)`,
gridTemplateRows: `repeat(${rows}, 1fr)`,
aspectRatio: `${cols}/${rows}`
}
},
currentVideos() {
const start = (this.currentPage - 1) * this.currentGrid
return this.videoList.slice(start, start + this.currentGrid)
},
totalPages() {
return Math.ceil(this.videoList.length / this.currentGrid)
}
},
watch: {
currentGrid() {
this.currentPage = 1
this.$nextTick(this.playCurrentPage)
},
currentPage() {
this.$nextTick(this.playCurrentPage)
}
},
methods: {
// 播放当前页所有视频
async playCurrentPage() {
try {
// 暂停所有视频
await this.pauseAllVideos()
// 等待DOM更新
await this.$nextTick()
// 获取当前页视频元素
const currentPlayers = (this.$refs.videoPlayers || [])
.slice(0, this.currentVideos.length)
// 批量播放处理
const playTasks = currentPlayers.map(player => {
player.muted = true
return player.play().catch(error => {
if (error.name !== 'AbortError') {
console.warn('视频播放失败:', error)
}
})
})
await Promise.all(playTasks)
} catch (error) {
console.error('页面播放异常:', error)
}
},
// 暂停所有视频(优化版)
async pauseAllVideos() {
const allPlayers = this.$refs.videoPlayers || []
const pauseTasks = allPlayers.map(player => {
return new Promise(resolve => {
if (!player.paused) {
player.addEventListener('pause', resolve, { once: true })
player.pause()
} else {
resolve()
}
})
})
await Promise.all(pauseTasks)
},
prevPage() {
if (this.currentPage > 1) this.currentPage--
},
nextPage() {
if (this.currentPage < this.totalPages) this.currentPage++
}
},
mounted() {
this.playCurrentPage()
}
}
</script>
<style scoped>
.app-container {
padding: 20px;
}
.grid-select {
margin-bottom: 20px;
width: 200px;
}
.video-container {
display: grid;
gap: 10px;
margin: 20px 0;
background: #2c3e50;
border-radius: 8px;
padding: 10px;
min-height: 300px;
}
.video-wrapper {
position: relative;
padding-top: 56.25%;
background: #000;
border-radius: 4px;
overflow: hidden;
}
.video-element {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin-top: 20px;
}
.page-text {
font-size: 16px;
color: #409EFF;
font-weight: 500;
}
</style>
\ No newline at end of file
<template>
<div class="video-player-component">
<!-- 视频播放器 -->
<video ref="videoPlayer" class="video" @timeupdate="onTimeUpdate">
<source :src="videoUrl" type="video/mp4" />
<span>Your browser does not support the video tag</span>
</video>
<!-- 控制栏 -->
<div class="control-bar">
<button @click="playPause" :class="{ playing }">{{ playing ? '暂停' : '播放' }}</button>
<div class="progress" @click="seekVideo">
<div class="progress-filled" :style="{ width: `${currentTimePercentage}%` }"></div>
<div class="progress-handle" :style="{ left: `${currentTimePercentage}%` }"></div>
</div>
<div class="status">
<span class="time">{{ formattedCurrentTime }}</span>
/
<span class="duration">{{ formattedDuration }}</span>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'VideoPlayer',
props: {
videoUrl: {
type: String,
required: true,
},
},
data() {
return {
playing: false,
currentTime: 0,
duration: 0,
speed: 1,
};
},
computed: {
currentTimePercentage() {
if (this.duration === 0) return 0;
return (this.currentTime / this.duration) * 100;
},
formattedCurrentTime() {
return this.formatTime(this.currentTime);
},
formattedDuration() {
return this.formatTime(this.duration);
},
},
mounted() {
this.$nextTick(() => {
this.$refs.videoPlayer.addEventListener('loadedmetadata', this.onLoadedMetadata);
});
},
beforeDestroy() {
this.$refs.videoPlayer.removeEventListener('loadedmetadata', this.onLoadedMetadata);
},
methods: {
playPause() {
const video = this.$refs.videoPlayer;
if (this.playing) {
video.pause();
} else {
video.play();
}
this.playing = !this.playing;
},
onLoadedMetadata() {
this.duration = this.$refs.videoPlayer.duration;
},
onTimeUpdate() {
this.currentTime = this.$refs.videoPlayer.currentTime;
},
seekVideo(event) {
if (!this.duration) return;
const progress = this.$refs.videoPlayer.querySelector('.progress');
const rect = progress.getBoundingClientRect();
const x = event.clientX - rect.left;
const percentage = x / rect.width;
this.currentTime = this.duration * percentage;
this.$refs.videoPlayer.currentTime = this.currentTime;
},
formatTime(timeInSeconds) {
const date = new Date(timeInSeconds * 1000);
const hours = date.getUTCHours();
const minutes = date.getUTCMinutes().toString().padStart(2, '0');
const seconds = date.getUTCSeconds().toString().padStart(2, '0');
return hours > 0 ? `${hours}:${minutes}:${seconds}` : `${minutes}:${seconds}`;
},
},
};
</script>
<style scoped>
.video-player-component {
width: 100%;
}
.video {
width: 100%;
height: auto;
}
.control-bar {
background-color: #333;
padding: 10px;
margin-top: 10px;
display: flex;
align-items: center;
}
button {
margin: 0 5px;
padding: 5px 10px;
color: white;
border: none;
border-radius: 3px;
background-color: #444;
}
button:hover {
background-color: #555;
}
.progress {
position: relative;
width: 250px;
height: 4px;
background-color: #555;
cursor: pointer;
margin: 0 10px;
}
.progress-filled {
height: 100%;
background-color: #4CAF50;
position: absolute;
left: 0;
top: 0;
}
.progress-handle {
width: 10px
\ No newline at end of file
<template>
<div class="surveillance-player">
<!-- 摄像头选择 -->
<div class="camera-selector">
<select v-model="selectedCamera">
<option v-for="camera in cameras" :key="camera.id" :value="camera">
{{ camera.name }}
</option>
</select>
</div>
<!-- 时间选择 -->
<div class="time-selector">
<div>
<label for="startTime">开始时间:</label>
<input type="datetime-local" id="startTime" v-model="startTime" />
</div>
<div>
<label for="endTime">结束时间:</label>
<input type="datetime-local" id="endTime" v-model="endTime" />
</div>
<button @click="loadVideo">加载视频</button>
</div>
<!-- 视频播放器 -->
<div class="video-player">
<video ref="videoPlayer" @timeupdate="onTimeUpdate">
<source :src="videoUrl" type="video/mp4" />
<span>Your browser does not support the video tag</span>
</video>
<!-- 控制栏 -->
<div class="control-bar">
<button @click="playPause" :class="{ playing }">
{{ playing ? '暂停' : '播放' }}
</button>
<div class="progress" @click="seekVideo">
<div class="progress-filled" :style="{ width: `${currentTimePercentage}%` }"></div>
<div class="progress-handle" :style="{ left: `${currentTimePercentage}%` }"></div>
</div>
<div class="status">
<span class="time"> {{ formattedCurrentTime }} </span>
/
<span class="duration"> {{ formattedDuration }} </span>
</div>
<!-- 播放速度控制 -->
<div class="speed-controls">
<button @click="setSpeed(0.5)">0.5x</button>
<button @click="setSpeed(1)">1x</button>
<button @click="setSpeed(2)">2x</button>
</div>
<!-- 时间轴 -->
<div class="timeline">
<div class="timeline-slider" ref="timelineSlider" @mousedown="startDrag"></div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'SurveillancePlayer',
data() {
return {
// 摄像头列表
cameras: [
{ id: 1, name: '入口', videoUrl: 'http://vjs.zencdn.net/v/oceans.mp4' },
{ id: 2, name: '出口', videoUrl: 'http://vjs.zencdn.net/v/oceans.mp4' },
{ id: 3, name: '大厅', videoUrl: 'http://vjs.zencdn.net/v/oceans.mp4' }
// ... 更多摄像头
],
selectedCamera: null,
startTime: new Date().toISOString().substr(0, 16),
endTime: new Date().toISOString().substr(0, 16),
videoUrl: '',
playing: false,
currentTime: 0,
duration: 0,
timeDragging: false,
speed: 1,
};
},
computed: {
currentTimePercentage() {
if (this.duration === 0) return 0;
return (this.currentTime / this.duration) * 100;
},
formattedCurrentTime() {
return this.formatTime(this.currentTime);
},
formattedDuration() {
return this.formatTime(this.duration);
},
},
methods: {
// 加载视频
loadVideo() {
this.videoUrl = this.selectedCamera.videoUrl; // 实际应用中需根据 startTime 和 endTime 动态拼接视频 URL
this.$refs.videoPlayer.load();
},
// 播放/暂停
playPause() {
const video = this.$refs.videoPlayer;
if (this.playing) {
video.pause();
} else {
video.play();
}
this.playing = !this.playing;
},
// 更新播放时间
onTimeUpdate() {
this.currentTime = this.$refs.videoPlayer.currentTime;
},
// 跳转到特定时间
seekVideo(event) {
if (!this.duration || !event) return;
const progress = this.$refs.videoPlayer.querySelector('.progress');
const rect = progress.getBoundingClientRect();
const x = event.clientX - rect.left;
const percentage = x / rect.width;
const newTime = this.duration * percentage;
this.$refs.videoPlayer.currentTime = newTime;
},
// 设置播放速度
setSpeed(speed) {
this.speed = speed;
this.$refs.videoPlayer.playbackRate = this.speed;
},
// 格式化时间
formatTime(timeInSeconds) {
const date = new Date(timeInSeconds * 1000);
const hours = date.getUTCHours();
const minutes = date.getUTCMinutes().toString().padStart(2, '0');
const seconds = date.getUTCSeconds().toString().padStart(2, '0');
return hours > 0 ? `${hours}:${minutes}:${seconds}` : `${minutes}:${seconds}`;
},
// 开始拖动时间轴
startDrag() {
this.timeDragging = true;
document.addEventListener('mousemove', this.onMouseMove);
document.addEventListener('mouseup', this.stopDrag);
},
// 响应鼠标移动
onMouseMove(event) {
if (!this.timeDragging) return;
const sliderRect = this.$refs.timelineSlider.getBoundingClientRect();
const x = event.clientX - sliderRect.left;
const percentage = x / sliderRect.width;
const newTime = this.duration * percentage;
this.$refs.videoPlayer.currentTime = newTime;
},
// 停止拖动
stopDrag() {
this.timeDragging = false;
document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('mouseup', this.stopDrag);
},
},
mounted() {
this.selectedCamera = this.cameras[0];
},
};
</script>
<style scoped>
.surveillance-player {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
background-color: #fff;
}
.camera-selector {
margin: 10px;
}
.time-selector {
display: flex;
align-items: center;
margin: 10px;
}
.time-selector div {
margin: 0 10px;
}
.video-player {
width: 100%;
max-width: 800px;
margin: 10px;
}
.video {
width: 100%;
height: auto;
}
.control-bar {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #333;
color: white;
padding: 5px 10px;
}
button {
margin: 0 10px;
background-color: #444;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
}
button:hover {
background-color: #555;
}
.progress {
position: relative;
width: 200px;
height: 4px;
background-color: #555;
margin: 0 10px;
cursor: pointer;
}
.progress-filled {
position: absolute;
height: 100%;
background-color: #4CAF50;
left: 0;
top: 0;
}
.progress-handle {
position: absolute;
width: 10px;
height: 10px;
background-color: white;
border-radius: 50%;
transform: translateX(-50%);
top: -3px;
}
.timeline {
width: 100%;
height: 20px;
background-color: #ddd;
margin: 10px 0;
}
.timeline-slider {
height: 20px;
background-color: transparent;
cursor: pointer;
width: 100%;
}
</style>
\ No newline at end of file
...@@ -36,8 +36,8 @@ module.exports = { ...@@ -36,8 +36,8 @@ module.exports = {
proxy: { proxy: {
// detail: https://cli.vuejs.org/config/#devserver-proxy // detail: https://cli.vuejs.org/config/#devserver-proxy
[process.env.VUE_APP_BASE_API]: { [process.env.VUE_APP_BASE_API]: {
// target: `http://182.92.170.89:10086`, target: `http://182.92.170.89:10086`,
target: `http://192.168.2.16:10080`, // target: `http://192.168.2.16:10080`,
// target: `http://60.212.188.152:12000`,//服务器 // target: `http://60.212.188.152:12000`,//服务器
// target: `http://localhost:8080`,//服务器 // target: `http://localhost:8080`,//服务器
changeOrigin: true, changeOrigin: true,
......
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