feat: 设备接入网关 自定义设备接入 二三步部分功能

This commit is contained in:
jackhoo_98 2023-01-12 18:12:52 +08:00
parent b0983a1988
commit ab8636c7fd
5 changed files with 507 additions and 309 deletions

View File

@ -9,7 +9,8 @@
</div> </div>
<div v-else> <div v-else>
<div v-if="!id"><a @click="goBack">返回</a></div> <div v-if="!id"><a @click="goBack">返回</a></div>
<AccessNetwork :provider="provider" :data="data" /> <AccessNetwork v-if="showType==='network'" :provider="provider" :data="data" />
<Media v-if="showType==='media'" :provider="provider" :data="data" />
</div> </div>
</a-card> </a-card>
</a-spin> </a-spin>
@ -17,10 +18,11 @@
<script lang="ts" setup name="AccessConfigDetail"> <script lang="ts" setup name="AccessConfigDetail">
import { getImage } from '@/utils/comm'; import { getImage } from '@/utils/comm';
import TitleComponent from '@/components/TitleComponent/index.vue';
import AccessNetwork from '../components/Network.vue'; import AccessNetwork from '../components/Network.vue';
import Provider from '../components/Provider/index.vue'; import Provider from '../components/Provider/index.vue';
import { getProviders, detail } from '@/api/link/accessConfig'; import { getProviders, detail } from '@/api/link/accessConfig';
import Media from '../components/Media/index.vue';
// const router = useRouter(); // const router = useRouter();
const route = useRoute(); const route = useRoute();
@ -32,10 +34,14 @@ const type = ref(false);
const loading = ref(true); const loading = ref(true);
const provider = ref({}); const provider = ref({});
const data = ref({}); const data = ref({});
const showType = ref('')
const goProviders = (param: object) => { const goProviders = (param: object) => {
showType.value = param.type
provider.value = param; provider.value = param;
type.value = false; type.value = false;
console.log(1123,showType.value,param);
}; };
const goBack = () => { const goBack = () => {
@ -46,10 +52,69 @@ const goBack = () => {
const queryProviders = async () => { const queryProviders = async () => {
const resp = await getProviders(); const resp = await getProviders();
if (resp.status === 200) { if (resp.status === 200) {
dataSource.value = resp.result.filter( const media: any[] = [];
(item) => const network: any[] = [];
item.channel === 'network' || item.channel === 'child-device', const cloud: any[] = [];
); const channel: any[] = [];
const edge: any[] = [];
resp.result.map((item) => {
if (item.id === 'fixed-media' || item.id === 'gb28181-2016') {
item.type='media'
media.push(item);
} else if (item.id === 'OneNet' || item.id === 'Ctwing') {
item.type='cloud'
cloud.push(item);
} else if (item.id === 'modbus-tcp' || item.id === 'opc-ua') {
item.type='channel'
channel.push(item);
} else if (
item.id === 'official-edge-gateway' ||
item.id === 'edge-child-device'
) {
item.type='edge'
edge.push(item);
} else {
item.type='network'
network.push(item);
}
});
const list = [];
if (network.length) {
list.push({
// type: 'network',
list: [...network],
title: '自定义设备接入',
});
}
if (media.length) {
list.push({
// type: 'media',
list: [...media],
title: '视频类设备接入',
});
}
if (cloud.length) {
list.push({
// type: 'cloud',
list: [...cloud],
title: '云平台接入',
});
}
if (channel.length) {
list.push({
// type: 'channel',
list: [...channel],
title: '通道类设备接入',
});
}
if (edge.length) {
list.push({
// type: 'edge',
list: [...edge],
title: '官方接入',
});
}
dataSource.value = list
} }
}; };

View File

@ -1,86 +1,106 @@
<template> <template>
<a-card hoverable :class="['card-render', checked === data.id ? 'checked' : '']" @click="checkedChange(data.id)"> <a-card
<div class="title"> hoverable
<a-tooltip placement="topLeft" :title="data.name">{{ data.name }}</a-tooltip> :class="['card-render', checked === data.id ? 'checked' : '']"
</div> @click="checkedChange(data.id)"
<slot name="other"></slot> >
<div class="desc"> <div class="title">
<a-tooltip placement="topLeft" :title="data.description">{{ data.description }}</a-tooltip> <a-tooltip placement="topLeft" :title="data.name">{{
</div> data.name
<div class="checked-icon"> }}</a-tooltip>
<div><a-icon type="check" /></div> </div>
</div> <slot name="other"></slot>
</a-card> <div class="desc">
<a-tooltip placement="topLeft" :title="data.description">{{
data.description
}}</a-tooltip>
</div>
<div class="checked-icon">
<div><a-icon type="check" /></div>
</div>
</a-card>
</template> </template>
<script> <script lang="ts" setup name="AccessCard">
export default {
name: "AccessCard",
props: ['data', 'checked'], const emit = defineEmits(['checkedChange']);
methods: {
checkedChange(id){ const props = defineProps({
this.$emit('checkedChange', id) checked: {
} type: Array,
} default: () => [],
}; },
data: {
type: Object,
default: () => {},
},
});
console.log(1112,props);
const checkedChange=(id:string)=>{
emit('checkedChange', id);
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.card-render { .card-render {
width: 100%;
overflow: hidden;
background: url("/public/images/access/access.png") no-repeat;
background-size: 100% 100%;
min-height: 105px;
.title {
width: calc(100% - 88px);
overflow: hidden;
font-weight: 800;
white-space: nowrap;
text-overflow: ellipsis;
}
.desc {
width: 100%; width: 100%;
margin-top: 10px;
color: rgba(0, 0, 0, 0.55);
font-weight: 400;
font-size: 13px;
overflow: hidden; overflow: hidden;
white-space: nowrap; background: url('/public/images/access.png') no-repeat;
text-overflow: ellipsis; background-size: 100% 100%;
} min-height: 105px;
.checked-icon { .title {
position: absolute; width: calc(100% - 88px);
right: -22px; overflow: hidden;
bottom: -22px; font-weight: 800;
z-index: 2; white-space: nowrap;
display: none; text-overflow: ellipsis;
width: 44px; }
height: 44px;
color: #fff; .desc {
background-color: red; width: 100%;
background-color: #2f54eb; margin-top: 10px;
transform: rotate(-45deg); color: rgba(0, 0, 0, 0.55);
font-weight: 400;
> div { font-size: 13px;
position: relative; overflow: hidden;
height: 100%; white-space: nowrap;
transform: rotate(45deg); text-overflow: ellipsis;
font-size: 12px;
padding: 4px 0 0 6px;
} }
}
&.checked {
position: relative;
color: #2f54eb;
border-color: #2f54eb;
.checked-icon { .checked-icon {
display: block; position: absolute;
right: -22px;
bottom: -22px;
z-index: 2;
display: none;
width: 44px;
height: 44px;
color: #fff;
background-color: red;
background-color: #2f54eb;
transform: rotate(-45deg);
> div {
position: relative;
height: 100%;
transform: rotate(45deg);
font-size: 12px;
padding: 4px 0 0 6px;
}
}
&.checked {
position: relative;
color: #2f54eb;
border-color: #2f54eb;
.checked-icon {
display: block;
}
} }
}
} }
</style> </style>

View File

@ -0,0 +1,30 @@
<template>
<div style="margin-top: 10px">
111
</div>
</template>
<script lang="ts" setup name="AccessMedia">
const props = defineProps({
provider: {
type: Object,
default: () => {},
},
data: {
type: Object,
default: () => {},
},
});
const channel = ref(props.provider.channel)
console.log(211,props);
</script>
<style lang="less" scoped>
</style>

View File

@ -35,18 +35,34 @@
: descriptionList[provider.id], : descriptionList[provider.id],
}" }"
> >
<div slot="other" class="other"> <template #other>
<a-tooltip placement="topLeft"> <div class="other">
<div <a-tooltip placement="topLeft">
slot="title"
v-if="
(item.addresses || []).length >
1
"
>
<div <div
v-for="i in item.addresses || v-if="
[]" (item.addresses || [])
.length > 1
"
>
<div
v-for="i in item.addresses ||
[]"
:key="i.address"
class="item"
>
<a-badge
:color="
i.health === -1
? 'red'
: 'green'
"
/>{{ i.address }}
</div>
</div>
<div
v-for="i in (
item.addresses || []
).slice(0, 1)"
:key="i.address" :key="i.address"
class="item" class="item"
> >
@ -56,34 +72,19 @@
? 'red' ? 'red'
: 'green' : 'green'
" "
/>{{ i.address }} :text="i.address"
/>
<span
v-if="
(item.addresses || [])
.length > 1
"
>...</span
>
</div> </div>
</div> </a-tooltip>
<div </div>
v-for="i in ( </template>
item.addresses || []
).slice(0, 1)"
:key="i.address"
class="item"
>
<a-badge
:color="
i.health === -1
? 'red'
: 'green'
"
:text="i.address"
/>
<span
v-if="
(item.addresses || [])
.length > 1
"
>...</span
>
</div>
</a-tooltip>
</div>
</access-card> </access-card>
</a-col> </a-col>
</a-row> </a-row>
@ -130,37 +131,31 @@
<a-col :span="12"> <a-col :span="12">
<title-component data="基本信息" /> <title-component data="基本信息" />
<div> <div>
<a-form :form="form" layout="vertical"> <a-form
<a-form-item label="名称"> ref="formRef"
:model="form"
layout="vertical"
>
<a-form-item
label="名称"
v-bind="validateInfos.name"
>
<a-input <a-input
v-model:value="form.name"
allowClear allowClear
placeholder="请输入名称" placeholder="请输入名称"
v-decorator="[
'name',
{
initialValue: data.name,
rules: [
{
required: true,
message:
'请输入名称!',
},
],
},
]"
/> />
</a-form-item> </a-form-item>
<a-form-item label="说明"> <a-form-item
label="说明"
v-bind="validateInfos.description"
>
<a-textarea <a-textarea
placeholder="请输入说明" placeholder="请输入说明"
:rows="4" :rows="4"
v-decorator="[ v-model:value="form.description"
'description', show-count
{ :maxlength="200"
initialValue:
data.description,
},
]"
/> />
</a-form-item> </a-form-item>
</a-form> </a-form>
@ -259,7 +254,7 @@
:scroll="{ y: 300 }" :scroll="{ y: 300 }"
> >
<template <template
slot="stream" #stream
slot-scope="text, record" slot-scope="text, record"
> >
<span <span
@ -292,7 +287,7 @@
> >
下一步 下一步
</a-button> </a-button>
<a-button v-if="current === 2" type="primary" @click="save"> <a-button v-if="current === 2" type="primary" @click="saveData">
保存 保存
</a-button> </a-button>
<a-button v-if="current > 0" style="margin-left: 8px" @click="prev"> <a-button v-if="current > 0" style="margin-left: 8px" @click="prev">
@ -317,8 +312,61 @@ import {
ProtocolMapping, ProtocolMapping,
} from '../Detail/data'; } from '../Detail/data';
import AccessCard from './AccessCard/index.vue'; import AccessCard from './AccessCard/index.vue';
import TitleComponent from '@/components/TitleComponent/index.vue';
import { message, Form } from 'ant-design-vue'; import { message, Form } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue';
//1
const resultList1 = [
{
id: '1610466717670780928',
name: '官方协议',
},
{
id: '1610205217785524224',
name: 'demo协议',
},
{
id: '1610204985806958592',
name: '水压协议',
},
{
id: '1605459961693745152',
name: '测试设备诊断日志显示',
},
{
id: '1582302200020783104',
name: 'demo',
},
{
id: '1581839391887794176',
name: '海康闸机协议',
},
{
id: '1567062365030637568',
name: '协议20220906160914',
},
{
id: '1561650927208628224',
name: 'local',
},
];
//2
// const result2 = {
// id: 'UDP',
// name: 'UDP',
// features: [],
// routes: [],
// metadata: '',
// };
const result2 = {
"id": "MQTT",
"name": "MQTT",
"features": [],
"routes": [],
"document": "# MQTT认证说明\r\nCONNECT报文:\r\n```text\r\nclientId: 设备ID\r\nusername: secureId+\"|\"+timestamp\r\npassword: md5(secureId+\"|\"+timestamp+\"|\"+secureKey)\r\n ```\r\n\r\n说明: secureId以及secureKey在创建设备产品或设备实例时进行配置. \r\ntimestamp为当前系统时间戳(毫秒),与系统时间不能相差5分钟.\r\nmd5为32位,不区分大小写.",
"metadata": "{\"functions\":[],\"name\":\"test\",\"description\":\"测试用\",\"id\":\"test\",\"properties\":[{\"valueType\":{\"round\":\"HALF_UP\",\"type\":\"double\"},\"name\":\"温度\",\"id\":\"t\"},{\"valueType\":{\"round\":\"HALF_UP\",\"type\":\"int\"},\"name\":\"状态\",\"id\":\"state\"}],\"events\":[],\"tags\":[]}"
}
function generateUUID() { function generateUUID() {
var d = new Date().getTime(); var d = new Date().getTime();
@ -349,7 +397,8 @@ const props = defineProps({
}, },
}); });
const formRef = ref<FormInstance>();
const useForm = Form.useForm;
const current = ref(0); const current = ref(0);
const stepCurrent = ref(0); const stepCurrent = ref(0);
@ -362,13 +411,21 @@ const procotolCurrent = ref('');
let config = ref({}); let config = ref({});
let columnsMQTT = ref([]); let columnsMQTT = ref([]);
const form = reactive({ const form = reactive({
name: 'access', name: '',
description: '', description: '',
}); });
const { resetFields, validate, validateInfos } = useForm(
form,
reactive({
name: [
{ required: true, message: '请输入证书名称', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' },
],
}),
);
const queryNetworkList = async (id: string, params: object, data = {}) => { const queryNetworkList = async (id: string, params: object, data = {}) => {
console.log('queryNetworkList',NetworkTypeMapping.get(id), data, params);
const resp = await getNetworkList(NetworkTypeMapping.get(id), data, params); const resp = await getNetworkList(NetworkTypeMapping.get(id), data, params);
if (resp.status === 200) { if (resp.status === 200) {
networkList.value = resp.result; networkList.value = resp.result;
@ -377,15 +434,19 @@ const queryNetworkList = async (id: string, params: object, data = {}) => {
// const queryProcotolList=async(id:string, params:object) =>{ // const queryProcotolList=async(id:string, params:object) =>{
const queryProcotolList = async (id: string, params = {}) => { const queryProcotolList = async (id: string, params = {}) => {
const resp = await getProtocolList(ProtocolMapping.get(id), { // const resp = await getProtocolList(ProtocolMapping.get(id), {
...params, // ...params,
'sorts[0].name': 'createTime', // 'sorts[0].name': 'createTime',
'sorts[0].order': 'desc', // 'sorts[0].order': 'desc',
}); // });
if (resp.status === 200) { // if (resp.status === 200) {
procotolList.value = resp.result; // procotolList.value = resp.result;
allProcotolList.value = resp.result; // allProcotolList.value = resp.result;
} // }
//使1
procotolList.value = resultList1;
allProcotolList.value = resultList1;
}; };
const addNetwork = () => { const addNetwork = () => {
@ -424,21 +485,6 @@ const checkedChange = (id: string) => {
}; };
const networkSearch = (value: string) => { const networkSearch = (value: string) => {
console.log('networkSearch',
props.provider.id,
{
include: networkCurrent.value || '',
},
{
terms: [
{
column: 'name$LIKE',
value: `%${value}%`,
},
],
},
);
queryNetworkList( queryNetworkList(
props.provider.id, props.provider.id,
{ {
@ -475,31 +521,28 @@ const procotolSearch = (value: string) => {
}; };
const saveData = () => { const saveData = () => {
form.validateFields(async (err, values) => { validate()
if (!err) { .then(async (values) => {
let resp = undefined; let resp = undefined;
let params = {
...props.data,
...values,
protocol: procotolCurrent.value,
channel: 'network', //
channelId: networkCurrent.value,
};
if (props.data && props.data.id) { if (props.data && props.data.id) {
resp = await update({ resp = await update(params);
...props.data,
name: values.name,
description: values.description,
protocol: procotolCurrent.value,
channel: 'network', //
channelId: networkCurrent.value,
});
} else { } else {
resp = await save({ params = {
name: values.name, ...params,
description: values.description,
provider: props.provider.id, provider: props.provider.id,
protocol: procotolCurrent.value,
transport: transport:
props.provider?.id === 'child-device' props.provider?.id === 'child-device'
? 'Gateway' ? 'Gateway'
: ProtocolMapping.get(props.provider.id), : ProtocolMapping.get(props.provider.id),
channel: 'network', // };
channelId: networkCurrent.value, resp = await save(params);
});
} }
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
@ -511,88 +554,139 @@ const saveData = () => {
// this.$store.dispatch('jumpPathByKey', { key: MenuKeys['Link/AccessConfig'] }) // this.$store.dispatch('jumpPathByKey', { key: MenuKeys['Link/AccessConfig'] })
} }
} }
} })
}); .catch((err) => {});
}; };
const next = async () => { const next = async () => {
if (current.value === 0) { if (current.value === 0) {
if (!networkCurrent.value) { if (!networkCurrent.value) {
message.error('请选择网络组件!'); message.error('请选择网络组件!');
} else { } else {
queryProcotolList(props.provider.id); queryProcotolList(props.provider.id);
current.value -= current.value; current.value = current.value + 1;
} }
} else if (current.value === 1) { } else if (current.value === 1) {
if (!procotolCurrent.value) { if (!procotolCurrent.value) {
message.error('请选择消息协议!'); message.error('请选择消息协议!');
} else { } else {
const resp = //使2
props.provider.channel !== 'child-device' config.value = result2;
? await getConfigView( current.value = current.value + 1;
procotolCurrent.value, columnsMQTT = [
ProtocolMapping.get(props.provider.id), {
) title: '分组',
: await getChildConfigView(procotolCurrent.value); dataIndex: 'group',
if (resp.status === 200) { key: 'group',
config.value = resp.result; ellipsis: true,
current.value += current.value; align: 'center',
columnsMQTT = [ width: 100,
{ customRender: (value, row, index) => {
title: '分组', const obj = {
dataIndex: 'group', children: value,
key: 'group', attrs: {},
ellipsis: true, };
align: 'center', const list = (config && config.routes) || [];
width: 100, const arr = list.filter((res) => {
customRender: (value, row, index) => { return res.group == row.group;
const obj = { });
children: value, if (index == 0 || list[index - 1].group !== row.group) {
attrs: {}, obj.attrs.rowSpan = arr.length;
}; } else {
const list = (config && config.routes) || []; obj.attrs.rowSpan = 0;
const arr = list.filter((res) => { }
return res.group == row.group; return obj;
});
if (
index == 0 ||
list[index - 1].group !== row.group
) {
obj.attrs.rowSpan = arr.length;
} else {
obj.attrs.rowSpan = 0;
}
return obj;
},
}, },
{ },
title: 'topic', {
dataIndex: 'topic', title: 'topic',
key: 'topic', dataIndex: 'topic',
ellipsis: true, key: 'topic',
}, ellipsis: true,
{ },
title: '上下行', {
dataIndex: 'stream', title: '上下行',
key: 'stream', dataIndex: 'stream',
ellipsis: true, key: 'stream',
align: 'center', ellipsis: true,
width: 100, align: 'center',
scopedSlots: { customRender: 'stream' }, width: 100,
}, scopedSlots: { customRender: 'stream' },
{ },
title: '说明', {
dataIndex: 'description', title: '说明',
key: 'description', dataIndex: 'description',
ellipsis: true, key: 'description',
}, ellipsis: true,
]; },
} ];
// const resp =
// props.provider.channel !== 'child-device'
// ? await getConfigView(
// procotolCurrent.value,
// ProtocolMapping.get(props.provider.id),
// )
// : await getChildConfigView(procotolCurrent.value);
// if (resp.status === 200) {
// config.value = resp.result;
// current.value = current.value + 1;
// columnsMQTT = [
// {
// title: '',
// dataIndex: 'group',
// key: 'group',
// ellipsis: true,
// align: 'center',
// width: 100,
// customRender: (value, row, index) => {
// const obj = {
// children: value,
// attrs: {},
// };
// const list = (config && config.routes) || [];
// const arr = list.filter((res) => {
// return res.group == row.group;
// });
// if (
// index == 0 ||
// list[index - 1].group !== row.group
// ) {
// obj.attrs.rowSpan = arr.length;
// } else {
// obj.attrs.rowSpan = 0;
// }
// return obj;
// },
// },
// {
// title: 'topic',
// dataIndex: 'topic',
// key: 'topic',
// ellipsis: true,
// },
// {
// title: '',
// dataIndex: 'stream',
// key: 'stream',
// ellipsis: true,
// align: 'center',
// width: 100,
// scopedSlots: { customRender: 'stream' },
// },
// {
// title: '',
// dataIndex: 'description',
// key: 'description',
// ellipsis: true,
// },
// ];
// }
} }
} }
}; };
const prev = () => { const prev = () => {
const currentValue = current.value; current.value = current.value - 1;
current.value -= currentValue;
}; };
onMounted(() => { onMounted(() => {
@ -601,10 +695,6 @@ onMounted(() => {
procotolCurrent.value = props.data.protocol; procotolCurrent.value = props.data.protocol;
current.value = 0; current.value = 0;
networkCurrent.value = props.data.channelId; networkCurrent.value = props.data.channelId;
console.log(11111111,props.provider.id, {
include: networkCurrent.value,
});
queryNetworkList(props.provider.id, { queryNetworkList(props.provider.id, {
include: networkCurrent.value, include: networkCurrent.value,
}); });
@ -618,18 +708,12 @@ onMounted(() => {
} else { } else {
if (props.provider?.id) { if (props.provider?.id) {
if (props.provider.channel !== 'child-device') { if (props.provider.channel !== 'child-device') {
console.log(3333333, props.provider.id, {
include: '',
});
queryNetworkList(props.provider.id, { queryNetworkList(props.provider.id, {
include: '', include: '',
}); });
steps.value = ['网络组件', '消息协议', '完成']; steps.value = ['网络组件', '消息协议', '完成'];
current.value = 0; current.value = 0;
} else { } else {
console.log(444444,props.provider.id);
steps.value = ['消息协议', '完成']; steps.value = ['消息协议', '完成'];
current.value = 1; current.value = 1;
queryProcotolList(props.provider.id); queryProcotolList(props.provider.id);
@ -638,25 +722,20 @@ onMounted(() => {
} }
}); });
// watch( watch(
// () => props.modelValue, current,
// (v) => { (v) => {
// keystoreBase64.value = v; if (props.provider.channel !== 'child-device') {
// }, stepCurrent.value = v;
// { } else {
// deep: true, stepCurrent.value = v - 1;
// immediate: true, }
// }, },
// ); {
// watch: { deep: true,
// current(val) { immediate: true,
// if (this.provider.channel !== 'child-device') { },
// this.stepCurrent = val );
// } else {
// this.stepCurrent = val - 1
// }
// },
// },
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -1,34 +1,36 @@
<template> <template>
<TitleComponent data="自定义设备接入"></TitleComponent> <div v-for="items in dataSource" :key="items.type">
<div> <a-card class="card-items">
<a-row :gutter="[24, 24]"> <TitleComponent :data="items.title"></TitleComponent>
<a-col :span="12" v-for="item in dataSource" :key="item.id"> <a-row :gutter="[24, 24]">
<div class="provider"> <a-col :span="12" v-for="item in items.list" :key="item.id">
<div class="box"> <div class="provider">
<div class="left"> <div class="box">
<div class="images"> <div class="left">
<img :src="backMap.get(item.id)" /> <div class="images">
</div> <img :src="backMap.get(item.id)" />
<div class="context">
<div class="title">
{{ item.name }}
</div> </div>
<div class="desc"> <div class="context">
<a-tooltip :title="item.description"> <div class="title">
{{ item.description || '' }} {{ item.name }}
</a-tooltip> </div>
<div class="desc">
<a-tooltip :title="item.description">
{{ item.description || '' }}
</a-tooltip>
</div>
</div> </div>
</div> </div>
</div> <div class="right">
<div class="right"> <a-button type="primary" @click="click(item)"
<a-button type="primary" @click="click(item)" >接入</a-button
>接入</a-button >
> </div>
</div> </div>
</div> </div>
</div> </a-col>
</a-col> </a-row>
</a-row> </a-card>
</div> </div>
</template> </template>
@ -38,8 +40,8 @@ import { getImage } from '@/utils/comm';
const props = defineProps({ const props = defineProps({
dataSource: { dataSource: {
type: Array, type: Object,
default: () => [], default: () => {},
}, },
}); });
@ -66,10 +68,12 @@ backMap.set('official-edge-gateway', getImage('/access/edge.png'));
const click = (value: object) => { const click = (value: object) => {
emit('onClick', value); emit('onClick', value);
}; };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.card-items{
margin-bottom: 24px;
}
.provider { .provider {
position: relative; position: relative;
width: 100%; width: 100%;