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

This commit is contained in:
wangshuaiswim 2023-02-28 20:06:33 +08:00
commit 5a613e40e9
8 changed files with 897 additions and 72 deletions

View File

@ -1,10 +1,11 @@
import server from '@/utils/request'
import type { CascadeItem } from '@/views/media/Cascade/typings'
export default {
// 列表
list: (data: any, id: string) => server.post(`/media/gb28181-cascade/_query`, data),
list: (data: any) => server.post<any>(`/media/gb28181-cascade/_query`, data),
// 列表字段通道数量, 来自下面接口的total
queryCount: (id: string) => server.post(`/media/gb28181-cascade/${id}/bindings/_query`),
queryCount: (id: string) => server.post<any>(`/media/gb28181-cascade/${id}/bindings/_query`),
// 详情
detail: (id: string): any => server.get(`/media/gb28181-cascade/${id}`),
// 新增
@ -13,4 +14,15 @@ export default {
update: (id: string, data: any) => server.put(`/media/gb28181-cascade/${id}`, data),
// 删除
del: (id: string) => server.remove(`media/gb28181-cascade/${id}`),
// 禁用
disabled: (id: string) => server.post<any>(`/media/gb28181-cascade/${id}/_disabled`),
// 启用
enabled: (id: string) => server.post<any>(`/media/gb28181-cascade/${id}/_enabled`),
// 新增/编辑
// 获取集群节点
clusters: () => server.get(`/network/resources/alive/clusters`),
// SIP本地地址
all: () => server.get(`/network/resources/alive/_all`),
}

View File

@ -48,7 +48,7 @@ const iconKeys = [
'ClockCircleOutlined',
'PartitionOutlined',
'ShareAltOutlined',
'playCircleOutlined',
'PlayCircleOutlined',
'RightOutlined',
'FileTextOutlined',
'UploadOutlined',

View File

@ -90,7 +90,7 @@ const insert = (val) => {
]);
}
watch(() => props.value,
watch(() => props.modelValue,
(val) => {
instance.setValue(val)
})

View File

@ -0,0 +1,35 @@
.doc {
height: 1050px;
padding: 24px;
overflow-y: auto;
color: rgba(#000, 0.8);
font-size: 14px;
background-color: #fafafa;
.url {
padding: 8px 16px;
color: #2f54eb;
background-color: rgba(#a7bdf7, 0.2);
}
h1 {
margin: 16px 0;
color: rgba(#000, 0.85);
font-weight: bold;
font-size: 14px;
&:first-child {
margin-top: 0;
}
}
h2 {
margin: 6px 0;
color: rgba(0, 0, 0, 0.8);
font-size: 14px;
}
.image {
margin: 16px 0;
}
}

View File

@ -0,0 +1,671 @@
<!-- 国标级联新增/编辑 -->
<template>
<page-container>
<a-card>
<a-row :gutter="24">
<a-col :span="12">
<a-form layout="vertical" :model="formData">
<a-row :gutter="24">
<TitleComponent data="基本信息" />
<a-col :span="12">
<a-form-item
label="名称"
name="name"
:rules="[
{
required: true,
message: '请输入名称',
},
{
max: 84,
message: '最多可输入84个字符',
},
]"
>
<a-input
v-model:value="formData.name"
placeholder="请输入名称"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="代理视频流"
name="name"
:rules="[
{
required: true,
message: '请选择代理视频流',
},
]"
>
<a-radio-group
button-style="solid"
v-model:value="formData.name"
>
<a-radio-button value="enabled">
启用
</a-radio-button>
<a-radio-button value="disabled">
禁用
</a-radio-button>
</a-radio-group>
</a-form-item>
</a-col>
<TitleComponent data="信令服务配置" />
<a-col :span="12">
<a-form-item
label="集群节点"
name="name"
:rules="[
{
required: true,
message: '请选择集群节点',
},
]"
>
<a-input
v-model:value="formData.name"
placeholder="请选择集群节点"
/>
</a-form-item>
</a-col>
<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="24">
<a-form-item
label="上级SIP ID"
name="name"
:rules="[
{
required: true,
message: '请输入上级SIP ID',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-input
v-model:value="formData.name"
placeholder="请输入上级SIP ID"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="上级SIP域"
name="name"
:rules="[
{
required: true,
message: '请输入上级平台SIP域',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-input
v-model:value="formData.name"
placeholder="请输入上级平台SIP域"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="上级SIP 地址"
name="name"
:rules="[
{
required: true,
message: '请输入上级SIP 地址',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-row :gutter="10">
<a-col :span="14">
<a-input
v-model:value="formData.name"
placeholder="请输入IP地址"
/>
</a-col>
<a-col :span="10">
<a-input
v-model:value="formData.name"
placeholder="请输入端口"
/>
</a-col>
</a-row>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
label="本地SIP ID"
name="name"
:rules="[
{
required: true,
message: '请输入网关侧的SIP ID',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-input
v-model:value="formData.name"
placeholder="网关侧的SIP ID"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="SIP本地地址"
name="name"
:rules="[
{
required: true,
message: '请输入SIP本地地址',
},
]"
>
<a-row :gutter="10">
<a-col :span="14">
<a-select
v-model:value="formData.name"
placeholder="请选择IP地址"
>
<a-select-option value="1">
1
</a-select-option>
</a-select>
</a-col>
<a-col :span="10">
<a-select
v-model:value="formData.name"
placeholder="请选择端口"
>
<a-select-option value="1">
1
</a-select-option>
</a-select>
</a-col>
</a-row>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="SIP远程地址"
name="name"
:rules="[
{
required: true,
message: '请输入SIP远程地址',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-row :gutter="10">
<a-col :span="14">
<a-input
v-model:value="formData.name"
placeholder="请输入IP地址"
/>
</a-col>
<a-col :span="10">
<a-input
v-model:value="formData.name"
placeholder="请输入端口"
/>
</a-col>
</a-row>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
label="传输协议"
name="name"
:rules="[
{
required: true,
message: '请选择传输协议',
},
]"
>
<a-radio-group
button-style="solid"
v-model:value="formData.name"
>
<a-radio-button value="UDP">
UDP
</a-radio-button>
<a-radio-button value="TCP_PASSIVE">
TCP
</a-radio-button>
</a-radio-group>
</a-form-item>
</a-col>
<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="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="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="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="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="name"
:rules="[
{
required: true,
message: '请输入心跳周期(秒)',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-input-number
v-model:value="formData.name"
placeholder="请输入心跳周期(秒)"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="注册间隔(秒)"
name="name"
:rules="[
{
required: true,
message: '请输入注册间隔(秒)',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-input-number
v-model:value="formData.name"
placeholder="请输入注册间隔(秒)"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item>
<a-button
type="primary"
@click="handleSubmit"
:loading="btnLoading"
>
保存
</a-button>
</a-form-item>
</a-form>
</a-col>
<a-col :span="12">
<div class="doc">
<h1>1.概述</h1>
<div>
配置国标级联平台可以将已经接入到自身的摄像头共享给第三方调用播放
</div>
<div>
<a-alert
message="注该配置只用于将本平台向上级联至第三方平台如需第三方平台向上级联至本平台请在“视频设备”页面新增设备时选择“GB/T28181”接入方式。"
type="info"
show-icon
/>
</div>
<h1>2.配置说明</h1>
<div>
以下配置说明以将本平台数据级联到LiveGBS平台为例
</div>
<h2>1上级SIP ID</h2>
<div>请填写第三方平台中配置的<b>SIP ID</b></div>
<div class="image">
<a-image
width="100%"
:src="getImage('/northbound/doc2.png')"
/>
</div>
<h2>2上级SIP </h2>
<div>请填写第三方平台中配置的<b>SIP ID域</b></div>
<div class="image">
<a-image
width="100%"
:src="getImage('/northbound/doc1.png')"
/>
</div>
<h2>3上级SIP 地址</h2>
<div>请填写第三方平台中配置的<b>SIP ID地址</b></div>
<div class="image">
<a-image
width="100%"
:src="getImage('/northbound/doc3.png')"
/>
</div>
<h2>4本地SIP ID</h2>
<div>
请填写本地的<b>SIP ID地址</b>
地址由中心编码(8)行业编码(2)类型编码(3)和序号(7)四个码段共20位十
进制数字字符构成详细规则请参见GB/T28181-2016中附录D部分
</div>
<h2>5SIP本地地址</h2>
<div>
请选择<b>指定的网卡和端口</b>如有疑问请联系系统运维人员
</div>
<h2>6用户</h2>
<div>
部分平台有基于用户和接入密码的特殊认证通常情况下,请填写<b
>本地SIP ID</b
>
</div>
<h2>7接入密码</h2>
<div>
需与上级平台设置的接入密码一致用于身份认证
</div>
<h2>8厂商/型号/版本号</h2>
<div>
本平台将以设备的身份级联到上级平台请设置本平台在上级平台中显示的厂商型号版本号
</div>
<h2>9心跳周期</h2>
<div>
需与上级平台设置的心跳周期保持一致通常默认60秒
</div>
<h2>10注册间隔</h2>
<div>
若SIP代理通过注册方式校时,其注册间隔时间宜设置为小于
SIP代理与 SIP服务器出现1s误 差所经过的运行时间
</div>
</div>
</a-col>
</a-row>
</a-card>
</page-container>
</template>
<script setup lang="ts">
import { getImage } from '@/utils/comm';
import { Form } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import DeviceApi from '@/api/media/device';
import { PROVIDER_OPTIONS } from '@/views/media/Device/const';
import type { ProductType } from '@/views/media/Device/typings';
const router = useRouter();
const route = useRoute();
const useForm = Form.useForm;
//
const formData = ref({
id: '',
name: '',
channel: 'gb28181-2016',
photoUrl: getImage('/device-media.png'),
productId: '',
others: {
access_pwd: '',
},
description: '',
//
streamMode: 'UDP',
manufacturer: '',
model: '',
firmware: '',
});
//
const formRules = ref({
id: [
{
required: true,
message: '请输入ID',
},
{ max: 64, message: '最多输入64个字符' },
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: '请输入英文或者数字或者-或者_',
},
],
name: [
{ required: true, message: '请输入名称' },
{ max: 64, message: '最多可输入64个字符' },
],
productId: [{ required: true, message: '请选择所属产品' }],
channel: [{ required: true, message: '请选择接入方式' }],
'others.access_pwd': [{ required: true, message: '请输入接入密码' }],
description: [{ max: 200, message: '最多可输入200个字符' }],
streamMode: [{ required: true, message: '请选择流传输模式' }],
});
watch(
() => formData.value.channel,
(val) => {
formRules.value['id'][0].required = val === 'gb28181-2016';
formRules.value['others.access_pwd'][0].required =
val === 'gb28181-2016';
validate();
getProductList();
},
);
const { resetFields, validate, validateInfos, clearValidate } = useForm(
formData.value,
formRules.value,
);
const clearValid = () => {
setTimeout(() => {
clearValidate();
}, 200);
};
/**
* 获取所属产品
*/
const productList = ref<ProductType[]>([]);
const getProductList = async () => {
// console.log('formData.productId: ', formData.value.productId);
const params = {
paging: false,
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [
{ column: 'accessProvider', value: formData.value.channel },
{ column: 'state', value: 1 },
],
};
const { result } = await DeviceApi.queryProductList(params);
productList.value = result;
};
getProductList();
/**
* 新增产品
*/
const saveProductVis = ref(false);
/**
* 获取详情
*/
const getDetail = async () => {
const res = await DeviceApi.detail(route.query.id as string);
// console.log('res: ', res);
// formData.value = res.result;
Object.assign(formData.value, res.result);
formData.value.channel = res.result.provider;
console.log('formData.value: ', formData.value);
};
onMounted(() => {
getDetail();
});
/**
* 表单提交
*/
const btnLoading = ref<boolean>(false);
const handleSubmit = () => {
// console.log('formData.value: ', formData.value);
validate()
.then(async () => {
btnLoading.value = true;
let res;
if (!route.query.id) {
res = await DeviceApi.save(formData.value);
} else {
res = await DeviceApi.update(formData.value);
}
if (res?.success) {
message.success('保存成功');
router.back();
}
})
.catch((err) => {
console.log('err: ', err);
})
.finally(() => {
btnLoading.value = false;
});
};
</script>
<style lang="less" scoped>
@import './index.less';
</style>

View File

@ -2,17 +2,19 @@
<page-container>
<Search
:columns="columns"
target="notice-config"
target="media-cascade"
@search="handleSearch"
/>
<JTable
ref="listRef"
:columns="columns"
:request="DeviceApi.list"
:request="(e:any) => lastValueFrom(e)"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
:params="params"
:gridColumn="2"
>
<template #headerTitle>
<a-button type="primary" @click="handleAdd"> 新增 </a-button>
@ -23,45 +25,37 @@
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:showStatus="true"
:status="
slotProps.state.value === 'online' ? 'success' : 'error'
"
:statusText="slotProps.state.text"
:statusNames="{ success: 'success', error: 'error' }"
:status="slotProps.status?.value"
:statusText="slotProps.status?.text"
:statusNames="{
enabled: 'success',
disabled: 'error',
}"
>
<template #img>
<slot name="img">
<img :src="getImage('/device-media.png')" />
<img
:src="
getImage('/device/instance/device-card.png')
"
/>
</slot>
</template>
<template #content>
<h3 class="card-item-content-title">
{{ slotProps.name }}
</h3>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">厂商</div>
<div>{{ slotProps.manufacturer }}</div>
</a-col>
<a-col :span="12">
<div class="card-item-content-text">
通道数量
</div>
<div>{{ slotProps.channelNumber }}</div>
</a-col>
<a-col :span="12">
<div class="card-item-content-text">型号</div>
<div>{{ slotProps.model }}</div>
</a-col>
<a-col :span="12">
<div class="card-item-content-text">
接入方式
</div>
<!-- <div>
{{ providerType[slotProps.provider] }}
</div> -->
</a-col>
</a-row>
<p>通道数量{{ slotProps.count }}</p>
<Ellipsis>
<a-badge
:text="`sip:${slotProps.sipConfigs[0]?.sipId}@${slotProps.sipConfigs[0]?.hostAndPort}`"
:status="
slotProps.status?.value === 'enabled'
? 'success'
: 'error'
"
/>
</Ellipsis>
</template>
<template #actions="item">
<a-tooltip
@ -73,9 +67,20 @@
v-bind="item.popConfirm"
:disabled="item.disabled"
>
<a-button :disabled="item.disabled">
<a-button
:disabled="item.disabled"
v-if="item.key === 'delete'"
>
<AIcon type="DeleteOutlined" />
</a-button>
<a-button
:disabled="item.disabled"
@click="item.onClick"
v-else
>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</a-button>
</a-popconfirm>
<template v-else>
<a-button
@ -87,9 +92,53 @@
</a-button>
</template>
</a-tooltip>
<!-- <PermissionButton
:disabled="item.disabled"
:popConfirm="item.popConfirm"
:tooltip="{
...item.tooltip,
}"
@click="item.onClick"
:hasPermission="`media/Cascade:${item.key}`"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</PermissionButton> -->
</template>
</CardBox>
</template>
<template #sipId="slotProps">
{{ slotProps.sipConfigs[0]?.sipId }}
</template>
<template #publicHost="slotProps">
{{ slotProps.sipConfigs[0]?.publicHost }}
</template>
<template #status="slotProps">
<a-badge
:text="slotProps.status?.text"
:status="
slotProps.status?.value === 'enabled'
? 'success'
: 'error'
"
/>
</template>
<template #onlineStatus="slotProps">
<a-badge
:text="slotProps.onlineStatus?.text"
:status="
slotProps.onlineStatus?.value === 'online'
? 'success'
: 'error'
"
/>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
@ -123,6 +172,24 @@
/></a-button>
</a-button>
</a-tooltip>
<!-- <template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
>
<PermissionButton
:disabled="i.disabled"
:popConfirm="i.popConfirm"
:tooltip="{
...i.tooltip,
}"
@click="i.onClick"
type="link"
style="padding: 0px"
:hasPermission="`device/Instance:${i.key}`"
>
<template #icon><AIcon :type="i.icon" /></template>
</PermissionButton>
</template> -->
</a-space>
</template>
</JTable>
@ -131,6 +198,7 @@
<script setup lang="ts">
import DeviceApi from '@/api/media/device';
import CascadeApi from '@/api/media/cascade';
import type { ActionsType } from '@/components/Table/index.vue';
import { message } from 'ant-design-vue';
import { getImage } from '@/utils/comm';
@ -156,14 +224,14 @@ const columns = [
},
{
title: '上级SIP ID',
dataIndex: 'sipConfigs',
key: 'sipConfigs',
dataIndex: 'sipId',
key: 'sipId',
scopedSlots: true,
},
{
title: '上级SIP 地址',
dataIndex: 'sipConfigs',
key: 'sipConfigs',
dataIndex: 'publicHost',
key: 'publicHost',
scopedSlots: true,
},
{
@ -217,17 +285,35 @@ const columns = [
* @param params
*/
const handleSearch = (e: any) => {
// console.log('handleSearch:', e);
params.value = e;
};
/**
* 处理表格数据
* @param params
*/
const lastValueFrom = async (params: any) => {
const res = await CascadeApi.list(params);
res.result.data.forEach(async (item: any) => {
const resp = await queryBindChannel(item.id);
item.count = resp.result.total;
});
return res;
};
/**
* 查询通道数量
* @param id
*/
const queryBindChannel = async (id: string) => {
return await CascadeApi.queryCount(id);
};
/**
* 新增
*/
const handleAdd = () => {
menuStory.jumpPage('media/Device/Save', {
id: ':id',
});
menuStory.jumpPage('media/Cascade/Save');
};
const getActions = (
@ -245,7 +331,7 @@ const getActions = (
icon: 'EditOutlined',
onClick: () => {
menuStory.jumpPage(
'media/Device/Save',
'media/Cascade/Save',
{},
{
id: data.id,
@ -255,58 +341,79 @@ const getActions = (
},
{
key: 'view',
text: '查看通道',
text: '选择通道',
tooltip: {
title: '查看通道',
title: '选择通道',
},
icon: 'PartitionOutlined',
icon: 'LinkOutlined',
onClick: () => {
// router.push(
// `/media/device/Channel?id=${data.id}&type=${data.provider}`,
// );
menuStory.jumpPage(
'media/Device/Channel',
'media/Cascade/Channel',
{},
{
id: data.id,
type: data.provider,
},
);
},
},
{
key: 'debug',
text: '更新通道',
text: '推送',
tooltip: {
title:
data.provider === 'fixed-media'
? '固定地址无法更新通道'
: data.state.value === 'offline'
? '设备已离线'
: data.state.value === 'notActive'
? '设备已禁用'
: '',
data.status?.value === 'disabled'
? '禁用状态下不可推送'
: '推送',
},
disabled:
data.state.value === 'offline' ||
data.state.value === 'notActive' ||
data.provider === 'fixed-media',
icon: 'SyncOutlined',
disabled: data.status?.value === 'disabled',
icon: 'ShareAltOutlined',
onClick: () => {
// updateChannel()
},
},
{
key: 'action',
text: data.status?.value === 'enabled' ? '禁用' : '启用',
tooltip: {
title: data.status?.value === 'enabled' ? '禁用' : '启用',
},
icon:
data.status?.value === 'enabled'
? 'StopOutlined'
: 'PlayCircleOutlined',
popConfirm: {
title: `确认${
data.status?.value === 'enabled' ? '禁用' : '启用'
}?`,
onConfirm: async () => {
let res =
data.status.value === 'enabled'
? await CascadeApi.disabled(data.id)
: await CascadeApi.enabled(data.id);
if (res.success) {
message.success('操作成功!');
listRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
},
{
key: 'delete',
text: '删除',
tooltip: {
title: '在线设备无法删除',
title:
data.status?.value === 'enabled'
? '请先禁用, 再删除'
: '删除',
},
disabled: data.state.value === 'online',
disabled: data.status?.value === 'enabled',
popConfirm: {
title: '确认删除?',
onConfirm: async () => {
const resp = await DeviceApi.del(data.id);
const resp = await CascadeApi.del(data.id);
if (resp.status === 200) {
message.success('操作成功!');
listRef.value?.reload();

View File

@ -31,7 +31,7 @@ type SipConfig = {
transport: string;
user: string;
};
type CascadeItem = {
export type CascadeItem = {
mediaServerId: string;
onlineStatus: State;
proxyStream: boolean;

View File

@ -1,4 +1,4 @@
<!-- 通知模板详情 -->
<!-- 视频设备新增/编辑 -->
<template>
<page-container>
<a-card>