Commit 3f7fc1aaa1f29c6a492d736e32f183c66a729528

Authored by xp.Huang
2 parents 454a08e0 5622ab46

Merge branch 'feat_device_access' into 'main_dev'

feat: 新增设备接入信息功能

See merge request yunteng/thingskit-front!1217
  1 +import { defHttp } from '/@/utils/http/axios';
  2 +
  3 +enum Api {
  4 + DEVICE_PROFILE_ACCESS_INFORMATIION = '/device_profile/access_information',
  5 +}
  6 +
  7 +/**
  8 + * 新增设备接入信息
  9 + */
  10 +export const deviceProfileAccessInformation = (params) => {
  11 + return defHttp.post<any>({
  12 + url: `${Api.DEVICE_PROFILE_ACCESS_INFORMATIION}`,
  13 + params,
  14 + });
  15 +};
  16 +
  17 +/**
  18 + * 获取设备接入信息列表
  19 + */
  20 +export const getDeviceAccessInformationList = (params) => {
  21 + return defHttp.get<any>({
  22 + url: Api.DEVICE_PROFILE_ACCESS_INFORMATIION,
  23 + params,
  24 + });
  25 +};
  26 +
  27 +/**
  28 + * 删除设备接入信息
  29 + */
  30 +export const deleteDeviceAccessInformation = (params) => {
  31 + return defHttp.delete<any>({
  32 + url: Api.DEVICE_PROFILE_ACCESS_INFORMATIION,
  33 + params,
  34 + });
  35 +};
... ...
... ... @@ -41,6 +41,7 @@ import InputGroup from './components/InputGroup.vue';
41 41 import RegisterAddressInput from '/@/views/task/center/components/PollCommandInput/RegisterAddressInput.vue';
42 42 import DeviceProfileForm from '/@/components/Form/src/components/DeviceProfileForm/index.vue';
43 43 import { Segmented } from './components/Segmented';
  44 +import FormInputGroup from './components/FormInputGroup.vue';
44 45
45 46 const componentMap = new Map<ComponentType, Component>();
46 47
... ... @@ -89,6 +90,7 @@ componentMap.set('ApiSelectScrollLoad', ApiSelectScrollLoad);
89 90 componentMap.set('InputGroup', InputGroup);
90 91 componentMap.set('RegisterAddressInput', RegisterAddressInput);
91 92 componentMap.set('DeviceProfileForm', DeviceProfileForm);
  93 +componentMap.set('FormInputGroup', FormInputGroup);
92 94
93 95 export function add(compName: ComponentType, component: Component) {
94 96 componentMap.set(compName, component);
... ...
  1 +<template>
  2 + <div class="form-input">
  3 + <!-- 简易封装InputGroup -->
  4 + <!-- 待完善封装InputGroup -->
  5 + <InputGroup compact>
  6 + <Input
  7 + :placeholder="inputPlaceholder"
  8 + v-model:value="inputReactive.inputIp"
  9 + @change="emitChange"
  10 + style="width: 42%"
  11 + :maxlength="255"
  12 + />
  13 + <InputNumber
  14 + :min="1"
  15 + :max="65535"
  16 + :placeholder="inputNumberPlaceholder"
  17 + v-model:value="inputReactive.inputPort"
  18 + @change="emitChange"
  19 + />
  20 + </InputGroup>
  21 + </div>
  22 +</template>
  23 +<script lang="ts" setup>
  24 + import { PropType, reactive, watch } from 'vue';
  25 + import { InputGroup, Input, InputNumber } from 'ant-design-vue';
  26 +
  27 + interface inputReactive {
  28 + inputIp: string;
  29 + inputPort: number | string | undefined;
  30 + }
  31 +
  32 + const props = defineProps({
  33 + value: Object as PropType<{}>,
  34 + inputPlaceholder: {
  35 + type: String,
  36 + default: '请输入ip',
  37 + },
  38 + inputNumberPlaceholder: {
  39 + type: String,
  40 + default: '端口',
  41 + },
  42 + });
  43 +
  44 + const emits = defineEmits(['change', 'update:value']);
  45 +
  46 + const inputReactive = reactive<inputReactive>({
  47 + inputIp: '',
  48 + inputPort: undefined,
  49 + });
  50 +
  51 + watch(
  52 + () => props.value,
  53 + () => {
  54 + setValues();
  55 + },
  56 + {
  57 + immediate: true,
  58 + }
  59 + );
  60 +
  61 + function setValues() {
  62 + if (props?.value) for (let i in props.value) Reflect.set(inputReactive, i, props.value[i]);
  63 + }
  64 +
  65 + function emitChange() {
  66 + emits('change', inputReactive);
  67 + emits('update:value', inputReactive);
  68 + }
  69 +</script>
  70 +
  71 +<style lang="less" scoped>
  72 + .form-input {
  73 + :deep .ant-input-number {
  74 + width: 29% !important;
  75 + }
  76 + }
  77 +</style>
... ...
... ... @@ -148,4 +148,5 @@ export type ComponentType =
148 148 | 'LockControlGroup'
149 149 | 'EnumList'
150 150 | 'Segmented'
  151 + | 'FormInputGroup'
151 152 | 'StructFormItem';
... ...
... ... @@ -208,6 +208,22 @@ export const CameraChannelNoRule: Rule[] = [
208 208 },
209 209 ];
210 210
  211 +// 验证ip地址正则
  212 +export const IpRule: Rule[] = [
  213 + {
  214 + required: true,
  215 + validator: (_, value: string) => {
  216 + const ipRegex =
  217 + /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
  218 + if (!ipRegex.test(value)) {
  219 + return Promise.reject('请输入正确格式的ip');
  220 + }
  221 + return Promise.resolve();
  222 + },
  223 + validateTrigger: 'blur',
  224 + },
  225 +];
  226 +
211 227 export const CameraVideoStreamUrl: Rule[] = [
212 228 {
213 229 required: true,
... ... @@ -423,3 +439,6 @@ export const MediaTypeValidate: Rule[] = [
423 439 validateTrigger: 'blur',
424 440 },
425 441 ];
  442 +
  443 +export const ipRegex =
  444 + /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
... ...
  1 +<template>
  2 + <BasicDrawer v-bind="$attrs" @register="registerDrawer" title="设备接入信息详情" width="25%">
  3 + <Description :column="3" size="middle" @register="registeDesc" />
  4 + </BasicDrawer>
  5 +</template>
  6 +<script lang="ts" setup>
  7 + import { detailSchema } from '../index';
  8 + import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
  9 + import { Description } from '/@/components/Description/index';
  10 + import { useDescription } from '/@/components/Description';
  11 +
  12 + defineEmits(['success', 'register']);
  13 +
  14 + const [registeDesc, { setDescProps }] = useDescription({
  15 + schema: detailSchema,
  16 + column: 2,
  17 + layout: 'vertical',
  18 + });
  19 +
  20 + const [registerDrawer, { setDrawerProps }] = useDrawerInner(async (data) => {
  21 + setDrawerProps({ confirmLoading: false });
  22 + const dataValue = {
  23 + ...data,
  24 + ...data.sipExtend,
  25 + };
  26 + setDescProps({ data: dataValue });
  27 + });
  28 +</script>
... ...
  1 +<script setup lang="ts">
  2 + import { computed, unref } from 'vue';
  3 + import { ref } from 'vue';
  4 + import { BasicModal, useModalInner } from '/@/components/Modal';
  5 + import { deleteFormField, schemas } from '../index';
  6 + import { useForm, BasicForm } from '/@/components/Form';
  7 + import { deviceProfileAccessInformation } from '/@/api/device/deviceAccess';
  8 + import { useMessage } from '/@/hooks/web/useMessage';
  9 + import { ipRegex } from '/@/utils/rules';
  10 +
  11 + const emit = defineEmits(['handleReload', 'register']);
  12 +
  13 + const isUpdate = ref<Boolean>(false);
  14 +
  15 + const getTitle = computed(() => (!unref(isUpdate) ? '新增设备接入信息' : '编辑设备接入信息'));
  16 +
  17 + const { createMessage } = useMessage();
  18 +
  19 + const [registerForm, { getFieldsValue, setFieldsValue, validate }] = useForm({
  20 + labelWidth: 150,
  21 + schemas,
  22 + actionColOptions: {
  23 + span: 14,
  24 + },
  25 + showActionButtonGroup: false,
  26 + });
  27 +
  28 + const recordInfo = ref<Recordable>({});
  29 +
  30 + const [register, { closeModal, setModalProps }] = useModalInner(async (data) => {
  31 + setModalProps({ confirmLoading: false, loading: true });
  32 + isUpdate.value = data?.isUpdate;
  33 + recordInfo.value = data?.record;
  34 + if (data?.record) {
  35 + const intranetIpAndPort = {
  36 + inputIp: data?.record.intranetIp,
  37 + inputPort: data?.record.intranetPort,
  38 + };
  39 + const outerIpAndPort = {
  40 + inputIp: data?.record.outerNetIp,
  41 + inputPort: data?.record.outerNetPort,
  42 + };
  43 + setFieldsValue(data?.record);
  44 + setFieldsValue({ ...data?.record?.sipExtend });
  45 + setFieldsValue({ intranetIpAndPort, outerIpAndPort });
  46 + }
  47 + setModalProps({ loading: false });
  48 + });
  49 +
  50 + const handleCancel = () => closeModal();
  51 +
  52 + const handleOk = async () => {
  53 + await validate();
  54 + let values = getFieldsValue();
  55 + if (unref(isUpdate)) {
  56 + values = { ...values, id: unref(recordInfo).id };
  57 + }
  58 + const { intranetIpAndPort, outerIpAndPort } = values;
  59 + values.intranetIp = intranetIpAndPort.inputIp;
  60 + values.intranetPort = intranetIpAndPort.inputPort;
  61 + values.outerNetIp = outerIpAndPort.inputIp;
  62 + values.outerNetPort = outerIpAndPort.inputPort;
  63 + if (!ipRegex.test(values.intranetIp)) {
  64 + return createMessage.error('请输入正确格式的ip');
  65 + }
  66 + if (!ipRegex.test(values.outerNetIp)) {
  67 + return createMessage.error('请输入正确格式的ip');
  68 + }
  69 + if (!values.intranetPort) {
  70 + return createMessage.error('请输入内网端口');
  71 + }
  72 + if (!values.outerNetPort) {
  73 + return createMessage.error('请输入外网端口');
  74 + }
  75 + const sipExtend = {
  76 + serverId: values['serverId'],
  77 + serverDomain: values['serverDomain'],
  78 + serverPassword: values['serverPassword'],
  79 + };
  80 + values.sipExtend = sipExtend;
  81 + deleteFormField.forEach((deleteItem) => {
  82 + Reflect.deleteProperty(values, deleteItem);
  83 + });
  84 + await deviceProfileAccessInformation(values);
  85 + createMessage.success('操作成功');
  86 + emit('handleReload');
  87 + handleCancel();
  88 + };
  89 +</script>
  90 +
  91 +<template>
  92 + <div>
  93 + <BasicModal
  94 + v-bind="$attrs"
  95 + width="35rem"
  96 + :title="getTitle"
  97 + @register="register"
  98 + @cancel="handleCancel"
  99 + @ok="handleOk"
  100 + destroyOnClose
  101 + >
  102 + <div>
  103 + <BasicForm @register="registerForm" />
  104 + </div>
  105 + </BasicModal>
  106 + </div>
  107 +</template>
... ...
  1 +import deviceAccessModal from './deviceAccessModal.vue';
  2 +import deviceAccessDetailDrawer from './deviceAccessDetailDrawer.vue';
  3 +
  4 +export { deviceAccessModal, deviceAccessDetailDrawer };
... ...
  1 +import { DescItem } from '/@/components/Description/src/typing';
  2 +import { BasicColumn, FormSchema } from '/@/components/Table';
  3 +
  4 +//设备接入信息权限标识枚举
  5 +export enum DEVICE_ACCESS_PERMISSION_ENUM {
  6 + CREATE = 'api:yt:device_profile:access_information:post',
  7 + UPDATE = 'api:yt:device_profile:access_information:update',
  8 + DELETE = 'api:yt:device_profile:access_information:delete',
  9 + GET = 'api:yt:device_profile:access_information:get',
  10 +}
  11 +
  12 +//要删除的表单字段
  13 +export const deleteFormField = [
  14 + 'serverId',
  15 + 'serverDomain',
  16 + 'serverPassword',
  17 + 'intranetIpAndPort',
  18 + 'outerIpAndPort',
  19 +];
  20 +
  21 +//表格字段
  22 +export const columns: BasicColumn[] = [
  23 + {
  24 + title: '内网ip',
  25 + dataIndex: 'intranetIp',
  26 + },
  27 + {
  28 + title: '内网端口',
  29 + dataIndex: 'intranetPort',
  30 + },
  31 + {
  32 + title: '外网ip',
  33 + dataIndex: 'outerNetIp',
  34 + },
  35 + {
  36 + title: '外网端口',
  37 + dataIndex: 'outerNetPort',
  38 + },
  39 + {
  40 + title: '接入协议',
  41 + dataIndex: 'deviceAgreement',
  42 + },
  43 +];
  44 +
  45 +//表单查询
  46 +export const searchFormSchema: FormSchema[] = [
  47 + {
  48 + field: 'intranetIp',
  49 + label: '内网ip',
  50 + component: 'Input',
  51 + colProps: { span: 6 },
  52 + componentProps: {
  53 + maxLength: 255,
  54 + placeholder: '请输入内网ip',
  55 + },
  56 + },
  57 + {
  58 + field: 'outerNetIp',
  59 + label: '外网ip',
  60 + component: 'Input',
  61 + colProps: { span: 6 },
  62 + componentProps: {
  63 + maxLength: 255,
  64 + placeholder: '请输入外网ip',
  65 + },
  66 + },
  67 + {
  68 + field: 'deviceAgreement',
  69 + label: '接入协议',
  70 + component: 'Select',
  71 + colProps: { span: 6 },
  72 + componentProps() {
  73 + return {
  74 + options: [
  75 + { label: '默认', value: 'DEFAULT' },
  76 + { label: 'MQTT', value: 'MQTT' },
  77 + { label: 'CoAP', value: 'COAP' },
  78 + { label: 'TCP', value: 'TCP' },
  79 + { label: 'GBT28181', value: 'GBT28181' },
  80 + ],
  81 + getPopupContainer: () => document.body,
  82 + placeholder: '请选择接入协议',
  83 + };
  84 + },
  85 + },
  86 +];
  87 +
  88 +//表单字段
  89 +export const schemas: FormSchema[] = [
  90 + {
  91 + field: 'intranetIpAndPort',
  92 + label: '内网ip&端口',
  93 + component: 'FormInputGroup',
  94 + required: true,
  95 + componentProps: {
  96 + inputPlaceholder: '请输入内网ip',
  97 + inputNumberPlaceholder: '内网端口',
  98 + },
  99 + colProps: { span: 24 },
  100 + },
  101 + {
  102 + field: 'outerIpAndPort',
  103 + label: '外网ip&端口',
  104 + component: 'FormInputGroup',
  105 + required: true,
  106 + componentProps: {
  107 + inputPlaceholder: '请输入外网ip',
  108 + inputNumberPlaceholder: '外网端口',
  109 + },
  110 + colProps: { span: 24 },
  111 + },
  112 + {
  113 + field: 'deviceAgreement',
  114 + component: 'Select',
  115 + required: true,
  116 + label: '接入协议',
  117 + componentProps() {
  118 + return {
  119 + options: [
  120 + { label: '默认', value: 'DEFAULT' },
  121 + { label: 'MQTT', value: 'MQTT' },
  122 + { label: 'CoAP', value: 'COAP' },
  123 + { label: 'TCP', value: 'TCP' },
  124 + { label: 'GBT28181', value: 'GBT28181' },
  125 + ],
  126 + getPopupContainer: () => document.body,
  127 + placeholder: '请选择接入协议',
  128 + };
  129 + },
  130 + colProps: { span: 19 },
  131 + },
  132 + {
  133 + field: 'serverId',
  134 + label: '服务器ID',
  135 + required: false,
  136 + colProps: { span: 19 },
  137 + component: 'Input',
  138 + componentProps: {
  139 + maxLength: 255,
  140 + placeholder: '请输入服务器ID',
  141 + },
  142 + ifShow({ values }) {
  143 + return values.deviceAgreement === 'GBT28181';
  144 + },
  145 + },
  146 + {
  147 + field: 'serverDomain',
  148 + label: '服务器域',
  149 + required: false,
  150 + colProps: { span: 19 },
  151 + component: 'Input',
  152 + componentProps: {
  153 + maxLength: 255,
  154 + placeholder: '请输入服务器域',
  155 + },
  156 + ifShow({ values }) {
  157 + return values.deviceAgreement === 'GBT28181';
  158 + },
  159 + },
  160 + {
  161 + field: 'serverPassword',
  162 + label: '密码',
  163 + required: false,
  164 + colProps: { span: 19 },
  165 + component: 'InputPassword',
  166 + componentProps: {
  167 + maxLength: 255,
  168 + placeholder: '请输入密码',
  169 + },
  170 + ifShow({ values }) {
  171 + return values.deviceAgreement === 'GBT28181';
  172 + },
  173 + },
  174 +];
  175 +
  176 +//详情字段
  177 +export const detailSchema: DescItem[] = [
  178 + {
  179 + field: 'intranetIp',
  180 + label: '内网ip',
  181 + },
  182 + {
  183 + field: 'intranetPort',
  184 + label: '内网端口',
  185 + },
  186 + {
  187 + field: 'outerNetIp',
  188 + label: '外网ip',
  189 + },
  190 + {
  191 + field: 'outerNetPort',
  192 + label: '外网端口',
  193 + },
  194 + {
  195 + field: 'deviceAgreement',
  196 + label: '接入协议',
  197 + },
  198 + {
  199 + field: 'serverId',
  200 + label: '服务器ID',
  201 + },
  202 + {
  203 + field: 'serverDomain',
  204 + label: '服务器域',
  205 + },
  206 + {
  207 + field: 'serverPassword',
  208 + label: '密码',
  209 + },
  210 +];
... ...
  1 +<script setup lang="ts" name="deviceAccess">
  2 + import { columns, searchFormSchema, DEVICE_ACCESS_PERMISSION_ENUM } from '.';
  3 + import { BasicTable, useTable, TableAction } from '/@/components/Table';
  4 + import { Button, Popconfirm } from 'ant-design-vue';
  5 + import { useBatchOperation } from '/@/utils/useBatchOperation';
  6 + import { useMessage } from '/@/hooks/web/useMessage';
  7 + import { Authority } from '/@/components/Authority';
  8 + import { useModal } from '/@/components/Modal';
  9 + import { deviceAccessModal, deviceAccessDetailDrawer } from './components/index';
  10 + import { useDrawer } from '/@/components/Drawer';
  11 + import { USER_INFO_KEY } from '/@/enums/cacheEnum';
  12 + import { getAuthCache } from '/@/utils/auth';
  13 + import { authBtn } from '/@/enums/roleEnum';
  14 + import {
  15 + getDeviceAccessInformationList,
  16 + deleteDeviceAccessInformation,
  17 + } from '/@/api/device/deviceAccess';
  18 +
  19 + const [
  20 + registerTable,
  21 + { reload, setLoading, getSelectRowKeys, setSelectedRowKeys, getRowSelection },
  22 + ] = useTable({
  23 + title: '设备接入信息列表',
  24 + api: getDeviceAccessInformationList,
  25 + columns,
  26 + beforeFetch: (params) => {
  27 + for (let i in params) if (!params[i]) Reflect.deleteProperty(params, i); // 如果没有值,则此字段也不传
  28 + return params;
  29 + },
  30 + formConfig: {
  31 + labelWidth: 100,
  32 + schemas: searchFormSchema,
  33 + },
  34 + immediate: true,
  35 + useSearchForm: true,
  36 + showTableSetting: true,
  37 + bordered: true,
  38 + showIndexColumn: false,
  39 + clickToRowSelect: false,
  40 + rowKey: 'id',
  41 + actionColumn: {
  42 + width: 230,
  43 + title: '操作',
  44 + slots: { customRender: 'action' },
  45 + fixed: 'right',
  46 + },
  47 + rowSelection: {
  48 + type: 'checkbox',
  49 + },
  50 + });
  51 +
  52 + const [registerModal, { openModal }] = useModal();
  53 +
  54 + const [registerDetailDrawer, { openDrawer }] = useDrawer();
  55 +
  56 + const { createMessage } = useMessage();
  57 +
  58 + const { isExistOption } = useBatchOperation(getRowSelection, setSelectedRowKeys);
  59 +
  60 + const userInfo: Recordable = getAuthCache(USER_INFO_KEY);
  61 +
  62 + const role: string = userInfo.roles[0];
  63 +
  64 + const handleReload = () => {
  65 + setSelectedRowKeys([]);
  66 + reload();
  67 + };
  68 +
  69 + //新增或编辑
  70 + const handleCreateOrEdit = (record) => {
  71 + const data = { isUpdate: !record ? false : true, record };
  72 + openModal(true, data);
  73 + };
  74 +
  75 + const handleDetail = (record?: Recordable) => {
  76 + openDrawer(true, record);
  77 + };
  78 +
  79 + // 删除
  80 + const handleDelete = async (record?: Recordable) => {
  81 + let ids: string[] = [];
  82 + if (record) {
  83 + ids = [record.id];
  84 + } else {
  85 + ids = getSelectRowKeys();
  86 + }
  87 + try {
  88 + setLoading(true);
  89 + await deleteDeviceAccessInformation({ ids });
  90 + createMessage.success('删除成功');
  91 + handleReload();
  92 + } catch (error) {
  93 + throw error;
  94 + } finally {
  95 + setLoading(false);
  96 + }
  97 + };
  98 +</script>
  99 +<template>
  100 + <div>
  101 + <BasicTable style="flex: auto" @register="registerTable">
  102 + <template #toolbar>
  103 + <Authority :value="DEVICE_ACCESS_PERMISSION_ENUM.CREATE">
  104 + <Button type="primary" @click="handleCreateOrEdit(null)"> 新增设备接入信息 </Button>
  105 + </Authority>
  106 + <Authority :value="DEVICE_ACCESS_PERMISSION_ENUM.DELETE">
  107 + <Popconfirm
  108 + title="您确定要批量删除数据"
  109 + ok-text="确定"
  110 + cancel-text="取消"
  111 + @confirm="handleDelete()"
  112 + >
  113 + <a-button color="error" :disabled="!isExistOption"> 批量删除 </a-button>
  114 + </Popconfirm>
  115 + </Authority>
  116 + </template>
  117 + <template #action="{ record }">
  118 + <TableAction
  119 + :actions="[
  120 + {
  121 + label: '详情',
  122 + icon: 'ant-design:eye-outlined',
  123 + auth: DEVICE_ACCESS_PERMISSION_ENUM.GET,
  124 + onClick: handleDetail.bind(null, record),
  125 + },
  126 + {
  127 + label: '编辑',
  128 + auth: DEVICE_ACCESS_PERMISSION_ENUM.UPDATE,
  129 + icon: 'clarity:note-edit-line',
  130 + ifShow: authBtn(role),
  131 + onClick: handleCreateOrEdit.bind(null, record),
  132 + },
  133 + {
  134 + label: '删除',
  135 + auth: DEVICE_ACCESS_PERMISSION_ENUM.DELETE,
  136 + icon: 'ant-design:delete-outlined',
  137 + ifShow: authBtn(role),
  138 + color: 'error',
  139 + popConfirm: {
  140 + title: '是否确认删除',
  141 + confirm: handleDelete.bind(null, record),
  142 + },
  143 + },
  144 + ]"
  145 + />
  146 + </template>
  147 + </BasicTable>
  148 + <deviceAccessModal @register="registerModal" @handleReload="handleReload" />
  149 + <deviceAccessDetailDrawer
  150 + title="设备接入信息详情"
  151 + @register="registerDetailDrawer"
  152 + width="40%"
  153 + destroy-on-close
  154 + />
  155 + </div>
  156 +</template>
... ...