Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev

This commit is contained in:
easy 2023-03-03 18:20:34 +08:00
commit 90359de6b5
9 changed files with 784 additions and 6 deletions

View File

@ -24,7 +24,7 @@
"event-source-polyfill": "^1.0.31",
"global": "^4.4.0",
"jetlinks-store": "^0.0.3",
"jetlinks-ui-components": "^1.0.1",
"jetlinks-ui-components": "^1.0.3",
"js-cookie": "^3.0.1",
"less": "^4.1.3",
"less-loader": "^11.1.0",

View File

@ -35,8 +35,12 @@ export default {
// 更改国标ID
updateGbChannelId: (id: string, data: any): any => server.put(`/media/gb28181-cascade/binding/${id}`, data),
// 查询通道分页列表
queryChannelList: (data: any): any => server.post(`media/channel/_query`, data),
queryChannelList: (data: any): any => server.post(`/media/channel/_query`, data),
// 推送
publish: (id: string, params: any) => server.get(`/media/gb28181-cascade/${id}/bindings/publish`, params)
publish: (id: string, params: any) => server.get(`/media/gb28181-cascade/${id}/bindings/publish`, params),
// 分屏展示接口
// 设备树
getMediaTree: (data?: any) => server.post<any>(`/media/device/_query/no-paging`, data),
}

View File

@ -0,0 +1,400 @@
<!-- 分屏组件 -->
<template>
<div class="live-player-warp">
<div class="live-player-content">
<!-- 工具栏 -->
<div class="player-screen-tool" v-if="showScreen">
<a-radio-group
v-model:value="screen"
button-style="solid"
@change="handleScreenChange"
>
<a-radio-button :value="1">单屏</a-radio-button>
<a-radio-button :value="4">四分屏</a-radio-button>
<a-radio-button :value="9">九分屏</a-radio-button>
<a-radio-button :value="0">全屏</a-radio-button>
</a-radio-group>
<div class="screen-tool-save">
<a-tooltip title="可保存分屏配置记录">
<AIcon type="QuestionCircleOutlined" />
</a-tooltip>
<a-popover
v-model:visible="visible"
trigger="click"
title="分屏名称"
>
<template #content>
<a-form
ref="formRef"
:model="formData"
layout="vertical"
>
<a-form-item
name="name"
:rules="[
{
required: true,
message: '请输入名称',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-textarea v-model:value="formData.name" />
</a-form-item>
<a-button
type="primary"
@click="saveHistory"
:loading="loading"
style="width: 100%; margin-top: 16px"
>
保存
</a-button>
</a-form>
</template>
<a-dropdown-button
type="primary"
@click="visible = true"
>
保存
<template #overlay>
<a-menu>
<a-empty v-if="!historyList.length" />
<a-menu-item
v-for="(item, index) in historyList"
:key="`his${index}`"
@click="handleHistory(item)"
>
<a-space>
<span>{{ item.name }}</span>
<a-popconfirm
title="确认删除?"
ok-text="确认"
cancel-text="取消"
@confirm="(e: any) => {
e?.stopPropagation();
deleteHistory(item.key);
}
"
>
<AIcon
type="DeleteOutlined"
@click="
(e:any) =>
e?.stopPropagation()
"
/>
</a-popconfirm>
</a-space>
</a-menu-item>
</a-menu>
</template>
</a-dropdown-button>
</a-popover>
</div>
</div>
<!-- 播放器 -->
<div class="player-body">
<div
ref="fullscreenRef"
class="player-screen"
:class="`screen-${screen}`"
>
<template v-for="(item, index) in players" :key="item.key">
<div
class="player-screen-item"
:class="{
active:
showScreen &&
playerActive === index &&
!isFullscreen,
'full-screen': isFullscreen,
}"
:style="{ display: item.show ? 'block' : 'none' }"
@click="playerActive = index"
>
<div
class="media-btn-refresh"
:style="{
display: item.url ? 'block' : 'none',
}"
@click="handleRefresh($event, item, index)"
>
刷新
</div>
<LivePlayer :src="item.url" />
</div>
</template>
</div>
</div>
<!-- 控制器 -->
</div>
<MediaTool @onMouseDown="handleMouseDown" @onMouseUp="handleMouseUp" />
</div>
</template>
<script setup lang="ts">
import { useFullscreen } from '@vueuse/core';
import {
deleteSearchHistory,
getSearchHistory,
saveSearchHistory,
} from '@/api/comm';
import { message } from 'ant-design-vue';
import LivePlayer from '@/components/Player/index.vue';
import MediaTool from '@/components/Player/mediaTool.vue';
type Player = {
id?: string;
url?: string;
channelId?: string;
key: string;
show: boolean;
};
interface ScreenProps {
url?: string;
id?: string;
channelId: string;
className?: string;
historyHandle?: (deviceId: string, channelId: string) => string;
/**
*
* @param id 当前选中播发视频ID
* @param type 当前操作动作
*/
onMouseDown?: (deviceId: string, channelId: string, type: string) => void;
/**
*
* @param id 当前选中播发视频ID
* @param type 当前操作动作
*/
onMouseUp?: (deviceId: string, channelId: string, type: string) => void;
showScreen?: boolean;
}
const props = defineProps<ScreenProps>();
const DEFAULT_SAVE_CODE = 'screen-save';
const screen = ref(1);
const players = ref<Player[]>([]);
const playerActive = ref(0);
const historyList = ref<any[]>([]);
const visible = ref(false);
const loading = ref(false);
const fullscreenRef = ref(null);
const { isFullscreen, enter, exit, toggle } = useFullscreen();
const formRef = ref();
const formData = ref({
name: '',
});
const reloadPlayer = (
id: string,
channelId: string,
url: string,
index: number,
) => {
const olPlayers = [...players.value];
olPlayers[index] = {
id: '',
channelId: '',
url: '',
key: olPlayers[index].key,
show: true,
};
const newPlayer = {
id,
url,
channelId,
key: olPlayers[index].key,
show: true,
};
players.value = [...olPlayers];
setTimeout(() => {
olPlayers[index] = newPlayer;
players.value = [...olPlayers];
}, 1000);
};
const replaceVideo = (id: string, channelId: string, url: string) => {
const olPlayers = [...players.value];
const newPlayer = {
id,
url,
channelId,
key: olPlayers[playerActive.value].key,
show: true,
};
if (olPlayers[playerActive.value].url === url) {
//
reloadPlayer(id, channelId, url, playerActive.value);
} else {
olPlayers[playerActive.value] = newPlayer;
players.value = olPlayers;
}
if (playerActive.value === screen.value - 1) {
//
playerActive.value = 0;
} else {
playerActive.value += 1;
}
};
const handleHistory = (item: any) => {
if (props.historyHandle) {
const log = JSON.parse(item.content || '{}');
screen.value = log.screen;
const oldPlayers = [...players.value];
players.value = oldPlayers.map((oldPlayer, index) => {
oldPlayer.show = false;
if (index < log.screen) {
const { deviceId, channelId } = log.players[index];
return {
...oldPlayer,
id: deviceId,
channelId: deviceId,
url: deviceId
? props.historyHandle!(deviceId, channelId)
: '',
show: true,
};
}
return oldPlayer;
});
}
};
const getHistory = async () => {
const res = await getSearchHistory(DEFAULT_SAVE_CODE);
if (res.success) {
historyList.value = res.result;
}
};
const deleteHistory = async (id: string) => {
const res = await deleteSearchHistory(DEFAULT_SAVE_CODE, id);
if (res.success) {
getHistory();
visible.value = false;
}
};
const saveHistory = async () => {
formRef.value
.validate()
.then(async () => {
const param = {
name: formData.value.name,
content: JSON.stringify({
screen: screen,
players: players.value.map((item: any) => ({
deviceId: item.id,
channelId: item.channelId,
})),
}),
};
loading.value = true;
const res = await saveSearchHistory(param, DEFAULT_SAVE_CODE);
loading.value = false;
if (res.success) {
visible.value = false;
getHistory();
message.success('保存成功');
} else {
message.error('保存失败');
}
})
.catch((err: any) => {
console.log(err);
});
};
const mediaInit = () => {
const newArr = [];
for (let i = 0; i < 9; i++) {
newArr.push({
id: '',
channelId: '',
url: '',
key: 'time_' + new Date().getTime() + i,
show: i === 0,
});
}
players.value = newArr;
};
const handleScreenChange = (e: any) => {
if (e.target.value) {
screenChange(e.target.value);
} else {
//
toggle();
}
};
const screenChange = (index: number) => {
players.value = players.value.map((m: any, i: number) => ({
id: '',
channelId: '',
url: '',
updateTime: 0,
key: m.key,
show: i < index,
}));
playerActive.value = 0;
screen.value = index;
};
const handleRefresh = (e: any, item: any, index: number) => {
e.stopPropagation();
if (item.url) {
reloadPlayer(item.id!, item.channelId!, item.url!, index);
}
};
/**
* 点击控制按钮
* @param type 控制类型
*/
const handleMouseDown = (type: string) => {
const { id, channelId } = players.value[playerActive.value];
console.log('players.value: ', players.value);
console.log('playerActive.value: ', playerActive.value);
console.log('id: ', id);
console.log('channelId: ', channelId);
if (id && channelId && props.onMouseDown) {
props.onMouseDown(id, channelId, type);
}
};
const handleMouseUp = (type: string) => {
const { id, channelId } = players.value[playerActive.value];
if (id && channelId && props.onMouseUp) {
props.onMouseUp(id, channelId, type);
}
};
watch(
() => props.url,
(val) => {
if (val && props.id) {
replaceVideo(props.id, props.channelId, val);
}
},
);
watchEffect(() => {
if (props.showScreen !== false) {
getHistory();
}
mediaInit();
});
</script>
<style lang="less" scoped>
@import './index.less';
</style>

View File

@ -0,0 +1,82 @@
.live-player-warp {
display: flex;
.live-player-content {
display: flex;
flex: 1;
flex-direction: column;
.player-screen-tool {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
.ant-radio-button-wrapper {
height: auto;
padding: 4px 20px;
}
}
.player-body {
flex: 1;
.player-screen {
position: relative;
display: grid;
box-sizing: border-box;
&.screen-1 {
grid-template-columns: 1fr;
}
&.screen-4 {
grid-template-rows: 1fr 1fr;
grid-template-columns: 1fr 1fr;
}
&.screen-9 {
grid-template-rows: 1fr 1fr 1fr;
grid-template-columns: 1fr 1fr 1fr;
}
&.screen-4,
&.screen-9 {
grid-gap: 12px;
}
.active {
border: 2px solid red;
}
.full-screen {
border: 1px solid #fff;
}
.player-screen-item {
position: relative;
.media-btn-refresh {
position: absolute;
top: 4px;
right: 4px;
z-index: 2;
padding: 2px 4px;
font-size: 12px;
background-color: #f0f0f0;
border-radius: 2px;
cursor: pointer;
&:hover {
background-color: #d9d9d9;
}
&:active {
background-color: #bfbfbf;
}
}
}
}
}
}
}

View File

@ -10,6 +10,8 @@ import vue3videoPlay from 'vue3-video-play';
const props = defineProps({
src: { type: String, default: '' },
type: { type: String, default: 'mp4' },
width: { type: String, default: '500px' },
height: { type: String, default: '280px' },
});
watch(
@ -21,8 +23,6 @@ watch(
const options = reactive({
...props,
width: '500px', //
height: '280px', //
color: '#409eff', //
title: '', //
// src: props.src,

View File

@ -0,0 +1,85 @@
@padding: 20px;
.split-screen {
display: flex;
.left-content {
width: 300px;
padding-right: @padding;
border-right: 1px solid #f0f0f0;
.online {
color: rgba(82, 196, 26, 1);
}
.offline {
color: rgba(0, 0, 0, 0.25);
}
.left-search {
margin-bottom: 12px;
}
}
.right-content {
display: flex;
flex-direction: column;
flex-grow: 1;
padding-left: @padding;
.top {
display: flex;
flex-basis: 30px;
justify-content: center;
padding-bottom: 12px;
}
.live-player {
display: flex;
flex: 1;
.live-player-content {
position: relative;
flex-grow: 1;
.player-screen {
display: grid;
&.screen-1 {
grid-template-columns: 1fr;
}
&.screen-4 {
grid-template-rows: 1fr 1fr;
grid-template-columns: 1fr 1fr;
}
&.screen-9 {
grid-template-rows: 1fr 1fr 1fr;
grid-template-columns: 1fr 1fr 1fr;
}
&.screen-4,
&.screen-9 {
grid-gap: 12px;
}
}
}
.player-tools {
flex-basis: 280px;
padding: 50px 12px 0 12px;
}
}
}
}
@media screen {
@media (min-width: 1300px) {
.split-screen {
.left-content {
width: 200px;
}
}
}
}

View File

@ -0,0 +1,46 @@
<template>
<page-container>
<a-card class="splitScreen">
<div class="split-screen">
<LeftTree @onSelect="mediaStart" />
<div class="right-content">
<ScreenPlayer
ref="player"
:id="deviceId"
:channelId="channelId"
:onMouseUp="(id, cId) => channelApi.ptzStop(id, cId)"
:onMouseDown="
(id, cId, type) => channelApi.ptzTool(id, cId, type)
"
:historyHandle="(dId, cId) => getMediaUrl(dId, cId)"
showScreen
/>
</div>
</div>
</a-card>
</page-container>
</template>
<script setup lang="ts">
import LeftTree from './tree.vue';
import channelApi from '@/api/media/channel';
import ScreenPlayer from '@/components/Player/ScreenPlayer.vue';
const deviceId = ref('');
const channelId = ref('');
const player = ref();
const getMediaUrl = (dId: string, cId: string): string => {
return channelApi.ptzStart(dId, cId, 'mp4');
};
const mediaStart = (e: { cId: string; dId: string }) => {
channelId.value = e.cId;
deviceId.value = e.dId;
player.value?.replaceVideo(e.dId, e.cId, getMediaUrl(e.dId, e.cId));
};
</script>
<style lang="less" scoped>
@import './index.less';
</style>

View File

@ -0,0 +1,161 @@
<template>
<div class="left-content">
<a-tree
:height="700"
:show-line="{ showLeafIcon: false }"
:show-icon="true"
:tree-data="treeData"
:loadData="onLoadData"
:fieldNames="{ title: 'name', key: 'id' }"
@select="onSelect"
>
<template #icon="{ key, selected }">
<AIcon type="VideoCameraOutlined" class="online" />
</template>
</a-tree>
</div>
</template>
<script setup lang="ts">
import cascadeApi from '@/api/media/cascade';
type Emits = {
(e: 'onSelect', data: { dId: string; cId: string }): void;
};
const emit = defineEmits<Emits>();
interface DataNode {
name: string;
id: string;
isLeaf?: boolean;
channelNumber?: number;
icon?: any;
status: {
text: string;
value: string;
};
children?: DataNode[];
}
const onSelect = (_: any, { node }: any) => {
console.log('node: ', node);
emit('onSelect', { dId: node.deviceId, cId: node.channelId });
};
/**
* 是否为子节点
* @param node
*/
const isLeaf = (node: any): boolean => {
if (node.channelNumber) {
return false;
}
return true;
};
/**
* 获取设备列表
*/
const treeData = ref<any[]>([]);
const getDeviceList = async () => {
const res = await cascadeApi.getMediaTree({ paging: false });
if (res.success) {
treeData.value = res.result.map((m: any) => {
const extra: any = {};
extra.isLeaf = isLeaf(m);
return {
...m,
...extra,
};
});
}
};
getDeviceList();
const updateTreeData = (
list: DataNode[],
key: any,
children: DataNode[],
): DataNode[] => {
return list.map((node) => {
if (node.id === key) {
return {
...node,
children: node.children
? [...node.children, ...children]
: children,
};
}
if (node.children) {
return {
...node,
children: updateTreeData(node.children, key, children),
};
}
return node;
});
};
/**
* 获取子节点
* @param key
* @param params
*/
const getChildren = (key: any, params: any): Promise<any> => {
return new Promise(async (resolve) => {
const res = await cascadeApi.queryChannelList(params);
if (res.status === 200) {
const { total, pageIndex, pageSize } = res.result;
treeData.value = updateTreeData(
treeData.value,
key,
res.result.data.map((item: DataNode) => ({
...item,
// icon: (<AIcon type="VideoCameraOutlined" className={item.status.value}/>),
// icon: `<AIcon type="VideoCameraOutlined" class="${item.status.value}"/>`,
// icon: (h:any) => h('h1', 22),
isLeaf: isLeaf(item),
})),
);
if (total > (pageIndex + 1) * pageSize) {
setTimeout(() => {
getChildren(key, {
...params,
pageIndex: params.pageIndex + 1,
});
}, 50);
}
console.log('treeData.value: ', treeData.value);
console.log('res.result: ', res.result);
resolve(res.result);
}
});
};
const onLoadData = ({ key, children }: any): Promise<void> => {
return new Promise(async (resolve) => {
if (children) {
resolve();
return;
}
await getChildren(key, {
pageIndex: 0,
pageSize: 100,
terms: [
{
column: 'deviceId',
value: key,
},
],
});
resolve();
});
};
</script>
<style lang="less" scoped>
@import './index.less';
</style>

View File

@ -3901,7 +3901,7 @@ jetlinks-store@^0.0.3:
resolved "https://registry.npmjs.org/jetlinks-store/-/jetlinks-store-0.0.3.tgz"
integrity sha512-AZf/soh1hmmwjBZ00fr1emuMEydeReaI6IBTGByQYhTmK1Zd5pQAxC7WLek2snRAn/HHDgJfVz2hjditKThl6Q==
jetlinks-ui-components@^1.0.1:
jetlinks-ui-components@^1.0.3:
version "1.0.3"
resolved "https://registry.jetlinks.cn/jetlinks-ui-components/-/jetlinks-ui-components-1.0.3.tgz#08a35ebfa016574affcfd11204359cdccf8e139f"
integrity sha512-kA/AxzdfNy+Sl8En8raMwIh3stofgElkUuJ+oRJMpQVTGbrOk29DifRsHJJFNvtEvclmLdKZkOkthOuEdG2mnw==