feat: 视频预置点位

* refactor: 预置点位

* fix: 修改预置点位
This commit is contained in:
孙超 2023-08-01 15:38:24 +08:00 committed by GitHub
parent 562223fec4
commit 43a49e95fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 517 additions and 113 deletions

View File

@ -67,4 +67,7 @@ export default {
// 播放云端回放
playbackStart: (recordId: string) => server.get(`/media/record/${recordId}.mp4`),
// 设备预置位相关接口
opFunction: (deviceId: string, functionId: string, data?: any) => server.post(`/device/invoked/${deviceId}/function/${functionId}`, data)
}

View File

@ -44,7 +44,7 @@
justify-content: center;
width: 45%;
height: 45%;
font-size: 30px;
// font-size: 30px;
background-color: #fff;
border-radius: 50%;
transform: translate(-50%, -50%) rotateZ(-45deg);

View File

@ -39,7 +39,7 @@
</div>
</div>
<div class="direction-audio">
<!-- <AIcon type="AudioOutlined" /> -->
<slot name="center"><!-- <AIcon type="AudioOutlined" /> --></slot>
</div>
</div>
<div class="zoom">

View File

@ -1,5 +1,5 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import menus, { AccountCenterBindPath, InitHomePath, InitLicense, LoginPath, OauthPath } from './menu'
import menus, { AccountCenterBindPath, InitHomePath, InitLicense, LoginPath, OauthPath, VideoSharePath } from './menu'
import { cleanToken, getToken } from '@/utils/comm'
import { useUserInfo } from '@/store/userInfo'
import { useSystem } from '@/store/system'
@ -15,7 +15,7 @@ const router = createRouter({
})
const filterPath = [ InitHomePath ]
const noTokenPath = [ AccountCenterBindPath, OauthPath, InitLicense ]
const noTokenPath = [ AccountCenterBindPath, OauthPath, InitLicense, VideoSharePath ]
router.beforeEach((to, from, next) => {
// TODO 切换路由取消请求

View File

@ -5,6 +5,7 @@ export const InitLicense = '/init-license'
export const NotificationSubscriptionCode = 'message-subscribe'
export const NotificationRecordCode = 'account/NotificationRecord'
export const OauthPath = '/oauth'
export const VideoSharePath = '/media/device/Share'
export const AccountMenu = {
path: '/account',
@ -86,5 +87,9 @@ export default [
},
component: () => import('@/views/oauth/WeChat.vue')
},
{
path: VideoSharePath,
component: () => import('@/views/media/Device/Channel/Share/index.vue')
},
AccountMenu
]

View File

@ -0,0 +1,200 @@
<template>
<j-table
size="small"
:columns="columns"
:dataSource="dataSource"
:pagination="false"
:scroll="{ y: 200 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'actions'">
<j-button
type="link"
style="padding: 0"
v-if="!record.flag"
:disabled="loading"
@click="onSetting(record)"
>设置</j-button
>
<template v-else>
<j-space>
<j-button
danger
type="link"
:disabled="loading"
style="padding: 0"
@click="onDelete(record)"
>删除</j-button
>
<j-button
type="link"
:disabled="loading"
style="padding: 0"
@click="onInvoke(record)"
>调用</j-button
>
</j-space>
</template>
</template>
<template v-else-if="column.dataIndex === 'name'">
<j-input
:disabled="record.flag"
v-model:value="record[column.dataIndex]"
/>
</template>
<template v-else>
{{ record[column.dataIndex] }}
</template>
</template>
</j-table>
</template>
<script lang="ts" setup>
import channelApi from '@/api/media/channel';
import { onlyMessage } from '@/utils/comm';
import { isNumber, unionBy } from 'lodash-es';
import { PropType } from 'vue';
type Item = { id: string | number; name: string; flag?: boolean };
const props = defineProps({
data: {
type: Object as PropType<Partial<Record<string, any>>>,
default: () => ({}),
},
});
const columns = [
{
title: '序号',
dataIndex: 'id',
width: 60,
},
{
title: '预置点位',
dataIndex: 'name',
},
{
title: '操作',
dataIndex: 'actions',
width: 90,
},
];
const init = new Array(50).fill(0).map((_, index) => {
return {
id: String(index + 1),
name: `预置点${index + 1}`,
flag: false,
};
});
const dataSource = ref<Item[]>(init);
const loading = ref(false);
const handleSearch = async (id: string, arr: Item[]) => {
const resp = await channelApi.opFunction(id, 'QueryPreset');
if (resp.status === 200) {
dataSource.value = unionBy([ ...arr, ...init], 'id').map((item) => {
const _item = (resp.result?.[0] || []).find(
(i: any) => i.id === item.id,
);
if (_item) {
return {
..._item,
flag: true,
};
}
return item;
});
}
};
const saveInfo = async (preset: Item[]) => {
const resp = await channelApi.update(props.data.id, {
id: props.data.id,
address: props.data.address,
channelId: props.data.channelId,
description: props.data.description,
deviceId: props.data.deviceId,
name: props.data.name,
manufacturer: props.data.manufacturer,
ptzType: props.data.ptzType?.value || 0,
others: {
...props.data?.others,
preset
},
});
if (resp.status === 200) {
console.log(resp);
}
};
const onFunction = (id: string, functionId: string, params: any) => {
loading.value = true;
channelApi
.opFunction(id, functionId, params)
.then(async (resp) => {
if (resp.status === 200) {
onlyMessage('操作成功!');
const preset = dataSource.value.map((item) => {
return {
id: item.id,
name: item.name,
};
});
if (params?.operation === 'SET') {
//
await saveInfo(preset);
}
if (props.data?.deviceId) {
await handleSearch(props.data?.deviceId, preset);
}
}
})
.finally(() => {
loading.value = false;
});
};
const onSetting = (obj: Item) => {
if (!obj.id) return;
const params = {
operation: 'SET',
presetIndex: isNumber(obj.id) ? Number(obj.id) : obj.id,
channel: props.data?.channelId,
};
onFunction(props.data?.deviceId, 'Preset', params);
};
const onInvoke = (obj: Item) => {
if (!obj.id) return;
const params = {
operation: 'CALL',
presetIndex: isNumber(obj.id) ? Number(obj.id) : obj.id,
channel: props.data?.channelId,
};
onFunction(props.data?.deviceId, 'Preset', params);
};
const onDelete = (obj: Item) => {
if (!obj.id) return;
const params = {
operation: 'DEL',
presetIndex: isNumber(obj.id) ? Number(obj.id) : obj.id,
channel: props.data?.channelId,
};
onFunction(props.data?.deviceId, 'Preset', params);
};
watch(
() => props.data.deviceId,
() => {
if (props.data?.deviceId) {
handleSearch(props.data?.deviceId, props.data?.others?.preset);
}
},
{
immediate: true,
},
);
</script>

View File

@ -0,0 +1,61 @@
<template>
<j-modal visible @cancel="emit('close')" :closable="false">
<div class="content">
<div style="margin-bottom: 5px;">
复制下方链接分享{{ data.name }}视频界面
</div>
<j-input-group compact>
<j-input
v-model:value="url"
ref="urlRef"
style="width: calc(100% - 50px)"
/>
<j-tooltip title="复制">
<j-button @click="onCopy">
<template #icon><AIcon type="CopyOutlined" /></template>
</j-button>
</j-tooltip>
</j-input-group>
</div>
<template #footer>
<j-button type="primary" @click="emit('close')">确定</j-button>
</template>
</j-modal>
</template>
<script lang="ts" setup>
import { getToken } from '@/utils/comm';
import { TOKEN_KEY } from '@/utils/variable';
import { PropType } from 'vue';
const emit = defineEmits(['close', 'save']);
const props = defineProps({
data: {
type: Object as PropType<Partial<Record<string, any>>>,
default: () => ({}),
},
});
const token = getToken();
const route = useRoute();
const url = ref('');
const urlRef = ref<HTMLInputElement>()
watchEffect(() => {
url.value = `${window.location.origin}#/media/device/Share?deviceId=${props.data.deviceId}&channelId=${props.data.channelId}&type=${route.query.type}&${TOKEN_KEY}=${token}`
})
const onCopy = () => {
if(urlRef.value) {
urlRef.value.select()
document.execCommand('copy')
}
}
</script>
<style lang="less" scoped>
.content {
display: flex;
flex-direction: column;
margin: 60px 10px 40px 10px;
}
</style>

View File

@ -1,22 +1,21 @@
.media-live {
display: flex;
.live-player-tools {
flex-basis: 230px;
// .live-player-tools {
// flex-basis: 300px;
.direction-item {
font-size: 30px !important;
}
// .direction-item {
// font-size: 30px !important;
// }
.zoom-item {
font-size: 20px !important;
}
}
// .zoom-item {
// font-size: 20px !important;
// }
// }
.media-live-video {
position: relative;
flex-grow: 1;
width: 0;
flex: 1;
.media-tool {
position: absolute;
@ -50,9 +49,20 @@
}
}
}
.media-live-actions {
width: 300px;
margin-left: 10px;
.actions-tool {
padding: 0 40px 5px 40px;
}
}
}
.media-live-tool {
display: flex;
margin-top: 24px;
margin-bottom: 24px;
justify-content: space-between;
align-items: center;
}

View File

@ -3,49 +3,82 @@
<j-modal
v-model:visible="_vis"
title="播放"
cancelText="取消"
okText="确定"
width="800px"
:width="_type ? 1200 : 900"
:maskClosable="false"
@ok="_vis = false"
@cancel="_vis = false"
:destroyOnClose="true"
>
<template #closeIcon>
<j-button :disabled="type === 'share'" type="text"><AIcon type="CloseOutlined" /></j-button>
</template>
<div class="media-live-tool">
<j-radio-group
v-model:value="mediaType"
button-style="solid"
@change="mediaStart"
>
<j-radio-button value="mp4">MP4</j-radio-button>
<j-radio-button value="flv">FLV</j-radio-button>
<j-radio-button value="m3u8">HLS</j-radio-button>
</j-radio-group>
<div class="media-live-share" v-if="type !== 'share'">
<j-button type="link" @click="onShare"
><AIcon type="ShareAltOutlined" />分享视频</j-button
>
</div>
</div>
<div class="media-live">
<div class="media-live-video">
<div :class="mediaToolClass" @mouseleave='mouseleave' @mouseenter='showTool = true' >
<div class="tool-item" >
<template v-if='isRecord === 0'>
<j-dropdown trigger='click' @visibleChange='visibleChange' @click='showToolLock = true'>
<div>
开始录像
</div>
<template #overlay>
<j-menu @click="recordStart">
<j-menu-item key='true' v-if='route.query.type !== "fixed-media"'>
<span style='padding-right: 12px;'>本地存储</span>
<j-tooltip title='存储在设备本地'>
<a-icon type='QuestionCircleOutlined' />
</j-tooltip>
</j-menu-item>
<j-menu-item key='false'>
<span style='padding-right: 12px;'>云端存储</span>
<j-tooltip title='存储在服务器中'>
<a-icon type='QuestionCircleOutlined' />
</j-tooltip>
</j-menu-item>
</j-menu>
<div
:class="mediaToolClass"
@mouseleave="mouseleave"
@mouseenter="showTool = true"
>
<div class="tool-item" v-if="type !== 'share'">
<template v-if="isRecord === 0">
<j-dropdown
trigger="click"
@visibleChange="visibleChange"
@click="showToolLock = true"
>
<div>开始录像</div>
<template #overlay>
<j-menu @click="recordStart">
<j-menu-item
key="true"
v-if="_type"
>
<span style="padding-right: 12px"
>本地存储</span
>
<j-tooltip title="存储在设备本地">
<a-icon
type="QuestionCircleOutlined"
/>
</j-tooltip>
</j-menu-item>
<j-menu-item key="false">
<span style="padding-right: 12px"
>云端存储</span
>
<j-tooltip title="存储在服务器中">
<a-icon
type="QuestionCircleOutlined"
/>
</j-tooltip>
</j-menu-item>
</j-menu>
</template>
</j-dropdown>
</template>
</j-dropdown>
</template>
<div v-else-if='isRecord === 1'>
请求录像中
<div v-else-if="isRecord === 1">请求录像中</div>
<div
v-else-if="isRecord === 2"
@click.stop="recordStop"
>
停止录像
</div>
</div>
<div v-else-if='isRecord === 2' @click.stop="recordStop">
停止录像
</div>
</div>
<div class="tool-item" @click.stop="handleRefresh">
刷新
@ -67,23 +100,45 @@
autoplay
/>
</div>
<MediaTool
@onMouseDown="handleMouseDown"
@onMouseUp="handleMouseUp"
/>
</div>
<div class="media-live-tool">
<j-radio-group
v-model:value="mediaType"
button-style="solid"
@change="mediaStart"
>
<j-radio-button value="mp4">MP4</j-radio-button>
<j-radio-button value="flv">FLV</j-radio-button>
<j-radio-button value="m3u8">HLS</j-radio-button>
</j-radio-group>
<div class="media-live-actions" v-if="_type">
<div class="actions-tool">
<MediaTool
@onMouseDown="handleMouseDown"
@onMouseUp="handleMouseUp"
>
<template #center>
<div class="center">
<div>转速控制</div>
<j-dropdown>
<span
>{{ _speed }}<AIcon type="DownOutlined"
/></span>
<template #overlay>
<j-menu @click="onMenuChange">
<j-menu-item
:key="item.value"
v-for="item in speedList"
>
{{ item.label }}
</j-menu-item>
</j-menu>
</template>
</j-dropdown>
</div>
</template>
</MediaTool>
</div>
<Preset :data="data" />
</div>
</div>
<template #footer>
<j-space>
<j-button :disabled="type === 'share'" @click="_vis = false">取消</j-button>
<j-button :disabled="type === 'share'" @click="_vis = false" type="primary">确定</j-button>
</j-space>
</template>
</j-modal>
<Share v-if="visible" :data="data" @close="visible = false" />
</template>
<script setup lang="ts">
@ -91,6 +146,8 @@ import { PropType } from 'vue';
import LivePlayer from '@/components/Player/index.vue';
import MediaTool from '@/components/Player/mediaTool.vue';
import channelApi from '@/api/media/channel';
import Share from './Share.vue';
import Preset from './Preset.vue';
type Emits = {
(e: 'update:visible', data: boolean): void;
@ -103,6 +160,10 @@ const props = defineProps({
type: Object as PropType<Partial<Record<string, any>>>,
default: () => ({}),
},
type: {
type: String as PropType<'share' | 'normal'>,
default: 'normal',
},
});
const route = useRoute();
@ -118,29 +179,50 @@ const player = ref();
const url = ref('');
//
const mediaType = ref<'mp4' | 'flv' | 'hls'>('mp4');
const showTool = ref(false)
const showToolLock = ref(false)
const showTool = ref(false);
const showToolLock = ref(false);
const visible = ref(false);
const _type = computed(() => {
return route.query.type !== 'fixed-media'
})
const speedList = [
{ label: '高', value: 180 },
{ label: '中', value: 90 },
{ label: '低', value: 45 },
];
const speed = ref(90);
const _speed = computed(() => {
return speedList.find((item) => item.value === speed.value)?.label;
});
const onMenuChange = (val: any) => {
speed.value = val.key;
};
const mouseleave = () => {
if (!showToolLock.value) {
showTool.value = false
}
}
if (!showToolLock.value) {
showTool.value = false;
}
};
const visibleChange = (v: boolean) => {
showTool.value = v
}
showTool.value = v;
};
const getPopupContainer = (trigger: HTMLElement) => {
return trigger?.parentNode || document.body
}
return trigger?.parentNode || document.body;
};
const mediaToolClass = computed(() => {
return {
'media-tool': true,
'media-tool-show': showTool.value
}
})
return {
'media-tool': true,
'media-tool-show': showTool.value,
};
});
/**
* 媒体开始播放
@ -150,7 +232,7 @@ const mediaStart = () => {
props.data.deviceId,
props.data.channelId,
mediaType.value,
)
);
};
//
@ -169,36 +251,33 @@ const getIsRecord = async () => {
/**
* 开始录像
*/
const recordStart = async ({ key }: { key: string}) => {
showToolLock.value = false
showTool.value = false
isRecord.value = 1;
const local = key === 'true'
const res = await channelApi.recordStart(
props.data.deviceId,
props.data.channelId,
{ local },
).catch(() => ({ success: false }))
if (res.success) {
isRecord.value = 2;
} else {
isRecord.value = 0;
}
}
const recordStart = async ({ key }: { key: string }) => {
showToolLock.value = false;
showTool.value = false;
isRecord.value = 1;
const local = key === 'true';
const res = await channelApi
.recordStart(props.data.deviceId, props.data.channelId, { local })
.catch(() => ({ success: false }));
if (res.success) {
isRecord.value = 2;
} else {
isRecord.value = 0;
}
};
/**
* 停止录像
*/
const recordStop = async () => {
const res = await channelApi.recordStop(
props.data.deviceId,
props.data.channelId
);
if (res.success) {
isRecord.value = 0;
}
}
const res = await channelApi.recordStop(
props.data.deviceId,
props.data.channelId,
);
if (res.success) {
isRecord.value = 0;
}
};
/**
* 刷新
@ -223,12 +302,19 @@ const handleReset = async () => {
* @param type 控制类型
*/
const handleMouseDown = (type: string) => {
channelApi.ptzTool(props.data.deviceId, props.data.channelId, type);
channelApi.ptzTool(props.data.deviceId, props.data.channelId, type, speed.value);
};
const handleMouseUp = () => {
channelApi.ptzStop(props.data.deviceId, props.data.channelId);
};
/**
* 分享视频
*/
const onShare = () => {
visible.value = true;
};
watch(
() => _vis.value,
(val: boolean) => {
@ -240,14 +326,23 @@ watch(
url.value = '';
}
},
{
immediate: true
}
);
</script>
<style lang="less" scoped>
@import './index.less';
:deep(.live-player-stretch-btn){
display: none;
:deep(.live-player-stretch-btn) {
display: none;
}
:deep(.vjs-icon-spinner){
display: none;
:deep(.vjs-icon-spinner) {
display: none;
}
.center {
display: flex;
flex-direction: column;
align-items: center;
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<Live :visible="true" type="share" :data="playData" />
</template>
<script lang="ts" setup>
import { LocalStore } from '@/utils/comm';
import { TOKEN_KEY } from '@/utils/variable';
import Live from '../Live/index.vue';
const playData = ref({
deviceId: '',
channelId: '',
type: ''
});
// url
const route = useRoute();
watchEffect(() => {
const obj: any = unref(route.query) || {};
playData.value = {
deviceId: obj?.deviceId || '',
channelId: obj?.channelId || '',
type: obj?.type
};
if(obj?.[TOKEN_KEY]){
LocalStore.set(TOKEN_KEY, obj?.[TOKEN_KEY]);
}
});
</script>