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`), 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; justify-content: center;
width: 45%; width: 45%;
height: 45%; height: 45%;
font-size: 30px; // font-size: 30px;
background-color: #fff; background-color: #fff;
border-radius: 50%; border-radius: 50%;
transform: translate(-50%, -50%) rotateZ(-45deg); transform: translate(-50%, -50%) rotateZ(-45deg);

View File

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

View File

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

View File

@ -5,6 +5,7 @@ export const InitLicense = '/init-license'
export const NotificationSubscriptionCode = 'message-subscribe' export const NotificationSubscriptionCode = 'message-subscribe'
export const NotificationRecordCode = 'account/NotificationRecord' export const NotificationRecordCode = 'account/NotificationRecord'
export const OauthPath = '/oauth' export const OauthPath = '/oauth'
export const VideoSharePath = '/media/device/Share'
export const AccountMenu = { export const AccountMenu = {
path: '/account', path: '/account',
@ -86,5 +87,9 @@ export default [
}, },
component: () => import('@/views/oauth/WeChat.vue') component: () => import('@/views/oauth/WeChat.vue')
}, },
{
path: VideoSharePath,
component: () => import('@/views/media/Device/Channel/Share/index.vue')
},
AccountMenu 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 { .media-live {
display: flex; display: flex;
.live-player-tools { // .live-player-tools {
flex-basis: 230px; // flex-basis: 300px;
.direction-item { // .direction-item {
font-size: 30px !important; // font-size: 30px !important;
} // }
.zoom-item { // .zoom-item {
font-size: 20px !important; // font-size: 20px !important;
} // }
} // }
.media-live-video { .media-live-video {
position: relative; position: relative;
flex-grow: 1; flex: 1;
width: 0;
.media-tool { .media-tool {
position: absolute; position: absolute;
@ -50,9 +49,20 @@
} }
} }
} }
.media-live-actions {
width: 300px;
margin-left: 10px;
.actions-tool {
padding: 0 40px 5px 40px;
}
}
} }
.media-live-tool { .media-live-tool {
display: flex; display: flex;
margin-top: 24px; margin-bottom: 24px;
justify-content: space-between;
align-items: center;
} }

View File

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