feat: 新增平台对接

This commit is contained in:
blp 2023-02-02 11:06:58 +08:00
parent 67d637cca9
commit 001bb6bce0
5 changed files with 1010 additions and 0 deletions

View File

@ -0,0 +1,31 @@
import server from '@/utils/request'
/**
*
* @param data
*/
export const queryList = (data: any) => server.post(`/network/card/platform/_query`, data)
/**
* id查询详情
* @param id
*/
export const queryById = (id: any) => server.get(`/network/card/platform/${id}`)
/**
*
* @param data
*/
export const save = (data: any) => server.post(`/network/card/platform`, data)
/**
*
* @param data
*/
export const update = (data: any) => server.patch(`/network/card/platform`, data)
/**
*
* @param id
*/
export const del = (id: string) => server.remove(`/network/card/platform/${id}`)

View File

@ -0,0 +1,249 @@
<template>
<page-container>
<a-card>
<a-row :gutter="24">
<a-col :span="14">
<TitleComponent data="详情" />
<a-form
:layout="'vertical'"
ref="formRef"
:rules="rules"
:model="form"
>
<a-form-item
label="平台类型"
name="operatorName"
required
>
<PlatformType
:disabled="false"
:model="'singular'"
:itemStyle="{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-around',
minWidth: '130px',
}"
:options="platformTypeList"
v-model:value="form.operatorName"
@change="typeChange"
></PlatformType
></a-form-item>
<a-form-item label="名称" name="name">
<a-input
v-model:value="form.name"
placeholder="请输入名称"
/>
</a-form-item>
<!-- onelink -->
<div v-if="form.operatorName === 'onelink'">
<a-form-item label="App ID" name="appId">
<a-input
v-model:value="form.appId"
placeholder="请输入App ID"
/>
</a-form-item>
<a-form-item label="Password" name="passWord">
<a-input-password
v-model:value="form.passWord"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item label="接口地址" name="apiAddr">
<a-input
v-model:value="form.apiAddr"
placeholder="请输入接口地址"
/>
</a-form-item>
</div>
<!-- ctwing -->
<div v-if="form.operatorName === 'ctwing'">
<a-form-item label="用户id" name="userId">
<a-input
v-model:value="form.userId"
placeholder="请输入用户id"
/>
</a-form-item>
<a-form-item label="密码" name="passWord">
<a-input-password
v-model:value="form.passWord"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item label="secretKey" name="secretKey">
<a-input
v-model:value="form.secretKey"
placeholder="请输入secretKey"
/>
</a-form-item>
</div>
<!-- unicom -->
<div v-if="form.operatorName === 'unicom'">
<a-form-item label="App ID" name="appId">
<a-input
v-model:value="form.appId"
placeholder="请输入App ID"
/>
</a-form-item>
<a-form-item label="App Secret" name="appSecret">
<a-input
v-model:value="form.appSecret"
placeholder="请输入App Secret"
/>
</a-form-item>
<a-form-item label="创建者ID" name="openId">
<a-input
v-model:value="form.openId"
placeholder="请输入创建者ID"
/>
</a-form-item>
</div>
<a-form-item label="说明" name="explain">
<a-textarea
v-model:value="form.explain"
placeholder="请输入说明"
showCount
:rows="3"
:maxlength="200"
/>
</a-form-item>
<a-form-item>
<a-divider />
<a-button
:loading="saveBtnLoading"
type="primary"
@click="handleSave"
>
保存
</a-button>
</a-form-item>
</a-form>
</a-col>
<a-col :span="10">
<Doc :type="form.operatorName" />
</a-col>
</a-row>
</a-card>
</page-container>
</template>
<script lang="ts" setup>
import { getImage } from '@/utils/comm';
import PlatformType from '@/views/iot-card/components/PlatformType.vue';
import { queryById, save, update } from '@/api/iot-card/platform';
import { message } from 'ant-design-vue';
import Doc from '../doc/index.vue';
const router = useRouter();
const route = useRoute();
const formRef = ref();
const saveBtnLoading = ref<boolean>(false);
const form = reactive({
operatorName: 'onelink',
name: undefined,
// onelink
appId: undefined,
passWord: undefined,
apiAddr: undefined,
// ctwing
userId: undefined,
secretKey: undefined,
// unicom
appSecret: undefined,
openId: undefined,
explain: undefined,
});
const platformTypeList = [
{
label: '移动OneLink',
value: 'onelink',
imgUrl: getImage('/iot-card/onelink.png'),
imgSize: ['78px', '20px'],
},
{
label: '电信Ctwing',
value: 'ctwing',
imgUrl: getImage('/iot-card/ctwingcmp.png'),
imgSize: ['52px', '25px'],
},
{
label: '联通Unicom',
value: 'unicom',
imgUrl: getImage('/iot-card/unicom.png'),
imgSize: ['56px', '41px'],
},
];
const rules = {
name: [
{ required: true, message: '请输入名称' },
{ max: 64, message: '最多可输入64个字符' },
],
appId: [
{ required: true, message: '请输入App ID' },
{ max: 64, message: '最多可输入64个字符' },
],
passWord: [
{ required: true, message: '请输入密码' },
{ max: 64, message: '最多可输入64个字符' },
],
apiAddr: [{ required: true, message: '请输入接口地址' }],
userId: [
{ required: true, message: '请输入用户 ID' },
{ max: 64, message: '最多可输入64个字符' },
],
secretKey: [{ required: true, message: '请输入secretKey' }],
appSecret: [{ required: true, message: '请输入App Secret' }],
openId: [{ required: true, message: '请输入创建者ID' }],
explain: [{ required: false, max: 200, message: '最多可输入200个字符' }],
};
const getDetail = async () => {
if (route.params.id === ':id') return;
const resp: any = await queryById(route.params.id);
if (resp.status === 200) {
Object.assign(form, resp.result, { ...resp.result.config });
}
};
const typeChange = (val: any) => {
formRef.value.resetFields();
form.operatorName = val;
};
const handleSave = async () => {
const data: any = await formRef.value.validate();
const formData = {
operatorName: data.operatorName,
name: data.name,
config: {
appId: data.appId,
passWord: data.passWord,
apiAddr: data.apiAddr,
userId: data.userId,
secretKey: data.secretKey,
appSecret: data.appSecret,
openId: data.openId,
},
explain: data.explain,
};
saveBtnLoading.value = true;
const res: any =
route.params.id === ':id'
? await save(formData)
: await update({ id: route.params.id, ...formData });
if (res.status === 200) {
message.success('保存成功!');
router.back();
}
saveBtnLoading.value = false;
};
getDetail();
</script>

View File

@ -0,0 +1,220 @@
<template>
<div v-if="type === 'onelink'" class="doc">
<div class="url">
中国移动物联卡能力开放平台
<a
style="word-break: break-all"
href="https://api.iot.10086.cn/api/index.html#/login"
target="_blank"
rel="noreferrer"
>
https://api.iot.10086.cn/api/index.html#/login
</a>
</div>
<h1>1.概述</h1>
<p>
平台对接通过API的方式与三方系统进行数据对接为物联卡的管理提供数据交互支持
</p>
<h1>2.配置说明</h1>
<h2>1APP ID</h2>
<p>
第三方应用唯一标识中国移动物联网全网管理员在 OneLink
能力开放平台上分配并展示给集团客户
<br />
获取路径中移物联卡能力开放平台--个人中心--客户信息--接入信息
</p>
<div class="image">
<a-image
width="100%"
:src="getImage('/iot-card/onelink-appid.png')"
/>
</div>
<h2>2Password</h2>
<p>
API 接入秘钥,由中国移动物联网提供集团客户从OneLink
能力开放平台获取
<br />
获取路径中移物联卡能力开放平台--个人中心--客户信息--接入信息
</p>
<div class="image">
<a-image
width="100%"
:src="getImage('/iot-card/onelink-pass.png')"
/>
</div>
<h2>3接口地址</h2>
<p>
https://api.iot.10086.cn/v5/ec/get/token
<br />
token后缀请根据实际情况填写
<br />
示例https://api.iot.10086.cn/v5/authService?appid=xxx&password=xxx&transid=xxx
</p>
</div>
<div v-if="type === 'ctwing'" class="doc">
<div class="url">
5G连接管理平台
<a
style="word-break: break-all"
href="https://cmp.ctwing.cn:4821/login"
target="_blank"
rel="noreferrer"
>
https://cmp.ctwing.cn:4821/login
</a>
</div>
<div>
<h1>1.概述</h1>
<p>
平台对接通过API的方式与三方系统进行数据对接为物联卡的管理提供数据交互支持
</p>
<h1>2.配置说明</h1>
<h2>1用户 id</h2>
<p>
5G连接管理平台用户的唯一标识用于身份识别
<br />
获取路径5G连接管理平台--能力开放--API网关账号管理
</p>
<div class="image">
<a-image
width="100%"
:src="getImage('/iot-card/ctwing-id.png')"
/>
</div>
<h2>2密码</h2>
<p>
用户id经加密之后的密码
<br />
获取路径5G连接管理平台--能力开放--API网关账号管理
</p>
<div class="image">
<a-image
width="100%"
:src="getImage('/iot-card/ctwing-pass.png')"
/>
</div>
<h2>3secretKey</h2>
<p>
APP secret唯一秘钥
<br />
获取路径5G连接管理平台--能力开放--API网关账号管理
</p>
<div class="image">
<a-image
width="100%"
:src="getImage('/iot-card/ctwing-secret.png')"
/>
</div>
</div>
</div>
<div v-if="type === 'unicom'" class="doc">
<div class="url">
雁飞智连CMP平台
<a
style="word-break: break-all"
href=" https://cmp.10646.cn/webframe/login"
target="_blank"
rel="noreferrer"
>
https://cmp.10646.cn/webframe/login
</a>
</div>
<div>
<h1>1.概述</h1>
<p>
平台对接通过API的方式与三方系统进行数据对接为物联卡的管理提供数据交互支持
</p>
<h1>2.配置说明</h1>
<h2>1APP ID</h2>
<p>
第三方应用唯一标识
<br />
获取路径雁飞智连CMP平台--我的应用--应用列表
</p>
<div class="image">
<a-image
width="100%"
:src="getImage('/iot-card/unicom-id.png')"
/>
</div>
<h2>2App Secret</h2>
<p>
API 接入秘钥
<br />
获取路径雁飞智连CMP平台--我的应用--应用列表
</p>
<div class="image">
<a-image
width="100%"
:src="getImage('/iot-card/unicom-secret.png')"
/>
</div>
<h2>3创建者ID</h2>
<p>
接口参数中的 OpenId
<br />
获取路径雁飞智连CMP平台--我的应用--应用列表
<br />
</p>
<div class="image">
<a-image
width="100%"
:src="getImage('/iot-card/unicom-openid.png')"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { getImage } from '@/utils/comm';
const props = defineProps({
type: { type: String, default: 'onelink' },
});
</script>
<style scoped lang="less">
.doc {
height: 800px;
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;
}
}
</style>

View File

@ -0,0 +1,305 @@
<!-- 平台对接 -->
<template>
<page-container>
<Search
:columns="columns"
target="platform-search"
@search="handleSearch"
/>
<JTable
ref="platformRef"
:columns="columns"
:request="queryList"
:defaultParams="{ sorts: [{ name: 'createTime', order: 'desc' }] }"
:params="params"
>
<template #headerTitle>
<a-space>
<a-button type="primary" @click="handleAdd">
<AIcon type="PlusOutlined" />新增
</a-button>
</a-space>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:status="slotProps.state.value"
:statusText="slotProps.state.text"
:statusNames="{
enabled: 'success',
disabled: 'error',
}"
>
<template #img>
<slot name="img">
<img :src="getImage('/iot-card/iot-card-bg.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.operatorName }}</div>
</a-col>
<a-col :span="12">
<div class="card-item-content-text">说明</div>
<div>{{ slotProps.explain }}</div>
</a-col>
</a-row>
</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>
<template #state="slotProps">
<a-badge
:text="slotProps.state.text"
:status="
slotProps.state.value === 'disabled'
? 'error'
: 'success'
"
/>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm v-if="i.popConfirm" v-bind="i.popConfirm">
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
</a-space>
</template>
</JTable>
</page-container>
</template>
<script setup lang="ts">
import { getImage } from '@/utils/comm';
import type { ActionsType } from '@/components/Table';
import { message } from 'ant-design-vue';
import { queryList, update, del } from '@/api/iot-card/platform';
const router = useRouter()
const platformRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '平台类型',
dataIndex: 'operatorName',
key: 'operatorName',
search: {
type: 'select',
options: [
{ label: '移动OneLink', value: 'onelink' },
{ label: '电信Ctwing', value: 'ctwing' },
{ label: '联通Unicom', value: 'unicom' },
],
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
width: 120,
search: {
type: 'select',
options: [
{ label: '启用', value: 'enabled' },
{ label: '禁用', value: 'disabled' },
],
},
},
{
title: '说明',
dataIndex: 'explain',
key: 'explain',
ellipsis: true,
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
scopedSlots: true,
},
];
const statusUpdate = async (data: any) => {
const res = await update(data);
if (res.status === 200) {
message.success('操作成功');
platformRef.value?.reload();
}
};
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
return [
{
key: 'edit',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
router.push(`/iot-card/Platform/detail/${data.id}`);
},
},
{
key: 'action',
text: data.state.value === 'enabled' ? '禁用' : '启用',
tooltip: {
title: data.state.value === 'enabled' ? '禁用' : '启用',
},
icon:
data.state.value === 'enabled'
? 'StopOutlined'
: 'PlayCircleOutlined',
popConfirm: {
title: `确认${
data.state.value === 'enabled' ? '禁用' : '启用'
}`,
okText: ' 确定',
cancelText: '取消',
onConfirm: () => {
if (data.state.value === 'enabled') {
statusUpdate({
id: data.id,
config: { ...data.config },
state: 'disabled',
operatorName: data.operatorName,
});
} else {
statusUpdate({
id: data.id,
config: { ...data.config },
state: 'enabled',
operatorName: data.operatorName,
});
}
},
},
},
{
key: 'delete',
text: '删除',
tooltip: {
title:
data.state.value !== 'enabled' ? '删除' : '请先禁用再删除',
},
disabled: data.state.value === 'enabled',
popConfirm: {
title: '确认删除?',
okText: ' 确定',
cancelText: '取消',
onConfirm: async () => {
const resp: any = await del(data.id);
if (resp.status === 200) {
message.success('操作成功!');
platformRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'DeleteOutlined',
},
];
};
const handleSearch = (params: any) => {
console.log(params);
params.value = params;
};
/**
* 新增
*/
const handleAdd = () => {
router.push(`/iot-card/Platform/detail/:id`)
};
</script>
<style scoped lang="less"></style>

View File

@ -0,0 +1,205 @@
<!-- 平台类型 -->
<template>
<div :class="['radio-card-items', className, disabled ? 'disabled' : '']">
<div
v-for="item in options"
:key="item.value"
:style="itemStyle"
:class="[
'radio-card-item',
keys.includes(item.value) ? 'checked' : '',
]"
@click="toggleOption(item.value)"
>
<div class="card-list">
<div>
<img
:style="`width: ${item.imgSize?.[0]}; height: ${item.imgSize?.[1]}`"
v-if="item.imgUrl"
:src="item.imgUrl"
alt=""
/>
</div>
<div>{{ item.label }}</div>
</div>
<div class="checked-icon">
<div><AIcon type="CheckOutlined" /></div>
</div>
</div>
</div>
</template>
<script setup>
const emit = defineEmits(['update:value', 'change'], );
const props = defineProps({
options: {
type: Array,
default: () => [],
required: true,
},
model: {
validator: function (value) {
return ['multiple', 'singular'].includes(value);
},
default: () => 'singular',
},
value: {
type: String,
default: () => '',
},
disabled: {
type: Boolean,
default: () => true,
},
className: {
type: String,
},
itemStyle: {
type: Object,
default: () => {},
},
});
const keys = ref(
!(props.model && props.model === 'singular') ? props.value : [props.value],
);
const toggleOption = (key) => {
if (props.disabled) {
return;
} else {
const optionIndex = keys.value.includes(key);
const newKeys = [...keys.value];
const singular = props.model && props.model === 'singular';
if (!optionIndex) {
if (!(props.model && props.model === 'singular')) {
newKeys.push(key);
} else {
newKeys[0] = key;
}
} else {
newKeys.splice(optionIndex, 1);
}
emit('update:value', singular ? newKeys[0] : newKeys);
emit('change', singular ? newKeys[0] : newKeys);
}
};
watch(
() => props.value,
(newVal) => {
keys.value = !(props.model && props.model === 'singular')
? newVal
: [newVal];
},
);
</script>
<style lang="less" scoped>
@border: 1px solid @border-color-base;
.radio-card-items {
display: flex;
.radio-card-item {
display: flex;
align-items: center;
min-width: 180px;
padding: 22px 28px;
overflow: hidden;
font-size: 14px;
border: @border;
border-radius: @border-radius-base;
.card-list {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
div {
text-align: center;
}
}
> img {
width: 32px;
height: 32px;
margin-right: 24px;
}
> span {
cursor: default;
}
&:not(:last-child) {
margin-right: 24px;
}
&:hover,
&:focus {
color: @primary-color-hover;
border-color: @primary-color-hover;
}
.checked-icon {
position: absolute;
right: -22px;
bottom: -22px;
z-index: 2;
display: none;
width: 44px;
height: 44px;
color: #fff;
background-color: @primary-color-active;
transform: rotate(-45deg);
> div {
position: relative;
height: 100%;
transform: rotate(45deg);
> span {
position: absolute;
top: 6px;
left: 6px;
font-size: 12px;
}
}
}
&.checked {
position: relative;
color: @primary-color-active;
border-color: @primary-color-active;
> .checked-icon {
display: block;
}
}
}
&.disabled {
.radio-card-item {
color: @disabled-color;
border-color: @disabled-bg;
cursor: not-allowed;
.checked-icon {
background-color: @disabled-active-bg;
}
&:hover,
&:focus {
color: @disabled-color;
border-color: @disabled-active-bg;
}
&.checked {
color: @disabled-color;
border-color: @disabled-active-bg;
}
}
}
}
</style>