Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev
This commit is contained in:
commit
4d099365d8
|
@ -0,0 +1,21 @@
|
|||
import server from '@/utils/request';
|
||||
|
||||
|
||||
export const save = (data: object) => server.post(`/media/server`, data);
|
||||
|
||||
export const update = (data: object) => server.patch(`/media/server`, data);
|
||||
|
||||
export const query = (data: object) => server.post(`/media/server/_query`, data);
|
||||
|
||||
export const queryDetail = (id: string) => server.get(`/media/server/${id}`);
|
||||
|
||||
export const remove = (id: string) => server.remove(`/media/server/${id}`);
|
||||
|
||||
export const queryProviders = () => server.get(`/media/server/providers`);
|
||||
|
||||
export const enalbe = (id: string) => server.post(`/media/server/${id}/_enable`);
|
||||
|
||||
export const disable = (id: string) => server.post(`/media/server/${id}/_disable`);
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<a-card>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
class="form"
|
||||
layout="vertical"
|
||||
:model="formData"
|
||||
name="basic"
|
||||
autocomplete="off"
|
||||
>
|
||||
<a-row :gutter="[16, 0]">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="流媒体名称"
|
||||
name="name"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入流媒体名称',
|
||||
},
|
||||
{
|
||||
max: 64,
|
||||
message: '最大可输入64个字符',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="formData.name"
|
||||
placeholder="请输入流媒体名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="服务商"
|
||||
name="provider"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择服务商',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-select
|
||||
ref="select"
|
||||
v-model:value="formData.provider"
|
||||
:options="options"
|
||||
placeholder="请选择服务商"
|
||||
></a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="密钥"
|
||||
:name="['configuration', 'secret']"
|
||||
:rules="[
|
||||
{
|
||||
max: 64,
|
||||
message: '最大可输入64个字符',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input-password
|
||||
placeholder="请输入密钥"
|
||||
v-model:value="formData.configuration.secret"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8" class="form-item">
|
||||
<a-form-item
|
||||
:name="['configuration', 'apiHost']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入API Host',
|
||||
},
|
||||
{
|
||||
pattern: regDomain,
|
||||
message: '请输入正确的IP地址或者域名',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div class="form-label">
|
||||
API Host
|
||||
<span class="form-label-required">*</span>
|
||||
<a-tooltip>
|
||||
<template #title>
|
||||
<p>调用流媒体接口时请求的服务地址</p>
|
||||
</template>
|
||||
<question-circle-outlined />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<a-input
|
||||
placeholder="请输入API Host"
|
||||
v-model:value="formData.configuration.apiHost"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-form-item
|
||||
class="form-item"
|
||||
:name="['configuration', 'apiPort']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入端口',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div class="form-label"></div>
|
||||
|
||||
<a-input-number
|
||||
style="width: 100%"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
:precision="0"
|
||||
placeholder="请输入输入端口"
|
||||
v-model:value="formData.configuration.apiPort"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8" class="form-item">
|
||||
<a-form-item
|
||||
:name="['configuration', 'rtpIp']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入RTP IP',
|
||||
},
|
||||
{
|
||||
pattern: regDomain,
|
||||
message: '请输入正确的IP地址或者域名',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div class="form-label">
|
||||
RTP IP
|
||||
<span class="form-label-required">*</span>
|
||||
<a-tooltip>
|
||||
<template #title>
|
||||
<p>
|
||||
视频设备将流推送到该IP地址下,部分设备仅支持IP地址,建议全是用IP地址
|
||||
</p>
|
||||
</template>
|
||||
<question-circle-outlined />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<a-input
|
||||
placeholder="请输入RTP IP"
|
||||
v-model:value="formData.configuration.rtpIp"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="4" v-if="!checked">
|
||||
<a-form-item
|
||||
class="form-item"
|
||||
:name="['configuration', 'rtpPort']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入端口',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div class="form-label"></div>
|
||||
|
||||
<a-input-number
|
||||
style="width: 100%"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
:precision="0"
|
||||
placeholder="请输入端口"
|
||||
v-model:value="formData.configuration.rtpPort"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="4" v-if="checked">
|
||||
<a-form-item
|
||||
class="form-item"
|
||||
:name="['configuration', 'dynamicRtpPortRange0']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入起始端口',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div class="form-label"></div>
|
||||
<a-input-number
|
||||
style="width: 100%"
|
||||
:min="1"
|
||||
:max="
|
||||
formData.configuration.dynamicRtpPortRange1
|
||||
"
|
||||
:precision="0"
|
||||
placeholder="起始端口"
|
||||
v-model:value="
|
||||
formData.configuration.dynamicRtpPortRange0
|
||||
"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<div class="form-item-checked" v-if="checked">至</div>
|
||||
<a-col :span="4" v-if="checked">
|
||||
<a-form-item
|
||||
class="form-item"
|
||||
:name="['configuration', 'dynamicRtpPortRange1']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入终止端口',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div class="form-label"></div>
|
||||
<a-input-number
|
||||
style="width: 100%"
|
||||
:min="
|
||||
formData.configuration.dynamicRtpPortRange0
|
||||
"
|
||||
:max="65535"
|
||||
:precision="0"
|
||||
placeholder="终止端口"
|
||||
v-model:value="
|
||||
formData.configuration.dynamicRtpPortRange1
|
||||
"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-form-item
|
||||
class="form-item-checked2"
|
||||
:name="['configuration', 'dynamicRtpPort']"
|
||||
>
|
||||
<div class="form-label"></div>
|
||||
<a-checkbox
|
||||
v-model:checked="
|
||||
formData.configuration.dynamicRtpPort
|
||||
"
|
||||
>
|
||||
动态端口
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="24">
|
||||
<a-form-item>
|
||||
<a-button
|
||||
v-if="view === 'false'"
|
||||
class="form-submit"
|
||||
html-type="submit"
|
||||
type="primary"
|
||||
@click.prevent="onSubmit"
|
||||
:loading="loading"
|
||||
>保存</a-button
|
||||
>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</page-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="CertificateDetail">
|
||||
import { message, Form } from 'ant-design-vue';
|
||||
import { queryProviders, queryDetail, save, update } from '@/api/media/stream';
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import { FormDataType } from '../type';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const view = route.query.view as string;
|
||||
const id = route.params.id as string;
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const loading = ref(false);
|
||||
const options = ref([]);
|
||||
const checked = ref(false);
|
||||
const regDomain =
|
||||
/[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?/;
|
||||
|
||||
const formData = ref<FormDataType>({
|
||||
name: '',
|
||||
provider: undefined,
|
||||
configuration: {
|
||||
secret: '',
|
||||
apiHost: '',
|
||||
apiPort: '',
|
||||
rtpIp: '',
|
||||
rtpPort: '',
|
||||
dynamicRtpPort: false,
|
||||
// dynamicRtpPortRange: [],
|
||||
dynamicRtpPortRange0: '',
|
||||
dynamicRtpPortRange1: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async () => {
|
||||
let data = await formRef.value?.validate();
|
||||
let params = { ...data };
|
||||
const { configuration } = data;
|
||||
if (configuration.dynamicRtpPort) {
|
||||
const { dynamicRtpPortRange0, dynamicRtpPortRange1 } = configuration;
|
||||
delete configuration.dynamicRtpPortRange0;
|
||||
delete configuration.dynamicRtpPortRange1;
|
||||
params.configuration = {
|
||||
...configuration,
|
||||
dynamicRtpPortRange: [dynamicRtpPortRange0, dynamicRtpPortRange1],
|
||||
};
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const response =
|
||||
id === ':id' ? await save(params) : await update({ ...params, id });
|
||||
if (response.status === 200) {
|
||||
message.success('操作成功');
|
||||
router.push('/iot/link/Stream');
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const detail = async (id: string) => {
|
||||
loading.value = true;
|
||||
const resp = await queryProviders();
|
||||
options.value = resp.result.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
}));
|
||||
if (id !== ':id') {
|
||||
const res = await queryDetail(id);
|
||||
if (res.success) {
|
||||
const result = res.result as any;
|
||||
formData.value = { ...result };
|
||||
|
||||
const { configuration } = result;
|
||||
if (configuration.dynamicRtpPort) {
|
||||
const { dynamicRtpPortRange } = configuration;
|
||||
delete configuration.dynamicRtpPortRange;
|
||||
formData.value.configuration = {
|
||||
...configuration,
|
||||
dynamicRtpPortRange0: dynamicRtpPortRange[0],
|
||||
dynamicRtpPortRange1: dynamicRtpPortRange[1],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
detail(id);
|
||||
|
||||
watch(
|
||||
() => formData.value.configuration.dynamicRtpPort,
|
||||
(value) => {
|
||||
checked.value = value;
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.form {
|
||||
.form-submit {
|
||||
background-color: @primary-color !important;
|
||||
}
|
||||
.form-item-checked {
|
||||
padding: 0;
|
||||
padding-top: 35px;
|
||||
}
|
||||
.form-item-checked2 {
|
||||
padding-top: 5px;
|
||||
}
|
||||
.form-label {
|
||||
height: 30px;
|
||||
padding-bottom: 8px;
|
||||
.form-label-required {
|
||||
color: red !important;
|
||||
margin: 0 4px 0 -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,319 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<div>
|
||||
<Search :columns="columns" target="search" @search="handleSearch" />
|
||||
|
||||
<JTable
|
||||
ref="tableRef"
|
||||
model="CARD"
|
||||
:columns="columns"
|
||||
:gridColumn="2"
|
||||
:gridColumns="[1, 2]"
|
||||
:request="query"
|
||||
:defaultParams="{
|
||||
sorts: [{ name: 'id', order: 'desc' }],
|
||||
}"
|
||||
:params="params"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<a-button type="primary" @click="handlAdd"
|
||||
><plus-outlined />新增</a-button
|
||||
>
|
||||
</template>
|
||||
<template #card="slotProps">
|
||||
<CardBox
|
||||
:showStatus="true"
|
||||
:value="slotProps"
|
||||
:actions="getActions(slotProps)"
|
||||
v-bind="slotProps"
|
||||
:class="
|
||||
slotProps.state.value === 'disabled'
|
||||
? 'tableCardDisabled'
|
||||
: 'tableCardEnabled'
|
||||
"
|
||||
:status="slotProps.state.value"
|
||||
:statusText="slotProps.state.text"
|
||||
:statusNames="{
|
||||
enabled: 'success',
|
||||
disabled: 'error',
|
||||
}"
|
||||
>
|
||||
<template #img>
|
||||
<slot name="img">
|
||||
<img :src="getImage('/stream.png')" />
|
||||
</slot>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="card-item-content">
|
||||
<a
|
||||
@click="handlEye(slotProps.id)"
|
||||
class="card-item-content-title-a"
|
||||
>
|
||||
{{ slotProps.name }}
|
||||
</a>
|
||||
<a-row class="card-item-content-box">
|
||||
<a-col
|
||||
:span="8"
|
||||
class="card-item-content-text"
|
||||
>
|
||||
<div class="card-item-content-text">
|
||||
服务商
|
||||
</div>
|
||||
<div class="card-item-content-text">
|
||||
<a-tooltip>
|
||||
<template #title>{{
|
||||
slotProps.provider
|
||||
}}</template>
|
||||
{{ slotProps.provider }}
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="card-item-content-text">
|
||||
RTP IP
|
||||
</div>
|
||||
<div class="card-item-content-text">
|
||||
<a-tooltip>
|
||||
<template #title>{{
|
||||
slotProps.configuration
|
||||
?.rtpIp
|
||||
}}</template>
|
||||
{{
|
||||
slotProps.configuration
|
||||
?.rtpIp
|
||||
}}
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="card-item-content-text">
|
||||
API HOST
|
||||
</div>
|
||||
<div class="card-item-content-text">
|
||||
<a-tooltip>
|
||||
<template #title>{{
|
||||
slotProps.configuration
|
||||
?.apiHost
|
||||
}}</template>
|
||||
{{
|
||||
slotProps.configuration
|
||||
?.apiHost
|
||||
}}
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="item">
|
||||
<a-tooltip
|
||||
v-bind="item.tooltip"
|
||||
:title="item.disabled && item.tooltip.title"
|
||||
>
|
||||
<a-popconfirm
|
||||
v-if="item.popConfirm"
|
||||
v-bind="item.popConfirm"
|
||||
:disabled="item.disabled"
|
||||
>
|
||||
<a-button :disabled="item.disabled">
|
||||
<AIcon
|
||||
type="DeleteOutlined"
|
||||
v-if="item.key === 'delete'"
|
||||
/>
|
||||
<template v-else>
|
||||
<AIcon :type="item.icon" />
|
||||
<span>{{ item.text }}</span>
|
||||
</template>
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<template v-else>
|
||||
<a-button
|
||||
:disabled="item.disabled"
|
||||
@click="item.onClick"
|
||||
>
|
||||
<AIcon
|
||||
type="DeleteOutlined"
|
||||
v-if="item.key === 'delete'"
|
||||
/>
|
||||
<template v-else>
|
||||
<AIcon :type="item.icon" />
|
||||
<span>{{ item.text }}</span>
|
||||
</template>
|
||||
</a-button>
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</CardBox>
|
||||
</template>
|
||||
</JTable>
|
||||
</div>
|
||||
</page-container>
|
||||
</template>
|
||||
<script lang="ts" setup name="AccessConfigPage">
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { query, remove, disable, enalbe } from '@/api/media/stream';
|
||||
import { message } from 'ant-design-vue';
|
||||
import Detail from './Detail/index.vue';
|
||||
|
||||
const tableRef = ref<Record<string, any>>({});
|
||||
const router = useRouter();
|
||||
const params = ref<Record<string, any>>({});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: '禁用',
|
||||
value: 'disabled',
|
||||
},
|
||||
{
|
||||
label: '正常',
|
||||
value: 'enabled',
|
||||
},
|
||||
],
|
||||
},
|
||||
scopedSlots: true,
|
||||
},
|
||||
];
|
||||
|
||||
const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
|
||||
if (!data) return [];
|
||||
const state = data.state.value;
|
||||
const actions = [
|
||||
{
|
||||
key: 'edit',
|
||||
text: '编辑',
|
||||
tooltip: {
|
||||
title: '编辑',
|
||||
},
|
||||
icon: 'EditOutlined',
|
||||
onClick: () => {
|
||||
handlEdit(data.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
text: state === 'enabled' ? '禁用' : '启用',
|
||||
tooltip: {
|
||||
title: state === 'enabled' ? '禁用' : '启用',
|
||||
},
|
||||
icon: state === 'enabled' ? 'StopOutlined' : 'CheckCircleOutlined',
|
||||
popConfirm: {
|
||||
title: `确认${state === 'enabled' ? '禁用' : '启用'}?`,
|
||||
onConfirm: async () => {
|
||||
let res =
|
||||
state === 'enabled'
|
||||
? await disable(data.id)
|
||||
: await enalbe(data.id);
|
||||
if (res.success) {
|
||||
message.success('操作成功');
|
||||
tableRef.value?.reload();
|
||||
} else {
|
||||
message.error('操作失败!');
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
text: '删除',
|
||||
disabled: state === 'enabled',
|
||||
tooltip: {
|
||||
title: state === 'enabled' ? '正常的流媒体不能删除' : '删除',
|
||||
},
|
||||
popConfirm: {
|
||||
title: '确认删除?',
|
||||
onConfirm: async () => {
|
||||
const res = await remove(data.id);
|
||||
if (res.success) {
|
||||
message.success('操作成功');
|
||||
tableRef.value.reload();
|
||||
} else {
|
||||
message.error('操作失败!');
|
||||
}
|
||||
},
|
||||
},
|
||||
icon: 'DeleteOutlined',
|
||||
},
|
||||
];
|
||||
return actions;
|
||||
};
|
||||
|
||||
const handlAdd = () => {
|
||||
router.push({
|
||||
path: `/iot/link/Stream/detail/:id`,
|
||||
query: { view: false },
|
||||
});
|
||||
};
|
||||
const handlEdit = (id: string) => {
|
||||
router.push({
|
||||
path: `/iot/link/Stream/detail/${id}`,
|
||||
query: { view: false },
|
||||
});
|
||||
};
|
||||
const handlEye = (id: string) => {
|
||||
router.push({
|
||||
path: `/iot/link/Stream/detail/${id}`,
|
||||
query: { view: true },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
* @param params
|
||||
*/
|
||||
const handleSearch = (e: any) => {
|
||||
params.value = e;
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.tableCardDisabled {
|
||||
width: 100%;
|
||||
background: url('/images/access-config-diaabled.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.tableCardEnabled {
|
||||
width: 100%;
|
||||
background: url('/images/access-config-enabled.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.card-item-content {
|
||||
min-height: 100px;
|
||||
|
||||
.card-item-content-title-a {
|
||||
// color: #000 !important;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
overflow: hidden; //超出的文本隐藏
|
||||
text-overflow: ellipsis; //溢出用省略号显示
|
||||
white-space: nowrap; //溢出不换行
|
||||
}
|
||||
.card-item-content-box {
|
||||
min-height: 50px;
|
||||
}
|
||||
.card-item-content-text {
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
font-size: 12px;
|
||||
overflow: hidden; //超出的文本隐藏
|
||||
text-overflow: ellipsis; //溢出用省略号显示
|
||||
white-space: nowrap; //溢出不换行
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,19 @@
|
|||
|
||||
export interface Configuration = {
|
||||
secret: string,
|
||||
apiHost: string,
|
||||
apiPort: number,
|
||||
rtpIp: string,
|
||||
rtpPort: number,
|
||||
dynamicRtpPort: boolean,
|
||||
dynamicRtpPortRange?: array,
|
||||
dynamicRtpPortRange0?: number,
|
||||
dynamicRtpPortRange1?: number,
|
||||
};
|
||||
export type FormDataType = {
|
||||
name: string;
|
||||
provider: undefined;
|
||||
configuration: Configuration;
|
||||
id?: string;
|
||||
};
|
||||
|
Loading…
Reference in New Issue