fix: bug#17651、17633、视频分享点位bug及样式
* fix: bug#17651、17633、视频分享点位bug及样式
This commit is contained in:
parent
fe3b636a78
commit
02eb16d0eb
|
@ -35,7 +35,7 @@ type PlayerProps = {
|
|||
updateTime?: number;
|
||||
key?: string | number;
|
||||
loading?: boolean;
|
||||
protocol?: 'mp4' | 'flv' | 'hls';
|
||||
protocol?: 'mp4' | 'flv' | 'hls' | 'rtc';
|
||||
onDestroy?: (e?: any) => void;
|
||||
onMessage?: (msg: any) => void;
|
||||
onError?: (err: any) => void;
|
||||
|
|
|
@ -150,9 +150,15 @@ const showNotification = (message: string, description: string, key?: string, sh
|
|||
* @returns {Promise<never>}
|
||||
*/
|
||||
const errorHandler = (error: any) => {
|
||||
|
||||
if (error.response) {
|
||||
const data = error.response.data
|
||||
const status = error.response.status
|
||||
if(data instanceof Blob){
|
||||
data.text().then((res)=>{
|
||||
showNotification(error.message, (JSON.parse(res).message + '').substr(0,90))
|
||||
})
|
||||
}else{
|
||||
if (data?.code === 'license required') {
|
||||
Modal.error({
|
||||
key: 'License',
|
||||
|
@ -188,6 +194,7 @@ const errorHandler = (error: any) => {
|
|||
const message = error?.response?.data?.message || `${data?.error} ${data?.path}`
|
||||
showNotification(error?.code, message, '404')
|
||||
}
|
||||
}
|
||||
} else if (error.response === undefined) {
|
||||
if (error.message.includes('timeout')) {
|
||||
showNotification(error.message, '接口响应超时', undefined)
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
:hasPermission="`link/AccessConfig:${
|
||||
id === ':id' ? 'add' : 'update'
|
||||
}`"
|
||||
:loading="loading"
|
||||
>
|
||||
保存
|
||||
</PermissionButton>
|
||||
|
@ -88,6 +89,7 @@ const route = useRoute();
|
|||
const view = route.query.view as string;
|
||||
const id = route.params.id as string;
|
||||
|
||||
const loading = ref(false)
|
||||
const props = defineProps({
|
||||
provider: {
|
||||
type: Object,
|
||||
|
@ -106,6 +108,7 @@ const formState = ref<FormState>({
|
|||
description: '',
|
||||
});
|
||||
const onFinish = async (values: any) => {
|
||||
loading.value = true
|
||||
const providerId = props.provider.id;
|
||||
const params = {
|
||||
...values,
|
||||
|
@ -124,6 +127,7 @@ const onFinish = async (values: any) => {
|
|||
setTimeout(() => window.close(), 300);
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
|
|
@ -328,6 +328,7 @@
|
|||
:hasPermission="`link/AccessConfig:${
|
||||
id === ':id' ? 'add' : 'update'
|
||||
}`"
|
||||
:loading="loading"
|
||||
>
|
||||
保存
|
||||
</PermissionButton>
|
||||
|
@ -376,6 +377,7 @@ const route = useRoute();
|
|||
const view = route.query.view as string;
|
||||
const id = route.params.id as string;
|
||||
|
||||
const loading = ref(false)
|
||||
const props = defineProps({
|
||||
provider: {
|
||||
type: Object,
|
||||
|
@ -434,6 +436,7 @@ const procotolSearch = (value: string) => {
|
|||
|
||||
const saveData = async () => {
|
||||
const data: any = await formRef2.value?.validate();
|
||||
loading.value = true
|
||||
const params = {
|
||||
...data,
|
||||
configuration: {
|
||||
|
@ -460,6 +463,7 @@ const saveData = async () => {
|
|||
setTimeout(() => window.close(), 300);
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
};
|
||||
|
||||
const queryProcotolList = async (id: string, params = {}) => {
|
||||
|
|
|
@ -406,6 +406,7 @@
|
|||
:hasPermission="`link/AccessConfig:${
|
||||
id === ':id' ? 'add' : 'update'
|
||||
}`"
|
||||
:loading="loading"
|
||||
>
|
||||
保存
|
||||
</PermissionButton>
|
||||
|
@ -486,6 +487,7 @@ const formData = ref<Form>({
|
|||
description: '',
|
||||
});
|
||||
|
||||
const loading = ref(false)
|
||||
const current = ref(0);
|
||||
const stepCurrent = ref(0);
|
||||
const steps = ref(['接入配置', '消息协议', '完成']);
|
||||
|
@ -515,6 +517,7 @@ const procotolSearch = (value: string) => {
|
|||
|
||||
const saveData = async () => {
|
||||
const data: any = await formRef2.value?.validate();
|
||||
loading.value = true
|
||||
const params = {
|
||||
...data,
|
||||
configuration: {
|
||||
|
@ -542,6 +545,7 @@ const saveData = async () => {
|
|||
setTimeout(() => window.close(), 300);
|
||||
}
|
||||
}
|
||||
loading.value =false
|
||||
};
|
||||
|
||||
const queryProcotolList = async (id: string, params = {}) => {
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
}`"
|
||||
html-type="submit"
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
>
|
||||
保存
|
||||
</PermissionButton>
|
||||
|
@ -88,6 +89,7 @@ const route = useRoute();
|
|||
const view = route.query.view as string;
|
||||
const id = route.params.id as string;
|
||||
|
||||
const loading = ref(false)
|
||||
const props = defineProps({
|
||||
provider: {
|
||||
type: Object,
|
||||
|
@ -106,6 +108,7 @@ const formState = ref<FormState>({
|
|||
description: '',
|
||||
});
|
||||
const onFinish = async (values: any) => {
|
||||
loading.value = true
|
||||
const providerId = props.provider.id;
|
||||
const params = {
|
||||
...values,
|
||||
|
@ -124,6 +127,7 @@ const onFinish = async (values: any) => {
|
|||
setTimeout(() => window.close(), 300);
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
|
|
@ -156,6 +156,7 @@
|
|||
:hasPermission="`link/AccessConfig:${
|
||||
id === ':id' ? 'add' : 'update'
|
||||
}`"
|
||||
:loading="loading"
|
||||
>
|
||||
保存
|
||||
</PermissionButton>
|
||||
|
@ -187,6 +188,7 @@
|
|||
:hasPermission="`link/AccessConfig:${
|
||||
id === ':id' ? 'add' : 'update'
|
||||
}`"
|
||||
:loading="loading"
|
||||
>
|
||||
保存
|
||||
</PermissionButton>
|
||||
|
@ -223,6 +225,7 @@ const route = useRoute();
|
|||
const view = route.query.view as string;
|
||||
const id = route.params.id as string;
|
||||
|
||||
const loading = ref(false)
|
||||
const props = defineProps({
|
||||
provider: {
|
||||
type: Object,
|
||||
|
@ -252,6 +255,7 @@ const networkList: any = ref([]);
|
|||
const allNetworkList: any = ref([]);
|
||||
|
||||
const onFinish = async (values: any) => {
|
||||
loading.value = true
|
||||
const providerId = props.provider.id;
|
||||
const params = {
|
||||
...values,
|
||||
|
@ -270,6 +274,7 @@ const onFinish = async (values: any) => {
|
|||
setTimeout(() => window.close(), 300);
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
};
|
||||
|
||||
const checkedChange = (id: string) => {
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
:hasPermission="`link/AccessConfig:${
|
||||
id === ':id' ? 'add' : 'update'
|
||||
}`"
|
||||
:loading="loading"
|
||||
>
|
||||
保存
|
||||
</PermissionButton>
|
||||
|
@ -104,6 +105,7 @@ const props = defineProps({
|
|||
},
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const channel = ref(props.provider.channel);
|
||||
|
||||
const formState = ref<FormState>({
|
||||
|
@ -111,6 +113,7 @@ const formState = ref<FormState>({
|
|||
description: '',
|
||||
});
|
||||
const onFinish = async (values: any) => {
|
||||
loading.value = true
|
||||
const params = {
|
||||
...values,
|
||||
provider: 'fixed-media',
|
||||
|
@ -132,6 +135,7 @@ const onFinish = async (values: any) => {
|
|||
history.back();
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
:columns="columns"
|
||||
:dataSource="dataSource"
|
||||
:pagination="false"
|
||||
:scroll="{ y: 200 }"
|
||||
:scroll="share ? { y: 560} :{ y: 200 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'actions'">
|
||||
|
@ -61,6 +61,10 @@ const props = defineProps({
|
|||
type: Object as PropType<Partial<Record<string, any>>>,
|
||||
default: () => ({}),
|
||||
},
|
||||
share:{
|
||||
type:Boolean,
|
||||
default:false
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(['refresh'])
|
||||
|
|
|
@ -41,7 +41,7 @@ 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}`
|
||||
url.value = `${window.location.origin}#/media/device/Share?deviceId=${props.data.deviceId}&channelId=${props.data.channelId}&type=${route.query.type}&id=${props.data.id}&${TOKEN_KEY}=${token}`
|
||||
})
|
||||
|
||||
const onCopy = () => {
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
<j-modal
|
||||
v-model:visible="_vis"
|
||||
title="播放"
|
||||
:width="_type ? 1200 : 900"
|
||||
:width="type === 'share'? '100%' : _type ? 1200 : 900"
|
||||
:class="{share: type === 'share'}"
|
||||
:maskClosable="false"
|
||||
@ok="_vis = false"
|
||||
:destroyOnClose="true"
|
||||
|
@ -20,6 +21,7 @@
|
|||
<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-button value='rtc'>RTC</j-radio-button>
|
||||
</j-radio-group>
|
||||
<div class="media-live-share" v-if="type !== 'share'">
|
||||
<j-button type="link" @click="onShare"
|
||||
|
@ -132,9 +134,9 @@
|
|||
</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 v-if="type !== 'share'">
|
||||
<j-button @click="_vis = false">取消</j-button>
|
||||
<j-button @click="_vis = false" type="primary">确定</j-button>
|
||||
</j-space>
|
||||
</template>
|
||||
</j-modal>
|
||||
|
@ -179,7 +181,7 @@ const player = ref();
|
|||
// 视频地址
|
||||
const url = ref('');
|
||||
// 视频类型
|
||||
const mediaType = ref<'mp4' | 'flv' | 'hls'>('mp4');
|
||||
const mediaType = ref<'mp4' | 'flv' | 'hls' | 'rtc'>('mp4');
|
||||
const showTool = ref(false);
|
||||
const showToolLock = ref(false);
|
||||
|
||||
|
|
|
@ -0,0 +1,336 @@
|
|||
<template>
|
||||
<div style="width: 95%; margin: 0 auto;">
|
||||
<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-button value='rtc'>RTC</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" 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>
|
||||
<div v-else-if="isRecord === 1">请求录像中</div>
|
||||
<div
|
||||
v-else-if="isRecord === 2"
|
||||
@click.stop="recordStop"
|
||||
>
|
||||
停止录像
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-item" @click.stop="handleRefresh">
|
||||
刷新
|
||||
</div>
|
||||
<div class="tool-item">
|
||||
<j-popconfirm
|
||||
title="重置将断开直播, 可能会影响其他播放者"
|
||||
@confirm="handleReset"
|
||||
>
|
||||
重置
|
||||
</j-popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
<LivePlayer
|
||||
ref="player"
|
||||
:live="true"
|
||||
:url="url"
|
||||
:protocol="mediaType"
|
||||
autoplay
|
||||
/>
|
||||
</div>
|
||||
<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" @refresh="onRefresh" :share="true"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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;
|
||||
(e: 'refresh'): void;
|
||||
};
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
data: {
|
||||
type: Object as PropType<Partial<Record<string, any>>>,
|
||||
default: () => ({}),
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<'share' | 'normal'>,
|
||||
default: 'normal',
|
||||
},
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const _vis = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit('update:visible', val),
|
||||
});
|
||||
|
||||
// 播放器
|
||||
const player = ref();
|
||||
// 视频地址
|
||||
const url = ref('');
|
||||
// 视频类型
|
||||
const mediaType = ref<'mp4' | 'flv' | 'hls' | 'rtc'>('mp4');
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const visibleChange = (v: boolean) => {
|
||||
showTool.value = v;
|
||||
};
|
||||
|
||||
const getPopupContainer = (trigger: HTMLElement) => {
|
||||
return trigger?.parentNode || document.body;
|
||||
};
|
||||
|
||||
const mediaToolClass = computed(() => {
|
||||
return {
|
||||
'media-tool': true,
|
||||
'media-tool-show': showTool.value,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 媒体开始播放
|
||||
*/
|
||||
const mediaStart = () => {
|
||||
url.value = channelApi.ptzStart(
|
||||
props.data.deviceId,
|
||||
props.data.channelId,
|
||||
mediaType.value,
|
||||
);
|
||||
};
|
||||
|
||||
// 录像状态
|
||||
const isRecord = ref(0); // 0:停止录像; 1:请求录像中; 2:开始录像
|
||||
/**
|
||||
* 查询录像状态
|
||||
*/
|
||||
const getIsRecord = async () => {
|
||||
const { result } = await channelApi.ptzIsRecord(
|
||||
props.data.deviceId,
|
||||
props.data.channelId,
|
||||
);
|
||||
isRecord.value = result ? 2 : 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 handleRefresh = () => {
|
||||
// player.value.play();
|
||||
url.value = '';
|
||||
setTimeout(() => {
|
||||
mediaStart();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
const handleReset = async () => {
|
||||
channelApi.mediaStop(props.data.deviceId, props.data.channelId);
|
||||
};
|
||||
|
||||
/**
|
||||
* 点击控制按钮
|
||||
* @param type 控制类型
|
||||
*/
|
||||
const handleMouseDown = (type: string) => {
|
||||
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;
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
watch(
|
||||
() => _vis.value,
|
||||
(val: boolean) => {
|
||||
if (val) {
|
||||
mediaStart();
|
||||
getIsRecord();
|
||||
} else {
|
||||
// url置空, 即销毁播放器
|
||||
url.value = '';
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@import './index.less';
|
||||
:deep(.live-player-stretch-btn) {
|
||||
display: none;
|
||||
}
|
||||
:deep(.vjs-icon-spinner) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -1,16 +1,17 @@
|
|||
<template>
|
||||
<Live :visible="true" type="share" :data="playData" />
|
||||
<ShareLive :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';
|
||||
import ShareLive from '../Live/shareLive.vue';
|
||||
|
||||
const playData = ref({
|
||||
deviceId: '',
|
||||
channelId: '',
|
||||
type: ''
|
||||
type: '',
|
||||
id:''
|
||||
});
|
||||
|
||||
// 获取url信息
|
||||
|
@ -21,7 +22,8 @@ watchEffect(() => {
|
|||
playData.value = {
|
||||
deviceId: obj?.deviceId || '',
|
||||
channelId: obj?.channelId || '',
|
||||
type: obj?.type
|
||||
type: obj?.type,
|
||||
id:obj.id || ''
|
||||
};
|
||||
if(obj?.[TOKEN_KEY]){
|
||||
LocalStore.set(TOKEN_KEY, obj?.[TOKEN_KEY]);
|
||||
|
|
Loading…
Reference in New Issue